view src/core/ @ 438:62145e50eae5

core: plugins can now have profileConnected/profileDisconnected method to initialise/free profile dependant resources
author Goffi <>
date Sat, 03 Dec 2011 15:45:48 +0100
parents 37285f2d37c8
children cf005701624b
line wrap: on
line source

# -*- coding: utf-8 -*-

SAT: a jabber client
Copyright (C) 2009, 2010, 2011  Jérôme Poisson (

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
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 <>.

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.profile = profile
        self.host_app = host_app
        self.client_initialized = defer.Deferred()
        self.conn_deferred = defer.Deferred()

    def getConnectionDeferred(self):
        """Return a deferred which fire when the client is connected"""
        return self.conn_deferred

    def _authd(self, xmlstream):
        if not self.host_app.trigger.point("XML Initialized", xmlstream, self.profile):
        client.XMPPClient._authd(self, xmlstream)
        info (_("********** [%s] CONNECTED **********") % self.profile)
        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.disco = SatDiscoProtocol(self)
        self.discoHandler = disco.DiscoHandler()
        if not self.host_app.trigger.point("Disco Handled", self.profile):

        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)
            client.XMPPClient.initializationFailed(self, reason)
            #we already send an error signal, no need to raise an exception

    def isConnected(self):
        return self.__connected
    def connectionLost(self, connector, unused_reason):
        info (_("********** [%s] DISCONNECTED **********") % self.profile)
        except AttributeError:
            debug (_("No keep_alife"))
        self.host_app.bridge.disconnected(self.profile) #we send the signal to the clients
        self.host_app.purgeClient(self.profile) #and we remove references to this client

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

    def onMessage(self, message):
      debug (_(u"got message from: %s"), message["from"])
      if not"MessageReceived",message, profile=self.parent.profile):
      for e in message.elements():
          if == "body":
              mess_type = message['type'] if message.hasAttribute('type') else 'normal'
              mess_body = e.children[0] if e.children else ""
    ["from"], mess_body, mess_type, message['to'], profile=self.parent.profile)
              if not u"delay" in [ for elem in message.elements()]: #we don't save delayed messages in history
        ["from"]), jid.JID(message["to"]), mess_body, profile=self.parent.profile)
class SatRosterProtocol(xmppim.RosterClientProtocol):

    def __init__(self, host):
        xmppim.RosterClientProtocol.__init__(self) = host
    def rosterCb(self, roster):
        for raw_jid, item in roster.iteritems():

    def requestRoster(self):
        """ ask the server for Roster list """

    def removeItem(self, to):
        """Remove a contact from roster list"""
        xmppim.RosterClientProtocol.removeItem(self, to)
        #TODO: check IQ result
    #XXX: disabled (cf
    #def addItem(self, to):
        #"""Add a contact to roster list"""
        #xmppim.RosterClientProtocol.addItem(self, to)
        #TODO: check IQ result"""

    def updateItem(self, roster_item):
        Update an item of the contact list.

        @param roster_item: item to update
        iq = compat.IQ(self.xmlstream, 'set')
        iq.addElement((xmppim.NS_ROSTER, 'query'))
        item = iq.query.addElement('item')
        item['jid'] = roster_item.jid.userhost()
            item['name'] =
        for group in roster_item.groups:
            item.addElement('group', content=group)
        return iq.send()
    def onRosterSet(self, item):
        """Called when a new/update roster item is received"""
        #TODO: send a signal to frontends
        if not item.subscriptionTo and not item.subscriptionFrom and not item.ask:
            #XXX: current behaviour: we don't want contact in our roster list
            #if there is no presence subscription
            #may change in the future
        item_attr = {'to': str(item.subscriptionTo),
                     'from': str(item.subscriptionFrom),
                     'ask': str(item.ask)
            item_attr['name'] =
        info (_("new contact in roster list: %s"), item.jid.full()), item_attr, item.groups, self.parent.profile), item_attr, item.groups, self.parent.profile)
    def onRosterRemove(self, entity):
        """Called when a roster removal event is received"""
        print _("removing %s from roster list") % entity.full(), self.parent.profile), 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) = 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 not statuses:
            statuses = {}
        if statuses.has_key(None):   #we only want string keys
            statuses["default"] = statuses[None]
            del statuses[None], show or "",
                int(priority), statuses, self.parent.profile)

        #now it's time to notify frontends,  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 not statuses:
            statuses = {}
        if statuses.has_key(None):   #we only want string keys
            statuses["default"] = statuses[None]
            del statuses[None], "unavailable", 0, statuses, self.parent.profile)

        #now it's time to notify frontends, "unavailable", 0, statuses, self.parent.profile)
    def available(self, entity=None, show=None, statuses=None, priority=0):
	if not statuses:
	    statuses = {}
        # default for us is None for wokkel
        # so we must temporarily switch to wokkel's convention...	
        if 'default' in statuses:
            statuses[None] = statuses['default']

        xmppim.PresenceClientProtocol.available(self, entity, show, statuses, priority)

	# ... before switching back
	if None in statuses:
	    del statuses[None]

    def subscribed(self, entity):
        xmppim.PresenceClientProtocol.subscribed(self, entity), self.parent.profile)
        contact =, self.parent.profile) 
        if not contact or contact[0]['to'] == 'False': #we automatically subscribe to 'to' presence
            debug(_('sending automatic "from" subscription request'))

    def unsubscribed(self, entity):
        xmppim.PresenceClientProtocol.unsubscribed(self, entity), self.parent.profile)

    def subscribedReceived(self, entity):
        debug (_("subscription approved for [%s]") % entity.userhost())'subscribed', entity.userhost(), self.parent.profile)

    def unsubscribedReceived(self, entity):
        debug (_("unsubscription confirmed for [%s]") % entity.userhost())'unsubscribed', entity.userhost(), self.parent.profile)

    def subscribeReceived(self, entity):
        debug (_("subscription request from [%s]") % entity.userhost())
        contact =, self.parent.profile) 
        if contact and contact[0]['to'] == 'True':
            #We automatically accept subscription if we are already subscribed to contact presence
            debug(_('sending automatic subscription acceptance'))
  'subscribe', entity.userhost(), self.parent.profile)
  'subscribe', entity.userhost(), self.parent.profile)

    def unsubscribeReceived(self, entity):
        debug (_("unsubscription asked for [%s]") % entity.userhost())
        contact =, self.parent.profile) 
        if contact and contact[0]['from'] == 'True': #we automatically remove contact
            debug(_('automatic contact deletion'))
  , self.parent.profile)'unsubscribe', entity.userhost(), self.parent.profile)

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

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

    def iqFallback(self, iq):
        if iq.handled == True:
        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, email, answer_id):
        xmlstream.ConnectAuthenticator.__init__(self, jabber_host) = host
        self.jabber_host = jabber_host
        self.user_login = user_login
        self.user_pass = user_pass
        self.user_email = email
        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"

        iq = compat.IQ(self.xmlstream, 'set')
        iq["to"] = self.jabber_host
        query = iq.addElement(('jabber:iq:register', 'query'))
        _user = query.addElement('username')
        _pass = query.addElement('password')
        if self.user_email:
            _email = query.addElement('email')
        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.answer_id, answer_data)
    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")}
            answer_data['reason'] = 'unknown'
            answer_data={"message":_("Registration failed (%s)") % str(failure.value.condition)}, self.answer_id, answer_data)

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)