diff src/core/xmpp.py @ 330:608a4a2ba94e

Core: created a new core module where xmpp classes are put
author Goffi <goffi@goffi.org>
date Mon, 23 May 2011 21:18:58 +0200
parents src/sat.tac@b069055320b1
children 4c835d614bdb
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/core/xmpp.py	Mon May 23 21:18:58 2011 +0200
@@ -0,0 +1,284 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+"""
+SAT: a jabber client
+Copyright (C) 2009, 2010, 2011  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 task, defer
+from twisted.words.protocols.jabber import jid, xmlstream
+from wokkel import client, disco, xmppim, generic, compat
+from logging import debug, info, error
+
+class SatXMPPClient(client.XMPPClient):
+    
+    def __init__(self, host_app, profile, user_jid, password, host=None, port=5222):
+        client.XMPPClient.__init__(self, user_jid, password, host, port)
+        self.factory.clientConnectionLost = self.connectionLost
+        self.__connected=False
+        self.profile = profile
+        self.host_app = host_app
+        self.client_initialized = defer.Deferred()
+
+    def _authd(self, xmlstream):
+        if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
+            return
+        client.XMPPClient._authd(self, xmlstream)
+        self.__connected=True
+        info (_("********** [%s] CONNECTED **********") % self.profile)
+        self.streamInitialized()
+        self.host_app.bridge.connected(self.profile) #we send the signal to the clients
+   
+
+    def streamInitialized(self):
+        """Called after _authd"""
+        debug (_("XML stream is initialized"))
+        self.keep_alife = task.LoopingCall(self.xmlstream.send, " ")  #Needed to avoid disconnection (specially with openfire)
+        self.keep_alife.start(180)
+        
+        self.disco = SatDiscoProtocol(self)
+        self.disco.setHandlerParent(self)
+        self.discoHandler = disco.DiscoHandler()
+        self.discoHandler.setHandlerParent(self)
+        
+        if not self.host_app.trigger.point("Disco Handled", self.profile):
+            return
+
+        self.roster.requestRoster()
+        
+        self.presence.available()
+       
+        self.disco.requestInfo(jid.JID(self.host_app.memory.getParamA("Server", "Connection", profile_key=self.profile))).addCallback(self.host_app.serverDisco, self.profile)  #FIXME: use these informations
+        self.disco.requestItems(jid.JID(self.host_app.memory.getParamA("Server", "Connection", profile_key=self.profile))).addCallback(self.host_app.serverDiscoItems, self.disco, self.profile, self.client_initialized)
+
+    def initializationFailed(self, reason):
+        print ("initializationFailed: %s" % reason)
+        self.host_app.bridge.connectionError("AUTH_ERROR", self.profile)
+        try:
+            client.XMPPClient.initializationFailed(self, reason)
+        except:
+            #we already send an error signal, no need to raise an exception
+            pass
+
+    def isConnected(self):
+        return self.__connected
+    
+    def connectionLost(self, connector, unused_reason):
+        self.__connected=False
+        info (_("********** [%s] DISCONNECTED **********") % self.profile)
+        try:
+            self.keep_alife.stop()
+        except AttributeError:
+            debug (_("No keep_alife"))
+        self.host_app.bridge.disconnected(self.profile) #we send the signal to the clients
+
+
+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"])
+      if not self.host.trigger.point("MessageReceived",message, profile=self.parent.profile):
+        return
+      for e in message.elements():
+        if e.name == "body":
+          mess_type = message['type'] if message.hasAttribute('type') else 'normal'
+          self.host.bridge.newMessage(message["from"], e.children[0], mess_type, message['to'], profile=self.parent.profile)
+          self.host.memory.addToHistory(self.parent.jid, jid.JID(message["from"]), self.parent.jid, "chat", e.children[0])
+          break
+    
+class SatRosterProtocol(xmppim.RosterClientProtocol):
+
+    def __init__(self, host):
+        xmppim.RosterClientProtocol.__init__(self)
+        self.host = host
+        self._groups=set()
+    
+    def rosterCb(self, roster):
+        for raw_jid, item in roster.iteritems():
+            self.onRosterSet(item)
+
+    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"""
+        xmppim.RosterClientProtocol.removeItem(self, to)
+        #TODO: check IQ result
+    
+    #XXX: disabled (cf http://wokkel.ik.nu/ticket/56))
+    #def addItem(self, to):
+        #"""Add a contact to roster list"""
+        #xmppim.RosterClientProtocol.addItem(self, to)
+        #TODO: check IQ result"""
+
+    def onRosterSet(self, item):
+        """Called when a new/update roster item is received"""
+        #TODO: send a signal to frontends
+        item_attr = {'to': str(item.subscriptionTo),
+                     'from': str(item.subscriptionFrom),
+                     'ask': str(item.ask)
+                     }
+        if item.name:
+            item_attr['name'] = item.name
+        info (_("new contact in roster list: %s"), item.jid.full())
+        self.host.memory.addContact(item.jid, item_attr, item.groups, self.parent.profile)
+        self.host.bridge.newContact(item.jid.full(), item_attr, item.groups, self.parent.profile)
+        self._groups.update(item.groups) 
+    
+    def onRosterRemove(self, entity):
+        """Called when a roster removal event is received"""
+        #TODO: send a signal to frontends
+        print _("removing %s from roster list") % entity.full()
+        self.host.memory.delContact(entity, self.parent.profile)
+
+    def getGroups(self):
+        """Return a set of groups"""
+        return self._groups
+
+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):
+        debug (_("presence update for [%(entity)s] (available, show=%(show)s statuses=%(statuses)s priority=%(priority)d)") % {'entity':entity, 'show':show, 'statuses':statuses, 'priority':priority})
+        
+        if statuses.has_key(None):   #we only want string keys
+            statuses["default"] = statuses[None]
+            del statuses[None]
+
+        self.host.memory.addPresenceStatus(entity, show or "",
+                int(priority), statuses, self.parent.profile)
+
+        #now it's time to notify frontends
+        self.host.bridge.presenceUpdate(entity.full(),  show or "",
+                int(priority), statuses, self.parent.profile)
+    
+    def unavailableReceived(self, entity, statuses=None):
+        debug (_("presence update for [%(entity)s] (unavailable, statuses=%(statuses)s)") % {'entity':entity, 'statuses':statuses})
+        if statuses and statuses.has_key(None):   #we only want string keys
+            statuses["default"] = statuses[None]
+            del statuses[None]
+        self.host.memory.addPresenceStatus(entity, "unavailable", 0, statuses, self.parent.profile)
+
+        #now it's time to notify frontends
+        self.host.bridge.presenceUpdate(entity.full(), "unavailable", 0, statuses, self.parent.profile)
+        
+    
+    def available(self, entity=None, show=None, statuses=None, priority=0):
+        if statuses and statuses.has_key('default'):
+            statuses[None] = statuses['default']
+            del statuses['default']
+        xmppim.PresenceClientProtocol.available(self, entity, show, statuses, priority)
+    
+    def subscribedReceived(self, entity):
+        debug (_("subscription approved for [%s]") % entity.userhost())
+        self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile)
+        self.host.bridge.subscribe('subscribed', entity.userhost(), self.parent.profile)
+
+    def unsubscribedReceived(self, entity):
+        debug (_("unsubscription confirmed for [%s]") % entity.userhost())
+        self.host.memory.delWaitingSub(entity.userhost(), self.parent.profile)
+        self.host.bridge.subscribe('unsubscribed', entity.userhost(), self.parent.profile)
+
+    def subscribeReceived(self, entity):
+        debug (_("subscription request for [%s]") % entity.userhost())
+        self.host.memory.addWaitingSub('subscribe', entity.userhost(), self.parent.profile)
+        self.host.bridge.subscribe('subscribe', entity.userhost(), self.parent.profile)
+
+    def unsubscribeReceived(self, entity):
+        debug (_("unsubscription asked for [%s]") % entity.userhost())
+        self.host.memory.addWaitingSub('unsubscribe', entity.userhost(), self.parent.profile)
+        self.host.bridge.subscribe('unsubscribe', entity.userhost(), self.parent.profile)
+
+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):
+        if iq.handled == True:
+            return
+        debug (u"iqFallback: xml = [%s]" % (iq.toXml()))
+        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 (%s)") % str(failure.value.condition)}
+        self.host.bridge.actionResult(answer_type, self.answer_id, answer_data)
+        self.xmlstream.sendFooter()
+
+class SatVersionHandler(generic.VersionHandler):
+
+    def getDiscoInfo(self, requestor, target, node):
+        #XXX: We need to work around wokkel's behavious (namespace not added if there is a
+        # node) as it cause issues with XEP-0115 & PEP (XEP-0163): there is a node when server
+        # ask for disco info, and not when we generate the key, so the hash is used with different
+        # disco features, and when the server (seen on ejabberd) generate its own hash for security check
+        # it reject our features (resulting in e.g. no notification on PEP)
+        return generic.VersionHandler.getDiscoInfo(self, requestor, target, None)
+