diff sat @ 0:c4bc297b82f0

sat: - first public release, initial commit
author goffi@necton2
date Sat, 29 Aug 2009 13:34:59 +0200
parents
children a06a151fc31f
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/sat	Sat Aug 29 13:34:59 2009 +0200
@@ -0,0 +1,367 @@
+#!/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.internet import glib2reactor, protocol
+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("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
+        info("Connecting...")
+        self.me = jid.JID(self.memory.getParamV("JabberID", "Connection"))
+        self.factory = client.XMPPClientFactory(self.me, self.memory.getParamV("Password", "Connection"))
+        self.factory.addBootstrap(xmlstream.STREAM_AUTHD_EVENT,self.authd)
+        self.factory.addBootstrap(xmlstream.INIT_FAILED_EVENT,self.failed)
+        reactor.connectTCP(self.memory.getParamV("Server", "Connection"), 5222, self.factory)
+        self.connectionStatus="online"  #TODO: check if connection is OK
+        self.connected=True         #TODO: use startedConnecting  and clientConnectionLost of XMPPClientFactory
+
+
+    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)
+      #reactor.callLater(2,self.sendFile,"goffi2@jabber.goffi.int/Psi", "/tmp/fakefile")
+
+    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
+
+
+app=SAT()  #TODO: tmp, use twisted way instead
+app.run()
+
+app.memory.save()  #FIXME: not the best place 
+debug("Good Bye")