view sat.tac @ 12:ef8060d365cb

whitespace ping to avoid disconnection (was disconnected after 6 min of inactivity with openfire)
author Goffi <goffi@goffi.org>
date Wed, 28 Oct 2009 00:39:29 +0100
parents 5799493fa548
children bd9e9997d540
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/>.
"""


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

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

from twisted.internet import reactor
import pdb

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


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




class SAT:
    
    def __init__(self):
        #self.reactor=reactor
        self.memory=Memory()
        self.server_features=[]  #XXX: temp dic, need to be transfered into self.memory in the future
        self.connected=False #FIXME: use twisted var instead

        self._iq_cb_map = {}  #callback called when ns is found on IQ
        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.plugins = {}

        self.bridge=DBusBridge()
        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("getParam", self.memory.getParam)
        self.bridge.register("getParams", self.memory.getParams)
        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("confirmationAnswer", self.confirmationAnswer)
        self.bridge.register("getProgress", self.getProgress)

        self._import_plugins()
        #self.connect()


    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)

    def connect(self):
        if (self.connected):
            info("already connected !")
            return
        print "connecting..."
        reactor.connectTCP(self.memory.getParamV("Server", "Connection"), 5222, self.factory)
    
    def disconnect(self):
        if (not self.connected):
            info("not connected !")
            return
        info("Disconnecting...")
        self.factory.stopTrying()
        if self.xmlstream:
            self.xmlstream.sendFooter()

    def getService(self):
        print "GetService !"
        """if (self.connected):
            info("already connected !")
            return"""
        info("Getting service...")
        self.me = jid.JID(self.memory.getParamV("JabberID", "Connection"))
        self.factory = client.XMPPClientFactory(self.me, self.memory.getParamV("Password", "Connection"))
        self.factory.clientConnectionLost = self.connectionLost
        self.factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,self.authd)
        self.factory.addBootstrap(xmlstream.INIT_FAILED_EVENT,self.failed)
        return internet.TCPClient(self.memory.getParamV("Server", "Connection"), 5222, self.factory)

    def run(self):
        debug("running app")
        reactor.run()
    
    def stop(self):
        debug("stopping app")
        reactor.stop()
        
    def authd(self,xmlstream):
        self.xmlstream=xmlstream
        roster=client.IQ(xmlstream,'get')
        roster.addElement(('jabber:iq:roster', 'query'))
        roster.addCallback(self.rosterCb)
        roster.send()
        debug("server = %s",self.memory.getParamV("Server", "Connection"))

        ###FIXME: tmp disco ###
        self.memory.registerFeature("http://jabber.org/protocol/disco#info")
        self.disco(self.memory.getParamV("Server", "Connection"), self.serverDisco)


        #we now send our presence status
        self.setPresence(status="Online")

        # add a callback for the messages
        xmlstream.addObserver('/message',  self.gotMessage)
        xmlstream.addObserver('/presence', self.presenceCb)
        xmlstream.addObserver("/iq[@type='set' or @type='get']", self.iqCb)
        print "********** CONNECTED **********"
        self.connected=True
        self.keep_alife = task.LoopingCall(self.xmlstream.send, " ")  #Needed to avoid disconnection (specially with openfire)
        self.keep_alife.start(180)

        #reactor.callLater(2,self.sendFile,"goffi2@jabber.goffi.int/Psi", "/tmp/fakefile")

    def connectionLost(self, connector, unused_reason):
        print "********** DISCONNECTED **********"
        if self.keep_alife:
            self.keep_alife.stop()
        self.connected=False      
        

    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 setParam(self, name, value, namespace):
        """set wanted paramater and notice observers"""
        info ("setting param: %s=%s in namespace %s", name, value, namespace)
        self.memory.setParam(name, value, namespace)
        self.bridge.paramUpdate(name, value, namespace)

    def setRoster(self, to):
        """Add a contact to roster list"""
        to_jid=jid.JID(to)
        roster=client.IQ(self.xmlstream,'set')
        query=roster.addElement(('jabber:iq:roster', 'query'))
        item=query.addElement("item")
        item.attributes["jid"]=to_jid.userhost()
        roster.send()
        #TODO: check IQ result
    
    def delRoster(self, to):
        """Remove a contact from roster list"""
        to_jid=jid.JID(to)
        roster=client.IQ(self.xmlstream,'set')
        query=roster.addElement(('jabber:iq:roster', 'query'))
        item=query.addElement("item")
        item.attributes["jid"]=to_jid.userhost()
        item.attributes["subscription"]="remove"
        roster.send()
        #TODO: check IQ result
        

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

    def isConnected(self):
        return self.connected

    ## jabber methods ##

    def disco (self, item, callback, node=None):
        """XEP-0030 Service discovery Feature."""
        disco=client.IQ(self.xmlstream,'get')
        disco["from"]=self.me.full()
        disco["to"]=item
        disco.addElement(('http://jabber.org/protocol/disco#info', 'query'))
        disco.addCallback(callback)
        disco.send()


    def setPresence(self, to="", type="", show="", status="", priority=0):
        """Send our presence information"""
        presence = domish.Element(('jabber:client', 'presence'))
        if not type in ["", "unavailable", "subscribed", "subscribe",
                        "unsubscribe", "unsubscribed", "prob", "error"]:
            error("Type error !")
            #TODO: throw an error
            return

        if to:
            presence.attributes["to"]=to
        if type:
            presence.attributes["type"]=type

        for element in ["show", "status", "priority"]:
            if locals()[element]:
                presence.addElement(element).addContent(unicode(locals()[element]))
        
        self.xmlstream.send(presence)

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

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

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

    ## callbacks ##

    def add_IQ_cb(self, ns, cb):
        """Add an IQ callback on namespace ns"""
        debug ("Registered callback for namespace %s", ns)
        self._iq_cb_map[ns]=cb

    def iqCb(self, stanza):
        info ("iqCb")
        debug ("="*20)
        debug ("DEBUG:\n")
        debug (stanza.toXml().encode('utf-8'))
        debug ("="*20)
        #FIXME: temporary ugly code
        uri = stanza.firstChildElement().uri
        if self._iq_cb_map.has_key(uri):
            self._iq_cb_map[uri](stanza)
        #TODO: manage errors stanza

    def presenceCb(self, elem):
        info ("presence update for [%s]", elem.getAttribute("from"))
        debug("\n\nXML=\n%s\n\n", elem.toXml())
        presence={}
        presence["jid"]=elem.getAttribute("from")
        presence["type"]=elem.getAttribute("type") or ""
        presence["show"]=""
        presence["status"]=""
        presence["priority"]=0
        
        for item in elem.elements():
            if presence.has_key(item.name):
                presence[item.name]=item.children[0]

        ### we check if the status is not about subscription ###
        #TODO: check that from jid is one we wants to subscribe (ie: check a recent subscription asking)
        if jid.JID(presence["jid"]).userhost()!=self.me.userhost():
            if presence["type"]=="subscribed":
                debug ("subscription answer")
            elif presence["type"]=="unsubscribed":
                debug ("unsubscription answer")
            elif presence["type"]=="subscribe":
                #FIXME: auto answer for subscribe request, must be checked !
                debug ("subscription request")
                self.setPresence(to=presence["jid"], type="subscribed")
            else:
                #We keep presence information only if it is not for subscription
                self.memory.addPresenceStatus(presence["jid"], presence["type"], presence["show"],
                                          presence["status"], int(presence["priority"]))

            #now it's time to notify frontends
            self.bridge.presenceUpdate(presence["jid"], presence["type"], presence["show"],
                                          presence["status"], int(presence["priority"]))

    def rosterCb(self,roster):
        for contact in roster.firstChildElement().elements():
            info ("new contact in roster list: %s", contact['jid'])
            #and now the groups
            groups=[]
            for group in contact.elements():
                if group.name!="group":
                    error("Unexpected element !")
                    break
                groups.append(str(group))
            self.memory.addContact(contact['jid'], contact.attributes, groups)
            self.bridge.newContact(contact['jid'], contact.attributes, groups)

    def serverDisco(self, disco):
        """xep-0030 Discovery Protocol."""
        for element in disco.firstChildElement().elements():
            if element.name=="feature":
                debug ("Feature dectetee: %s",element["var"])
                self.server_features.append(element["var"])
            elif element.name=="identity":
                debug ("categorie= %s",element["category"])
        debug ("features= %s",self.server_features)

    ## Generic HMI ## 

    def askConfirmation(self, id, type, data, cb):
        """Add a confirmation callback"""
        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


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


#app.memory.save()  #FIXME: not the best place 
#debug("Good Bye")