diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index afb7d9d..933f661 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -8,7 +8,7 @@ See the file LICENSE for copying permission. """ from __future__ import absolute_import, unicode_literals -from . basexmpp import basexmpp +from . basexmpp import BaseXMPP, basexmpp from xml.etree import cElementTree as ET from . xmlstream.xmlstream import XMLStream from . xmlstream.xmlstream import RestartStream @@ -39,17 +39,17 @@ except ImportError: #class PresenceStanzaType(object): # # def fromXML(self, xml): + # self.ptype = xml.get('type') -class ClientXMPP(basexmpp, XMLStream): +class ClientXMPP(BaseXMPP): """SleekXMPP's client class. Use only for good, not evil.""" def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], escape_quotes=True): + BaseXMPP.__init__(self, 'jabber:client') global srvsupport - XMLStream.__init__(self) self.default_ns = 'jabber:client' - basexmpp.__init__(self) self.plugin_config = plugin_config self.escape_quotes = escape_quotes self.set_jid(jid) @@ -80,16 +80,6 @@ class ClientXMPP(basexmpp, XMLStream): #self.registerStanzaExtension('PresenceStanza', PresenceStanzaType) #self.register_plugins() - def __getitem__(self, key): - if key in self.plugin: - return self.plugin[key] - else: - logging.warning("""Plugin "%s" is not loaded.""" % key) - return False - - def get(self, key, default): - return self.plugin.get(key, default) - def connect(self, address=tuple()): """Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses the JID server.""" diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 927e1fc..edb20d0 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -5,238 +5,558 @@ See the file LICENSE for copying permission. """ + from __future__ import with_statement, unicode_literals - -from xml.etree import cElementTree as ET -from . xmlstream.xmlstream import XMLStream -from . xmlstream.matcher.xmlmask import MatchXMLMask -from . xmlstream.matcher.many import MatchMany -from . xmlstream.handler.xmlcallback import XMLCallback -from . xmlstream.handler.xmlwaiter import XMLWaiter -from . xmlstream.handler.waiter import Waiter -from . xmlstream.handler.callback import Callback -from . xmlstream.stanzabase import registerStanzaPlugin -from . import plugins -from . stanza.message import Message -from . stanza.iq import Iq -from . stanza.presence import Presence -from . stanza.roster import Roster -from . stanza.nick import Nick -from . stanza.htmlim import HTMLIM -from . stanza.error import Error -from sleekxmpp.xmlstream.tostring import tostring - -import logging -import threading -import copy - import sys +import copy +import logging -if sys.version_info < (3,0): - reload(sys) - sys.setdefaultencoding('utf8') +import sleekxmpp +from sleekxmpp import plugins -class basexmpp(object): - def __init__(self): - self.sentpresence = False - self.fulljid = '' - self.resource = '' - self.jid = '' - self.username = '' - self.server = '' - self.plugin = {} - self.auto_authorize = True - self.auto_subscribe = True - self.roster = {} - self.registerHandler(Callback('IM', MatchXMLMask("" % self.default_ns), self._handleMessage)) - self.registerHandler(Callback('Presence', MatchXMLMask("" % self.default_ns), self._handlePresence)) - self.add_event_handler('presence_subscribe', self._handlePresenceSubscribe) - self.registerStanza(Message) - self.registerStanza(Iq) - self.registerStanza(Presence) - registerStanzaPlugin(Iq, Roster) - registerStanzaPlugin(Message, Nick) - registerStanzaPlugin(Message, HTMLIM) +from sleekxmpp.stanza import Message, Presence, Iq, Error +from sleekxmpp.stanza.roster import Roster +from sleekxmpp.stanza.nick import Nick +from sleekxmpp.stanza.htmlim import HTMLIM - def Message(self, *args, **kwargs): - return Message(self, *args, **kwargs) +from sleekxmpp.xmlstream import XMLStream, JID, tostring +from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin +from sleekxmpp.xmlstream.matcher import * +from sleekxmpp.xmlstream.handler import * - def Iq(self, *args, **kwargs): - return Iq(self, *args, **kwargs) - def Presence(self, *args, **kwargs): - return Presence(self, *args, **kwargs) +# In order to make sure that Unicode is handled properly +# in Python 2.x, reset the default encoding. +if sys.version_info < (3, 0): + reload(sys) + sys.setdefaultencoding('utf8') - def set_jid(self, jid): - """Rip a JID apart and claim it as our own.""" - self.fulljid = jid - self.resource = self.getjidresource(jid) - self.jid = self.getjidbare(jid) - self.username = jid.split('@', 1)[0] - self.server = jid.split('@',1)[-1].split('/', 1)[0] - def process(self, *args, **kwargs): - for idx in self.plugin: - if not self.plugin[idx].post_inited: self.plugin[idx].post_init() - return super(basexmpp, self).process(*args, **kwargs) +class BaseXMPP(XMLStream): - def registerPlugin(self, plugin, pconfig = {}): - """Register a plugin not in plugins.__init__.__all__ but in the plugins - directory.""" - # discover relative "path" to the plugins module from the main app, and import it. - # TODO: - # gross, this probably isn't necessary anymore, especially for an installed module - __import__("%s.%s" % (globals()['plugins'].__name__, plugin)) - # init the plugin class - self.plugin[plugin] = getattr(getattr(plugins, plugin), plugin)(self, pconfig) # eek - # all of this for a nice debug? sure. - xep = '' - if hasattr(self.plugin[plugin], 'xep'): - xep = "(XEP-%s) " % self.plugin[plugin].xep - logging.debug("Loaded Plugin %s%s" % (xep, self.plugin[plugin].description)) + """ + The BaseXMPP class adapts the generic XMLStream class for use + with XMPP. It also provides a plugin mechanism to easily extend + and add support for new XMPP features. - def register_plugins(self): - """Initiates all plugins in the plugins/__init__.__all__""" - if self.plugin_whitelist: - plugin_list = self.plugin_whitelist - else: - plugin_list = plugins.__all__ - for plugin in plugin_list: - if plugin in plugins.__all__: - self.registerPlugin(plugin, self.plugin_config.get(plugin, {}), False) - else: - raise NameError("No plugin by the name of %s listed in plugins.__all__." % plugin) - # run post_init() for cross-plugin interaction - for plugin in self.plugin: - self.plugin[plugin].post_init() + Attributes: + auto_authorize -- Manage automatically accepting roster + subscriptions. + auto_subscribe -- Manage automatically requesting mutual + subscriptions. + is_component -- Indicates if this stream is for an XMPP component. + jid -- The XMPP JID for this stream. + plugin -- A dictionary of loaded plugins. + plugin_config -- A dictionary of plugin configurations. + plugin_whitelist -- A list of approved plugins. + sentpresence -- Indicates if an initial presence has been sent. + roster -- A dictionary containing subscribed JIDs and + their presence statuses. - def makeIq(self, id=0, ifrom=None): - return self.Iq().setStanzaValues({'id': str(id), 'from': ifrom}) + Methods: + Iq -- Factory for creating an Iq stanzas. + Message -- Factory for creating Message stanzas. + Presence -- Factory for creating Presence stanzas. + get -- Return a plugin given its name. + make_iq -- Create and initialize an Iq stanza. + make_iq_error -- Create an Iq stanza of type 'error'. + make_iq_get -- Create an Iq stanza of type 'get'. + make_iq_query -- Create an Iq stanza with a given query. + make_iq_result -- Create an Iq stanza of type 'result'. + make_iq_set -- Create an Iq stanza of type 'set'. + make_message -- Create and initialize a Message stanza. + make_presence -- Create and initialize a Presence stanza. + make_query_roster -- Create a roster query. + process -- Overrides XMLStream.process. + register_plugin -- Load and configure a plugin. + register_plugins -- Load and configure multiple plugins. + send_message -- Create and send a Message stanza. + send_presence -- Create and send a Presence stanza. + send_presence_subscribe -- Send a subscription request. + """ - def makeIqGet(self, queryxmlns = None): - iq = self.Iq().setStanzaValues({'type': 'get'}) - if queryxmlns: - iq.append(ET.Element("{%s}query" % queryxmlns)) - return iq + def __init__(self, default_ns='jabber:client'): + """ + Adapt an XML stream for use with XMPP. - def makeIqResult(self, id): - return self.Iq().setStanzaValues({'id': id, 'type': 'result'}) + Arguments: + default_ns -- Ensure that the correct default XML namespace + is used during initialization. + """ + XMLStream.__init__(self) - def makeIqSet(self, sub=None): - iq = self.Iq().setStanzaValues({'type': 'set'}) - if sub != None: - iq.append(sub) - return iq + # To comply with PEP8, method names now use underscores. + # Deprecated method names are re-mapped for backwards compatibility. + self.registerPlugin = self.register_plugin + self.makeIq = self.make_iq + self.makeIqGet = self.make_iq_get + self.makeIqResult = self.make_iq_result + self.makeIqSet = self.make_iq_set + self.makeIqError = self.make_iq_error + self.makeIqQuery = self.make_iq_query + self.makeQueryRoster = self.make_query_roster + self.makeMessage = self.make_message + self.makePresence = self.make_presence + self.sendMessage = self.send_message + self.sendPresence = self.send_presence + self.sendPresenceSubscription = self.send_presence_subscription - def makeIqError(self, id, type='cancel', condition='feature-not-implemented', text=None): - iq = self.Iq().setStanzaValues({'id': id}) - iq['error'].setStanzaValues({'type': type, 'condition': condition, 'text': text}) - return iq + self.default_ns = default_ns - def makeIqQuery(self, iq, xmlns): - query = ET.Element("{%s}query" % xmlns) - iq.append(query) - return iq + self.jid = '' + self.fulljid = '' + self.resource = '' + self.jid = '' + self.username = '' + self.server = '' - def makeQueryRoster(self, iq=None): - query = ET.Element("{jabber:iq:roster}query") - if iq: - iq.append(query) - return query + self.plugin = {} + self.roster = {} + self.is_component = False + self.auto_authorize = True + self.auto_subscribe = True - def makeMessage(self, mto, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): - message = self.Message(sto=mto, stype=mtype, sfrom=mfrom) - message['body'] = mbody - message['subject'] = msubject - if mnick is not None: message['nick'] = mnick - if mhtml is not None: message['html']['html'] = mhtml - return message + self.sentpresence = False - def makePresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, ptype=None, pfrom=None): - presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto) - if pshow is not None: presence['type'] = pshow - if pfrom is None: #maybe this should be done in stanzabase - presence['from'] = self.fulljid - presence['priority'] = ppriority - presence['status'] = pstatus - return presence + logging.warning(self.default_ns) + self.register_handler( + Callback('IM', + MatchXPath('{%s}message/{%s}body' % (self.default_ns, + self.default_ns)), + self._handle_message)) + self.register_handler( + Callback('Presence', + MatchXPath("{%s}presence" % self.default_ns), + self._handle_presence)) - def sendMessage(self, mto, mbody, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): - self.send(self.makeMessage(mto,mbody,msubject,mtype,mhtml,mfrom,mnick)) + self.add_event_handler('presence_subscribe', + self._handle_subscribe) - def sendPresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, pfrom=None, ptype=None): - self.send(self.makePresence(pshow,pstatus,ppriority,pto, ptype=ptype, pfrom=pfrom)) - if not self.sentpresence: - self.event('sent_presence') - self.sentpresence = True + # Set up the XML stream with XMPP's root stanzas. + self.registerStanza(Message) + self.registerStanza(Iq) + self.registerStanza(Presence) - def sendPresenceSubscription(self, pto, pfrom=None, ptype='subscribe', pnick=None) : - presence = self.makePresence(ptype=ptype, pfrom=pfrom, pto=self.getjidbare(pto)) - if pnick : - nick = ET.Element('{http://jabber.org/protocol/nick}nick') - nick.text = pnick - presence.append(nick) - self.send(presence) + # Initialize a few default stanza plugins. + registerStanzaPlugin(Iq, Roster) + registerStanzaPlugin(Message, Nick) + registerStanzaPlugin(Message, HTMLIM) - def getjidresource(self, fulljid): - if '/' in fulljid: - return fulljid.split('/', 1)[-1] - else: - return '' + def process(self, *args, **kwargs): + """ + Ensure that plugin inter-dependencies are handled before starting + event processing. - def getjidbare(self, fulljid): - return fulljid.split('/', 1)[0] + Overrides XMLStream.process. + """ + for name in self.plugin: + if not self.plugin[name].post_inited: + self.plugin[name].post_init() + return XMLStream.process(self, *args, **kwargs) - def _handleMessage(self, msg): - self.event('message', msg) + def register_plugin(self, plugin, pconfig={}, module=None): + """ + Register and configure a plugin for use in this stream. - def _handlePresence(self, presence): - """Update roster items based on presence""" - self.event("presence_%s" % presence['type'], presence) - if presence['type'] in ('subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'): - self.event('changed_subscription', presence) - return - elif not presence['type'] in ('available', 'unavailable') and not presence['type'] in presence.showtypes: - return - jid = presence['from'].bare - resource = presence['from'].resource - show = presence['type'] - status = presence['status'] - priority = presence['priority'] - wasoffline = False - oldroster = self.roster.get(jid, {}).get(resource, {}) - if not presence['from'].bare in self.roster: - self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': False} - if not resource in self.roster[jid]['presence']: - if (show == 'available' or show in presence.showtypes): - self.event("got_online", presence) - wasoffline = True - self.roster[jid]['presence'][resource] = {} - if self.roster[jid]['presence'][resource].get('show', 'unavailable') == 'unavailable': - wasoffline = True - self.roster[jid]['presence'][resource] = {'show': show, 'status': status, 'priority': priority} - name = self.roster[jid].get('name', '') - if show == 'unavailable': - logging.debug("%s %s got offline" % (jid, resource)) - del self.roster[jid]['presence'][resource] - if len(self.roster[jid]['presence']) == 0 and not self.roster[jid]['in_roster']: - del self.roster[jid] - if not wasoffline: - self.event("got_offline", presence) - else: - return False - self.event("changed_status", presence) - name = '' - if name: - name = "(%s) " % name - logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, show,status)) + Arguments: + plugin -- The name of the plugin class. Plugin names must + be unique. + pconfig -- A dictionary of configuration data for the plugin. + Defaults to an empty dictionary. + module -- Optional refence to the module containing the plugin + class if using custom plugins. + """ + try: + # Import the given module that contains the plugin. + if not module: + module = sleekxmpp.plugins + module = __import__("%s.%s" % (module.__name__, plugin), + globals(), locals(), [plugin]) - def _handlePresenceSubscribe(self, presence): - """Handling subscriptions automatically.""" - if self.auto_authorize == True: - self.send(self.makePresence(ptype='subscribed', pto=presence['from'].bare)) - if self.auto_subscribe: - self.send(self.makePresence(ptype='subscribe', pto=presence['from'].bare)) - elif self.auto_authorize == False: - self.send(self.makePresence(ptype='unsubscribed', pto=presence['from'].bare)) + # Load the plugin class from the module. + self.plugin[plugin] = getattr(module, plugin)(self, pconfig) + + # Let XEP implementing plugins have some extra logging info. + xep = '' + if hasattr(self.plugin[plugin], 'xep'): + xep = "(XEP-%s) " % self.plugin[plugin].xep + + desc = (xep, self.plugin[plugin].description) + logging.debug("Loaded Plugin %s%s" % desc) + except: + logging.exception("Unable to load plugin: %s", plugin) + + def register_plugins(self): + """ + Register and initialize all built-in plugins. + + Optionally, the list of plugins loaded may be limited to those + contained in self.plugin_whitelist. + + Plugin configurations stored in self.plugin_config will be used. + """ + if self.plugin_whitelist: + plugin_list = self.plugin_whitelist + else: + plugin_list = plugins.__all__ + + for plugin in plugin_list: + if plugin in plugins.__all__: + self.register_plugin(plugin, + self.plugin_config.get(plugin, {})) + else: + raise NameError("Plugin %s not in plugins.__all__." % plugin) + + # Resolve plugin inter-dependencies. + for plugin in self.plugin: + self.plugin[plugin].post_init() + + def __getitem__(self, key): + """ + Return a plugin given its name, if it has been registered. + """ + if key in self.plugin: + return self.plugin[key] + else: + logging.warning("""Plugin "%s" is not loaded.""" % key) + return False + + def get(self, key, default): + """ + Return a plugin given its name, if it has been registered. + """ + return self.plugin.get(key, default) + + def Message(self, *args, **kwargs): + """Create a Message stanza associated with this stream.""" + return Message(self, *args, **kwargs) + + def Iq(self, *args, **kwargs): + """Create an Iq stanza associated with this stream.""" + return Iq(self, *args, **kwargs) + + def Presence(self, *args, **kwargs): + """Create a Presence stanza associated with this stream.""" + return Presence(self, *args, **kwargs) + + def make_iq(self, id=0, ifrom=None): + """ + Create a new Iq stanza with a given Id and from JID. + + Arguments: + id -- An ideally unique ID value for this stanza thread. + Defaults to 0. + ifrom -- The from JID to use for this stanza. + """ + return self.Iq().setStanzaValues({'id': str(id), + 'from': ifrom}) + + def make_iq_get(self, queryxmlns=None): + """ + Create an Iq stanza of type 'get'. + + Optionally, a query element may be added. + + Arguments: + queryxmlns -- The namespace of the query to use. + """ + return self.Iq().setStanzaValues({'type': 'get', + 'query': queryxmlns}) + + def make_iq_result(self, id): + """ + Create an Iq stanza of type 'result' with the given ID value. + + Arguments: + id -- An ideally unique ID value. May use self.new_id(). + """ + return self.Iq().setStanzaValues({'id': id, + 'type': 'result'}) + + def make_iq_set(self, sub=None): + """ + Create an Iq stanza of type 'set'. + + Optionally, a substanza may be given to use as the + stanza's payload. + + Arguments: + sub -- A stanza or XML object to use as the Iq's payload. + """ + iq = self.Iq().setStanzaValues({'type': 'set'}) + if sub != None: + iq.append(sub) + return iq + + def make_iq_error(self, id, type='cancel', + condition='feature-not-implemented', text=None): + """ + Create an Iq stanza of type 'error'. + + Arguments: + id -- An ideally unique ID value. May use self.new_id(). + type -- The type of the error, such as 'cancel' or 'modify'. + Defaults to 'cancel'. + condition -- The error condition. + Defaults to 'feature-not-implemented'. + text -- A message describing the cause of the error. + """ + iq = self.Iq().setStanzaValues({'id': id}) + iq['error'].setStanzaValues({'type': type, + 'condition': condition, + 'text': text}) + return iq + + def make_iq_query(self, iq=None, xmlns=''): + """ + Create or modify an Iq stanza to use the given + query namespace. + + Arguments: + iq -- Optional Iq stanza to modify. A new + stanza is created otherwise. + xmlns -- The query's namespace. + """ + if not iq: + iq = self.Iq() + iq['query'] = xmlns + return iq + + def make_query_roster(self, iq=None): + """ + Create a roster query element. + + Arguments: + iq -- Optional Iq stanza to modify. A new stanza + is created otherwise. + """ + if iq: + iq['query'] = 'jabber:iq:roster' + return ET.Element("{jabber:iq:roster}query") + + def make_message(self, mto, mbody=None, msubject=None, mtype=None, + mhtml=None, mfrom=None, mnick=None): + """ + Create and initialize a new Message stanza. + + Arguments: + mto -- The recipient of the message. + mbody -- The main contents of the message. + msubject -- Optional subject for the message. + mtype -- The message's type, such as 'chat' or 'groupchat'. + mhtml -- Optional HTML body content. + mfrom -- The sender of the message. If sending from a client, + be aware that some servers require that the full JID + of the sender be used. + mnick -- Optional nickname of the sender. + """ + message = self.Message(sto=mto, stype=mtype, sfrom=mfrom) + message['body'] = mbody + message['subject'] = msubject + if mnick is not None: + message['nick'] = mnick + if mhtml is not None: + message['html']['body'] = mhtml + return message + + def make_presence(self, pshow=None, pstatus=None, ppriority=None, + pto=None, ptype=None, pfrom=None): + """ + Create and initialize a new Presence stanza. + + Arguments: + pshow -- The presence's show value. + pstatus -- The presence's status message. + ppriority -- This connections' priority. + pto -- The recipient of a directed presence. + ptype -- The type of presence, such as 'subscribe'. + pfrom -- The sender of the presence. + """ + presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto) + if pshow is not None: + presence['type'] = pshow + if pfrom is None: + presence['from'] = self.fulljid + presence['priority'] = ppriority + presence['status'] = pstatus + return presence + + def send_message(self, mto, mbody, msubject=None, mtype=None, + mhtml=None, mfrom=None, mnick=None): + """ + Create, initialize, and send a Message stanza. + + + """ + self.makeMessage(mto, mbody, msubject, mtype, + mhtml, mfrom, mnick).send() + + def send_presence(self, pshow=None, pstatus=None, ppriority=None, + pto=None, pfrom=None, ptype=None): + """ + Create, initialize, and send a Presence stanza. + + Arguments: + pshow -- The presence's show value. + pstatus -- The presence's status message. + ppriority -- This connections' priority. + pto -- The recipient of a directed presence. + ptype -- The type of presence, such as 'subscribe'. + pfrom -- The sender of the presence. + """ + self.makePresence(pshow, pstatus, ppriority, pto, + ptype=ptype, pfrom=pfrom).send() + # Unexpected errors may occur if + if not self.sentpresence: + self.event('sent_presence') + self.sentpresence = True + + def send_presence_subscription(self, pto, pfrom=None, + ptype='subscribe', pnick=None): + """ + Create, initialize, and send a Presence stanza of type 'subscribe'. + + Arguments: + pto -- The recipient of a directed presence. + pfrom -- The sender of the presence. + ptype -- The type of presence. Defaults to 'subscribe'. + pnick -- Nickname of the presence's sender. + """ + presence = self.makePresence(ptype=ptype, + pfrom=pfrom, + pto=self.getjidbare(pto)) + if pnick: + nick = ET.Element('{http://jabber.org/protocol/nick}nick') + nick.text = pnick + presence.append(nick) + presence.send() + + def set_jid(self, jid): + """Rip a JID apart and claim it as our own.""" + self.fulljid = jid + self.resource = self.getjidresource(jid) + self.jid = self.getjidbare(jid) + self.username = jid.split('@', 1)[0] + self.server = jid.split('@', 1)[-1].split('/', 1)[0] + + def getjidresource(self, fulljid): + if '/' in fulljid: + return fulljid.split('/', 1)[-1] + else: + return '' + + def getjidbare(self, fulljid): + return fulljid.split('/', 1)[0] + + def _handle_message(self, msg): + """Process incoming message stanzas.""" + self.event('message', msg) + + def _handle_presence(self, presence): + """ + Process incoming presence stanzas. + + Update the roster with presence information. + """ + self.event("presence_%s" % presence['type'], presence) + + # Check for changes in subscription state. + if presence['type'] in ('subscribe', 'subscribed', + 'unsubscribe', 'unsubscribed'): + self.event('changed_subscription', presence) + return + elif not presence['type'] in ('available', 'unavailable') and \ + not presence['type'] in presence.showtypes: + return + + # Strip the information from the stanza. + jid = presence['from'].bare + resource = presence['from'].resource + show = presence['type'] + status = presence['status'] + priority = presence['priority'] + + was_offline = False + old_roster = self.roster.get(jid, {}).get(resource, {}) + + # Create a new roster entry if needed. + if not jid in self.roster: + self.roster[jid] = {'groups': [], + 'name': '', + 'subscription': 'none', + 'presence': {}, + 'in_roster': False} + + # Alias to simplify some references. + connections = self.roster[jid]['presence'] + + # Determine if the user has just come online. + if not resource in connections: + if show == 'available' or show in presence.showtypes: + self.event("got_online", presence) + was_offline = True + connections[resource] = {} + + if connections[resource].get('show', 'unavailable') == 'unavailable': + was_offline = True + + # Update the roster's state for this JID's resource. + connections[resource] = {'show': show, + 'status': status, + 'priority': priority} + + name = self.roster[jid].get('name', '') + + # Remove unneeded state information after a resource + # disconnects. Determine if this was the last connection + # for the JID. + if show == 'unavailable': + logging.debug("%s %s got offline" % (jid, resource)) + del connections[resource] + + if not connections and not self.roster[jid]['in_roster']: + del self.roster[jid] + if not was_offline: + self.event("got_offline", presence) + else: + return False + + name = '(%s) ' % name if name else '' + + # Presence state has changed. + self.event("changed_status", presence) + logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, + show, status)) + + def _handle_subscribe(self, presence): + """ + Automatically managage subscription requests. + + Subscription behavior is controlled by the settings + self.auto_authorize and self.auto_subscribe. + + auto_auth auto_sub Result: + True True Create bi-directional subsriptions. + True False Create only directed subscriptions. + False * Decline all subscriptions. + None * Disable automatic handling and use + a custom handler. + """ + presence = self.Presence() + presence['to'] = presence['from'].bare + + # We are using trinary logic, so conditions have to be + # more explicit than usual. + if self.auto_authorize == True: + presence['type'] = 'subscribed' + presence.send() + if self.auto_subscribe: + presence['type'] = 'subscribe' + presence.send() + elif self.auto_authorize == False: + presence['type'] = 'unsubscribed' + presence.send() + +# Restore the old, lowercased name for backwards compatibility. +basexmpp = BaseXMPP diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index 5534a45..00e5252 100755 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -8,7 +8,7 @@ See the file LICENSE for copying permission. """ from __future__ import absolute_import -from . basexmpp import basexmpp +from . basexmpp import BaseXMPP from xml.etree import cElementTree as ET from . xmlstream.xmlstream import XMLStream @@ -39,12 +39,11 @@ class ComponentXMPP(basexmpp, XMLStream): """SleekXMPP's client class. Use only for good, not evil.""" def __init__(self, jid, secret, host, port, plugin_config = {}, plugin_whitelist=[], use_jc_ns=False): - XMLStream.__init__(self) if use_jc_ns: - self.default_ns = 'jabber:client' + default_ns = 'jabber:client' else: - self.default_ns = 'jabber:component:accept' - basexmpp.__init__(self) + default_ns = 'jabber:component:accept' + BaseXMPP.__init__(self, default_ns) self.auto_authorize = None self.stream_header = "" % jid self.stream_footer = "" @@ -54,17 +53,7 @@ class ComponentXMPP(basexmpp, XMLStream): self.secret = secret self.is_component = True self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake)) - - def __getitem__(self, key): - if key in self.plugin: - return self.plugin[key] - else: - logging.warning("""Plugin "%s" is not loaded.""" % key) - return False - - def get(self, key, default): - return self.plugin.get(key, default) - + def incoming_filter(self, xmlobj): if xmlobj.tag.startswith('{jabber:client}'): xmlobj.tag = xmlobj.tag.replace('jabber:client', self.default_ns) @@ -80,10 +69,10 @@ class ComponentXMPP(basexmpp, XMLStream): else: handshake.text = hashlib.sha1(bytes("%s%s" % (sid, self.secret), 'utf-8')).hexdigest().lower() self.sendXML(handshake) - + def _handleHandshake(self, xml): self.event("session_start") - + def connect(self): logging.debug("Connecting to %s:%s" % (self.server_host, self.server_port)) return xmlstreammod.XMLStream.connect(self, self.server_host, self.server_port) diff --git a/tests/test_presencestanzas.py b/tests/test_presencestanzas.py index d6a5a38..f4a33aa 100644 --- a/tests/test_presencestanzas.py +++ b/tests/test_presencestanzas.py @@ -46,7 +46,7 @@ class TestPresenceStanzas(SleekTest): happened.append(True) c.add_event_handler("changed_status", handlechangedpresence) - c._handlePresence(p) + c._handle_presence(p) self.failUnless(happened == [], "changed_status event triggered for extra unavailable presence")