Source

notifyfrndc / friendika.py

Full commit
#!/usr/bin/env python2
# -*- coding: utf-8 -*-

"""
PyFriendika

A module to handle communication with Frienidka via the StatusNet compatible
API providing all the functions that the Friendika API has available.

Author: Tobias Diekershoff <tobias.diekershoff@gmx.net>
        https://diekershoff.homeunix.net/friendika/profile/tobias

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
    * Redistributions of source code must retain the above copyright
      notice, this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above
      copyright notice, this list of conditions and the following
      disclaimer in the documentation and/or other materials provided
      with the distribution.
    * Neither the name of the <organization> nor the  names of its
      contributors may be used to endorse or promote products
      derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""

import json
import urllib2
import cookielib
from urllib import urlencode
from xml.dom import minidom

_debug = 0

__name__ = 'PyFriendika'
__version_major__ = 0
__version_minor__ = 0
__version_release__ = 3
__version__ = '%d.%d.%d' % (__version_major__, __version_minor__,
        __version_release__)

def getPoco (server, user):
    """
    get the json data from server/poco/user and return it
    """
    try:
        url = urllib2.urlopen(server+'/poco/' + user)
        return json.load(url)
    except:
        return None

class FPoco:
    """
    Class to access the poco informations of a friendika account.
    """
    def __init__ (self, server, user):
        self.url = server
        self.user = user
        self.data = None
        self.contacts = None
        self.total = -1
    def getPoco (self):
        """
        retrieve the Personal Contact data for this account
        """
        self.data = getPoco(self.url, self.user)
        if self.data == None:
            self.total = -1
            self.contacts = None
        else:
            self.total = int(self.data['totalResults'])
            self.contacts = self.data['entry'] 
    def getContact (self, cid):
        """
        from the contact data, get one contact information set
        """
        return self.contacts[cid]
    def getContactName (self, cid):
        """
        get the contact display name for contact n
        """
        return self.getContact(cid)['displayName']
    def getContactProfileUrl (self, cid):
        """
        get the contact profile url for contact n
        """
        return self.getContact(cid)['urls']['value']
    def getContactImageUrl (self, cid):
        """
        get the contact image url for contact n
        """
        return self.getContact(cid)['photos']['value']

class Friendika:
    """
    Class to handle the communitaction with a Friendika server via the
    StatusNet compatible API.

    Basic usage:

    conn = Friendika( apipath, username, password)
    if conn.login:
        conn.post('posting from the API')

    The apipath is <your friendika server>/api

    You can use this class over a proxy connection, but you don't have to
    (the default proxy is not set!).
    """
    def __init__ (self, apipath, username, password, proxy=""):
        """
        initialize the class and the connection to the Friendika server.
        if login fails self.login == False.
        """
        self.apipath = apipath
        self.username = username
        self.password = password
        self.poco = FPoco (apipath[:-4], username)
        self.lasterror = ""
        if not proxy=="":
            self.proxy = proxy
        else:
            self.proxy = False
        # set up the urllib communications stuff
        self.cj = cookielib.CookieJar()
        self.pwd_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
        self.pwd_mgr.add_password(None, apipath, username, password)
        self.handler = urllib2.HTTPBasicAuthHandler(self.pwd_mgr)
        if not self.proxy:
            self.opener = urllib2.build_opener(self.handler,
                    urllib2.HTTPCookieProcessor(self.cj))
        else:
            self.proxy_handler = urllib2.ProxyHandler( {'http': proxy,
                'https':proxy } )
            self.opener = urllib2.build_opener(self.proxy_handler,
                    self.handler, urllib2.HTTPCookieProcessor(self.cj)) 
        try:
            self.login = self.verify_credentials()['verified']
        except:
            self.lasterror = "verify account credentials failed"
            self.login = False

        # get the config of the server
        self.config = self.get_config()

    def apicall(self, call):
        """
        Make an API call.

        Friendika supports the following calls from the
        StautsNet/Twitter API (the also the wrapper functions):
          * /statusnet/config.json
          * /statuses/home_timeline.json
          * /statuses/update.json
          * /account/rate_limit_status.json
          * /statusnet/config.json
          * /statusnet/version.json
          * /users/show.json
        """
        urllib2.install_opener(self.opener)
        res = {}
        try:
            url = urllib2.urlopen(self.apipath + call)
            res = json.load(url)
            self.lasterror = ""
        except ValueError as e:
            if not str(e).find('No JSON') == -1:
                print 'could not get a JSON Object from the URL'
                self.lasterror = str(e)
            else:
                raise( e )
        except urllib2.HTTPError as e:
            if not str(e).find('Error 401') == -1:
                self.lasterror = str(e)
                self.login = False
            else:
                raise( e )
        return res

    def verify_credentials (self):
        """
        Verify that we are logged in and get the users credentials
        """
        return self.apicall('/account/verify_credentials.json')

    def get_config(self):
        """
        Wrapper call to get the config.json from the server.

        Note the Friendika.config dictionary where the stuff
        gets stored once you initialize the connection.
        """
        res = self.apicall('/statusnet/config.json')
        if res == {}:
            res = { 'site': {
                    'name':'failed',
                    'host':'failed'
                }}
        return res

    def get_items(self, timeline, count=20):
        """
        Wrapper call to get items from a given timeline, which
        is at the moment alsways the users Wall.

        NOTE: Friendika returns 20 items
              the count parameter is there if you only need less items
        """
        res = self.apicall('/statuses/%s.json?count=%d' % (timeline, count))
        if count > 20: count = 20
        if len(res) > count:
            return res[:count]
        else:
            return res

    def home_timeline(self, count=20):
        """
        returns the wall items from the users wall as JSON array
        """
        return self.get_items('home_timeline', count)

    def friends_timeline(self, count=20):
        """
        returns the wall items from the users wall as JSON array
        """
        return self.get_items('friends_timeline', count)

    def user_timeline(self, count=20):
        """
        returns the wall items from the users wall as JSON array
        """
        return self.get_items('user_timeline', count)

    def post(self, text, lat=None, lon=None, in_reply_to=None,
            source=__name__):
        """
        Post a new notice to the users wall

        Optional parameters are
          lat / lon  : latitude and longitude have to be used
                       together otherwhise will be discarded
          in_reply_to: id of the notice you reply to
        """
        themsg = urlencode( { 'status':text,'source':source })
        if lat and lon:
            themsg['lat'] = str( lat )
            themsg['long'] = str( lon )
        if in_reply_to:
            themsg['in_reply_to_status_id'] = str( in_reply_to )
        urllib2.install_opener(self.opener)
        urllib2.urlopen(self.apipath + '/statuses/update.json', themsg)

    def rate_limit_status (self):
        """
        Always returns 150 actions for the next hour, whenever
        you call it ;-)
        """
        return self.apicall('/account/rate_limit_status.json')

    def version (self):
        """
        Returns the API version this Friendika server offers
        compatible functions for.
        """
        return self.apicall('/statusnet/version.json')

    def users_show (self):
        """

        """
        return self.apicall('/users/show.json')

    def favorites (self):
        """
        get users favorites
        """
        return self.apicall('/favorites.json')

    def friend_ids(self):
        """
        get the ids of the friends
        """
        return self.apicall('/friends/ids.json')

    def followers_ids(self):
        """
        get the ids of the followers
        """
        return self.apicall('/followers/ids.json')

    def ping(self):
        """
        ping the server to get new notifications about
          + new introductions: intro
          + new private mails: mail
          + new postings to the network: net
          + new portings to the personal wall: home
          + new registrations: register
          + rich content notifications: notif
          + system messages: sysmsgs
        return value is a dictionary holding the above information
        """
        urllib2.install_opener(self.opener)
        res = urllib2.urlopen(self.apipath[:-4] + '/ping').read()
        aping = minidom.parseString(res)
        try:
            intro = aping.getElementsByTagName("intro")[0].childNodes[0].data
        except:
            intro = 0
        try:
            mail = aping.getElementsByTagName("mail")[0].childNodes[0].data
        except:
            mail = 0
        try:
            net = aping.getElementsByTagName("net")[0].childNodes[0].data
        except:
            net = 0
        try:
            home = aping.getElementsByTagName("home")[0].childNodes[0].data
        except:
            home = 0
        try:
            register = aping.getElementsByTagName("register")[0].childNodes[0].data
        except:
            register = 0
        nodes = []
        try:
            notif = aping.getElementsByTagName('notif')[0].childNodes
            for node in notif[:-1]:
                nodes.append( {
                        "url":node.attributes['url'].value,
                        "photo":node.attributes['photo'].value,
                        "href":node.attributes['href'].value,
                        "date":node.attributes['date'].value,
                        "name":node.attributes['name'].value,
                        "data":node.firstChild.data.replace('{0}',node.attributes['name'].value)
                    } )
        except:
            pass
        sys_notices = []
        sys_info = []
        try:
            sysmsgs = aping.getElementsByTagName('sysmsgs')[0]
            notices = sysmsgs.getElementsByTagName('notice')
            info = sysmsgs.getElementsByTagName('info')
            for i in notices:
                sys_notices.append(i.firstChild.data)
            for i in info:
                sys_info.append(i.firstChild.data)
        except:
            pass
        return {'intro':int(intro), 'mail':int(mail), 'net':int(net),
                'home':int(home), 'register':int(register), 'notif':nodes,
                'sysmsgs':{'notice':sys_notices, 'info':sys_info} }