view sat.tac @ 31:7b34ffa2ff45

Updated README and added CHANGELOG - README: added privacy warning for http://www.goffi.org/sat_tools/get_ip.php access - CHANGELOG: first changelog (first public release for v0.0.1)
author Goffi <goffi@goffi.org>
date Tue, 08 Dec 2009 08:05:38 +0100
parents d6b613764dd7
children b9bb5d8e0cc7
line wrap: on
line source

#!/usr/bin/python
# -*- coding: utf-8 -*-

"""
SAT: a jabber client
Copyright (C) 2009  Jérôme Poisson (goffi@goffi.org)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
"""

client_name = u'SàT (Salut à toi)'
client_version = '0.0.1D'   #Please add 'D' at the end for dev versions

from twisted.application import internet, service
from twisted.internet import glib2reactor, protocol, task
glib2reactor.install()

from twisted.words.protocols.jabber import jid, xmlstream, error
from twisted.words.xish import domish

from twisted.internet import reactor
import pdb

from wokkel import client, disco, xmppim, generic, compat

from sat_bridge.DBus import DBusBridge
import logging
from logging import debug, info, error

import signal, sys
import os.path

from tools.memory import Memory
from glob import glob

try:
    from twisted.words.protocols.xmlstream import XMPPHandler
except ImportError:
    from wokkel.subprotocols import XMPPHandler


### logging configuration FIXME: put this elsewhere ###
logging.basicConfig(level=logging.DEBUG,
                    format='%(message)s')
###


sat_id = 0

def sat_next_id():
    global sat_id
    sat_id+=1
    return "sat_id_"+str(sat_id)

class SatXMPPClient(client.XMPPClient):
    
    def __init__(self, jid, password, host=None, port=5222):
        client.XMPPClient.__init__(self, jid, password, host, port)
        self.factory.clientConnectionLost = self.connectionLost
        self.__connected=False

    def _authd(self, xmlstream):
        print "SatXMPPClient"
        client.XMPPClient._authd(self, xmlstream)
        self.__connected=True
        print "********** CONNECTED **********"
        self.streamInitialized()

    def streamInitialized(self):
        """Called after _authd"""
        self.keep_alife = task.LoopingCall(self.xmlstream.send, " ")  #Needed to avoid disconnection (specially with openfire)
        self.keep_alife.start(180)

    def isConnected(self):
        return self.__connected
    
    def connectionLost(self, connector, unused_reason):
        self.__connected=False
        print "********** DISCONNECTED **********"
        try:
            self.keep_alife.stop()
        except AttributeError:
            debug("No keep_alife")


class SatMessageProtocol(xmppim.MessageProtocol):
    
    def __init__(self, host):
        xmppim.MessageProtocol.__init__(self)
        self.host = host

    def onMessage(self, message):
      debug (u"got_message from: %s", message["from"])
      for e in message.elements():
        if e.name == "body":
          self.host.bridge.newMessage(message["from"], e.children[0])
          self.host.memory.addToHistory(self.host.me, jid.JID(message["from"]), self.host.me, "chat", e.children[0])
          break
    
class SatRosterProtocol(xmppim.RosterClientProtocol):

    def __init__(self, host):
        xmppim.RosterClientProtocol.__init__(self)
        self.host = host
    
    def rosterCb(self, roster):
        for jid, item in roster.iteritems():
            info ("new contact in roster list: %s", jid)
            #FIXME: fill attributes
            self.host.memory.addContact(jid, {}, item.groups)
            self.host.bridge.newContact(jid, {}, item.groups)

    def requestRoster(self):
        """ ask the server for Roster list """
        debug("requestRoster")
        self.getRoster().addCallback(self.rosterCb)

    def removeItem(self, to):
        """Remove a contact from roster list"""
        to_jid=jid.JID(to)
        xmppim.RosterClientProtocol.removeItem(self, to_jid)
        #TODO: check IQ result
    
    def addItem(self, to):
        """Add a contact to roster list"""
        to_jid=jid.JID(to)
        xmppim.RosterClientProtocol.addItem(self, to_jid)
        #TODO: check IQ result

class SatPresenceProtocol(xmppim.PresenceClientProtocol):

    def __init__(self, host):
        xmppim.PresenceClientProtocol.__init__(self)
        self.host = host
    
    def availableReceived(self, entity, show=None, statuses=None, priority=0):
        info ("presence update for [%s]", entity)

        ### we check if the status is not about subscription ###
        #FIXME: type is not needed anymore
        #TODO: management of differents statuses (differents languages)
        status = statuses.values()[0] if len(statuses) else ""
        self.host.memory.addPresenceStatus(entity.full(), "", show or "",
                status or "", int(priority))

        #now it's time to notify frontends
        self.host.bridge.presenceUpdate(entity.full(), "", show or "",
                status or "", int(priority))
    
    def unavailableReceived(self, entity, statuses=None):
        #TODO: management of differents statuses (differents languages)
        status = statuses.values()[0] if len(statuses) else ""
        self.host.memory.addPresenceStatus(entity.full(), "unavailable", "",
                status or "", 0)

        #now it's time to notify frontends
        self.host.bridge.presenceUpdate(entity.full(), "unavailable", "",
                status or "", 0)
        

    def subscribedReceived(self, entity):
        debug ("subscription approved for [%s]" % entity)

    def unsubscribedReceived(self, entity):
        debug ("unsubscription confirmed for [%s]" % entity)

    def subscribeReceived(self, entity):
        #FIXME: auto answer for subscribe request, must be checked !
        debug ("subscription request for [%s]" % entity)
        self.subscribed(entity)

    def unsubscribeReceived(self, entity):
        debug ("unsubscription asked for [%s]" % entity)

class SatDiscoProtocol(disco.DiscoClientProtocol):
    def __init__(self, host):
        disco.DiscoClientProtocol.__init__(self)

class SatFallbackHandler(generic.FallbackHandler):
    def __init__(self, host):
        generic.FallbackHandler.__init__(self)

    def iqFallback(self, iq):
        #pdb.set_trace()
        print "iqFallback: xml = [%s], handled=%s" % (iq.toXml(), "True" if iq.handled else "False")
        generic.FallbackHandler.iqFallback(self, iq)

class RegisteringAuthenticator(xmlstream.ConnectAuthenticator):

    def __init__(self, host, jabber_host, user_login, user_pass, answer_id):
        xmlstream.ConnectAuthenticator.__init__(self, jabber_host)
        self.host = host
        self.jabber_host = jabber_host
        self.user_login = user_login
        self.user_pass = user_pass
        self.answer_id = answer_id
        print "Registration asked for",user_login, user_pass, jabber_host
    
    def connectionMade(self):
        print "connectionMade"
        
        self.xmlstream.namespace = "jabber:client"
        self.xmlstream.sendHeader()

        iq = compat.IQ(self.xmlstream, 'set')
        iq["to"] = self.jabber_host
        query = iq.addElement(('jabber:iq:register', 'query'))
        _user = query.addElement('username')
        _user.addContent(self.user_login)
        _pass = query.addElement('password')
        _pass.addContent(self.user_pass)
        reg = iq.send(self.jabber_host).addCallbacks(self.registrationAnswer, self.registrationFailure)

    def registrationAnswer(self, answer):
        debug ("registration answer: %s" % answer.toXml())
        answer_type = "SUCCESS"
        answer_data={"message":"Registration successfull"}
        self.host.bridge.actionResult(answer_type, self.answer_id, answer_data)
        self.xmlstream.sendFooter()
        
    def registrationFailure(self, failure):
        info ("Registration failure: %s" % str(failure.value))
        answer_type = "ERROR"
        answer_data = {}
        if failure.value.condition == 'conflict':
            answer_data['reason'] = 'conflict'
            answer_data={"message":"Username already exists, please choose an other one"}
        else:
            answer_data['reason'] = 'unknown'
            answer_data={"message":"Registration failed"}
        self.host.bridge.actionResult(answer_type, self.answer_id, answer_data)
        self.xmlstream.sendFooter()
        

class SAT(service.Service):
   
    def get_next_id(self):
        return sat_next_id()

    def __init__(self):
        #TODO: standardize callback system
        self.__waiting_conf = {}  #callback called when a confirmation is received
        self.__progress_cb_map = {}  #callback called when a progress is requested (key = progress id)
        self.__general_cb_map = {}  #callback called for general reasons (key = name) 
        self.__private_data = {}  #used for internal callbacks (key = id)
        self.plugins = {}
        
        self.memory=Memory(self)
        self.server_features=[]  #XXX: temp dic, need to be transfered into self.memory in the future

        self.bridge=DBusBridge()
        self.bridge.register("registerNewAccount", self.registerNewAccount)
        self.bridge.register("connect", self.connect)
        self.bridge.register("disconnect", self.disconnect)
        self.bridge.register("getContacts", self.memory.getContacts)
        self.bridge.register("getPresenceStatus", self.memory.getPresenceStatus)
        self.bridge.register("sendMessage", self.sendMessage)
        self.bridge.register("setParam", self.setParam)
        self.bridge.register("getParamA", self.memory.getParamA)
        self.bridge.register("getParams", self.memory.getParams)
        self.bridge.register("getParamsForCategory", self.memory.getParamsForCategory)
        self.bridge.register("getParamsCategories", self.memory.getParamsCategories)
        self.bridge.register("getHistory", self.memory.getHistory)
        self.bridge.register("setPresence", self.setPresence)
        self.bridge.register("addContact", self.addContact)
        self.bridge.register("delContact", self.delContact)
        self.bridge.register("isConnected", self.isConnected)
        self.bridge.register("launchAction", self.launchAction)
        self.bridge.register("confirmationAnswer", self.confirmationAnswer)
        self.bridge.register("getProgress", self.getProgress)

        self._import_plugins()


    def _import_plugins(self):
        """Import all plugins found in plugins directory"""
        #TODO: manage dependencies
        plug_lst = [os.path.splitext(plugin)[0] for plugin in map(os.path.basename,glob ("plugins/plugin*.py"))]
        
        for plug in plug_lst:
            plug_path = 'plugins.'+plug
            __import__(plug_path)
            mod = sys.modules[plug_path]
            plug_info = mod.PLUGIN_INFO
            info ("importing plugin: %s", plug_info['name'])
            self.plugins[plug_info['import_name']] = getattr(mod, plug_info['main'])(self)
            #TODO: test xmppclient presence and register handler parent

    def connect(self):
        """Connect to jabber server"""

        if (self.isConnected()):
            info("already connected !")
            return
        print "connecting..."
        self.me = jid.JID(self.memory.getParamA("JabberID", "Connection"))
        self.xmppclient = SatXMPPClient(self.me, self.memory.getParamA("Password", "Connection"),
            self.memory.getParamA("Server", "Connection"), 5222)
        self.xmppclient.streamInitialized = self.streamInitialized

        self.messageProt = SatMessageProtocol(self)
        self.messageProt.setHandlerParent(self.xmppclient)
        
        self.roster = SatRosterProtocol(self)
        self.roster.setHandlerParent(self.xmppclient)

        self.presence = SatPresenceProtocol(self)
        self.presence.setHandlerParent(self.xmppclient)

        self.fallBack = SatFallbackHandler(self)
        self.fallBack.setHandlerParent(self.xmppclient)

        self.versionHandler = generic.VersionHandler(unicode(client_name), client_version)
        self.versionHandler.setHandlerParent(self.xmppclient)

        debug ("setting plugins parents")
        for plugin in self.plugins.iteritems():
            if isinstance(plugin[1], XMPPHandler):
                plugin[1].setHandlerParent(self.xmppclient)

        self.xmppclient.startService()
    
    def disconnect(self):
        """disconnect from jabber server"""
        if (not self.isConnected()):
            info("not connected !")
            return
        info("Disconnecting...")
        self.xmppclient.stopService()

    def startService(self):
        info("Salut à toi ô mon frère !")
        self.connect()
    
    def stopService(self):
        self.memory.save()
        info("Salut aussi à Rantanplan")

    def run(self):
        debug("running app")
        reactor.run()
    
    def stop(self):
        debug("stopping app")
        reactor.stop()
        
    def streamInitialized(self):
        """Called when xmlstream is OK"""
        SatXMPPClient.streamInitialized(self.xmppclient)
        debug ("XML stream is initialized")
        self.xmlstream = self.xmppclient.xmlstream
        self.me = self.xmppclient.jid #in case of the ressource has changed
        
        self.disco = SatDiscoProtocol(self)
        self.disco.setHandlerParent(self.xmppclient)
        self.discoHandler = disco.DiscoHandler()
        self.discoHandler.setHandlerParent(self.xmppclient)

        self.roster.requestRoster()
        
        self.presence.available()
        
        self.disco.requestInfo(jid.JID(self.memory.getParamA("Server", "Connection"))).addCallback(self.serverDisco)
    
   ## Misc methods ##
   
    def registerNewAccount(self, login, password, server, port = 5222, id = None):
        """Connect to a server and create a new account using in-band registration"""

        next_id = id or sat_next_id()  #the id is used to send server's answer
        serverRegistrer = xmlstream.XmlStreamFactory(RegisteringAuthenticator(self, server, login, password, next_id))
        connector = reactor.connectTCP(server, port, serverRegistrer)
        serverRegistrer.clientConnectionLost = lambda conn, reason: connector.disconnect()
        
        return next_id 

    def registerNewAccountCB(self, id, data):
        user = jid.parse(self.memory.getParamA("JabberID", "Connection"))[0]
        password = self.memory.getParamA("Password", "Connection")
        server = self.memory.getParamA("Server", "Connection")

        if not user or not password or not server:
            info ('No user or server given')
            #TODO: a proper error message must be sent to frontend
            self.actionResult(id, "ERROR", {'message':"No user, password or server given, can't register new account."})
            return

        confirm_id = sat_next_id()
        self.__private_data[confirm_id]=id
    
        self.askConfirmation(confirm_id, "YES/NO",
            {"message":"Are you sure to register new account [%s] to server %s ?" % (user, server)},
            self.regisConfirmCB)
        print ("===============+++++++++++ REGISTER NEW ACCOUNT++++++++++++++============")
        print "id=",id
        print "data=",data

    def regisConfirmCB(self, id, accepted, data):
        print "register Confirmation CB ! (%s)" % str(accepted)
        action_id = self.__private_data[id]
        del self.__private_data[id]
        user = jid.parse(self.memory.getParamA("JabberID", "Connection"))[0]
        password = self.memory.getParamA("Password", "Connection")
        server = self.memory.getParamA("Server", "Connection")
        if accepted:
            self.registerNewAccount(user, password, server, id=action_id)
        else:
            self.actionResult(action_id, "SUPPRESS", {})

    ## Client management ##

    def setParam(self, name, value, category):
        """set wanted paramater and notice observers"""
        info ("setting param: %s=%s in category %s", name, value, category)
        self.memory.setParam(name, value, category)

    def failed(self,xmlstream):
        debug("failed: %s", xmlstream.getErrorMessage())
        debug("failed: %s", dir(xmlstream))

    def isConnected(self):
        try:
            if self.xmppclient.isConnected():
                return True
        except AttributeError:
            #xmppclient not available
            pass
        return False

    def launchAction(self, type, data):
        """Launch a specific action asked by client
        @param type: action type (button)
        @data: needed data to launch the action

        @return: action id for result, or empty string in case or error
        """
        if type=="button":
            try:
                cb_name = self.memory.getParamA(data["name"], data["category"], "callback")
            except KeyError:
                error ("Incomplete data")
                return ""
            id = sat_next_id()
            self.callGeneralCB(cb_name, id, data)
            return id
        else:
            error ("Unknown action type")
            return ""


    ## jabber methods ##
    
    def sendMessage(self,to,msg,type='chat'):
        #FIXME: check validity of recipient
        debug("Sending jabber message to %s...", to)
        message = domish.Element(('jabber:client','message'))
        message["to"] = jid.JID(to).full()
        message["from"] = self.me.full()
        message["type"] = type
        message.addElement("body", "jabber:client", msg)
        self.xmlstream.send(message)
        self.memory.addToHistory(self.me, self.me, jid.JID(to), message["type"], unicode(msg))
        self.bridge.newMessage(message['from'], unicode(msg), to=message['to']) #We send back the message, so all clients are aware of it


    def setPresence(self, to="", type="", show="", status="", priority=0):
        """Send our presence information"""
        if not type in ["", "unavailable", "subscribed", "subscribe",
                        "unsubscribe", "unsubscribed", "prob", "error"]:
            error("Type error !")
            #TODO: throw an error
            return
        to_jid=jid.JID(to)
        #TODO: refactor subscription bridge API
        if type=="":
            self.presence.available(to_jid, show, status, priority)
        elif type=="subscribe":
            self.presence.subscribe(to_jid)
        elif type=="subscribed":
            self.presence.subscribed(to_jid)
        elif type=="unsubscribe":
            self.presence.unsubscribe(to_jid)
        elif type=="unsubscribed":
            self.presence.unsubscribed(to_jid)


    def addContact(self, to):
        """Add a contact in roster list"""
        to_jid=jid.JID(to)
        self.roster.addItem(to_jid.userhost())
        self.setPresence(to_jid.userhost(), "subscribe")

    def delContact(self, to):
        """Remove contact from roster list"""
        to_jid=jid.JID(to)
        self.roster.removeItem(to_jid.userhost())
        self.bridge.contactDeleted(to)


    ## callbacks ##

    def serverDisco(self, disco):
        """xep-0030 Discovery Protocol."""
        for feature in disco.features:
            debug ("Feature found: %s",feature)
            self.server_features.append(feature)
        for cat, type in disco.identities:
            debug ("Identity found: [%s/%s] %s" % (cat, type, disco.identities[(cat,type)]))

    
    ## Generic HMI ## 
    
    def actionResult(self, id, type, data):
        """Send the result of an action
        @param id: same id used with action
        @type: result type ("PARAM", "SUCCESS", "ERROR")
        @data: dictionary
        """
        self.bridge.actionResult(type, id, data)

    def actionResultExt(self, id, type, data):
        """Send the result of an action, extended version
        @param id: same id used with action
        @type: result type ("PARAM", "SUCCESS", "ERROR")
        @data: dictionary of dictionaries
        """
        if type != "DICT_DICT":
            error("type for actionResultExt must be DICT_DICT, fixing it")
            type = "DICT_DICT"
        self.bridge.actionResultExt(type, id, data)



    def askConfirmation(self, id, type, data, cb):
        """Add a confirmation callback
        @param id: id used to get answer
        @type: confirmation type ("YES/NO", "FILE_TRANSFERT")
        @data: data (depend of confirmation type)
        @cb: callback called with the answer
        """
        if self.__waiting_conf.has_key(id):
            error ("Attempt to register two callbacks for the same confirmation")
        else:
            self.__waiting_conf[id] = cb
            self.bridge.askConfirmation(type, id, data)


    def confirmationAnswer(self, id, accepted, data):
        """Called by frontends to answer confirmation requests"""
        debug ("Received confirmation answer for id [%s]: %s", id, "accepted" if accepted else "refused")
        if not self.__waiting_conf.has_key(id):
            error ("Received an unknown confirmation")
        else:
            cb = self.__waiting_conf[id]
            del self.__waiting_conf[id]
            cb(id, accepted, data)

    def registerProgressCB(self, id, CB):
        """Register a callback called when progress is requested for id"""
        self.__progress_cb_map[id] = CB

    def removeProgressCB(self, id):
        """Remove a progress callback"""
        if not self.__progress_cb_map.has_key(id):
            error ("Trying to remove an unknow progress callback")
        else:
            del self.__progress_cb_map[id]

    def getProgress(self, id):
        """Return a dict with progress information
        data['position'] : current possition
        data['size'] : end_position
        """
        data = {}
        try:
            self.__progress_cb_map[id](data)
        except KeyError:
            pass
            #debug("Requested progress for unknown id")
        return data

    def registerGeneralCB(self, name, CB):
        """Register a callback called for general reason"""
        self.__general_cb_map[name] = CB

    def removeGeneralCB(self, name):
        """Remove a general callback"""
        if not self.__general_cb_map.has_key(name):
            error ("Trying to remove an unknow general callback")
        else:
            del self.__general_cb_map[name]

    def callGeneralCB(self, name, *args, **kwargs):
        """Call general function back"""
        try:
            return self.__general_cb_map[name](*args, **kwargs)
        except KeyError:
            error("Trying to call unknown function")
            return None

application = service.Application('SàT')
service = SAT()
service.setServiceParent(application)