diff options
Diffstat (limited to 'sleekxmpp')
136 files changed, 5331 insertions, 1016 deletions
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 11e787ad..dc1f6b94 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -15,22 +15,24 @@ from __future__ import with_statement, unicode_literals import sys -import copy import logging import sleekxmpp -from sleekxmpp import plugins, roster +from sleekxmpp import plugins, features, roster from sleekxmpp.exceptions import IqError, IqTimeout -from sleekxmpp.stanza import Message, Presence, Iq, Error, StreamError +from sleekxmpp.stanza import Message, Presence, Iq, StreamError from sleekxmpp.stanza.roster import Roster from sleekxmpp.stanza.nick import Nick from sleekxmpp.stanza.htmlim import HTMLIM -from sleekxmpp.xmlstream import XMLStream, JID, tostring +from sleekxmpp.xmlstream import XMLStream, JID from sleekxmpp.xmlstream import ET, register_stanza_plugin -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.xmlstream.handler import Callback + +from sleekxmpp.features import * +from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin log = logging.getLogger(__name__) @@ -67,7 +69,7 @@ class BaseXMPP(XMLStream): self.boundjid = JID(jid) #: A dictionary mapping plugin names to plugins. - self.plugin = {} + self.plugin = PluginManager(self) #: Configuration options for whitelisted plugins. #: If a plugin is registered without any configuration, @@ -186,9 +188,18 @@ class BaseXMPP(XMLStream): - The send queue processor - The scheduler """ + if 'xep_0115' in self.plugin: + name = 'xep_0115' + if not hasattr(self.plugin[name], 'post_inited'): + if hasattr(self.plugin[name], 'post_init'): + self.plugin[name].post_init() + self.plugin[name].post_inited = True + for name in self.plugin: - if not self.plugin[name].post_inited: - self.plugin[name].post_init() + if not hasattr(self.plugin[name], 'post_inited'): + if hasattr(self.plugin[name], 'post_init'): + self.plugin[name].post_init() + self.plugin[name].post_inited = True return XMLStream.process(self, *args, **kwargs) def register_plugin(self, plugin, pconfig={}, module=None): @@ -201,42 +212,14 @@ class BaseXMPP(XMLStream): :param 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: - try: - module = sleekxmpp.plugins - module = __import__( - str("%s.%s" % (module.__name__, plugin)), - globals(), locals(), [str(plugin)]) - except ImportError: - module = sleekxmpp.features - module = __import__( - str("%s.%s" % (module.__name__, plugin)), - globals(), locals(), [str(plugin)]) - if isinstance(module, str): - # We probably want to load a module from outside - # the sleekxmpp package, so leave out the globals(). - module = __import__(module, fromlist=[plugin]) - - # Use the global plugin config cache, if applicable - if not pconfig: - pconfig = self.plugin_config.get(plugin, {}) - - # Load the plugin class from the module. - self.plugin[plugin] = getattr(module, plugin)(self, pconfig) - - # Let XEP/RFC implementing plugins have some extra logging info. - spec = '(CUSTOM) %s' - if self.plugin[plugin].xep: - spec = "(XEP-%s) " % self.plugin[plugin].xep - elif self.plugin[plugin].rfc: - spec = "(RFC-%s) " % self.plugin[plugin].rfc - - desc = (spec, self.plugin[plugin].description) - log.debug("Loaded Plugin %s %s" % desc) - except: - log.exception("Unable to load plugin: %s", plugin) + + # Use the global plugin config cache, if applicable + if not pconfig: + pconfig = self.plugin_config.get(plugin, {}) + + if not self.plugin.registered(plugin): + load_plugin(plugin, module) + self.plugin.enable(plugin, pconfig) def register_plugins(self): """Register and initialize all built-in plugins. @@ -253,15 +236,10 @@ class BaseXMPP(XMLStream): for plugin in plugin_list: if plugin in plugins.__all__: - self.register_plugin(plugin, - self.plugin_config.get(plugin, {})) + self.register_plugin(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: @@ -675,11 +653,15 @@ class BaseXMPP(XMLStream): def _handle_available(self, presence): pto = presence['to'].bare + if not pto: + pto = self.boundjid.bare pfrom = presence['from'].bare self.roster[pto][pfrom].handle_available(presence) def _handle_unavailable(self, presence): pto = presence['to'].bare + if not pto: + pto = self.boundjid.bare pfrom = presence['from'].bare self.roster[pto][pfrom].handle_unavailable(presence) @@ -763,6 +745,11 @@ class BaseXMPP(XMLStream): iq = exception.iq log.error('Request timed out: %s', iq) log.warning('You should catch IqTimeout exceptions') + elif isinstance(exception, SyntaxError): + # Hide stream parsing errors that occur when the + # stream is disconnected (they've been handled, we + # don't need to make a mess in the logs). + pass else: log.exception(exception) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 20012b5f..590192db 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -15,22 +15,13 @@ from __future__ import absolute_import, unicode_literals import logging -import base64 -import sys -import hashlib -import random -import threading - -import sleekxmpp -from sleekxmpp import plugins -from sleekxmpp import stanza -from sleekxmpp import features + +from sleekxmpp.stanza import StreamFeatures from sleekxmpp.basexmpp import BaseXMPP -from sleekxmpp.stanza import * -from sleekxmpp.xmlstream import XMLStream, RestartStream -from sleekxmpp.xmlstream import StanzaBase, ET, register_stanza_plugin -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream import XMLStream +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.xmlstream.handler import Callback # Flag indicating if DNS SRV records are available for use. try: @@ -74,12 +65,15 @@ class ClientXMPP(BaseXMPP): BaseXMPP.__init__(self, jid, 'jabber:client') self.set_jid(jid) - self.password = password self.escape_quotes = escape_quotes self.plugin_config = plugin_config self.plugin_whitelist = plugin_whitelist self.default_port = 5222 + self.credentials = {} + + self.password = password + self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % ( self.boundjid.host, "xmlns:stream='%s'" % self.stream_ns, @@ -97,6 +91,7 @@ class ClientXMPP(BaseXMPP): self.bindfail = False self.add_event_handler('connected', self._handle_connected) + self.add_event_handler('session_bind', self._handle_session_bind) self.register_stanza(StreamFeatures) @@ -117,6 +112,15 @@ class ClientXMPP(BaseXMPP): self.register_plugin('feature_session') self.register_plugin('feature_mechanisms', pconfig={'use_mech': sasl_mech} if sasl_mech else None) + self.register_plugin('feature_rosterver') + + @property + def password(self): + return self.credentials.get('password', '') + + @password.setter + def password(self, value): + self.credentials['password'] = value def connect(self, address=tuple(), reattempt=True, use_tls=True, use_ssl=False): @@ -154,8 +158,10 @@ class ClientXMPP(BaseXMPP): try: record = "_xmpp-client._tcp.%s" % domain answers = [] + log.debug("Querying SRV records for %s" % domain) for answer in dns.resolver.query(record, dns.rdatatype.SRV): address = (answer.target.to_text()[:-1], answer.port) + log.debug("Found SRV record: %s", address) answers.append((address, answer.priority, answer.weight)) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): log.warning("No SRV records for %s", domain) @@ -167,7 +173,7 @@ class ClientXMPP(BaseXMPP): return answers else: log.warning("dnspython is not installed -- " + \ - "relying on OS A record resolution") + "relying on OS A/AAAA record resolution") return [((domain, port), 0, 0)] def register_feature(self, name, handler, restart=False, order=5000): @@ -236,10 +242,17 @@ class ClientXMPP(BaseXMPP): iq = self.Iq() iq['type'] = 'get' iq.enable('roster') + if 'rosterver' in self.features: + iq['roster']['ver'] = self.client_roster.version + + if not block and callback is None: + callback = lambda resp: self._handle_roster(resp, request=True) + response = iq.send(block, timeout, callback) - if callback is None: - return self._handle_roster(response, request=True) + if block: + self._handle_roster(response, request=True) + return response def _handle_connected(self, event=None): #TODO: Use stream state here @@ -270,15 +283,22 @@ class ClientXMPP(BaseXMPP): to a request for the roster, and not an empty acknowledgement from the server. """ + if iq['from'].bare and iq['from'].bare != self.boundjid.bare: + raise XMPPError(condition='service-unavailable') if iq['type'] == 'set' or (iq['type'] == 'result' and request): + roster = self.client_roster + if iq['roster']['ver']: + roster.version = iq['roster']['ver'] for jid in iq['roster']['items']: item = iq['roster']['items'][jid] - roster = self.roster[iq['to'].bare] roster[jid]['name'] = item['name'] roster[jid]['groups'] = item['groups'] roster[jid]['from'] = item['subscription'] in ['from', 'both'] roster[jid]['to'] = item['subscription'] in ['to', 'both'] roster[jid]['pending_out'] = (item['ask'] == 'subscribe') + + roster[jid].save(remove=(item['subscription'] == 'remove')) + self.event('roster_received', iq) self.event("roster_update", iq) @@ -286,7 +306,14 @@ class ClientXMPP(BaseXMPP): iq.reply() iq.enable('roster') iq.send() - return True + + def _handle_session_bind(self, jid): + """Set the client roster to the JID set by the server. + + :param :class:`sleekxmpp.xmlstream.jid.JID` jid: The bound JID as + dictated by the server. The same as :attr:`boundjid`. + """ + self.client_roster = self.roster[jid] # To comply with PEP8, method names now use underscores. diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index 5b16c5ef..df23c2f6 100644 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -15,17 +15,14 @@ from __future__ import absolute_import import logging -import base64 import sys import hashlib -from sleekxmpp import plugins -from sleekxmpp import stanza from sleekxmpp.basexmpp import BaseXMPP -from sleekxmpp.xmlstream import XMLStream, RestartStream -from sleekxmpp.xmlstream import StanzaBase, ET -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * +from sleekxmpp.xmlstream import XMLStream +from sleekxmpp.xmlstream import ET +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.xmlstream.handler import Callback log = logging.getLogger(__name__) diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py index 5bfe173d..c63d72bf 100644 --- a/sleekxmpp/features/__init__.py +++ b/sleekxmpp/features/__init__.py @@ -6,4 +6,10 @@ See the file LICENSE for copying permission. """ -__all__ = ['feature_starttls', 'feature_mechanisms', 'feature_bind'] +__all__ = [ + 'feature_starttls', + 'feature_mechanisms', + 'feature_bind', + 'feature_session', + 'feature_rosterver' +] diff --git a/sleekxmpp/features/feature_bind/__init__.py b/sleekxmpp/features/feature_bind/__init__.py index aa854f87..9e0831dd 100644 --- a/sleekxmpp/features/feature_bind/__init__.py +++ b/sleekxmpp/features/feature_bind/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.features.feature_bind.bind import feature_bind +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.features.feature_bind.bind import FeatureBind from sleekxmpp.features.feature_bind.stanza import Bind + + +register_plugin(FeatureBind) + + +# Retain some backwards compatibility +feature_bind = FeatureBind diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py index d3b2b737..b828e26f 100644 --- a/sleekxmpp/features/feature_bind/bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -11,22 +11,20 @@ import logging from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.features.feature_bind import stanza from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin, register_plugin log = logging.getLogger(__name__) -class feature_bind(base_plugin): +class FeatureBind(BasePlugin): - def plugin_init(self): - self.name = 'Bind Resource' - self.rfc = '6120' - self.description = 'Resource Binding Stream Feature' - self.stanza = stanza + name = 'feature_bind' + description = 'RFC 6120: Stream Feature: Resource Binding' + dependencies = set() + stanza = stanza + def plugin_init(self): self.xmpp.register_feature('bind', self._handle_bind_resource, restart=False, @@ -52,6 +50,7 @@ class feature_bind(base_plugin): self.xmpp.set_jid(response['bind']['jid']) self.xmpp.bound = True + self.xmpp.event('session_bind', self.xmpp.boundjid, direct=True) self.xmpp.features.add('bind') diff --git a/sleekxmpp/features/feature_bind/stanza.py b/sleekxmpp/features/feature_bind/stanza.py index 2c1484e0..8ce7536f 100644 --- a/sleekxmpp/features/feature_bind/stanza.py +++ b/sleekxmpp/features/feature_bind/stanza.py @@ -6,8 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Iq, StreamFeatures -from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin +from sleekxmpp.xmlstream import ElementBase class Bind(ElementBase): diff --git a/sleekxmpp/features/feature_mechanisms/__init__.py b/sleekxmpp/features/feature_mechanisms/__init__.py index 5379ef4e..9f7611ed 100644 --- a/sleekxmpp/features/feature_mechanisms/__init__.py +++ b/sleekxmpp/features/feature_mechanisms/__init__.py @@ -6,8 +6,17 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.features.feature_mechanisms.mechanisms import feature_mechanisms +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.features.feature_mechanisms.mechanisms import FeatureMechanisms from sleekxmpp.features.feature_mechanisms.stanza import Mechanisms from sleekxmpp.features.feature_mechanisms.stanza import Auth from sleekxmpp.features.feature_mechanisms.stanza import Success from sleekxmpp.features.feature_mechanisms.stanza import Failure + + +register_plugin(FeatureMechanisms) + + +# Retain some backwards compatibility +feature_mechanisms = FeatureMechanisms diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index 2b8321c2..6f01cb14 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -9,36 +9,47 @@ import logging from sleekxmpp.thirdparty import suelta +from sleekxmpp.thirdparty.suelta.exceptions import SASLCancelled, SASLError from sleekxmpp.stanza import StreamFeatures from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.features.feature_mechanisms import stanza log = logging.getLogger(__name__) -class feature_mechanisms(base_plugin): +class FeatureMechanisms(BasePlugin): - def plugin_init(self): - self.name = 'SASL Mechanisms' - self.rfc = '6120' - self.description = "SASL Stream Feature" - self.stanza = stanza + name = 'feature_mechanisms' + description = 'RFC 6120: Stream Feature: SASL' + dependencies = set() + stanza = stanza + def plugin_init(self): self.use_mech = self.config.get('use_mech', None) + if not self.use_mech and not self.xmpp.boundjid.user: + self.use_mech = 'ANONYMOUS' + def tls_active(): return 'starttls' in self.xmpp.features def basic_callback(mech, values): - if 'username' in values: - values['username'] = self.xmpp.boundjid.user - if 'password' in values: - values['password'] = self.xmpp.password + creds = self.xmpp.credentials + for value in values: + if value == 'username': + values['username'] = self.xmpp.boundjid.user + elif value == 'password': + values['password'] = creds['password'] + elif value == 'email': + jid = self.xmpp.boundjid.bare + values['email'] = creds.get('email', jid) + elif value in creds: + values[value] = creds[value] mech.fulfill(values) sasl_callback = self.config.get('sasl_callback', None) @@ -53,6 +64,9 @@ class feature_mechanisms(base_plugin): tls_active=tls_active, mech=self.use_mech) + self.mech_list = set() + self.attempted_mechs = set() + register_stanza_plugin(StreamFeatures, stanza.Mechanisms) self.xmpp.register_stanza(stanza.Success) @@ -60,19 +74,18 @@ class feature_mechanisms(base_plugin): self.xmpp.register_stanza(stanza.Auth) self.xmpp.register_stanza(stanza.Challenge) self.xmpp.register_stanza(stanza.Response) + self.xmpp.register_stanza(stanza.Abort) self.xmpp.register_handler( Callback('SASL Success', MatchXPath(stanza.Success.tag_name()), self._handle_success, - instream=True, - once=True)) + instream=True)) self.xmpp.register_handler( Callback('SASL Failure', MatchXPath(stanza.Failure.tag_name()), self._handle_fail, - instream=True, - once=True)) + instream=True)) self.xmpp.register_handler( Callback('SASL Challenge', MatchXPath(stanza.Challenge.tag_name()), @@ -95,14 +108,29 @@ class feature_mechanisms(base_plugin): # server has incorrectly offered it again. return False - mech_list = features['mechanisms'] + if not self.use_mech: + self.mech_list = set(features['mechanisms']) + else: + self.mech_list = set([self.use_mech]) + return self._send_auth() + + def _send_auth(self): + mech_list = self.mech_list - self.attempted_mechs self.mech = self.sasl.choose_mechanism(mech_list) - if self.mech is not None: + if mech_list and self.mech is not None: resp = stanza.Auth(self.xmpp) resp['mechanism'] = self.mech.name - resp['value'] = self.mech.process() - resp.send(now=True) + try: + resp['value'] = self.mech.process() + except SASLCancelled: + self.attempted_mechs.add(self.mech.name) + self._send_auth() + except SASLError: + self.attempted_mechs.add(self.mech.name) + self._send_auth() + else: + resp.send(now=True) else: log.error("No appropriate login method.") self.xmpp.event("no_auth", direct=True) @@ -112,18 +140,26 @@ class feature_mechanisms(base_plugin): def _handle_challenge(self, stanza): """SASL challenge received. Process and send response.""" resp = self.stanza.Response(self.xmpp) - resp['value'] = self.mech.process(stanza['value']) - resp.send(now=True) + try: + resp['value'] = self.mech.process(stanza['value']) + except SASLCancelled: + self.stanza.Abort(self.xmpp).send() + except SASLError: + self.stanza.Abort(self.xmpp).send() + else: + resp.send(now=True) def _handle_success(self, stanza): """SASL authentication succeeded. Restart the stream.""" + self.attempted_mechs = set() self.xmpp.authenticated = True self.xmpp.features.add('mechanisms') raise RestartStream() def _handle_fail(self, stanza): """SASL authentication failed. Disconnect and shutdown.""" + self.attempted_mechs.add(self.mech.name) log.info("Authentication failed: %s", stanza['condition']) self.xmpp.event("failed_auth", stanza, direct=True) - self.xmpp.disconnect() + self._send_auth() return True diff --git a/sleekxmpp/features/feature_mechanisms/stanza/__init__.py b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py index 8b80f358..38991d89 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/__init__.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/__init__.py @@ -13,3 +13,4 @@ from sleekxmpp.features.feature_mechanisms.stanza.success import Success from sleekxmpp.features.feature_mechanisms.stanza.failure import Failure from sleekxmpp.features.feature_mechanisms.stanza.challenge import Challenge from sleekxmpp.features.feature_mechanisms.stanza.response import Response +from sleekxmpp.features.feature_mechanisms.stanza.abort import Abort diff --git a/sleekxmpp/features/feature_mechanisms/stanza/abort.py b/sleekxmpp/features/feature_mechanisms/stanza/abort.py new file mode 100644 index 00000000..aaca348d --- /dev/null +++ b/sleekxmpp/features/feature_mechanisms/stanza/abort.py @@ -0,0 +1,24 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import StanzaBase + + +class Abort(StanzaBase): + + """ + """ + + name = 'abort' + namespace = 'urn:ietf:params:xml:ns:xmpp-sasl' + interfaces = set() + plugin_attrib = name + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py index e069b57f..69769507 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/auth.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py @@ -10,9 +10,7 @@ import base64 from sleekxmpp.thirdparty.suelta.util import bytes -from sleekxmpp.stanza import StreamFeatures -from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream import StanzaBase class Auth(StanzaBase): @@ -25,15 +23,28 @@ class Auth(StanzaBase): interfaces = set(('mechanism', 'value')) plugin_attrib = name + #: Some SASL mechs require sending values as is, + #: without converting base64. + plain_mechs = set(['X-MESSENGER-OAUTH2']) + def setup(self, xml): StanzaBase.setup(self, xml) self.xml.tag = self.tag_name() def get_value(self): - return base64.b64decode(bytes(self.xml.text)) + if not self['mechanism'] in self.plain_mechs: + return base64.b64decode(bytes(self.xml.text)) + else: + return self.xml.text def set_value(self, values): - self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + if not self['mechanism'] in self.plain_mechs: + if values: + self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + else: + self.xml.text = '=' + else: + self.xml.text = bytes(values).decode('utf-8') def del_value(self): self.xml.text = '' diff --git a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py index 82af869f..85d65403 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/challenge.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/challenge.py @@ -10,9 +10,7 @@ import base64 from sleekxmpp.thirdparty.suelta.util import bytes -from sleekxmpp.stanza import StreamFeatures -from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream import StanzaBase class Challenge(StanzaBase): @@ -33,7 +31,10 @@ class Challenge(StanzaBase): return base64.b64decode(bytes(self.xml.text)) def set_value(self, values): - self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + if values: + self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + else: + self.xml.text = '=' def del_value(self): self.xml.text = '' diff --git a/sleekxmpp/features/feature_mechanisms/stanza/failure.py b/sleekxmpp/features/feature_mechanisms/stanza/failure.py index 027cc5af..5dd0de56 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/failure.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/failure.py @@ -6,9 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import StreamFeatures -from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream import StanzaBase, ET class Failure(StanzaBase): diff --git a/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py index c09cafbd..bbd56813 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/mechanisms.py @@ -6,9 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import StreamFeatures -from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream import ElementBase, ET class Mechanisms(ElementBase): diff --git a/sleekxmpp/features/feature_mechanisms/stanza/response.py b/sleekxmpp/features/feature_mechanisms/stanza/response.py index 45bb8207..78636c9e 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/response.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/response.py @@ -10,9 +10,7 @@ import base64 from sleekxmpp.thirdparty.suelta.util import bytes -from sleekxmpp.stanza import StreamFeatures -from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream import StanzaBase class Response(StanzaBase): @@ -33,7 +31,10 @@ class Response(StanzaBase): return base64.b64decode(bytes(self.xml.text)) def set_value(self, values): - self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + if values: + self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') + else: + self.xml.text = '=' def del_value(self): self.xml.text = '' diff --git a/sleekxmpp/features/feature_mechanisms/stanza/success.py b/sleekxmpp/features/feature_mechanisms/stanza/success.py index 028e28a3..7a5a73f2 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/success.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/success.py @@ -6,9 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import StreamFeatures -from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream import StanzaBase class Success(StanzaBase): diff --git a/sleekxmpp/features/feature_rosterver/__init__.py b/sleekxmpp/features/feature_rosterver/__init__.py new file mode 100644 index 00000000..33bbf416 --- /dev/null +++ b/sleekxmpp/features/feature_rosterver/__init__.py @@ -0,0 +1,19 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.features.feature_rosterver.rosterver import FeatureRosterVer +from sleekxmpp.features.feature_rosterver.stanza import RosterVer + + +register_plugin(FeatureRosterVer) + + +# Retain some backwards compatibility +feature_rosterver = FeatureRosterVer diff --git a/sleekxmpp/features/feature_rosterver/rosterver.py b/sleekxmpp/features/feature_rosterver/rosterver.py new file mode 100644 index 00000000..9e0bb8e8 --- /dev/null +++ b/sleekxmpp/features/feature_rosterver/rosterver.py @@ -0,0 +1,42 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.features.feature_rosterver import stanza +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.base import BasePlugin + + +log = logging.getLogger(__name__) + + +class FeatureRosterVer(BasePlugin): + + name = 'feature_rosterver' + description = 'RFC 6121: Stream Feature: Roster Versioning' + dependences = set() + stanza = stanza + + def plugin_init(self): + self.xmpp.register_feature('rosterver', + self._handle_rosterver, + restart=False, + order=9000) + + register_stanza_plugin(StreamFeatures, stanza.RosterVer) + + def _handle_rosterver(self, features): + """Enable using roster versioning. + + Arguments: + features -- The stream features stanza. + """ + log.debug("Enabling roster versioning.") + self.xmpp.features.add('rosterver') diff --git a/sleekxmpp/features/feature_rosterver/stanza.py b/sleekxmpp/features/feature_rosterver/stanza.py new file mode 100644 index 00000000..025872fa --- /dev/null +++ b/sleekxmpp/features/feature_rosterver/stanza.py @@ -0,0 +1,17 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase + + +class RosterVer(ElementBase): + + name = 'ver' + namespace = 'urn:xmpp:features:rosterver' + interfaces = set() + plugin_attrib = 'rosterver' diff --git a/sleekxmpp/features/feature_session/__init__.py b/sleekxmpp/features/feature_session/__init__.py index 3c84baed..28bb3f77 100644 --- a/sleekxmpp/features/feature_session/__init__.py +++ b/sleekxmpp/features/feature_session/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.features.feature_session.session import feature_session +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.features.feature_session.session import FeatureSession from sleekxmpp.features.feature_session.stanza import Session + + +register_plugin(FeatureSession) + + +# Retain some backwards compatibility +feature_session = FeatureSession diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py index 0daec5da..c799a763 100644 --- a/sleekxmpp/features/feature_session/session.py +++ b/sleekxmpp/features/feature_session/session.py @@ -10,9 +10,7 @@ import logging from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.features.feature_session import stanza @@ -20,14 +18,14 @@ from sleekxmpp.features.feature_session import stanza log = logging.getLogger(__name__) -class feature_session(base_plugin): +class FeatureSession(BasePlugin): - def plugin_init(self): - self.name = 'Start Session' - self.rfc = '3920' - self.description = 'Start Session Stream Feature' - self.stanza = stanza + name = 'feature_session' + description = 'RFC 3920: Stream Feature: Start Session' + dependencies = set() + stanza = stanza + def plugin_init(self): self.xmpp.register_feature('session', self._handle_start_session, restart=False, @@ -46,7 +44,7 @@ class feature_session(base_plugin): iq = self.xmpp.Iq() iq['type'] = 'set' iq.enable('session') - response = iq.send(now=True) + iq.send(now=True) self.xmpp.features.add('session') diff --git a/sleekxmpp/features/feature_session/stanza.py b/sleekxmpp/features/feature_session/stanza.py index 40ea583d..94e949ee 100644 --- a/sleekxmpp/features/feature_session/stanza.py +++ b/sleekxmpp/features/feature_session/stanza.py @@ -6,8 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Iq, StreamFeatures -from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin +from sleekxmpp.xmlstream import ElementBase class Session(ElementBase): diff --git a/sleekxmpp/features/feature_starttls/__init__.py b/sleekxmpp/features/feature_starttls/__init__.py index 4ae89433..68697ce5 100644 --- a/sleekxmpp/features/feature_starttls/__init__.py +++ b/sleekxmpp/features/feature_starttls/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.features.feature_starttls.starttls import feature_starttls +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.features.feature_starttls.starttls import FeatureSTARTTLS from sleekxmpp.features.feature_starttls.stanza import * + + +register_plugin(FeatureSTARTTLS) + + +# Retain some backwards compatibility +feature_starttls = FeatureSTARTTLS diff --git a/sleekxmpp/features/feature_starttls/stanza.py b/sleekxmpp/features/feature_starttls/stanza.py index 8b09ad94..b968e134 100644 --- a/sleekxmpp/features/feature_starttls/stanza.py +++ b/sleekxmpp/features/feature_starttls/stanza.py @@ -6,9 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import StreamFeatures from sleekxmpp.xmlstream import StanzaBase, ElementBase -from sleekxmpp.xmlstream import register_stanza_plugin class STARTTLS(ElementBase): diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py index 4e2b6621..212b9da5 100644 --- a/sleekxmpp/features/feature_starttls/starttls.py +++ b/sleekxmpp/features/feature_starttls/starttls.py @@ -10,23 +10,23 @@ import logging from sleekxmpp.stanza import StreamFeatures from sleekxmpp.xmlstream import RestartStream, register_stanza_plugin -from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.handler import * -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.features.feature_starttls import stanza log = logging.getLogger(__name__) -class feature_starttls(base_plugin): +class FeatureSTARTTLS(BasePlugin): - def plugin_init(self): - self.name = "STARTTLS" - self.rfc = '6120' - self.description = "STARTTLS Stream Feature" - self.stanza = stanza + name = 'feature_starttls' + description = 'RFC 6120: Stream Feature: STARTTLS' + dependencies = set() + stanza = stanza + def plugin_init(self): self.xmpp.register_handler( Callback('STARTTLS Proceed', MatchXPath(stanza.Proceed.tag_name()), diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index c0b1121b..c374f27b 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -5,9 +5,46 @@ See the file LICENSE for copying permission. """ -__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', - 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082', - 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199', - 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify'] -# Don't automatically load xep_0078 +from sleekxmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin +from sleekxmpp.plugins.base import register_plugin, load_plugin + + +__all__ = [ + # Non-standard + 'gmail_notify', # Gmail searching and notifications + + # XEPS + 'xep_0004', # Data Forms + 'xep_0009', # Jabber-RPC + 'xep_0012', # Last Activity + 'xep_0030', # Service Discovery + 'xep_0033', # Extended Stanza Addresses + 'xep_0045', # Multi-User Chat (Client) + 'xep_0047', # In-Band Bytestreams + 'xep_0050', # Ad-hoc Commands + 'xep_0059', # Result Set Management + 'xep_0060', # Pubsub (Client) + 'xep_0066', # Out of Band Data + 'xep_0077', # In-Band Registration +# 'xep_0078', # Non-SASL auth. Don't automatically load + 'xep_0080', # User Location + 'xep_0082', # XMPP Date and Time Profiles + 'xep_0085', # Chat State Notifications + 'xep_0086', # Legacy Error Codes + 'xep_0092', # Software Version + 'xep_0107', # User Mood + 'xep_0108', # User Activity + 'xep_0115', # Entity Capabilities + 'xep_0118', # User Tune + 'xep_0128', # Extended Service Discovery + 'xep_0163', # Personal Eventing Protocol + 'xep_0172', # User Nickname + 'xep_0184', # Message Receipts + 'xep_0198', # Stream Management + 'xep_0199', # Ping + 'xep_0202', # Entity Time + 'xep_0203', # Delayed Delivery + 'xep_0224', # Attention + 'xep_0249', # Direct MUC Invitations +] diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index 561421d8..f08023ba 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -1,91 +1,293 @@ +# -*- encoding: utf-8 -*- + """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.plugins.base + ~~~~~~~~~~~~~~~~~~~~~~ + + This module provides XMPP functionality that + is specific to client connections. + + Part of SleekXMPP: The Sleek XMPP Library - See the file LICENSE for copying permission. + :copyright: (c) 2012 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ +import sys +import logging +import threading + + +log = logging.getLogger(__name__) + + +#: Associate short string names of plugins with implementations. The +#: plugin names are based on the spec used by the plugin, such as +#: `'xep_0030'` for a plugin that implements XEP-0030. +PLUGIN_REGISTRY = {} + +#: In order to do cascading plugin disabling, reverse dependencies +#: must be tracked. +PLUGIN_DEPENDENTS = {} + +#: Only allow one thread to manipulate the plugin registry at a time. +REGISTRY_LOCK = threading.RLock() -class base_plugin(object): +class PluginNotFound(Exception): + """Raised if an unknown plugin is accessed.""" + + +def register_plugin(impl, name=None): + """Add a new plugin implementation to the registry. + + :param class impl: The plugin class. + + The implementation class must provide a :attr:`~BasePlugin.name` + value that will be used as a short name for enabling and disabling + the plugin. The name should be based on the specification used by + the plugin. For example, a plugin implementing XEP-0030 would be + named `'xep_0030'`. """ - The base_plugin class serves as a base for user created plugins - that provide support for existing or experimental XEPS. - - Each plugin has a dictionary for configuration options, as well - as a name and description. - - The lifecycle of a plugin is: - 1. The plugin is instantiated during registration. - 2. Once the XML stream begins processing, the method - plugin_init() is called (if the plugin is configured - as enabled with {'enable': True}). - 3. After all plugins have been initialized, the - method post_init() is called. - - Recommended event handlers: - session_start -- Plugins which require the use of the current - bound JID SHOULD wait for the session_start - event to perform any initialization (or - resetting). This is a transitive recommendation, - plugins that use other plugins which use the - bound JID should also wait for session_start - before making such calls. - session_end -- If the plugin keeps any per-session state, - such as joined MUC rooms, such state SHOULD - be cleared when the session_end event is raised. - - Attributes: - xep -- The XEP number the plugin implements, if any. - description -- A short description of the plugin, typically - the long name of the implemented XEP. - xmpp -- The main SleekXMPP instance. - config -- A dictionary of custom configuration values. - The value 'enable' is special and controls - whether or not the plugin is initialized - after registration. - post_initted -- Executed after all plugins have been initialized - to handle any cross-plugin interactions, such as - registering service discovery items. - enable -- Indicates that the plugin is enabled for use and - will be initialized after registration. - - Methods: - plugin_init -- Initialize the plugin state. - post_init -- Handle any cross-plugin concerns. + if name is None: + name = impl.name + with REGISTRY_LOCK: + PLUGIN_REGISTRY[name] = impl + if name not in PLUGIN_DEPENDENTS: + PLUGIN_DEPENDENTS[name] = set() + for dep in impl.dependencies: + if dep not in PLUGIN_DEPENDENTS: + PLUGIN_DEPENDENTS[dep] = set() + PLUGIN_DEPENDENTS[dep].add(name) + + +def load_plugin(name, module=None): + """Find and import a plugin module so that it can be registered. + + This function is called to import plugins that have selected for + enabling, but no matching registered plugin has been found. + + :param str name: The name of the plugin. It is expected that + plugins are in packages matching their name, + even though the plugin class name does not + have to match. + :param str module: The name of the base module to search + for the plugin. """ + try: + if not module: + try: + module = 'sleekxmpp.plugins.%s' % name + __import__(module) + mod = sys.modules[module] + except: + module = 'sleekxmpp.features.%s' % name + __import__(module) + mod = sys.modules[module] + else: + __import__(module) + mod = sys.modules[module] + # Add older style plugins to the registry. + if hasattr(mod, name): + plugin = getattr(mod, name) + if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'): + plugin.name = name + # Mark the plugin as an older style plugin so + # we can work around dependency issues. + plugin.old_style = True + register_plugin(plugin, name) + except: + log.exception("Unable to load plugin: %s", name) + + +class PluginManager(object): def __init__(self, xmpp, config=None): + #: We will track all enabled plugins in a set so that we + #: can enable plugins in batches and pull in dependencies + #: without problems. + self._enabled = set() + + #: Maintain references to active plugins. + self._plugins = {} + + self._plugin_lock = threading.RLock() + + #: Globally set default plugin configuration. This will + #: be used for plugins that are auto-enabled through + #: dependency loading. + self.config = config if config else {} + + self.xmpp = xmpp + + def register(self, plugin, enable=True): + """Register a new plugin, and optionally enable it. + + :param class plugin: The implementation class of the plugin + to register. + :param bool enable: If ``True``, immediately enable the + plugin after registration. """ - Instantiate a new plugin and store the given configuration. + register_plugin(plugin) + if enable: + self.enable(plugin.name) + + def enable(self, name, config=None, enabled=None): + """Enable a plugin, including any dependencies. - Arguments: - xmpp -- The main SleekXMPP instance. - config -- A dictionary of configuration values. + :param string name: The short name of the plugin. + :param dict config: Optional settings dictionary for + configuring plugin behaviour. """ + top_level = False + if enabled is None: + enabled = set() + + with self._plugin_lock: + if name not in self._enabled: + enabled.add(name) + self._enabled.add(name) + if not self.registered(name): + load_plugin(name) + + plugin_class = PLUGIN_REGISTRY.get(name, None) + if not plugin_class: + raise PluginNotFound(name) + + if config is None: + config = self.config.get(name, None) + + plugin = plugin_class(self.xmpp, config) + self._plugins[name] = plugin + for dep in plugin.dependencies: + self.enable(dep, enabled=enabled) + plugin.plugin_init() + log.debug("Loaded Plugin: %s", plugin.description) + + if top_level: + for name in enabled: + if hasattr(self.plugins[name], 'old_style'): + # Older style plugins require post_init() + # to run just before stream processing begins, + # so we don't call it here. + pass + self.plugins[name].post_init() + + def enable_all(self, names=None, config=None): + """Enable all registered plugins. + + :param list names: A list of plugin names to enable. If + none are provided, all registered plugins + will be enabled. + :param dict config: A dictionary mapping plugin names to + configuration dictionaries, as used by + :meth:`~PluginManager.enable`. + """ + names = names if names else PLUGIN_REGISTRY.keys() if config is None: config = {} - self.xep = None - self.rfc = None - self.description = 'Base Plugin' - self.xmpp = xmpp - self.config = config - self.post_inited = False - self.enable = config.get('enable', True) - if self.enable: - self.plugin_init() + for name in names: + self.enable(name, config.get(name, {})) - def plugin_init(self): + def enabled(self, name): + """Check if a plugin has been enabled. + + :param string name: The name of the plugin to check. + :return: boolean + """ + return name in self._enabled + + def registered(self, name): + """Check if a plugin has been registered. + + :param string name: The name of the plugin to check. + :return: boolean + """ + return name in PLUGIN_REGISTRY + + def disable(self, name, _disabled=None): + """Disable a plugin, including any dependent upon it. + + :param string name: The name of the plugin to disable. + :param set _disabled: Private set used to track the + disabled status of plugins during + the cascading process. + """ + if _disabled is None: + _disabled = set() + with self._plugin_lock: + if name not in _disabled and name in self._enabled: + _disabled.add(name) + plugin = self._plugins.get(name, None) + if plugin is None: + raise PluginNotFound(name) + for dep in PLUGIN_DEPENDENTS[name]: + self.disable(dep, _disabled) + plugin.plugin_end() + if name in self._enabled: + self._enabled.remove(name) + del self._plugins[name] + + def __keys__(self): + """Return the set of enabled plugins.""" + return self._plugins.keys() + + def __getitem__(self, name): """ - Initialize plugin state, such as registering any stream or - event handlers, or new stanza types. + Allow plugins to be accessed through the manager as if + it were a dictionary. """ + plugin = self._plugins.get(name, None) + if plugin is None: + raise PluginNotFound(name) + return plugin + + def __iter__(self): + """Return an iterator over the set of enabled plugins.""" + return self._plugins.__iter__() + + def __len__(self): + """Return the number of enabled plugins.""" + return len(self._plugins) + + +class BasePlugin(object): + + #: A short name for the plugin based on the implemented specification. + #: For example, a plugin for XEP-0030 would use `'xep_0030'`. + name = '' + + #: A longer name for the plugin, describing its purpose. For example, + #: a plugin for XEP-0030 would use `'Service Discovery'` as its + #: description value. + description = '' + + #: Some plugins may depend on others in order to function properly. + #: Any plugin names included in :attr:`~BasePlugin.dependencies` will + #: be initialized as needed if this plugin is enabled. + dependencies = set() + + def __init__(self, xmpp, config=None): + self.xmpp = xmpp + + #: A plugin's behaviour may be configurable, in which case those + #: configuration settings will be provided as a dictionary. + self.config = config if config is not None else {} + + def plugin_init(self): + """Initialize plugin state, such as registering event handlers.""" + pass + + def plugin_end(self): + """Cleanup plugin state, and prepare for plugin removal.""" pass def post_init(self): + """Initialize any cross-plugin state. + + Only needed if the plugin has circular dependencies. """ - Perform any cross-plugin interactions, such as registering - service discovery identities or items. - """ - self.post_inited = True + pass + + +base_plugin = BasePlugin diff --git a/sleekxmpp/plugins/xep_0004/__init__.py b/sleekxmpp/plugins/xep_0004/__init__.py index aad4e15f..2cd18ec8 100644 --- a/sleekxmpp/plugins/xep_0004/__init__.py +++ b/sleekxmpp/plugins/xep_0004/__init__.py @@ -6,6 +6,17 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0004.stanza import Form from sleekxmpp.plugins.xep_0004.stanza import FormField, FieldOption -from sleekxmpp.plugins.xep_0004.dataforms import xep_0004 +from sleekxmpp.plugins.xep_0004.dataforms import XEP_0004 + + +register_plugin(XEP_0004) + + +# Retain some backwards compatibility +xep_0004 = XEP_0004 +xep_0004.makeForm = xep_0004.make_form +xep_0004.buildForm = xep_0004.build_form diff --git a/sleekxmpp/plugins/xep_0004/dataforms.py b/sleekxmpp/plugins/xep_0004/dataforms.py index 5414be5c..1097bd29 100644 --- a/sleekxmpp/plugins/xep_0004/dataforms.py +++ b/sleekxmpp/plugins/xep_0004/dataforms.py @@ -6,29 +6,27 @@ See the file LICENSE for copying permission. """ -import copy - -from sleekxmpp.thirdparty import OrderedDict - from sleekxmpp import Message -from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0004 import stanza from sleekxmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption -class xep_0004(base_plugin): +class XEP_0004(BasePlugin): + """ XEP-0004: Data Forms """ - def plugin_init(self): - self.xep = '0004' - self.description = 'Data Forms' - self.stanza = stanza + name = 'xep_0004' + description = 'XEP-0004: Data Forms' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): self.xmpp.registerHandler( Callback('Data Form', StanzaPath('message/form'), @@ -38,6 +36,8 @@ class xep_0004(base_plugin): register_stanza_plugin(Form, FormField, iterable=True) register_stanza_plugin(Message, Form) + self.xmpp['xep_0030'].add_feature('jabber:x:data') + def make_form(self, ftype='form', title='', instructions=''): f = Form() f['type'] = ftype @@ -45,16 +45,8 @@ class xep_0004(base_plugin): f['instructions'] = instructions return f - def post_init(self): - base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') - def handle_form(self, message): self.xmpp.event("message_xform", message) def build_form(self, xml): return Form(xml=xml) - - -xep_0004.makeForm = xep_0004.make_form -xep_0004.buildForm = xep_0004.build_form diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py index 8131233c..1e175966 100644 --- a/sleekxmpp/plugins/xep_0004/stanza/field.py +++ b/sleekxmpp/plugins/xep_0004/stanza/field.py @@ -79,19 +79,21 @@ class FormField(ElementBase): reqXML = self.xml.find('{%s}required' % self.namespace) return reqXML is not None - def get_value(self): + def get_value(self, convert=True): valsXML = self.xml.findall('{%s}value' % self.namespace) if len(valsXML) == 0: return None elif self._type == 'boolean': - return valsXML[0].text in self.true_values + if convert: + return valsXML[0].text in self.true_values + return valsXML[0].text elif self._type in self.multi_value_types or len(valsXML) > 1: values = [] for valXML in valsXML: if valXML.text is None: valXML.text = '' values.append(valXML.text) - if self._type == 'text-multi': + if self._type == 'text-multi' and convert: values = "\n".join(values) return values else: @@ -136,6 +138,8 @@ class FormField(ElementBase): valXML.text = '0' self.xml.append(valXML) elif self._type in self.multi_value_types or self._type in ('', None): + if isinstance(value, bool): + value = [value] if not isinstance(value, list): value = value.replace('\r', '') value = value.split('\n') diff --git a/sleekxmpp/plugins/xep_0009/__init__.py b/sleekxmpp/plugins/xep_0009/__init__.py index 2cd14170..0ce3cf2c 100644 --- a/sleekxmpp/plugins/xep_0009/__init__.py +++ b/sleekxmpp/plugins/xep_0009/__init__.py @@ -6,6 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0009 import stanza -from sleekxmpp.plugins.xep_0009.rpc import xep_0009 +from sleekxmpp.plugins.xep_0009.rpc import XEP_0009 from sleekxmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse + + +register_plugin(XEP_0009) + + +# Retain some backwards compatibility +xep_0009 = XEP_0009 diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py index b4395707..a55993ad 100644 --- a/sleekxmpp/plugins/xep_0009/binding.py +++ b/sleekxmpp/plugins/xep_0009/binding.py @@ -6,10 +6,14 @@ See the file LICENSE for copying permission. """ -from xml.etree import cElementTree as ET +from sleekxmpp.xmlstream import ET import base64 import logging import time +import sys + +if sys.version_info > (3, 0): + unicode = str log = logging.getLogger(__name__) @@ -54,7 +58,7 @@ def _py2xml(*args): boolean = ET.Element("{%s}boolean" % _namespace) boolean.text = str(int(x)) val.append(boolean) - elif type(x) is str: + elif type(x) in (str, unicode): string = ET.Element("{%s}string" % _namespace) string.text = x val.append(string) @@ -152,7 +156,7 @@ class rpctime(object): def __init__(self,data=None): #assume string data is in iso format YYYYMMDDTHH:MM:SS - if type(data) is str: + if type(data) in (str, unicode): self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S") elif type(data) is time.struct_time: self.timestamp = data diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py index 4f749f30..4e1c538b 100644 --- a/sleekxmpp/plugins/xep_0009/rpc.py +++ b/sleekxmpp/plugins/xep_0009/rpc.py @@ -6,28 +6,28 @@ See the file LICENSE for copying permission.
"""
-from sleekxmpp.plugins import base
-from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
-from sleekxmpp.stanza.iq import Iq
-from sleekxmpp.xmlstream.handler.callback import Callback
-from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
-from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
-from xml.etree import cElementTree as ET
import logging
+from sleekxmpp import Iq
+from sleekxmpp.xmlstream import ET, register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0009 import stanza
+from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
log = logging.getLogger(__name__)
+class XEP_0009(BasePlugin):
-class xep_0009(base.base_plugin):
+ name = 'xep_0009'
+ description = 'XEP-0009: Jabber-RPC'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
def plugin_init(self):
- self.xep = '0009'
- self.description = 'Jabber-RPC'
- #self.stanza = sleekxmpp.plugins.xep_0009.stanza
-
register_stanza_plugin(Iq, RPCQuery)
register_stanza_plugin(RPCQuery, MethodCall)
register_stanza_plugin(RPCQuery, MethodResponse)
@@ -51,10 +51,8 @@ class xep_0009(base.base_plugin): self.xmpp.add_event_handler('error', self._handle_error)
#self.activeCalls = []
- def post_init(self):
- base.base_plugin.post_init(self)
- self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
- self.xmpp.plugin['xep_0030'].add_identity('automation','rpc')
+ self.xmpp['xep_0030'].add_feature('jabber:iq:rpc')
+ self.xmpp['xep_0030'].add_identity('automation','rpc')
def make_iq_method_call(self, pto, pmethod, params):
iq = self.xmpp.makeIqSet()
@@ -218,4 +216,3 @@ class xep_0009(base.base_plugin): def _extract_method(self, stanza):
xml = ET.fromstring("%s" % stanza)
return xml.find("./methodCall/methodName").text
-
diff --git a/sleekxmpp/plugins/xep_0012.py b/sleekxmpp/plugins/xep_0012.py index c5532bd4..01fb60a8 100644 --- a/sleekxmpp/plugins/xep_0012.py +++ b/sleekxmpp/plugins/xep_0012.py @@ -9,11 +9,11 @@ from datetime import datetime
import logging
-from . import base
-from .. stanza.iq import Iq
-from .. xmlstream.handler.callback import Callback
-from .. xmlstream.matcher.xpath import MatchXPath
-from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin, register_plugin
+from sleekxmpp import Iq
+from sleekxmpp.xmlstream.handler.callback import Callback
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+from sleekxmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin
log = logging.getLogger(__name__)
@@ -40,12 +40,18 @@ class LastActivity(ElementBase): def del_status(self):
self.xml.text = ''
-class xep_0012(base.base_plugin):
+
+class XEP_0012(BasePlugin):
+
"""
XEP-0012 Last Activity
"""
+
+ name = 'xep_0012'
+ description = 'XEP-0012: Last Activity'
+ dependencies = set(['xep_0030'])
+
def plugin_init(self):
- self.description = "Last Activity"
self.xep = "0012"
self.xmpp.registerHandler(
@@ -57,9 +63,6 @@ class xep_0012(base.base_plugin): self.xmpp.add_event_handler('last_activity_request', self.handle_last_activity)
-
- def post_init(self):
- base.base_plugin.post_init(self)
if self.xmpp.is_component:
# We are a component, so we track the uptime
self.xmpp.add_event_handler("session_start", self._reset_uptime)
@@ -113,3 +116,7 @@ class xep_0012(base.base_plugin): id = iq.get('id')
result = iq.send()
return result['last_activity']['seconds']
+
+
+xep_0012 = XEP_0012
+register_plugin(XEP_0012)
diff --git a/sleekxmpp/plugins/xep_0030/__init__.py b/sleekxmpp/plugins/xep_0030/__init__.py index 2e183852..0d1de65b 100644 --- a/sleekxmpp/plugins/xep_0030/__init__.py +++ b/sleekxmpp/plugins/xep_0030/__init__.py @@ -6,7 +6,18 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0030 import stanza from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems from sleekxmpp.plugins.xep_0030.static import StaticDisco -from sleekxmpp.plugins.xep_0030.disco import xep_0030 +from sleekxmpp.plugins.xep_0030.disco import XEP_0030 + + +register_plugin(XEP_0030) + +# Retain some backwards compatibility +xep_0030 = XEP_0030 +XEP_0030.getInfo = XEP_0030.get_info +XEP_0030.getItems = XEP_0030.get_items +XEP_0030.make_static = XEP_0030.restore_defaults diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 53086d4e..a5e8fd1c 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -8,20 +8,19 @@ import logging -import sleekxmpp from sleekxmpp import Iq -from sleekxmpp.exceptions import XMPPError -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID -from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems +from sleekxmpp.plugins.xep_0030 import StaticDisco log = logging.getLogger(__name__) -class xep_0030(base_plugin): +class XEP_0030(BasePlugin): """ XEP-0030: Service Discovery @@ -85,14 +84,15 @@ class xep_0030(base_plugin): add_item -- """ + name = 'xep_0030' + description = 'XEP-0030: Service Discovery' + dependencies = set() + stanza = stanza + def plugin_init(self): """ Start the XEP-0030 plugin. """ - self.xep = '0030' - self.description = 'Service Discovery' - self.stanza = sleekxmpp.plugins.xep_0030.stanza - self.xmpp.register_handler( Callback('Disco Info', StanzaPath('iq/disco_info'), @@ -106,25 +106,23 @@ class xep_0030(base_plugin): register_stanza_plugin(Iq, DiscoInfo) register_stanza_plugin(Iq, DiscoItems) - self.static = StaticDisco(self.xmpp) + self.static = StaticDisco(self.xmpp, self) + + self.use_cache = self.config.get('use_cache', True) + self.wrap_results = self.config.get('wrap_results', False) + + self._disco_ops = [ + 'get_info', 'set_info', 'set_identities', 'set_features', + 'get_items', 'set_items', 'del_items', 'add_identity', + 'del_identity', 'add_feature', 'del_feature', 'add_item', + 'del_item', 'del_identities', 'del_features', 'cache_info', + 'get_cached_info', 'supports', 'has_identity'] - self._disco_ops = ['get_info', 'set_identities', 'set_features', - 'get_items', 'set_items', 'del_items', - 'add_identity', 'del_identity', 'add_feature', - 'del_feature', 'add_item', 'del_item', - 'del_identities', 'del_features'] self.default_handlers = {} self._handlers = {} for op in self._disco_ops: self._add_disco_op(op, getattr(self.static, op)) - def post_init(self): - """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) - if 'xep_0059' in self.xmpp.plugin: - register_stanza_plugin(DiscoItems, - self.xmpp['xep_0059'].stanza.Set) - def _add_disco_op(self, op, default_handler): self.default_handlers[op] = default_handler self._handlers[op] = {'global': default_handler, @@ -237,7 +235,78 @@ class xep_0030(base_plugin): self.del_node_handler(op, jid, node) self.set_node_handler(op, jid, node, self.default_handlers[op]) - def get_info(self, jid=None, node=None, local=False, **kwargs): + def supports(self, jid=None, node=None, feature=None, local=False, + cached=True, ifrom=None): + """ + Check if a JID supports a given feature. + + Return values: + True -- The feature is supported + False -- The feature is not listed as supported + None -- Nothing could be found due to a timeout + + Arguments: + jid -- Request info from this JID. + node -- The particular node to query. + feature -- The name of the feature to check. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + ifrom -- Specifiy the sender's JID. + """ + data = {'feature': feature, + 'local': local, + 'cached': cached} + return self._run_node_handler('supports', jid, node, ifrom, data) + + def has_identity(self, jid=None, node=None, category=None, itype=None, + lang=None, local=False, cached=True, ifrom=None): + """ + Check if a JID provides a given identity. + + Return values: + True -- The identity is provided + False -- The identity is not listed + None -- Nothing could be found due to a timeout + + Arguments: + jid -- Request info from this JID. + node -- The particular node to query. + category -- The category of the identity to check. + itype -- The type of the identity to check. + lang -- The language of the identity to check. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + ifrom -- Specifiy the sender's JID. + """ + data = {'category': category, + 'itype': itype, + 'lang': lang, + 'local': local, + 'cached': cached} + return self._run_node_handler('has_identity', jid, node, ifrom, data) + + def get_info(self, jid=None, node=None, local=False, + cached=None, **kwargs): """ Retrieve the disco#info results from a given JID/node combination. @@ -257,6 +326,13 @@ class xep_0030(base_plugin): no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. ifrom -- Specifiy the sender's JID. block -- If true, block and wait for the stanzas' reply. timeout -- The time in seconds to block while waiting for @@ -266,11 +342,31 @@ class xep_0030(base_plugin): received instead of blocking and waiting for the reply. """ - if local or jid is None: + if jid is not None and not isinstance(jid, JID): + jid = JID(jid) + if self.xmpp.is_component: + if jid.domain == self.xmpp.boundjid.domain: + local = True + else: + if str(jid) == str(self.xmpp.boundjid): + local = True + jid = jid.full + + if local or jid in (None, ''): log.debug("Looking up local disco#info data " + \ "for %s, node %s.", jid, node) - info = self._run_node_handler('get_info', jid, node, kwargs) - return self._fix_default_info(info) + info = self._run_node_handler('get_info', + jid, node, kwargs.get('ifrom', None), kwargs) + info = self._fix_default_info(info) + return self._wrap(kwargs.get('ifrom', None), jid, info) + + if cached: + log.debug("Looking up cached disco#info data " + \ + "for %s, node %s.", jid, node) + info = self._run_node_handler('get_cached_info', + jid, node, kwargs.get('ifrom', None), kwargs) + if info is not None: + return self._wrap(kwargs.get('ifrom', None), jid, info) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility @@ -282,6 +378,15 @@ class xep_0030(base_plugin): block=kwargs.get('block', True), callback=kwargs.get('callback', None)) + def set_info(self, jid=None, node=None, info=None): + """ + Set the disco#info data for a JID/node based on an existing + disco#info stanza. + """ + if isinstance(info, Iq): + info = info['disco_info'] + self._run_node_handler('set_info', jid, node, None, info) + def get_items(self, jid=None, node=None, local=False, **kwargs): """ Retrieve the disco#items results from a given JID/node combination. @@ -314,7 +419,9 @@ class xep_0030(base_plugin): Otherwise the parameter is ignored. """ if local or jid is None: - return self._run_node_handler('get_items', jid, node, kwargs) + items = self._run_node_handler('get_items', + jid, node, kwargs.get('ifrom', None), kwargs) + return self._wrap(kwargs.get('ifrom', None), jid, items) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility @@ -341,7 +448,7 @@ class xep_0030(base_plugin): node -- Optional node to modify. items -- A series of items in tuple format. """ - self._run_node_handler('set_items', jid, node, kwargs) + self._run_node_handler('set_items', jid, node, None, kwargs) def del_items(self, jid=None, node=None, **kwargs): """ @@ -351,7 +458,7 @@ class xep_0030(base_plugin): jid -- The JID to modify. node -- Optional node to modify. """ - self._run_node_handler('del_items', jid, node, kwargs) + self._run_node_handler('del_items', jid, node, None, kwargs) def add_item(self, jid='', name='', node=None, subnode='', ijid=None): """ @@ -372,7 +479,7 @@ class xep_0030(base_plugin): kwargs = {'ijid': jid, 'name': name, 'inode': subnode} - self._run_node_handler('add_item', ijid, node, kwargs) + self._run_node_handler('add_item', ijid, node, None, kwargs) def del_item(self, jid=None, node=None, **kwargs): """ @@ -384,7 +491,7 @@ class xep_0030(base_plugin): ijid -- The item's JID. inode -- The item's node. """ - self._run_node_handler('del_item', jid, node, kwargs) + self._run_node_handler('del_item', jid, node, None, kwargs) def add_identity(self, category='', itype='', name='', node=None, jid=None, lang=None): @@ -411,7 +518,7 @@ class xep_0030(base_plugin): 'itype': itype, 'name': name, 'lang': lang} - self._run_node_handler('add_identity', jid, node, kwargs) + self._run_node_handler('add_identity', jid, node, None, kwargs) def add_feature(self, feature, node=None, jid=None): """ @@ -423,7 +530,7 @@ class xep_0030(base_plugin): jid -- The JID to modify. """ kwargs = {'feature': feature} - self._run_node_handler('add_feature', jid, node, kwargs) + self._run_node_handler('add_feature', jid, node, None, kwargs) def del_identity(self, jid=None, node=None, **kwargs): """ @@ -437,7 +544,7 @@ class xep_0030(base_plugin): name -- Optional, human readable name for the identity. lang -- Optional, the identity's xml:lang value. """ - self._run_node_handler('del_identity', jid, node, kwargs) + self._run_node_handler('del_identity', jid, node, None, kwargs) def del_feature(self, jid=None, node=None, **kwargs): """ @@ -448,7 +555,7 @@ class xep_0030(base_plugin): node -- The node to modify. feature -- The feature's namespace. """ - self._run_node_handler('del_feature', jid, node, kwargs) + self._run_node_handler('del_feature', jid, node, None, kwargs) def set_identities(self, jid=None, node=None, **kwargs): """ @@ -463,7 +570,7 @@ class xep_0030(base_plugin): identities -- A set of identities in tuple form. lang -- Optional, xml:lang value. """ - self._run_node_handler('set_identities', jid, node, kwargs) + self._run_node_handler('set_identities', jid, node, None, kwargs) def del_identities(self, jid=None, node=None, **kwargs): """ @@ -478,7 +585,7 @@ class xep_0030(base_plugin): lang -- Optional. If given, only remove identities using this xml:lang value. """ - self._run_node_handler('del_identities', jid, node, kwargs) + self._run_node_handler('del_identities', jid, node, None, kwargs) def set_features(self, jid=None, node=None, **kwargs): """ @@ -490,7 +597,7 @@ class xep_0030(base_plugin): node -- The node to modify. features -- The new set of supported features. """ - self._run_node_handler('set_features', jid, node, kwargs) + self._run_node_handler('set_features', jid, node, None, kwargs) def del_features(self, jid=None, node=None, **kwargs): """ @@ -500,9 +607,9 @@ class xep_0030(base_plugin): jid -- The JID to modify. node -- The node to modify. """ - self._run_node_handler('del_features', jid, node, kwargs) + self._run_node_handler('del_features', jid, node, None, kwargs) - def _run_node_handler(self, htype, jid, node, data={}): + def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}): """ Execute the most specific node handler for the given JID/node combination. @@ -513,7 +620,10 @@ class xep_0030(base_plugin): node -- The node requested. data -- Optional, custom data to pass to the handler. """ - if jid is None: + if isinstance(jid, JID): + jid = jid.full + + if jid in (None, ''): if self.xmpp.is_component: jid = self.xmpp.boundjid.full else: @@ -521,14 +631,28 @@ class xep_0030(base_plugin): if node is None: node = '' - if self._handlers[htype]['node'].get((jid, node), False): - return self._handlers[htype]['node'][(jid, node)](jid, node, data) - elif self._handlers[htype]['jid'].get(jid, False): - return self._handlers[htype]['jid'][jid](jid, node, data) - elif self._handlers[htype]['global']: - return self._handlers[htype]['global'](jid, node, data) - else: - return None + try: + args = (jid, node, ifrom, data) + if self._handlers[htype]['node'].get((jid, node), False): + return self._handlers[htype]['node'][(jid, node)](*args) + elif self._handlers[htype]['jid'].get(jid, False): + return self._handlers[htype]['jid'][jid](*args) + elif self._handlers[htype]['global']: + return self._handlers[htype]['global'](*args) + else: + return None + except TypeError: + # To preserve backward compatibility, drop the ifrom parameter + # for existing handlers that don't understand it. + args = (jid, node, data) + if self._handlers[htype]['node'].get((jid, node), False): + return self._handlers[htype]['node'][(jid, node)](*args) + elif self._handlers[htype]['jid'].get(jid, False): + return self._handlers[htype]['jid'][jid](*args) + elif self._handlers[htype]['global']: + return self._handlers[htype]['global'](*args) + else: + return None def _handle_disco_info(self, iq): """ @@ -550,6 +674,7 @@ class xep_0030(base_plugin): info = self._run_node_handler('get_info', jid, iq['disco_info']['node'], + iq['from'], iq) if isinstance(info, Iq): info.send() @@ -560,8 +685,20 @@ class xep_0030(base_plugin): iq.set_payload(info.xml) iq.send() elif iq['type'] == 'result': - log.debug("Received disco info result from" + \ - "%s to %s.", iq['from'], iq['to']) + log.debug("Received disco info result from " + \ + "<%s> to <%s>.", iq['from'], iq['to']) + if self.use_cache: + log.debug("Caching disco info result from " \ + "<%s> to <%s>.", iq['from'], iq['to']) + if self.xmpp.is_component: + ito = iq['to'].full + else: + ito = None + self._run_node_handler('cache_info', + iq['from'].full, + iq['disco_info']['node'], + ito, + iq) self.xmpp.event('disco_info', iq) def _handle_disco_items(self, iq): @@ -583,6 +720,7 @@ class xep_0030(base_plugin): items = self._run_node_handler('get_items', jid, iq['disco_items']['node'], + iq['from'].full, iq) if isinstance(items, Iq): items.send() @@ -592,7 +730,7 @@ class xep_0030(base_plugin): iq.set_payload(items.xml) iq.send() elif iq['type'] == 'result': - log.debug("Received disco items result from" + \ + log.debug("Received disco items result from " + \ "%s to %s.", iq['from'], iq['to']) self.xmpp.event('disco_items', iq) @@ -607,24 +745,43 @@ class xep_0030(base_plugin): Arguments: info -- The disco#info quest (not the full Iq stanza) to modify. """ + result = info + if isinstance(info, Iq): + info = info['disco_info'] if not info['node']: if not info['identities']: if self.xmpp.is_component: - log.debug("No identity found for this entity." + \ + log.debug("No identity found for this entity. " + \ "Using default component identity.") info.add_identity('component', 'generic') else: - log.debug("No identity found for this entity." + \ + log.debug("No identity found for this entity. " + \ "Using default client identity.") info.add_identity('client', 'bot') if not info['features']: - log.debug("No features found for this entity." + \ + log.debug("No features found for this entity. " + \ "Using default disco#info feature.") info.add_feature(info.namespace) - return info + return result + def _wrap(self, ito, ifrom, payload, force=False): + """ + Ensure that results are wrapped in an Iq stanza + if self.wrap_results has been set to True. -# Retain some backwards compatibility -xep_0030.getInfo = xep_0030.get_info -xep_0030.getItems = xep_0030.get_items -xep_0030.make_static = xep_0030.restore_defaults + Arguments: + ito -- The JID to use as the 'to' value + ifrom -- The JID to use as the 'from' value + payload -- The disco data to wrap + force -- Force wrapping, regardless of self.wrap_results + """ + if (force or self.wrap_results) and not isinstance(payload, Iq): + iq = self.xmpp.Iq() + # Since we're simulating a result, we have to treat + # the 'from' and 'to' values opposite the normal way. + iq['to'] = self.xmpp.boundjid if ito is None else ito + iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom + iq['type'] = 'result' + iq.append(payload) + return iq + return payload diff --git a/sleekxmpp/plugins/xep_0030/stanza/info.py b/sleekxmpp/plugins/xep_0030/stanza/info.py index 6764acbb..25d1d07f 100644 --- a/sleekxmpp/plugins/xep_0030/stanza/info.py +++ b/sleekxmpp/plugins/xep_0030/stanza/info.py @@ -146,7 +146,7 @@ class DiscoInfo(ElementBase): return True return False - def get_identities(self, lang=None): + def get_identities(self, lang=None, dedupe=True): """ Return a set of all identities in tuple form as so: (category, type, lang, name) @@ -155,17 +155,25 @@ class DiscoInfo(ElementBase): that language. Arguments: - lang -- Optional, standard xml:lang value. + lang -- Optional, standard xml:lang value. + dedupe -- If True, de-duplicate identities, otherwise + return a list of all identities. """ - identities = set() + if dedupe: + identities = set() + else: + identities = [] for id_xml in self.findall('{%s}identity' % self.namespace): xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None) if lang is None or xml_lang == lang: - identities.add(( - id_xml.attrib['category'], - id_xml.attrib['type'], - id_xml.attrib.get('{%s}lang' % self.xml_ns, None), - id_xml.attrib.get('name', None))) + id = (id_xml.attrib['category'], + id_xml.attrib['type'], + id_xml.attrib.get('{%s}lang' % self.xml_ns, None), + id_xml.attrib.get('name', None)) + if dedupe: + identities.add(id) + else: + identities.append(id) return identities def set_identities(self, identities, lang=None): @@ -237,11 +245,17 @@ class DiscoInfo(ElementBase): return True return False - def get_features(self): + def get_features(self, dedupe=True): """Return the set of all supported features.""" - features = set() + if dedupe: + features = set() + else: + features = [] for feature_xml in self.findall('{%s}feature' % self.namespace): - features.add(feature_xml.attrib['var']) + if dedupe: + features.add(feature_xml.attrib['var']) + else: + features.append(feature_xml.attrib['var']) return features def set_features(self, features): diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py index a1fb819c..512f2336 100644 --- a/sleekxmpp/plugins/xep_0030/stanza/items.py +++ b/sleekxmpp/plugins/xep_0030/stanza/items.py @@ -6,7 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream import ElementBase, ET +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin class DiscoItems(ElementBase): @@ -78,13 +78,11 @@ class DiscoItems(ElementBase): """ if (jid, node) not in self._items: self._items.add((jid, node)) - item_xml = ET.Element('{%s}item' % self.namespace) - item_xml.attrib['jid'] = jid - if name: - item_xml.attrib['name'] = name - if node: - item_xml.attrib['node'] = node - self.xml.append(item_xml) + item = DiscoItem(parent=self) + item['jid'] = jid + item['node'] = node + item['name'] = name + self.iterables.append(item) return True return False @@ -108,11 +106,9 @@ class DiscoItems(ElementBase): def get_items(self): """Return all items.""" items = set() - for item_xml in self.findall('{%s}item' % self.namespace): - item = (item_xml.attrib['jid'], - item_xml.attrib.get('node'), - item_xml.attrib.get('name')) - items.add(item) + for item in self['substanzas']: + if isinstance(item, DiscoItem): + items.add((item['jid'], item['node'], item['name'])) return items def set_items(self, items): @@ -132,5 +128,24 @@ class DiscoItems(ElementBase): def del_items(self): """Remove all items.""" self._items = set() - for item_xml in self.findall('{%s}item' % self.namespace): - self.xml.remove(item_xml) + for item in self['substanzas']: + if isinstance(item, DiscoItem): + self.xml.remove(item.xml) + + +class DiscoItem(ElementBase): + name = 'item' + namespace = 'http://jabber.org/protocol/disco#items' + plugin_attrib = name + interfaces = set(('jid', 'node', 'name')) + + def get_node(self): + """Return the item's node name or ``None``.""" + return self._get_attr('node', None) + + def get_name(self): + """Return the item's human readable name, or ``None``.""" + return self._get_attr('name', None) + + +register_stanza_plugin(DiscoItems, DiscoItem, iterable=True) diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py index 7e7f0353..7306461a 100644 --- a/sleekxmpp/plugins/xep_0030/static.py +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -7,14 +7,11 @@ """ import logging +import threading -import sleekxmpp from sleekxmpp import Iq -from sleekxmpp.exceptions import XMPPError -from sleekxmpp.plugins.base import base_plugin -from sleekxmpp.xmlstream.handler import Callback -from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout +from sleekxmpp.xmlstream import JID from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems @@ -38,7 +35,7 @@ class StaticDisco(object): xmpp -- The main SleekXMPP object. """ - def __init__(self, xmpp): + def __init__(self, xmpp, disco): """ Create a static disco interface. Sets of disco#info and disco#items are maintained for every given JID and node @@ -50,8 +47,10 @@ class StaticDisco(object): """ self.nodes = {} self.xmpp = xmpp + self.disco = disco + self.lock = threading.RLock() - def add_node(self, jid=None, node=None): + def add_node(self, jid=None, node=None, ifrom=None): """ Create a new set of stanzas for the provided JID and node combination. @@ -60,83 +59,218 @@ class StaticDisco(object): jid -- The JID that will own the new stanzas. node -- The node that will own the new stanzas. """ - if jid is None: - jid = self.xmpp.boundjid.full - if node is None: - node = '' - if (jid, node) not in self.nodes: - self.nodes[(jid, node)] = {'info': DiscoInfo(), - 'items': DiscoItems()} - self.nodes[(jid, node)]['info']['node'] = node - self.nodes[(jid, node)]['items']['node'] = node + with self.lock: + if jid is None: + jid = self.xmpp.boundjid.full + if node is None: + node = '' + if ifrom is None: + ifrom = '' + if isinstance(ifrom, JID): + ifrom = ifrom.full + if (jid, node, ifrom) not in self.nodes: + self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(), + 'items': DiscoItems()} + self.nodes[(jid, node, ifrom)]['info']['node'] = node + self.nodes[(jid, node, ifrom)]['items']['node'] = node + + def get_node(self, jid=None, node=None, ifrom=None): + with self.lock: + if jid is None: + jid = self.xmpp.boundjid.full + if node is None: + node = '' + if ifrom is None: + ifrom = '' + if isinstance(ifrom, JID): + ifrom = ifrom.full + if (jid, node, ifrom) not in self.nodes: + self.add_node(jid, node, ifrom) + return self.nodes[(jid, node, ifrom)] + + def node_exists(self, jid=None, node=None, ifrom=None): + with self.lock: + if jid is None: + jid = self.xmpp.boundjid.full + if node is None: + node = '' + if ifrom is None: + ifrom = '' + if isinstance(ifrom, JID): + ifrom = ifrom.full + if (jid, node, ifrom) not in self.nodes: + return False + return True # ================================================================= # Node Handlers # - # Each handler accepts three arguments: jid, node, and data. - # The jid and node parameters together determine the set of - # info and items stanzas that will be retrieved or added. - # The data parameter is a dictionary with additional paramters - # that will be passed to other calls. + # Each handler accepts four arguments: jid, node, ifrom, and data. + # The jid and node parameters together determine the set of info + # and items stanzas that will be retrieved or added. Additionally, + # the ifrom value allows for cached results when results vary based + # on the requester's JID. The data parameter is a dictionary with + # additional parameters that will be passed to other calls. + # + # This implementation does not allow different responses based on + # the requester's JID, except for cached results. To do that, + # register a custom node handler. - def get_info(self, jid, node, data): + def supports(self, jid, node, ifrom, data): + """ + Check if a JID supports a given feature. + + The data parameter may provide: + feature -- The feature to check for support. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + """ + feature = data.get('feature', None) + + data = {'local': data.get('local', False), + 'cached': data.get('cached', True)} + + if not feature: + return False + + try: + info = self.disco.get_info(jid=jid, node=node, + ifrom=ifrom, **data) + info = self.disco._wrap(ifrom, jid, info, True) + features = info['disco_info']['features'] + return feature in features + except IqError: + return False + except IqTimeout: + return None + + def has_identity(self, jid, node, ifrom, data): + """ + Check if a JID has a given identity. + + The data parameter may provide: + category -- The category of the identity to check. + itype -- The type of the identity to check. + lang -- The language of the identity to check. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + """ + identity = (data.get('category', None), + data.get('itype', None), + data.get('lang', None)) + + data = {'local': data.get('local', False), + 'cached': data.get('cached', True)} + + if node in (None, ''): + info = self.caps.get_caps(jid) + if info and identity in info['identities']: + return True + + try: + info = self.disco.get_info(jid=jid, node=node, + ifrom=ifrom, **data) + info = self.disco._wrap(ifrom, jid, info, True) + trunc = lambda i: (i[0], i[1], i[2]) + return identity in map(trunc, info['disco_info']['identities']) + except IqError: + return False + except IqTimeout: + return None + + def get_info(self, jid, node, ifrom, data): """ Return the stored info data for the requested JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.nodes: - if not node: - return DiscoInfo() + with self.lock: + if not self.node_exists(jid, node): + if not node: + return DiscoInfo() + else: + raise XMPPError(condition='item-not-found') else: - raise XMPPError(condition='item-not-found') - else: - return self.nodes[(jid, node)]['info'] + return self.get_node(jid, node)['info'] - def del_info(self, jid, node, data): + def set_info(self, jid, node, ifrom, data): + """ + Set the entire info stanza for a JID/node at once. + + The data parameter is a disco#info substanza. + """ + with self.lock: + self.add_node(jid, node) + self.get_node(jid, node)['info'] = data + + def del_info(self, jid, node, ifrom, data): """ Reset the info stanza for a given JID/node combination. The data parameter is not used. """ - if (jid, node) in self.nodes: - self.nodes[(jid, node)]['info'] = DiscoInfo() + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['info'] = DiscoInfo() - def get_items(self, jid, node, data): + def get_items(self, jid, node, ifrom, data): """ Return the stored items data for the requested JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.nodes: - if not node: - return DiscoInfo() + with self.lock: + if not self.node_exists(jid, node): + if not node: + return DiscoInfo() + else: + raise XMPPError(condition='item-not-found') else: - raise XMPPError(condition='item-not-found') - else: - return self.nodes[(jid, node)]['items'] + return self.get_node(jid, node)['items'] - def set_items(self, jid, node, data): + def set_items(self, jid, node, ifrom, data): """ Replace the stored items data for a JID/node combination. - The data parameter may provided: + The data parameter may provide: items -- A set of items in tuple format. """ - items = data.get('items', set()) - self.add_node(jid, node) - self.nodes[(jid, node)]['items']['items'] = items + with self.lock: + items = data.get('items', set()) + self.add_node(jid, node) + self.get_node(jid, node)['items']['items'] = items - def del_items(self, jid, node, data): + def del_items(self, jid, node, ifrom, data): """ Reset the items stanza for a given JID/node combination. The data parameter is not used. """ - if (jid, node) in self.nodes: - self.nodes[(jid, node)]['items'] = DiscoItems() + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['items'] = DiscoItems() - def add_identity(self, jid, node, data): + def add_identity(self, jid, node, ifrom, data): """ Add a new identity to te JID/node combination. @@ -146,14 +280,15 @@ class StaticDisco(object): name -- Optional human readable name for this identity. lang -- Optional standard xml:lang value. """ - self.add_node(jid, node) - self.nodes[(jid, node)]['info'].add_identity( - data.get('category', ''), - data.get('itype', ''), - data.get('name', None), - data.get('lang', None)) + with self.lock: + self.add_node(jid, node) + self.get_node(jid, node)['info'].add_identity( + data.get('category', ''), + data.get('itype', ''), + data.get('name', None), + data.get('lang', None)) - def set_identities(self, jid, node, data): + def set_identities(self, jid, node, ifrom, data): """ Add or replace all identities for a JID/node combination. @@ -161,11 +296,12 @@ class StaticDisco(object): identities -- A list of identities in tuple form: (category, type, name, lang) """ - identities = data.get('identities', set()) - self.add_node(jid, node) - self.nodes[(jid, node)]['info']['identities'] = identities + with self.lock: + identities = data.get('identities', set()) + self.add_node(jid, node) + self.get_node(jid, node)['info']['identities'] = identities - def del_identity(self, jid, node, data): + def del_identity(self, jid, node, ifrom, data): """ Remove an identity from a JID/node combination. @@ -175,67 +311,72 @@ class StaticDisco(object): name -- Optional human readable name for this identity. lang -- Optional, standard xml:lang value. """ - if (jid, node) not in self.nodes: - return - self.nodes[(jid, node)]['info'].del_identity( - data.get('category', ''), - data.get('itype', ''), - data.get('name', None), - data.get('lang', None)) + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['info'].del_identity( + data.get('category', ''), + data.get('itype', ''), + data.get('name', None), + data.get('lang', None)) - def del_identities(self, jid, node, data): + def del_identities(self, jid, node, ifrom, data): """ Remove all identities from a JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.nodes: - return - del self.nodes[(jid, node)]['info']['identities'] + with self.lock: + if self.node_exists(jid, node): + del self.get_node(jid, node)['info']['identities'] - def add_feature(self, jid, node, data): + def add_feature(self, jid, node, ifrom, data): """ Add a feature to a JID/node combination. The data parameter should include: feature -- The namespace of the supported feature. """ - self.add_node(jid, node) - self.nodes[(jid, node)]['info'].add_feature(data.get('feature', '')) + with self.lock: + self.add_node(jid, node) + self.get_node(jid, node)['info'].add_feature( + data.get('feature', '')) - def set_features(self, jid, node, data): + def set_features(self, jid, node, ifrom, data): """ Add or replace all features for a JID/node combination. The data parameter should include: features -- The new set of supported features. """ - features = data.get('features', set()) - self.add_node(jid, node) - self.nodes[(jid, node)]['info']['features'] = features + with self.lock: + features = data.get('features', set()) + self.add_node(jid, node) + self.get_node(jid, node)['info']['features'] = features - def del_feature(self, jid, node, data): + def del_feature(self, jid, node, ifrom, data): """ Remove a feature from a JID/node combination. The data parameter should include: feature -- The namespace of the removed feature. """ - if (jid, node) not in self.nodes: - return - self.nodes[(jid, node)]['info'].del_feature(data.get('feature', '')) + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['info'].del_feature( + data.get('feature', '')) - def del_features(self, jid, node, data): + def del_features(self, jid, node, ifrom, data): """ Remove all features from a JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.nodes: - return - del self.nodes[(jid, node)]['info']['features'] + with self.lock: + if not self.node_exists(jid, node): + return + del self.get_node(jid, node)['info']['features'] - def add_item(self, jid, node, data): + def add_item(self, jid, node, ifrom, data): """ Add an item to a JID/node combination. @@ -245,13 +386,14 @@ class StaticDisco(object): non-addressable items. name -- Optional human readable name for the item. """ - self.add_node(jid, node) - self.nodes[(jid, node)]['items'].add_item( - data.get('ijid', ''), - node=data.get('inode', ''), - name=data.get('name', '')) + with self.lock: + self.add_node(jid, node) + self.get_node(jid, node)['items'].add_item( + data.get('ijid', ''), + node=data.get('inode', ''), + name=data.get('name', '')) - def del_item(self, jid, node, data): + def del_item(self, jid, node, ifrom, data): """ Remove an item from a JID/node combination. @@ -259,7 +401,38 @@ class StaticDisco(object): ijid -- JID of the item to remove. inode -- Optional extra identifying information. """ - if (jid, node) in self.nodes: - self.nodes[(jid, node)]['items'].del_item( - data.get('ijid', ''), - node=data.get('inode', None)) + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['items'].del_item( + data.get('ijid', ''), + node=data.get('inode', None)) + + def cache_info(self, jid, node, ifrom, data): + """ + Cache disco information for an external JID. + + The data parameter is the Iq result stanza + containing the disco info to cache, or + the disco#info substanza itself. + """ + with self.lock: + if isinstance(data, Iq): + data = data['disco_info'] + + self.add_node(jid, node, ifrom) + self.get_node(jid, node, ifrom)['info'] = data + + def get_cached_info(self, jid, node, ifrom, data): + """ + Retrieve cached disco info data. + + The data parameter is not used. + """ + with self.lock: + if isinstance(jid, JID): + jid = jid.full + + if not self.node_exists(jid, node, ifrom): + return None + else: + return self.get_node(jid, node, ifrom)['info'] diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py index c0c4d89d..feef5a13 100644 --- a/sleekxmpp/plugins/xep_0033.py +++ b/sleekxmpp/plugins/xep_0033.py @@ -7,155 +7,161 @@ """ import logging -from . import base -from .. xmlstream.handler.callback import Callback -from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID -from .. stanza.message import Message +from sleekxmpp import Message +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from sleekxmpp.plugins import BasePlugin, register_plugin class Addresses(ElementBase): - namespace = 'http://jabber.org/protocol/address' - name = 'addresses' - plugin_attrib = 'addresses' - interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + namespace = 'http://jabber.org/protocol/address' + name = 'addresses' + plugin_attrib = 'addresses' + interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) - def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False): - address = Address(parent=self) - address['type'] = atype - address['jid'] = jid - address['node'] = node - address['uri'] = uri - address['desc'] = desc - address['delivered'] = delivered - return address + def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False): + address = Address(parent=self) + address['type'] = atype + address['jid'] = jid + address['node'] = node + address['uri'] = uri + address['desc'] = desc + address['delivered'] = delivered + return address - def getAddresses(self, atype=None): - addresses = [] - for addrXML in self.xml.findall('{%s}address' % Address.namespace): - # ElementTree 1.2.6 does not support [@attr='value'] in findall - if atype is None or addrXML.attrib.get('type') == atype: - addresses.append(Address(xml=addrXML, parent=None)) - return addresses + def getAddresses(self, atype=None): + addresses = [] + for addrXML in self.xml.findall('{%s}address' % Address.namespace): + # ElementTree 1.2.6 does not support [@attr='value'] in findall + if atype is None or addrXML.attrib.get('type') == atype: + addresses.append(Address(xml=addrXML, parent=None)) + return addresses - def setAddresses(self, addresses, set_type=None): - self.delAddresses(set_type) - for addr in addresses: - addr = dict(addr) - # Remap 'type' to 'atype' to match the add method - if set_type is not None: - addr['type'] = set_type - curr_type = addr.get('type', None) - if curr_type is not None: - del addr['type'] - addr['atype'] = curr_type - self.addAddress(**addr) + def setAddresses(self, addresses, set_type=None): + self.delAddresses(set_type) + for addr in addresses: + addr = dict(addr) + # Remap 'type' to 'atype' to match the add method + if set_type is not None: + addr['type'] = set_type + curr_type = addr.get('type', None) + if curr_type is not None: + del addr['type'] + addr['atype'] = curr_type + self.addAddress(**addr) - def delAddresses(self, atype=None): - if atype is None: - return - for addrXML in self.xml.findall('{%s}address' % Address.namespace): - # ElementTree 1.2.6 does not support [@attr='value'] in findall - if addrXML.attrib.get('type') == atype: - self.xml.remove(addrXML) + def delAddresses(self, atype=None): + if atype is None: + return + for addrXML in self.xml.findall('{%s}address' % Address.namespace): + # ElementTree 1.2.6 does not support [@attr='value'] in findall + if addrXML.attrib.get('type') == atype: + self.xml.remove(addrXML) - # -------------------------------------------------------------- + # -------------------------------------------------------------- - def delBcc(self): - self.delAddresses('bcc') + def delBcc(self): + self.delAddresses('bcc') - def delCc(self): - self.delAddresses('cc') + def delCc(self): + self.delAddresses('cc') - def delNoreply(self): - self.delAddresses('noreply') + def delNoreply(self): + self.delAddresses('noreply') - def delReplyroom(self): - self.delAddresses('replyroom') + def delReplyroom(self): + self.delAddresses('replyroom') - def delReplyto(self): - self.delAddresses('replyto') + def delReplyto(self): + self.delAddresses('replyto') - def delTo(self): - self.delAddresses('to') + def delTo(self): + self.delAddresses('to') - # -------------------------------------------------------------- + # -------------------------------------------------------------- - def getBcc(self): - return self.getAddresses('bcc') + def getBcc(self): + return self.getAddresses('bcc') - def getCc(self): - return self.getAddresses('cc') + def getCc(self): + return self.getAddresses('cc') - def getNoreply(self): - return self.getAddresses('noreply') + def getNoreply(self): + return self.getAddresses('noreply') - def getReplyroom(self): - return self.getAddresses('replyroom') + def getReplyroom(self): + return self.getAddresses('replyroom') - def getReplyto(self): - return self.getAddresses('replyto') + def getReplyto(self): + return self.getAddresses('replyto') - def getTo(self): - return self.getAddresses('to') + def getTo(self): + return self.getAddresses('to') - # -------------------------------------------------------------- + # -------------------------------------------------------------- - def setBcc(self, addresses): - self.setAddresses(addresses, 'bcc') + def setBcc(self, addresses): + self.setAddresses(addresses, 'bcc') - def setCc(self, addresses): - self.setAddresses(addresses, 'cc') + def setCc(self, addresses): + self.setAddresses(addresses, 'cc') - def setNoreply(self, addresses): - self.setAddresses(addresses, 'noreply') + def setNoreply(self, addresses): + self.setAddresses(addresses, 'noreply') - def setReplyroom(self, addresses): - self.setAddresses(addresses, 'replyroom') + def setReplyroom(self, addresses): + self.setAddresses(addresses, 'replyroom') - def setReplyto(self, addresses): - self.setAddresses(addresses, 'replyto') + def setReplyto(self, addresses): + self.setAddresses(addresses, 'replyto') - def setTo(self, addresses): - self.setAddresses(addresses, 'to') + def setTo(self, addresses): + self.setAddresses(addresses, 'to') class Address(ElementBase): - namespace = 'http://jabber.org/protocol/address' - name = 'address' - plugin_attrib = 'address' - interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri')) - address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) - - def getDelivered(self): - return self.xml.attrib.get('delivered', False) - - def setDelivered(self, delivered): - if delivered: - self.xml.attrib['delivered'] = "true" - else: - del self['delivered'] - - def setUri(self, uri): - if uri: - del self['jid'] - del self['node'] - self.xml.attrib['uri'] = uri - elif 'uri' in self.xml.attrib: - del self.xml.attrib['uri'] - - -class xep_0033(base.base_plugin): - """ - XEP-0033: Extended Stanza Addressing - """ - - def plugin_init(self): - self.xep = '0033' - self.description = 'Extended Stanza Addressing' - - registerStanzaPlugin(Message, Addresses) - - def post_init(self): - base.base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace) + namespace = 'http://jabber.org/protocol/address' + name = 'address' + plugin_attrib = 'address' + interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri')) + address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + + def getDelivered(self): + return self.xml.attrib.get('delivered', False) + + def setDelivered(self, delivered): + if delivered: + self.xml.attrib['delivered'] = "true" + else: + del self['delivered'] + + def setUri(self, uri): + if uri: + del self['jid'] + del self['node'] + self.xml.attrib['uri'] = uri + elif 'uri' in self.xml.attrib: + del self.xml.attrib['uri'] + + +class XEP_0033(BasePlugin): + + """ + XEP-0033: Extended Stanza Addressing + """ + + name = 'xep_0033' + description = 'XEP-0033: Extended Stanza Addressing' + dependencies = set(['xep_0033']) + + def plugin_init(self): + self.xep = '0033' + + register_stanza_plugin(Message, Addresses) + + self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace) + + +xep_0033 = XEP_0033 +register_plugin(XEP_0033) diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index ab3f750a..5035faae 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -6,14 +6,15 @@ See the file LICENSE for copying permission. """ from __future__ import with_statement -from . import base + import logging -from xml.etree import cElementTree as ET -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID -from .. stanza.presence import Presence -from .. xmlstream.handler.callback import Callback -from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.matcher.xmlmask import MatchXMLMask + +from sleekxmpp import Presence +from sleekxmpp.plugins import BasePlugin, register_plugin +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath +from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask from sleekxmpp.exceptions import IqError, IqTimeout @@ -107,18 +108,23 @@ class MUCPresence(ElementBase): log.warning("Cannot delete room through mucpresence plugin.") return self -class xep_0045(base.base_plugin): + +class XEP_0045(BasePlugin): + """ - Implements XEP-0045 Multi User Chat + Implements XEP-0045 Multi-User Chat """ + name = 'xep_0045' + description = 'XEP-0045: Multi-User Chat' + dependencies = set(['xep_0030', 'xep_0004']) + def plugin_init(self): self.rooms = {} self.ourNicks = {} self.xep = '0045' - self.description = 'Multi User Chat' # load MUC support in presence stanzas - registerStanzaPlugin(Presence, MUCPresence) + register_stanza_plugin(Presence, MUCPresence) self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence)) self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message)) self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject)) @@ -374,3 +380,7 @@ class xep_0045(base.base_plugin): if room not in self.rooms.keys(): return None return self.rooms[room].keys() + + +xep_0045 = XEP_0045 +register_plugin(XEP_0045) diff --git a/sleekxmpp/plugins/xep_0047/__init__.py b/sleekxmpp/plugins/xep_0047/__init__.py new file mode 100644 index 00000000..5cd7df2e --- /dev/null +++ b/sleekxmpp/plugins/xep_0047/__init__.py @@ -0,0 +1,21 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0047 import stanza +from sleekxmpp.plugins.xep_0047.stanza import Open, Close, Data +from sleekxmpp.plugins.xep_0047.stream import IBBytestream +from sleekxmpp.plugins.xep_0047.ibb import XEP_0047 + + +register_plugin(XEP_0047) + + +# Retain some backwards compatibility +xep_0047 = XEP_0047 diff --git a/sleekxmpp/plugins/xep_0047/ibb.py b/sleekxmpp/plugins/xep_0047/ibb.py new file mode 100644 index 00000000..c8a4b5e7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0047/ibb.py @@ -0,0 +1,148 @@ +import uuid +import logging +import threading + +from sleekxmpp import Message, Iq +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0047 import stanza, Open, Close, Data, IBBytestream + + +log = logging.getLogger(__name__) + + +class XEP_0047(BasePlugin): + + name = 'xep_0047' + description = 'XEP-0047: In-band Bytestreams' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + self.streams = {} + self.pending_streams = {3: 5} + self.pending_close_streams = {} + self._stream_lock = threading.Lock() + + self.max_block_size = self.config.get('max_block_size', 8192) + self.window_size = self.config.get('window_size', 1) + self.auto_accept = self.config.get('auto_accept', True) + self.accept_stream = self.config.get('accept_stream', None) + + register_stanza_plugin(Iq, Open) + register_stanza_plugin(Iq, Close) + register_stanza_plugin(Iq, Data) + + self.xmpp.register_handler(Callback( + 'IBB Open', + StanzaPath('iq@type=set/ibb_open'), + self._handle_open_request)) + + self.xmpp.register_handler(Callback( + 'IBB Close', + StanzaPath('iq@type=set/ibb_close'), + self._handle_close)) + + self.xmpp.register_handler(Callback( + 'IBB Data', + StanzaPath('iq@type=set/ibb_data'), + self._handle_data)) + + self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb') + + def _accept_stream(self, iq): + if self.accept_stream is not None: + return self.accept_stream(iq) + if self.auto_accept: + if iq['ibb_open']['block_size'] <= self.max_block_size: + return True + return False + + def open_stream(self, jid, block_size=4096, sid=None, window=1, + ifrom=None, block=True, timeout=None, callback=None): + if sid is None: + sid = str(uuid.uuid4()) + + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = jid + iq['from'] = ifrom + iq['ibb_open']['block_size'] = block_size + iq['ibb_open']['sid'] = sid + iq['ibb_open']['stanza'] = 'iq' + + stream = IBBytestream(self.xmpp, sid, block_size, + iq['to'], iq['from'], window) + + with self._stream_lock: + self.pending_streams[iq['id']] = stream + + self.pending_streams[iq['id']] = stream + + if block: + resp = iq.send(timeout=timeout) + self._handle_opened_stream(resp) + return stream + else: + cb = None + if callback is not None: + def chained(resp): + self._handle_opened_stream(resp) + callback(resp) + cb = chained + else: + cb = self._handle_opened_stream + return iq.send(block=block, timeout=timeout, callback=cb) + + def _handle_opened_stream(self, iq): + if iq['type'] == 'result': + with self._stream_lock: + stream = self.pending_streams.get(iq['id'], None) + if stream is not None: + stream.sender = iq['to'] + stream.receiver = iq['from'] + stream.stream_started.set() + self.streams[stream.sid] = stream + self.xmpp.event('ibb_stream_start', stream) + + with self._stream_lock: + if iq['id'] in self.pending_streams: + del self.pending_streams[iq['id']] + + def _handle_open_request(self, iq): + sid = iq['ibb_open']['sid'] + size = iq['ibb_open']['block_size'] + if not self._accept_stream(iq): + raise XMPPError('not-acceptable') + + if size > self.max_block_size: + raise XMPPError('resource-constraint') + + stream = IBBytestream(self.xmpp, sid, size, + iq['from'], iq['to'], + self.window_size) + stream.stream_started.set() + self.streams[sid] = stream + iq.reply() + iq.send() + + self.xmpp.event('ibb_stream_start', stream) + + def _handle_data(self, iq): + sid = iq['ibb_data']['sid'] + stream = self.streams.get(sid, None) + if stream is not None and iq['from'] != stream.sender: + stream._recv_data(iq) + else: + raise XMPPError('item-not-found') + + def _handle_close(self, iq): + sid = iq['ibb_close']['sid'] + stream = self.streams.get(sid, None) + if stream is not None and iq['from'] != stream.sender: + stream._closed(iq) + else: + raise XMPPError('item-not-found') diff --git a/sleekxmpp/plugins/xep_0047/stanza.py b/sleekxmpp/plugins/xep_0047/stanza.py new file mode 100644 index 00000000..afba07a8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0047/stanza.py @@ -0,0 +1,67 @@ +import re +import base64 + +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.thirdparty.suelta.util import bytes + + +VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*') + + +def to_b64(data): + return bytes(base64.b64encode(bytes(data))).decode('utf-8') + + +def from_b64(data): + return bytes(base64.b64decode(bytes(data))).decode('utf-8') + + +class Open(ElementBase): + name = 'open' + namespace = 'http://jabber.org/protocol/ibb' + plugin_attrib = 'ibb_open' + interfaces = set(('block_size', 'sid', 'stanza')) + + def get_block_size(self): + return int(self._get_attr('block-size')) + + def set_block_size(self, value): + self._set_attr('block-size', str(value)) + + def del_block_size(self): + self._del_attr('block-size') + + +class Data(ElementBase): + name = 'data' + namespace = 'http://jabber.org/protocol/ibb' + plugin_attrib = 'ibb_data' + interfaces = set(('seq', 'sid', 'data')) + sub_interfaces = set(['data']) + + def get_seq(self): + return int(self._get_attr('seq', '0')) + + def set_seq(self, value): + self._set_attr('seq', str(value)) + + def get_data(self): + b64_data = self.xml.text.strip() + if VALID_B64.match(b64_data).group() == b64_data: + return from_b64(b64_data) + else: + raise XMPPError('not-acceptable') + + def set_data(self, value): + self.xml.text = to_b64(value) + + def del_data(self): + self.xml.text = '' + + +class Close(ElementBase): + name = 'close' + namespace = 'http://jabber.org/protocol/ibb' + plugin_attrib = 'ibb_close' + interfaces = set(['sid']) diff --git a/sleekxmpp/plugins/xep_0047/stream.py b/sleekxmpp/plugins/xep_0047/stream.py new file mode 100644 index 00000000..49f56f36 --- /dev/null +++ b/sleekxmpp/plugins/xep_0047/stream.py @@ -0,0 +1,137 @@ +import socket +import threading +import logging +try: + import queue +except ImportError: + import Queue as queue + +from sleekxmpp.exceptions import XMPPError + + +log = logging.getLogger(__name__) + + +class IBBytestream(object): + + def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1): + self.xmpp = xmpp + self.sid = sid + self.block_size = block_size + self.window_size = window_size + + self.receiver = to + self.sender = ifrom + + self.send_seq = -1 + self.recv_seq = -1 + + self._send_seq_lock = threading.Lock() + self._recv_seq_lock = threading.Lock() + + self.stream_started = threading.Event() + self.stream_in_closed = threading.Event() + self.stream_out_closed = threading.Event() + + self.recv_queue = queue.Queue() + + self.send_window = threading.BoundedSemaphore(value=self.window_size) + self.window_ids = set() + self.window_empty = threading.Event() + self.window_empty.set() + + def send(self, data): + if not self.stream_started.is_set() or \ + self.stream_out_closed.is_set(): + raise socket.error + data = data[0:self.block_size] + self.send_window.acquire() + with self._send_seq_lock: + self.send_seq = (self.send_seq + 1) % 65535 + seq = self.send_seq + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = self.receiver + iq['from'] = self.sender + iq['ibb_data']['sid'] = self.sid + iq['ibb_data']['seq'] = seq + iq['ibb_data']['data'] = data + self.window_empty.clear() + self.window_ids.add(iq['id']) + iq.send(block=False, callback=self._recv_ack) + return len(data) + + def sendall(self, data): + sent_len = 0 + while sent_len < len(data): + sent_len += self.send(data[sent_len:]) + + def _recv_ack(self, iq): + self.window_ids.remove(iq['id']) + if not self.window_ids: + self.window_empty.set() + self.send_window.release() + if iq['type'] == 'error': + self.close() + + def _recv_data(self, iq): + with self._recv_seq_lock: + new_seq = iq['ibb_data']['seq'] + if new_seq != (self.recv_seq + 1) % 65535: + self.close() + raise XMPPError('unexpected-request') + self.recv_seq = new_seq + + data = iq['ibb_data']['data'] + if len(data) > self.block_size: + self.close() + raise XMPPError('not-acceptable') + + self.recv_queue.put(data) + self.xmpp.event('ibb_stream_data', {'stream': self, 'data': data}) + iq.reply() + iq.send() + + def recv(self, *args, **kwargs): + return self.read(block=True) + + def read(self, block=True, timeout=None, **kwargs): + if not self.stream_started.is_set() or \ + self.stream_in_closed.is_set(): + raise socket.error + if timeout is not None: + block = True + try: + return self.recv_queue.get(block, timeout) + except: + return None + + def close(self): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = self.receiver + iq['from'] = self.sender + iq['ibb_close']['sid'] = self.sid + self.stream_out_closed.set() + iq.send(block=False, + callback=lambda x: self.stream_in_closed.set()) + self.xmpp.event('ibb_stream_end', self) + + def _closed(self, iq): + self.stream_in_closed.set() + self.stream_out_closed.set() + while not self.window_empty.is_set(): + log.info('waiting for send window to empty') + self.window_empty.wait(timeout=1) + iq.reply() + iq.send() + self.xmpp.event('ibb_stream_end', self) + + def makefile(self, *args, **kwargs): + return self + + def connect(*args, **kwargs): + return None + + def shutdown(self, *args, **kwargs): + return None diff --git a/sleekxmpp/plugins/xep_0050/__init__.py b/sleekxmpp/plugins/xep_0050/__init__.py index 99f44f2a..640b182d 100644 --- a/sleekxmpp/plugins/xep_0050/__init__.py +++ b/sleekxmpp/plugins/xep_0050/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0050.stanza import Command -from sleekxmpp.plugins.xep_0050.adhoc import xep_0050 +from sleekxmpp.plugins.xep_0050.adhoc import XEP_0050 + + +register_plugin(XEP_0050) + + +# Retain some backwards compatibility +xep_0050 = XEP_0050 diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py index ec7b7041..8f2ea5c2 100644 --- a/sleekxmpp/plugins/xep_0050/adhoc.py +++ b/sleekxmpp/plugins/xep_0050/adhoc.py @@ -14,7 +14,7 @@ from sleekxmpp.exceptions import IqError from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream import register_stanza_plugin, JID -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0050 import stanza from sleekxmpp.plugins.xep_0050 import Command from sleekxmpp.plugins.xep_0004 import Form @@ -23,7 +23,7 @@ from sleekxmpp.plugins.xep_0004 import Form log = logging.getLogger(__name__) -class xep_0050(base_plugin): +class XEP_0050(BasePlugin): """ XEP-0050: Ad-Hoc Commands @@ -78,12 +78,13 @@ class xep_0050(base_plugin): terminate_command -- Command user API: delete a command's session """ + name = 'xep_0050' + description = 'XEP-0050: Ad-Hoc Commands' + dependencies = set(['xep_0030', 'xep_0004']) + stanza = stanza + def plugin_init(self): """Start the XEP-0050 plugin.""" - self.xep = '0050' - self.description = 'Ad-Hoc Commands' - self.stanza = stanza - self.threaded = self.config.get('threaded', True) self.commands = {} self.sessions = self.config.get('session_db', {}) @@ -109,10 +110,8 @@ class xep_0050(base_plugin): self._handle_command_complete, threaded=self.threaded) - def post_init(self): - """Handle cross-plugin interactions.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Command.namespace) + self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple()) def set_backend(self, db): """ @@ -369,7 +368,6 @@ class xep_0050(base_plugin): del self.sessions[sessionid] - # ================================================================= # Client side (command user) API diff --git a/sleekxmpp/plugins/xep_0059/__init__.py b/sleekxmpp/plugins/xep_0059/__init__.py index 3a9b8edf..3464ce32 100644 --- a/sleekxmpp/plugins/xep_0059/__init__.py +++ b/sleekxmpp/plugins/xep_0059/__init__.py @@ -6,5 +6,13 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0059.stanza import Set -from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, xep_0059 +from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, XEP_0059 + + +register_plugin(XEP_0059) + +# Retain some backwards compatibility +xep_0059 = XEP_0059 diff --git a/sleekxmpp/plugins/xep_0059/rsm.py b/sleekxmpp/plugins/xep_0059/rsm.py index 35908473..9335ed22 100644 --- a/sleekxmpp/plugins/xep_0059/rsm.py +++ b/sleekxmpp/plugins/xep_0059/rsm.py @@ -10,9 +10,10 @@ import logging import sleekxmpp from sleekxmpp import Iq -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin, register_plugin from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins.xep_0059 import Set +from sleekxmpp.plugins.xep_0059 import stanza, Set +from sleekxmpp.exceptions import XMPPError log = logging.getLogger(__name__) @@ -70,38 +71,49 @@ class ResultIterator(): elif self.start: self.query[self.interface]['rsm']['after'] = self.start - r = self.query.send(block=True) + try: + r = self.query.send(block=True) - if not r or not r[self.interface]['rsm']['first'] and \ - not r[self.interface]['rsm']['last']: - raise StopIteration + if not r[self.interface]['rsm']['first'] and \ + not r[self.interface]['rsm']['last']: + raise StopIteration + + if r[self.interface]['rsm']['count'] and \ + r[self.interface]['rsm']['first_index']: + count = int(r[self.interface]['rsm']['count']) + first = int(r[self.interface]['rsm']['first_index']) + num_items = len(r[self.interface]['substanzas']) + if first + num_items == count: + raise StopIteration - if self.reverse: - self.start = r[self.interface]['rsm']['first'] - else: - self.start = r[self.interface]['rsm']['last'] + if self.reverse: + self.start = r[self.interface]['rsm']['first'] + else: + self.start = r[self.interface]['rsm']['last'] - return r + return r + except XMPPError: + raise StopIteration -class xep_0059(base_plugin): +class XEP_0059(BasePlugin): """ XEP-0050: Result Set Management """ + name = 'xep_0059' + description = 'XEP-0059: Result Set Management' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """ Start the XEP-0059 plugin. """ - self.xep = '0059' - self.description = 'Result Set Management' - self.stanza = sleekxmpp.plugins.xep_0059.stanza - - def post_init(self): - """Handle inter-plugin dependencies.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Set.namespace) + register_stanza_plugin(self.xmpp['xep_0030'].stanza.DiscoItems, + self.stanza.Set) def iterate(self, stanza, interface): """ diff --git a/sleekxmpp/plugins/xep_0060/__init__.py b/sleekxmpp/plugins/xep_0060/__init__.py index 026f7c2b..86e2f472 100644 --- a/sleekxmpp/plugins/xep_0060/__init__.py +++ b/sleekxmpp/plugins/xep_0060/__init__.py @@ -1,2 +1,19 @@ -from sleekxmpp.plugins.xep_0060.pubsub import xep_0060 +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0060.pubsub import XEP_0060 from sleekxmpp.plugins.xep_0060 import stanza + + +register_plugin(XEP_0060) + + +# Retain some backwards compatibility +xep_0060 = XEP_0060 diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py index 9e394ef2..31e59be9 100644 --- a/sleekxmpp/plugins/xep_0060/pubsub.py +++ b/sleekxmpp/plugins/xep_0060/pubsub.py @@ -9,23 +9,138 @@ import logging from sleekxmpp.xmlstream import JID -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.base import BasePlugin from sleekxmpp.plugins.xep_0060 import stanza log = logging.getLogger(__name__) -class xep_0060(base_plugin): +class XEP_0060(BasePlugin): """ XEP-0060 Publish Subscribe """ + name = 'xep_0060' + description = 'XEP-0060: Publish-Subscribe' + dependencies = set(['xep_0030', 'xep_0004']) + stanza = stanza + def plugin_init(self): - self.xep = '0060' - self.description = 'Publish-Subscribe' - self.stanza = stanza + self.node_event_map = {} + + self.xmpp.register_handler( + Callback('Pubsub Event: Items', + StanzaPath('message/pubsub_event/items'), + self._handle_event_items)) + self.xmpp.register_handler( + Callback('Pubsub Event: Purge', + StanzaPath('message/pubsub_event/purge'), + self._handle_event_purge)) + self.xmpp.register_handler( + Callback('Pubsub Event: Delete', + StanzaPath('message/pubsub_event/delete'), + self._handle_event_delete)) + self.xmpp.register_handler( + Callback('Pubsub Event: Configuration', + StanzaPath('message/pubsub_event/configuration'), + self._handle_event_configuration)) + self.xmpp.register_handler( + Callback('Pubsub Event: Subscription', + StanzaPath('message/pubsub_event/subscription'), + self._handle_event_subscription)) + + def _handle_event_items(self, msg): + """Raise events for publish and retraction notifications.""" + node = msg['pubsub_event']['items']['node'] + + multi = len(msg['pubsub_event']['items']) > 1 + values = {} + if multi: + values = msg.values + del values['pubsub_event'] + + for item in msg['pubsub_event']['items']: + event_name = self.node_event_map.get(node, None) + event_type = 'publish' + if item.name == 'retract': + event_type = 'retract' + + if multi: + condensed = self.xmpp.Message() + condensed.values = values + condensed['pubsub_event']['items']['node'] = node + condensed['pubsub_event']['items'].append(item) + self.xmpp.event('pubsub_%s' % event_type, msg) + if event_name: + self.xmpp.event('%s_%s' % (event_name, event_type), + condensed) + else: + self.xmpp.event('pubsub_%s' % event_type, msg) + if event_name: + self.xmpp.event('%s_%s' % (event_name, event_type), msg) + + def _handle_event_purge(self, msg): + """Raise events for node purge notifications.""" + node = msg['pubsub_event']['purge']['node'] + event_name = self.node_event_map.get(node, None) + + self.xmpp.event('pubsub_purge', msg) + if event_name: + self.xmpp.event('%s_purge' % event_name, msg) + + def _handle_event_delete(self, msg): + """Raise events for node deletion notifications.""" + node = msg['pubsub_event']['delete']['node'] + event_name = self.node_event_map.get(node, None) + + self.xmpp.event('pubsub_delete', msg) + if event_name: + self.xmpp.event('%s_delete' % event_name, msg) + + def _handle_event_configuration(self, msg): + """Raise events for node configuration notifications.""" + node = msg['pubsub_event']['configuration']['node'] + event_name = self.node_event_map.get(node, None) + + self.xmpp.event('pubsub_config', msg) + if event_name: + self.xmpp.event('%s_config' % event_name, msg) + + def _handle_event_subscription(self, msg): + """Raise events for node subscription notifications.""" + node = msg['pubsub_event']['subscription']['node'] + event_name = self.node_event_map.get(node, None) + + self.xmpp.event('pubsub_subscription', msg) + if event_name: + self.xmpp.event('%s_subscription' % event_name, msg) + + def map_node_event(self, node, event_name): + """ + Map node names to events. + + When a pubsub event is received for the given node, + raise the provided event. + + For example:: + + map_node_event('http://jabber.org/protocol/tune', + 'user_tune') + + will produce the events 'user_tune_publish' and 'user_tune_retract' + when the respective notifications are received from the node + 'http://jabber.org/protocol/tune', among other events. + + Arguments: + node -- The node name to map to an event. + event_name -- The name of the event to raise when a + notification from the given node is received. + """ + self.node_event_map[node] = event_name def create_node(self, jid, node, config=None, ntype=None, ifrom=None, block=True, callback=None, timeout=None): @@ -98,8 +213,9 @@ class xep_0060(base_plugin): ifrom -- Specify the sender's JID. block -- Specify if the send call will block until a response is received, or a timeout occurs. Defaults to True. - timeout -- The length of time (in seconds) to wait for a response - before exiting the send call if blocking is used. + timeout -- The length of time (in seconds) to wait for a + response before exiting the send call if blocking + is used. Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. @@ -146,8 +262,9 @@ class xep_0060(base_plugin): ifrom -- Specify the sender's JID. block -- Specify if the send call will block until a response is received, or a timeout occurs. Defaults to True. - timeout -- The length of time (in seconds) to wait for a response - before exiting the send call if blocking is used. + timeout -- The length of time (in seconds) to wait for a + response before exiting the send call if blocking + is used. Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. @@ -183,8 +300,9 @@ class xep_0060(base_plugin): iq['pubsub']['affiliations']['node'] = node return iq.send(block=block, callback=callback, timeout=timeout) - def get_subscription_options(self, jid, node=None, user_jid=None, ifrom=None, - block=True, callback=None, timeout=None): + def get_subscription_options(self, jid, node=None, user_jid=None, + ifrom=None, block=True, callback=None, + timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') if user_jid is None: iq['pubsub']['default']['node'] = node @@ -364,7 +482,7 @@ class xep_0060(base_plugin): """ Discover the nodes provided by a Pubsub service, using disco. """ - return self.xmpp.plugin['xep_0030'].get_items(*args, **kwargs) + return self.xmpp['xep_0030'].get_items(*args, **kwargs) def get_item(self, jid, node, item_id, ifrom=None, block=True, callback=None, timeout=None): @@ -372,7 +490,7 @@ class xep_0060(base_plugin): Retrieve the content of an individual item. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') - item = self.stanza.Item() + item = stanza.Item() item['id'] = item_id iq['pubsub']['items']['node'] = node iq['pubsub']['items'].append(item) @@ -396,7 +514,7 @@ class xep_0060(base_plugin): if item_ids is not None: for item_id in item_ids: - item = self.stanza.Item() + item = stanza.Item() item['id'] = item_id iq['pubsub']['items'].append(item) @@ -410,12 +528,12 @@ class xep_0060(base_plugin): """ Retrieve the ItemIDs hosted by a given node, using disco. """ - return self.xmpp.plugin['xep_0030'].get_items(jid, node, - ifrom=ifrom, - block=block, - callback=callback, - timeout=timeout, - iterator=iterator) + return self.xmpp['xep_0030'].get_items(jid, node, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout, + iterator=iterator) def modify_affiliations(self, jid, node, affiliations=None, ifrom=None, block=True, callback=None, timeout=None): @@ -426,7 +544,7 @@ class xep_0060(base_plugin): affiliations = [] for jid, affiliation in affiliations: - aff = self.stanza.OwnerAffiliation() + aff = stanza.OwnerAffiliation() aff['jid'] = jid aff['affiliation'] = affiliation iq['pubsub_owner']['affiliations'].append(aff) @@ -442,7 +560,7 @@ class xep_0060(base_plugin): subscriptions = [] for jid, subscription in subscriptions: - sub = self.stanza.OwnerSubscription() + sub = stanza.OwnerSubscription() sub['jid'] = jid sub['subscription'] = subscription iq['pubsub_owner']['subscriptions'].append(sub) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py index c7263577..c0d4929e 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py @@ -6,23 +6,26 @@ See the file LICENSE for copying permission. """ +import datetime as dt + from sleekxmpp import Message from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from sleekxmpp.plugins.xep_0004 import Form +from sleekxmpp.plugins import xep_0082 class Event(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'event' plugin_attrib = 'pubsub_event' - interfaces = set(('node',)) + interfaces = set() class EventItem(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'item' plugin_attrib = name - interfaces = set(('id', 'payload')) + interfaces = set(('id', 'payload', 'node', 'publisher')) def set_payload(self, value): self.xml.append(value) @@ -76,7 +79,7 @@ class EventConfiguration(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'configuration' plugin_attrib = name - interfaces = set(('node', 'config')) + interfaces = set(('node',)) class EventPurge(ElementBase): @@ -86,12 +89,47 @@ class EventPurge(ElementBase): interfaces = set(('node',)) +class EventDelete(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'delete' + plugin_attrib = name + interfaces = set(('node', 'redirect')) + + def set_redirect(self, uri): + del self['redirect'] + redirect = ET.Element('{%s}redirect' % self.namespace) + redirect.attrib['uri'] = uri + self.xml.append(redirect) + + def get_redirect(self): + redirect = self.xml.find('{%s}redirect' % self.namespace) + if redirect is not None: + return redirect.attrib.get('uri', '') + return '' + + def del_redirect(self): + redirect = self.xml.find('{%s}redirect' % self.namespace) + if redirect is not None: + self.xml.remove(redirect) + + class EventSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'subscription' plugin_attrib = name interfaces = set(('node', 'expiry', 'jid', 'subid', 'subscription')) + def get_expiry(self): + expiry = self._get_attr('expiry') + if expiry.lower() == 'presence': + return expiry + return xep_0082.parse(expiry) + + def set_expiry(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('expiry', value) + def set_jid(self, value): self._set_attr('jid', str(value)) @@ -102,8 +140,9 @@ class EventSubscription(ElementBase): register_stanza_plugin(Message, Event) register_stanza_plugin(Event, EventCollection) register_stanza_plugin(Event, EventConfiguration) -register_stanza_plugin(Event, EventItems) register_stanza_plugin(Event, EventPurge) +register_stanza_plugin(Event, EventDelete) +register_stanza_plugin(Event, EventItems) register_stanza_plugin(Event, EventSubscription) register_stanza_plugin(EventCollection, EventAssociate) register_stanza_plugin(EventCollection, EventDisassociate) diff --git a/sleekxmpp/plugins/xep_0066/__init__.py b/sleekxmpp/plugins/xep_0066/__init__.py index ebfbd0c2..68a50180 100644 --- a/sleekxmpp/plugins/xep_0066/__init__.py +++ b/sleekxmpp/plugins/xep_0066/__init__.py @@ -6,6 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0066 import stanza from sleekxmpp.plugins.xep_0066.stanza import OOB, OOBTransfer -from sleekxmpp.plugins.xep_0066.oob import xep_0066 +from sleekxmpp.plugins.xep_0066.oob import XEP_0066 + + +register_plugin(XEP_0066) + + +# Retain some backwards compatibility +xep_0066 = XEP_0066 diff --git a/sleekxmpp/plugins/xep_0066/oob.py b/sleekxmpp/plugins/xep_0066/oob.py index d1f4b3ff..dc215e83 100644 --- a/sleekxmpp/plugins/xep_0066/oob.py +++ b/sleekxmpp/plugins/xep_0066/oob.py @@ -13,19 +13,19 @@ from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0066 import stanza log = logging.getLogger(__name__) -class xep_0066(base_plugin): +class XEP_0066(BasePlugin): """ - XEP-0066: Out-of-Band Data + XEP-0066: Out of Band Data - Out-of-Band Data is a basic method for transferring files between + Out of Band Data is a basic method for transferring files between XMPP agents. The URL of the resource in question is sent to the receiving entity, which then downloads the resource before responding to the OOB request. OOB is also used as a generic means to transmit URLs in other @@ -42,11 +42,13 @@ class xep_0066(base_plugin): or other addressable resource. """ + name = 'xep_0066' + description = 'XEP-0066: Out of Band Data' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """Start the XEP-0066 plugin.""" - self.xep = '0066' - self.description = 'Out-of-Band Transfer' - self.stanza = stanza self.url_handlers = {'global': self._default_handler, 'jid': {}} @@ -60,9 +62,6 @@ class xep_0066(base_plugin): StanzaPath('iq@type=set/oob_transfer'), self._handle_transfer)) - def post_init(self): - """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace) self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace) @@ -121,7 +120,7 @@ class xep_0066(base_plugin): iq -- The Iq stanza containing the OOB transfer request. """ if iq['to'] in self.url_handlers['jid']: - return self.url_handlers['jid'][jid](iq) + return self.url_handlers['jid'][iq['to']](iq) else: if self.url_handlers['global']: self.url_handlers['global'](iq) diff --git a/sleekxmpp/plugins/xep_0077/__init__.py b/sleekxmpp/plugins/xep_0077/__init__.py new file mode 100644 index 00000000..779ae0ac --- /dev/null +++ b/sleekxmpp/plugins/xep_0077/__init__.py @@ -0,0 +1,19 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0077.stanza import Register, RegisterFeature +from sleekxmpp.plugins.xep_0077.register import XEP_0077 + + +register_plugin(XEP_0077) + + +# Retain some backwards compatibility +xep_0077 = XEP_0077 diff --git a/sleekxmpp/plugins/xep_0077/register.py b/sleekxmpp/plugins/xep_0077/register.py new file mode 100644 index 00000000..53cc9ef5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0077/register.py @@ -0,0 +1,90 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import StreamFeatures, Iq +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0077 import stanza, Register, RegisterFeature + + +log = logging.getLogger(__name__) + + +class XEP_0077(BasePlugin): + + """ + XEP-0077: In-Band Registration + """ + + name = 'xep_0077' + description = 'XEP-0077: In-Band Registration' + dependencies = set(['xep_0004', 'xep_0066']) + stanza = stanza + + def plugin_init(self): + self.create_account = self.config.get('create_account', True) + + register_stanza_plugin(StreamFeatures, RegisterFeature) + register_stanza_plugin(Iq, Register) + + if self.xmpp.is_component: + pass + else: + self.xmpp.register_feature('register', + self._handle_register_feature, + restart=False, + order=self.config.get('order', 50)) + + register_stanza_plugin(Register, self.xmpp['xep_0004'].stanza.Form) + register_stanza_plugin(Register, self.xmpp['xep_0066'].stanza.OOB) + + def _handle_register_feature(self, features): + if 'mechanisms' in self.xmpp.features: + # We have already logged in with an account + return False + + if self.create_account: + form = self.get_registration() + self.xmpp.event('register', form, direct=True) + return True + return False + + def get_registration(self, jid=None, ifrom=None, block=True, + timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['to'] = jid + iq['from'] = ifrom + iq.enable('register') + return iq.send(block=block, timeout=timeout, + callback=callback, now=True) + + def cancel_registration(self, jid=None, ifrom=None, block=True, + timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = jid + iq['from'] = ifrom + iq['register']['remove'] = True + return iq.send(block=block, timeout=timeout, callback=callback) + + def change_password(self, password, jid=None, ifrom=None, block=True, + timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = jid + iq['from'] = ifrom + if self.xmpp.is_component: + ifrom = JID(ifrom) + iq['register']['username'] = ifrom.user + else: + iq['register']['username'] = self.xmpp.boundjid.user + iq['register']['password'] = password + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0077/stanza.py b/sleekxmpp/plugins/xep_0077/stanza.py new file mode 100644 index 00000000..e06c1910 --- /dev/null +++ b/sleekxmpp/plugins/xep_0077/stanza.py @@ -0,0 +1,73 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from __future__ import unicode_literals + +from sleekxmpp.xmlstream import ElementBase, ET + + +class Register(ElementBase): + + namespace = 'jabber:iq:register' + name = 'query' + plugin_attrib = 'register' + interfaces = set(('username', 'password', 'email', 'nick', 'name', + 'first', 'last', 'address', 'city', 'state', 'zip', + 'phone', 'url', 'date', 'misc', 'text', 'key', + 'registered', 'remove', 'instructions', 'fields')) + sub_interfaces = interfaces + form_fields = set(('username', 'password', 'email', 'nick', 'name', + 'first', 'last', 'address', 'city', 'state', 'zip', + 'phone', 'url', 'date', 'misc', 'text', 'key')) + + def get_registered(self): + present = self.xml.find('{%s}registered' % self.namespace) + return present is not None + + def get_remove(self): + present = self.xml.find('{%s}remove' % self.namespace) + return present is not None + + def set_registered(self, value): + if value: + self.add_field('registered') + else: + del self['registered'] + + def set_remove(self, value): + if value: + self.add_field('remove') + else: + del self['remove'] + + def add_field(self, value): + self._set_sub_text(value, '', keep=True) + + def get_fields(self): + fields = set() + for field in self.form_fields: + if self.xml.find('{%s}%s' % (self.namespace, field)) is not None: + fields.add(field) + return fields + + def set_fields(self, fields): + del self['fields'] + for field in fields: + self._set_sub_text(field, '', keep=True) + + def del_fields(self): + for field in self.form_fields: + self._del_sub(field) + + +class RegisterFeature(ElementBase): + + name = 'register' + namespace = 'http://jabber.org/features/iq-register' + plugin_attrib = name + interfaces = set() diff --git a/sleekxmpp/plugins/xep_0078/__init__.py b/sleekxmpp/plugins/xep_0078/__init__.py index 5a2bda77..2ea72ffb 100644 --- a/sleekxmpp/plugins/xep_0078/__init__.py +++ b/sleekxmpp/plugins/xep_0078/__init__.py @@ -6,7 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0078 import stanza from sleekxmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature -from sleekxmpp.plugins.xep_0078.legacyauth import xep_0078 +from sleekxmpp.plugins.xep_0078.legacyauth import XEP_0078 + + +register_plugin(XEP_0078) + +# Retain some backwards compatibility +xep_0078 = XEP_0078 diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py index dec775a3..95587843 100644 --- a/sleekxmpp/plugins/xep_0078/legacyauth.py +++ b/sleekxmpp/plugins/xep_0078/legacyauth.py @@ -9,17 +9,19 @@ import logging import hashlib import random +import sys +from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0078 import stanza log = logging.getLogger(__name__) -class xep_0078(base_plugin): +class XEP_0078(BasePlugin): """ XEP-0078 NON-SASL Authentication @@ -28,11 +30,12 @@ class xep_0078(base_plugin): unless you are forced to use an old XMPP server implementation. """ - def plugin_init(self): - self.xep = "0078" - self.description = "Non-SASL Authentication" - self.stanza = stanza + name = 'xep_0078' + description = 'XEP-0078: Non-SASL Authentication' + dependencies = set() + stanza = stanza + def plugin_init(self): self.xmpp.register_feature('auth', self._handle_auth, restart=False, @@ -41,7 +44,6 @@ class xep_0078(base_plugin): register_stanza_plugin(Iq, stanza.IqAuth) register_stanza_plugin(StreamFeatures, stanza.AuthFeature) - def _handle_auth(self, features): # If we can or have already authenticated with SASL, do nothing. if 'mechanisms' in features['features']: diff --git a/sleekxmpp/plugins/xep_0080/__init__.py b/sleekxmpp/plugins/xep_0080/__init__.py new file mode 100644 index 00000000..cad23d22 --- /dev/null +++ b/sleekxmpp/plugins/xep_0080/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0080.stanza import Geoloc +from sleekxmpp.plugins.xep_0080.geoloc import XEP_0080 + + +register_plugin(XEP_0080) diff --git a/sleekxmpp/plugins/xep_0080/geoloc.py b/sleekxmpp/plugins/xep_0080/geoloc.py new file mode 100644 index 00000000..20dde4dd --- /dev/null +++ b/sleekxmpp/plugins/xep_0080/geoloc.py @@ -0,0 +1,122 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +import sleekxmpp +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0080 import stanza, Geoloc + + +log = logging.getLogger(__name__) + + +class XEP_0080(BasePlugin): + + """ + XEP-0080: User Location + """ + + name = 'xep_0080' + description = 'XEP-0080: User Location' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + """Start the XEP-0080 plugin.""" + self.xmpp['xep_0163'].register_pep('user_location', Geoloc) + + def publish_location(self, **kwargs): + """ + Publish the user's current location. + + Arguments: + accuracy -- Horizontal GPS error in meters. + alt -- Altitude in meters above or below sea level. + area -- A named area such as a campus or neighborhood. + bearing -- GPS bearing (direction in which the entity is + heading to reach its next waypoint), measured in + decimal degrees relative to true north. + building -- A specific building on a street or in an area. + country -- The nation where the user is located. + countrycode -- The ISO 3166 two-letter country code. + datum -- GPS datum. + description -- A natural-language name for or description of + the location. + error -- Horizontal GPS error in arc minutes. Obsoleted by + the accuracy parameter. + floor -- A particular floor in a building. + lat -- Latitude in decimal degrees North. + locality -- A locality within the administrative region, such + as a town or city. + lon -- Longitude in decimal degrees East. + postalcode -- A code used for postal delivery. + region -- An administrative region of the nation, such + as a state or province. + room -- A particular room in a building. + speed -- The speed at which the entity is moving, + in meters per second. + street -- A thoroughfare within the locality, or a crossing + of two thoroughfares. + text -- A catch-all element that captures any other + information about the location. + timestamp -- UTC timestamp specifying the moment when the + reading was taken. + uri -- A URI or URL pointing to information about + the location. + + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + options = kwargs.get('options', None) + ifrom = kwargs.get('ifrom', None) + block = kwargs.get('block', None) + callback = kwargs.get('callback', None) + timeout = kwargs.get('timeout', None) + for param in ('ifrom', 'block', 'callback', 'timeout', 'options'): + if param in kwargs: + del kwargs[param] + + geoloc = Geoloc() + geoloc.values = kwargs + + return self.xmpp['xep_0163'].publish(geoloc, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user location information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + geoloc = Geoloc() + return self.xmpp['xep_0163'].publish(geoloc, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0080/stanza.py b/sleekxmpp/plugins/xep_0080/stanza.py new file mode 100644 index 00000000..a83a8b1b --- /dev/null +++ b/sleekxmpp/plugins/xep_0080/stanza.py @@ -0,0 +1,266 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class Geoloc(ElementBase): + + """ + XMPP's <geoloc> stanza allows entities to know the current + geographical or physical location of an entity. (XEP-0080: User Location) + + Example <geoloc> stanzas: + <geoloc xmlns='http://jabber.org/protocol/geoloc'/> + + <geoloc xmlns='http://jabber.org/protocol/geoloc' xml:lang='en'> + <accuracy>20</accuracy> + <country>Italy</country> + <lat>45.44</lat> + <locality>Venice</locality> + <lon>12.33</lon> + </geoloc> + + Stanza Interface: + accuracy -- Horizontal GPS error in meters. + alt -- Altitude in meters above or below sea level. + area -- A named area such as a campus or neighborhood. + bearing -- GPS bearing (direction in which the entity is + heading to reach its next waypoint), measured in + decimal degrees relative to true north. + building -- A specific building on a street or in an area. + country -- The nation where the user is located. + countrycode -- The ISO 3166 two-letter country code. + datum -- GPS datum. + description -- A natural-language name for or description of + the location. + error -- Horizontal GPS error in arc minutes. Obsoleted by + the accuracy parameter. + floor -- A particular floor in a building. + lat -- Latitude in decimal degrees North. + locality -- A locality within the administrative region, such + as a town or city. + lon -- Longitude in decimal degrees East. + postalcode -- A code used for postal delivery. + region -- An administrative region of the nation, such + as a state or province. + room -- A particular room in a building. + speed -- The speed at which the entity is moving, + in meters per second. + street -- A thoroughfare within the locality, or a crossing + of two thoroughfares. + text -- A catch-all element that captures any other + information about the location. + timestamp -- UTC timestamp specifying the moment when the + reading was taken. + uri -- A URI or URL pointing to information about + the location. + """ + + namespace = 'http://jabber.org/protocol/geoloc' + name = 'geoloc' + interfaces = set(('accuracy', 'alt', 'area', 'bearing', 'building', + 'country', 'countrycode', 'datum', 'dscription', + 'error', 'floor', 'lat', 'locality', 'lon', + 'postalcode', 'region', 'room', 'speed', 'street', + 'text', 'timestamp', 'uri')) + sub_interfaces = interfaces + plugin_attrib = name + + def exception(self, e): + """ + Override exception passback for presence. + """ + pass + + def set_accuracy(self, accuracy): + """ + Set the value of the <accuracy> element. + + Arguments: + accuracy -- Horizontal GPS error in meters + """ + self._set_sub_text('accuracy', text=str(accuracy)) + return self + + def get_accuracy(self): + """ + Return the value of the <accuracy> element as an integer. + """ + p = self._get_sub_text('accuracy') + if not p: + return None + else: + try: + return int(p) + except ValueError: + return None + + def set_alt(self, alt): + """ + Set the value of the <alt> element. + + Arguments: + alt -- Altitude in meters above or below sea level + """ + self._set_sub_text('alt', text=str(alt)) + return self + + def get_alt(self): + """ + Return the value of the <alt> element as an integer. + """ + p = self._get_sub_text('alt') + if not p: + return None + else: + try: + return int(p) + except ValueError: + return None + + def set_bearing(self, bearing): + """ + Set the value of the <bearing> element. + + Arguments: + bearing -- GPS bearing (direction in which the entity is heading + to reach its next waypoint), measured in decimal + degrees relative to true north + """ + self._set_sub_text('bearing', text=str(bearing)) + return self + + def get_bearing(self): + """ + Return the value of the <bearing> element as a float. + """ + p = self._get_sub_text('bearing') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_error(self, error): + """ + Set the value of the <error> element. + + Arguments: + error -- Horizontal GPS error in arc minutes; this + element is deprecated in favor of <accuracy/> + """ + self._set_sub_text('error', text=str(error)) + return self + + def get_error(self): + """ + Return the value of the <error> element as a float. + """ + p = self._get_sub_text('error') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_lat(self, lat): + """ + Set the value of the <lat> element. + + Arguments: + lat -- Latitude in decimal degrees North + """ + self._set_sub_text('lat', text=str(lat)) + return self + + def get_lat(self): + """ + Return the value of the <lat> element as a float. + """ + p = self._get_sub_text('lat') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_lon(self, lon): + """ + Set the value of the <lon> element. + + Arguments: + lon -- Longitude in decimal degrees East + """ + self._set_sub_text('lon', text=str(lon)) + return self + + def get_lon(self): + """ + Return the value of the <lon> element as a float. + """ + p = self._get_sub_text('lon') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_speed(self, speed): + """ + Set the value of the <speed> element. + + Arguments: + speed -- The speed at which the entity is moving, + in meters per second + """ + self._set_sub_text('speed', text=str(speed)) + return self + + def get_speed(self): + """ + Return the value of the <speed> element as a float. + """ + p = self._get_sub_text('speed') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_timestamp(self, timestamp): + """ + Set the value of the <timestamp> element. + + Arguments: + timestamp -- UTC timestamp specifying the moment when + the reading was taken + """ + self._set_sub_text('timestamp', text=str(xep_0082.datetime(timestamp))) + return self + + def get_timestamp(self): + """ + Return the value of the <timestamp> element as a DateTime. + """ + p = self._get_sub_text('timestamp') + if not p: + return None + else: + return xep_0082.datetime(p) diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py index 25c80fd0..96eb331a 100644 --- a/sleekxmpp/plugins/xep_0082.py +++ b/sleekxmpp/plugins/xep_0082.py @@ -9,7 +9,7 @@ import logging import datetime as dt -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin, register_plugin from sleekxmpp.thirdparty import tzutc, tzoffset, parse_iso @@ -184,7 +184,8 @@ def datetime(year=None, month=None, day=None, hour=None, return value return format_datetime(value) -class xep_0082(base_plugin): + +class XEP_0082(BasePlugin): """ XEP-0082: XMPP Date and Time Profiles @@ -205,11 +206,12 @@ class xep_0082(base_plugin): parse -- Convert a time string into a Python datetime object. """ + name = 'xep_0082' + description = 'XEP-0082: XMPP Date and Time Profiles' + dependencies = set() + def plugin_init(self): """Start the XEP-0082 plugin.""" - self.xep = '0082' - self.description = 'XMPP Date and Time Profiles' - self.date = date self.datetime = datetime self.time = time @@ -217,3 +219,6 @@ class xep_0082(base_plugin): self.format_datetime = format_datetime self.format_time = format_time self.parse = parse + + +register_plugin(XEP_0082) diff --git a/sleekxmpp/plugins/xep_0085/__init__.py b/sleekxmpp/plugins/xep_0085/__init__.py index ff882f05..445d5059 100644 --- a/sleekxmpp/plugins/xep_0085/__init__.py +++ b/sleekxmpp/plugins/xep_0085/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permissio """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0085.stanza import ChatState -from sleekxmpp.plugins.xep_0085.chat_states import xep_0085 +from sleekxmpp.plugins.xep_0085.chat_states import XEP_0085 + + +register_plugin(XEP_0085) + + +# Retain some backwards compatibility +xep_0085 = XEP_0085 diff --git a/sleekxmpp/plugins/xep_0085/chat_states.py b/sleekxmpp/plugins/xep_0085/chat_states.py index e95434d2..d10b317b 100644 --- a/sleekxmpp/plugins/xep_0085/chat_states.py +++ b/sleekxmpp/plugins/xep_0085/chat_states.py @@ -13,34 +13,36 @@ from sleekxmpp.stanza import Message from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0085 import stanza, ChatState log = logging.getLogger(__name__) -class xep_0085(base_plugin): +class XEP_0085(BasePlugin): """ XEP-0085 Chat State Notifications """ - def plugin_init(self): - self.xep = '0085' - self.description = 'Chat State Notifications' - self.stanza = stanza + name = 'xep_0085' + description = 'XEP-0085: Chat State Notifications' + dependencies = set(['xep_0030']) + stanza = stanza - for state in ChatState.states: - self.xmpp.register_handler( - Callback('Chat State: %s' % state, - StanzaPath('message@chat_state=%s' % state), - self._handle_chat_state)) + def plugin_init(self): + self.xmpp.register_handler( + Callback('Chat State', + StanzaPath('message/chat_state'), + self._handle_chat_state)) - register_stanza_plugin(Message, ChatState) + register_stanza_plugin(Message, stanza.Active) + register_stanza_plugin(Message, stanza.Composing) + register_stanza_plugin(Message, stanza.Gone) + register_stanza_plugin(Message, stanza.Inactive) + register_stanza_plugin(Message, stanza.Paused) - def post_init(self): - base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace) def _handle_chat_state(self, msg): diff --git a/sleekxmpp/plugins/xep_0085/stanza.py b/sleekxmpp/plugins/xep_0085/stanza.py index 8c46758c..c2cafb19 100644 --- a/sleekxmpp/plugins/xep_0085/stanza.py +++ b/sleekxmpp/plugins/xep_0085/stanza.py @@ -38,6 +38,7 @@ class ChatState(ElementBase): namespace = 'http://jabber.org/protocol/chatstates' plugin_attrib = 'chat_state' interfaces = set(('chat_state',)) + sub_interfaces = interfaces is_extension = True states = set(('active', 'composing', 'gone', 'inactive', 'paused')) @@ -71,3 +72,23 @@ class ChatState(ElementBase): if state_xml is not None: self.xml = ET.Element('') parent.xml.remove(state_xml) + + +class Active(ChatState): + name = 'active' + + +class Composing(ChatState): + name = 'composing' + + +class Gone(ChatState): + name = 'gone' + + +class Inactive(ChatState): + name = 'inactive' + + +class Paused(ChatState): + name = 'paused' diff --git a/sleekxmpp/plugins/xep_0086/__init__.py b/sleekxmpp/plugins/xep_0086/__init__.py index b021e2b5..94600e85 100644 --- a/sleekxmpp/plugins/xep_0086/__init__.py +++ b/sleekxmpp/plugins/xep_0086/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0086.stanza import LegacyError -from sleekxmpp.plugins.xep_0086.legacy_error import xep_0086 +from sleekxmpp.plugins.xep_0086.legacy_error import XEP_0086 + + +register_plugin(XEP_0086) + + +# Retain some backwards compatibility +xep_0086 = XEP_0086 diff --git a/sleekxmpp/plugins/xep_0086/legacy_error.py b/sleekxmpp/plugins/xep_0086/legacy_error.py index 25b98c5a..bed22ee2 100644 --- a/sleekxmpp/plugins/xep_0086/legacy_error.py +++ b/sleekxmpp/plugins/xep_0086/legacy_error.py @@ -8,11 +8,11 @@ from sleekxmpp.stanza import Error
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0086 import stanza, LegacyError
-class xep_0086(base_plugin):
+class XEP_0086(BasePlugin):
"""
XEP-0086: Error Condition Mappings
@@ -33,10 +33,11 @@ class xep_0086(base_plugin): iq['error']['legacy']['condition'] = ...
"""
- def plugin_init(self):
- self.xep = '0086'
- self.description = 'Error Condition Mappings'
- self.stanza = stanza
+ name = 'xep_0086'
+ description = 'XEP-0086: Error Condition Mappings'
+ dependencies = set()
+ stanza = stanza
+ def plugin_init(self):
register_stanza_plugin(Error, LegacyError,
overrides=self.config.get('override', True))
diff --git a/sleekxmpp/plugins/xep_0092/__init__.py b/sleekxmpp/plugins/xep_0092/__init__.py index 7c5bdb76..293eaae6 100644 --- a/sleekxmpp/plugins/xep_0092/__init__.py +++ b/sleekxmpp/plugins/xep_0092/__init__.py @@ -6,6 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0092 import stanza from sleekxmpp.plugins.xep_0092.stanza import Version -from sleekxmpp.plugins.xep_0092.version import xep_0092 +from sleekxmpp.plugins.xep_0092.version import XEP_0092 + + +register_plugin(XEP_0092) + + +# Retain some backwards compatibility +xep_0092 = XEP_0092 diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index ba72a9c3..c6223c10 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -13,27 +13,28 @@ from sleekxmpp import Iq from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.base import base_plugin -from sleekxmpp.plugins.xep_0092 import Version +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0092 import Version, stanza log = logging.getLogger(__name__) -class xep_0092(base_plugin): +class XEP_0092(BasePlugin): """ XEP-0092: Software Version """ + name = 'xep_0092' + description = 'XEP-0092: Software Version' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """ Start the XEP-0092 plugin. """ - self.xep = "0092" - self.description = "Software Version" - self.stanza = sleekxmpp.plugins.xep_0092.stanza - self.name = self.config.get('name', 'SleekXMPP') self.version = self.config.get('version', sleekxmpp.__version__) self.os = self.config.get('os', '') @@ -47,11 +48,6 @@ class xep_0092(base_plugin): register_stanza_plugin(Iq, Version) - def post_init(self): - """ - Handle cross-plugin dependencies. - """ - base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version') def _handle_version(self, iq): diff --git a/sleekxmpp/plugins/xep_0107/__init__.py b/sleekxmpp/plugins/xep_0107/__init__.py new file mode 100644 index 00000000..04302df8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0107/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0107 import stanza +from sleekxmpp.plugins.xep_0107.stanza import UserMood +from sleekxmpp.plugins.xep_0107.user_mood import XEP_0107 + + +register_plugin(XEP_0107) diff --git a/sleekxmpp/plugins/xep_0107/stanza.py b/sleekxmpp/plugins/xep_0107/stanza.py new file mode 100644 index 00000000..2c5814ea --- /dev/null +++ b/sleekxmpp/plugins/xep_0107/stanza.py @@ -0,0 +1,55 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class UserMood(ElementBase): + + name = 'mood' + namespace = 'http://jabber.org/protocol/mood' + plugin_attrib = 'mood' + interfaces = set(['value', 'text']) + sub_interfaces = set(['text']) + moods = set(['afraid', 'amazed', 'amorous', 'angry', 'annoyed', 'anxious', + 'aroused', 'ashamed', 'bored', 'brave', 'calm', 'cautious', + 'cold', 'confident', 'confused', 'contemplative', 'contented', + 'cranky', 'crazy', 'creative', 'curious', 'dejected', + 'depressed', 'disappointed', 'disgusted', 'dismayed', + 'distracted', 'embarrassed', 'envious', 'excited', + 'flirtatious', 'frustrated', 'grateful', 'grieving', 'grumpy', + 'guilty', 'happy', 'hopeful', 'hot', 'humbled', 'humiliated', + 'hungry', 'hurt', 'impressed', 'in_awe', 'in_love', + 'indignant', 'interested', 'intoxicated', 'invincible', + 'jealous', 'lonely', 'lost', 'lucky', 'mean', 'moody', + 'nervous', 'neutral', 'offended', 'outraged', 'playful', + 'proud', 'relaxed', 'relieved', 'remorseful', 'restless', + 'sad', 'sarcastic', 'satisfied', 'serious', 'shocked', + 'shy', 'sick', 'sleepy', 'spontaneous', 'stressed', 'strong', + 'surprised', 'thankful', 'thirsty', 'tired', 'undefined', + 'weak', 'worried']) + + def set_value(self, value): + self.del_value() + if value in self.moods: + self._set_sub_text(value, '', keep=True) + else: + raise ValueError('Unknown mood value') + + def get_value(self): + for child in self.xml: + if child.tag.startswith('{%s}' % self.namespace): + elem_name = child.tag.split('}')[-1] + if elem_name in self.moods: + return elem_name + return '' + + def del_value(self): + curr_value = self.get_value() + if curr_value: + self._set_sub_text(curr_value, '', keep=False) diff --git a/sleekxmpp/plugins/xep_0107/user_mood.py b/sleekxmpp/plugins/xep_0107/user_mood.py new file mode 100644 index 00000000..11aaace4 --- /dev/null +++ b/sleekxmpp/plugins/xep_0107/user_mood.py @@ -0,0 +1,87 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Message +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0107 import stanza, UserMood + + +log = logging.getLogger(__name__) + + +class XEP_0107(BasePlugin): + + """ + XEP-0107: User Mood + """ + + name = 'xep_0107' + description = 'XEP-0107: User Mood' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, UserMood) + self.xmpp['xep_0163'].register_pep('user_mood', UserMood) + + def publish_mood(self, value=None, text=None, options=None, + ifrom=None, block=True, callback=None, timeout=None): + """ + Publish the user's current mood. + + Arguments: + value -- The name of the mood to publish. + text -- Optional natural-language description or reason + for the mood. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + mood = UserMood() + mood['value'] = value + mood['text'] = text + return self.xmpp['xep_0163'].publish(mood, + node=UserMood.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user mood information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + mood = UserMood() + return self.xmpp['xep_0163'].publish(mood, + node=UserMood.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0108/__init__.py b/sleekxmpp/plugins/xep_0108/__init__.py new file mode 100644 index 00000000..34d45113 --- /dev/null +++ b/sleekxmpp/plugins/xep_0108/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0108 import stanza +from sleekxmpp.plugins.xep_0108.stanza import UserActivity +from sleekxmpp.plugins.xep_0108.user_activity import XEP_0108 + + +register_plugin(XEP_0108) diff --git a/sleekxmpp/plugins/xep_0108/stanza.py b/sleekxmpp/plugins/xep_0108/stanza.py new file mode 100644 index 00000000..4dc18f43 --- /dev/null +++ b/sleekxmpp/plugins/xep_0108/stanza.py @@ -0,0 +1,83 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class UserActivity(ElementBase): + + name = 'activity' + namespace = 'http://jabber.org/protocol/activity' + plugin_attrib = 'activity' + interfaces = set(['value', 'text']) + sub_interfaces = set(['text']) + general = set(['doing_chores', 'drinking', 'eating', 'exercising', + 'grooming', 'having_appointment', 'inactive', 'relaxing', + 'talking', 'traveling', 'undefined', 'working']) + specific = set(['at_the_spa', 'brushing_teeth', 'buying_groceries', + 'cleaning', 'coding', 'commuting', 'cooking', 'cycling', + 'dancing', 'day_off', 'doing_maintenance', + 'doing_the_dishes', 'doing_the_laundry', 'driving', + 'fishing', 'gaming', 'gardening', 'getting_a_haircut', + 'going_out', 'hanging_out', 'having_a_beer', + 'having_a_snack', 'having_breakfast', 'having_coffee', + 'having_dinner', 'having_lunch', 'having_tea', 'hiding', + 'hiking', 'in_a_car', 'in_a_meeting', 'in_real_life', + 'jogging', 'on_a_bus', 'on_a_plane', 'on_a_train', + 'on_a_trip', 'on_the_phone', 'on_vacation', + 'on_video_phone', 'other', 'partying', 'playing_sports', + 'praying', 'reading', 'rehearsing', 'running', + 'running_an_errand', 'scheduled_holiday', 'shaving', + 'shopping', 'skiing', 'sleeping', 'smoking', + 'socializing', 'studying', 'sunbathing', 'swimming', + 'taking_a_bath', 'taking_a_shower', 'thinking', + 'walking', 'walking_the_dog', 'watching_a_movie', + 'watching_tv', 'working_out', 'writing']) + + def set_value(self, value): + self.del_value() + general = value + specific = None + if isinstance(value, tuple) or isinstance(value, list): + general = value[0] + specific = value[1] + + if general in self.general: + gen_xml = ET.Element('{%s}%s' % (self.namespace, general)) + if specific: + spec_xml = ET.Element('{%s}%s' % (self.namespace, specific)) + if specific in self.specific: + gen_xml.append(spec_xml) + else: + raise ValueError('Unknown specific activity') + self.xml.append(gen_xml) + else: + raise ValueError('Unknown general activity') + + def get_value(self): + general = None + specific = None + gen_xml = None + for child in self.xml: + if child.tag.startswith('{%s}' % self.namespace): + elem_name = child.tag.split('}')[-1] + if elem_name in self.general: + general = elem_name + gen_xml = child + if gen_xml is not None: + for child in gen_xml: + if child.tag.startswith('{%s}' % self.namespace): + elem_name = child.tag.split('}')[-1] + if elem_name in self.specific: + specific = elem_name + return (general, specific) + + def del_value(self): + curr_value = self.get_value() + if curr_value[0]: + self._set_sub_text(curr_value[0], '', keep=False) diff --git a/sleekxmpp/plugins/xep_0108/user_activity.py b/sleekxmpp/plugins/xep_0108/user_activity.py new file mode 100644 index 00000000..43270486 --- /dev/null +++ b/sleekxmpp/plugins/xep_0108/user_activity.py @@ -0,0 +1,84 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0108 import stanza, UserActivity + + +log = logging.getLogger(__name__) + + +class XEP_0108(BasePlugin): + + """ + XEP-0108: User Activity + """ + + name = 'xep_0108' + description = 'XEP-0108: User Activity' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + self.xmpp['xep_0163'].register_pep('user_activity', UserActivity) + + def publish_activity(self, general, specific=None, text=None, options=None, + ifrom=None, block=True, callback=None, timeout=None): + """ + Publish the user's current activity. + + Arguments: + general -- The required general category of the activity. + specific -- Optional specific activity being done as part + of the general category. + text -- Optional natural-language description or reason + for the activity. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + activity = UserActivity() + activity['value'] = (general, specific) + activity['text'] = text + return self.xmpp['xep_0163'].publish(activity, + node=UserActivity.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user activity information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + activity = UserActivity() + return self.xmpp['xep_0163'].publish(activity, + node=UserActivity.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0115/__init__.py b/sleekxmpp/plugins/xep_0115/__init__.py new file mode 100644 index 00000000..31a2c03a --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/__init__.py @@ -0,0 +1,20 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0115.stanza import Capabilities +from sleekxmpp.plugins.xep_0115.static import StaticCaps +from sleekxmpp.plugins.xep_0115.caps import XEP_0115 + + +register_plugin(XEP_0115) + + +# Retain some backwards compatibility +xep_0115 = XEP_0115 diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py new file mode 100644 index 00000000..3aa0f70f --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -0,0 +1,305 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import hashlib +import base64 + +import sleekxmpp +from sleekxmpp.stanza import StreamFeatures, Presence, Iq +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0115 import stanza, StaticCaps + + +log = logging.getLogger(__name__) + + +class XEP_0115(BasePlugin): + + """ + XEP-0115: Entity Capabalities + """ + + name = 'xep_0115' + description = 'XEP-0115: Entity Capabilities' + dependencies = set(['xep_0030', 'xep_0128', 'xep_0004']) + stanza = stanza + + def plugin_init(self): + self.hashes = {'sha-1': hashlib.sha1, + 'md5': hashlib.md5} + + self.hash = self.config.get('hash', 'sha-1') + self.caps_node = self.config.get('caps_node', None) + self.broadcast = self.config.get('broadcast', True) + + if self.caps_node is None: + ver = sleekxmpp.__version__ + self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver + + register_stanza_plugin(Presence, stanza.Capabilities) + register_stanza_plugin(StreamFeatures, stanza.Capabilities) + + self._disco_ops = ['cache_caps', + 'get_caps', + 'assign_verstring', + 'get_verstring', + 'supports', + 'has_identity'] + + self.xmpp.register_handler( + Callback('Entity Capabilites', + StanzaPath('presence/caps'), + self._handle_caps)) + + self.xmpp.add_filter('out', self._filter_add_caps) + + self.xmpp.add_event_handler('entity_caps', self._process_caps, + threaded=True) + + if not self.xmpp.is_component: + self.xmpp.register_feature('caps', + self._handle_caps_feature, + restart=False, + order=10010) + + self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) + + disco = self.xmpp['xep_0030'] + self.static = StaticCaps(self.xmpp, disco.static) + + for op in self._disco_ops: + disco._add_disco_op(op, getattr(self.static, op)) + + self._run_node_handler = disco._run_node_handler + + disco.cache_caps = self.cache_caps + disco.update_caps = self.update_caps + disco.assign_verstring = self.assign_verstring + disco.get_verstring = self.get_verstring + + def _filter_add_caps(self, stanza): + if isinstance(stanza, Presence) and self.broadcast: + ver = self.get_verstring(stanza['from']) + if ver: + stanza['caps']['node'] = self.caps_node + stanza['caps']['hash'] = self.hash + stanza['caps']['ver'] = ver + return stanza + + def _handle_caps(self, presence): + if not self.xmpp.is_component: + if presence['from'] == self.xmpp.boundjid: + return + self.xmpp.event('entity_caps', presence) + + def _handle_caps_feature(self, features): + # We already have a method to process presence with + # caps, so wrap things up and use that. + p = Presence() + p['from'] = self.xmpp.boundjid.domain + p.append(features['caps']) + self.xmpp.features.add('caps') + + self.xmpp.event('entity_caps', p) + + def _process_caps(self, pres): + if not pres['caps']['hash']: + log.debug("Received unsupported legacy caps.") + self.xmpp.event('entity_caps_legacy', pres) + return + + existing_verstring = self.get_verstring(pres['from'].full) + if str(existing_verstring) == str(pres['caps']['ver']): + return + + if pres['caps']['hash'] not in self.hashes: + try: + log.debug("Unknown caps hash: %s", pres['caps']['hash']) + self.xmpp['xep_003'].get_info(jid=pres['from'].full) + return + except XMPPError: + return + + log.debug("New caps verification string: %s", pres['caps']['ver']) + try: + caps = self.xmpp['xep_0030'].get_info( + jid=pres['from'].full, + node='%s#%s' % (pres['caps']['node'], + pres['caps']['ver'])) + + if self._validate_caps(caps['disco_info'], + pres['caps']['hash'], + pres['caps']['ver']): + self.assign_verstring(pres['from'], pres['caps']['ver']) + except XMPPError: + log.debug("Could not retrieve disco#info results for caps") + + def _validate_caps(self, caps, hash, check_verstring): + # Check Identities + full_ids = caps.get_identities(dedupe=False) + deduped_ids = caps.get_identities() + if len(full_ids) != len(deduped_ids): + log.debug("Duplicate disco identities found, invalid for caps") + return False + + # Check Features + + full_features = caps.get_features(dedupe=False) + deduped_features = caps.get_features() + if len(full_features) != len(deduped_features): + log.debug("Duplicate disco features found, invalid for caps") + return False + + # Check Forms + form_types = [] + deduped_form_types = set() + for stanza in caps['substanzas']: + if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): + if 'FORM_TYPE' in stanza['fields']: + f_type = tuple(stanza['fields']['FORM_TYPE']['value']) + form_types.append(f_type) + deduped_form_types.add(f_type) + if len(form_types) != len(deduped_form_types): + log.debug("Duplicated FORM_TYPE values, invalid for caps") + return False + + if len(f_type) > 1: + deduped_type = set(f_type) + if len(f_type) != len(deduped_type): + log.debug("Extra FORM_TYPE data, invalid for caps") + return False + + if stanza['fields']['FORM_TYPE']['type'] != 'hidden': + log.debug("Field FORM_TYPE type not 'hidden', ignoring form for caps") + caps.xml.remove(stanza.xml) + else: + log.debug("No FORM_TYPE found, ignoring form for caps") + caps.xml.remove(stanza.xml) + + verstring = self.generate_verstring(caps, hash) + if verstring != check_verstring: + log.debug("Verification strings do not match: %s, %s" % ( + verstring, check_verstring)) + return False + + self.cache_caps(verstring, caps) + return True + + def generate_verstring(self, info, hash): + hash = self.hashes.get(hash, None) + if hash is None: + return None + + S = '' + + # Convert None to '' in the identities + def clean_identity(id): + return map(lambda i: i or '', id) + identities = map(clean_identity, info['identities']) + + identities = sorted(('/'.join(i) for i in identities)) + features = sorted(info['features']) + + S += '<'.join(identities) + '<' + S += '<'.join(features) + '<' + + form_types = {} + + for stanza in info['substanzas']: + if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): + if 'FORM_TYPE' in stanza['fields']: + f_type = stanza['values']['FORM_TYPE'] + if len(f_type): + f_type = f_type[0] + if f_type not in form_types: + form_types[f_type] = [] + form_types[f_type].append(stanza) + + sorted_forms = sorted(form_types.keys()) + for f_type in sorted_forms: + for form in form_types[f_type]: + S += '%s<' % f_type + fields = sorted(form['fields'].keys()) + fields.remove('FORM_TYPE') + for field in fields: + S += '%s<' % field + vals = form['fields'][field].get_value(convert=False) + if vals is None: + S += '<' + else: + if not isinstance(vals, list): + vals = [vals] + S += '<'.join(sorted(vals)) + '<' + + binary = hash(S.encode('utf8')).digest() + return base64.b64encode(binary).decode('utf-8') + + def update_caps(self, jid=None, node=None): + try: + info = self.xmpp['xep_0030'].get_info(jid, node, local=True) + if isinstance(info, Iq): + info = info['disco_info'] + ver = self.generate_verstring(info, self.hash) + self.xmpp['xep_0030'].set_info( + jid=jid, + node='%s#%s' % (self.caps_node, ver), + info=info) + self.cache_caps(ver, info) + self.assign_verstring(jid, ver) + + if self.xmpp.session_started_event.is_set() and self.broadcast: + # Check if we've sent directed presence. If we haven't, we + # can just send a normal presence stanza. If we have, then + # we will send presence to each contact individually so + # that we don't clobber existing statuses. + directed = False + for contact in self.xmpp.roster[jid]: + if self.xmpp.roster[jid][contact].last_status is not None: + directed = True + if not directed: + self.xmpp.roster[jid].send_last_presence() + else: + for contact in self.xmpp.roster[jid]: + self.xmpp.roster[jid][contact].send_last_presence() + except XMPPError: + return + + def get_verstring(self, jid=None): + if jid in ('', None): + jid = self.xmpp.boundjid.full + if isinstance(jid, JID): + jid = jid.full + return self._run_node_handler('get_verstring', jid) + + def assign_verstring(self, jid=None, verstring=None): + if jid in (None, ''): + jid = self.xmpp.boundjid.full + if isinstance(jid, JID): + jid = jid.full + return self._run_node_handler('assign_verstring', jid, + data={'verstring': verstring}) + + def cache_caps(self, verstring=None, info=None): + data = {'verstring': verstring, 'info': info} + return self._run_node_handler('cache_caps', None, None, data=data) + + def get_caps(self, jid=None, verstring=None): + if verstring is None: + if jid is not None: + verstring = self.get_verstring(jid) + else: + return None + if isinstance(jid, JID): + jid = jid.full + data = {'verstring': verstring} + return self._run_node_handler('get_caps', jid, None, None, data) diff --git a/sleekxmpp/plugins/xep_0115/stanza.py b/sleekxmpp/plugins/xep_0115/stanza.py new file mode 100644 index 00000000..3e80b5cf --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/stanza.py @@ -0,0 +1,19 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from __future__ import unicode_literals + +from sleekxmpp.xmlstream import ElementBase + + +class Capabilities(ElementBase): + + namespace = 'http://jabber.org/protocol/caps' + name = 'c' + plugin_attrib = 'caps' + interfaces = set(('hash', 'node', 'ver', 'ext')) diff --git a/sleekxmpp/plugins/xep_0115/static.py b/sleekxmpp/plugins/xep_0115/static.py new file mode 100644 index 00000000..a0a8fb23 --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/static.py @@ -0,0 +1,146 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.xmlstream import JID +from sleekxmpp.exceptions import IqError, IqTimeout + + +log = logging.getLogger(__name__) + + +class StaticCaps(object): + + """ + Extend the default StaticDisco implementation to provide + support for extended identity information. + """ + + def __init__(self, xmpp, static): + """ + Augment the default XEP-0030 static handler object. + + Arguments: + static -- The default static XEP-0030 handler object. + """ + self.xmpp = xmpp + self.disco = self.xmpp['xep_0030'] + self.caps = self.xmpp['xep_0115'] + self.static = static + self.ver_cache = {} + self.jid_vers = {} + + def supports(self, jid, node, ifrom, data): + """ + Check if a JID supports a given feature. + + The data parameter may provide: + feature -- The feature to check for support. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + """ + feature = data.get('feature', None) + + data = {'local': data.get('local', False), + 'cached': data.get('cached', True)} + + if not feature: + return False + + if node in (None, ''): + info = self.caps.get_caps(jid) + if info and feature in info['features']: + return True + + try: + info = self.disco.get_info(jid=jid, node=node, + ifrom=ifrom, **data) + info = self.disco._wrap(ifrom, jid, info, True) + return feature in info['disco_info']['features'] + except IqError: + return False + except IqTimeout: + return None + + def has_identity(self, jid, node, ifrom, data): + """ + Check if a JID has a given identity. + + The data parameter may provide: + category -- The category of the identity to check. + itype -- The type of the identity to check. + lang -- The language of the identity to check. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + """ + identity = (data.get('category', None), + data.get('itype', None), + data.get('lang', None)) + + data = {'local': data.get('local', False), + 'cached': data.get('cached', True)} + + trunc = lambda i: (i[0], i[1], i[2]) + + if node in (None, ''): + info = self.caps.get_caps(jid) + if info and identity in map(trunc, info['identities']): + return True + + try: + info = self.disco.get_info(jid=jid, node=node, + ifrom=ifrom, **data) + info = self.disco._wrap(ifrom, jid, info, True) + return identity in map(trunc, info['disco_info']['identities']) + except IqError: + return False + except IqTimeout: + return None + + def cache_caps(self, jid, node, ifrom, data): + with self.static.lock: + verstring = data.get('verstring', None) + info = data.get('info', None) + if not verstring or not info: + return + self.ver_cache[verstring] = info + + def assign_verstring(self, jid, node, ifrom, data): + with self.static.lock: + if isinstance(jid, JID): + jid = jid.full + self.jid_vers[jid] = data.get('verstring', None) + + def get_verstring(self, jid, node, ifrom, data): + with self.static.lock: + return self.jid_vers.get(jid, None) + + def get_caps(self, jid, node, ifrom, data): + with self.static.lock: + return self.ver_cache.get(data.get('verstring', None), None) diff --git a/sleekxmpp/plugins/xep_0118/__init__.py b/sleekxmpp/plugins/xep_0118/__init__.py new file mode 100644 index 00000000..565f7844 --- /dev/null +++ b/sleekxmpp/plugins/xep_0118/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0118 import stanza +from sleekxmpp.plugins.xep_0118.stanza import UserTune +from sleekxmpp.plugins.xep_0118.user_tune import XEP_0118 + + +register_plugin(XEP_0118) diff --git a/sleekxmpp/plugins/xep_0118/stanza.py b/sleekxmpp/plugins/xep_0118/stanza.py new file mode 100644 index 00000000..80e0358a --- /dev/null +++ b/sleekxmpp/plugins/xep_0118/stanza.py @@ -0,0 +1,25 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class UserTune(ElementBase): + + name = 'tune' + namespace = 'http://jabber.org/protocol/tune' + plugin_attrib = 'tune' + interfaces = set(['artist', 'length', 'rating', 'source', + 'title', 'track', 'uri']) + sub_interfaces = interfaces + + def set_length(self, value): + self._set_sub_text('length', str(value)) + + def set_rating(self, value): + self._set_sub_text('rating', str(value)) diff --git a/sleekxmpp/plugins/xep_0118/user_tune.py b/sleekxmpp/plugins/xep_0118/user_tune.py new file mode 100644 index 00000000..c848eaa8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0118/user_tune.py @@ -0,0 +1,92 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0118 import stanza, UserTune + + +log = logging.getLogger(__name__) + + +class XEP_0118(BasePlugin): + + """ + XEP-0118: User Tune + """ + + name = 'xep_0118' + description = 'XEP-0118: User Tune' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + self.xmpp['xep_0163'].register_pep('user_tune', UserTune) + + def publish_tune(self, artist=None, length=None, rating=None, source=None, + title=None, track=None, uri=None, options=None, + ifrom=None, block=True, callback=None, timeout=None): + """ + Publish the user's current tune. + + Arguments: + artist -- The artist or performer of the song. + length -- The length of the song in seconds. + rating -- The user's rating of the song (from 1 to 10) + source -- The album name, website, or other source of the song. + title -- The title of the song. + track -- The song's track number, or other unique identifier. + uri -- A URL to more information about the song. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + tune = UserTune() + tune['artist'] = artist + tune['length'] = length + tune['rating'] = rating + tune['source'] = source + tune['title'] = title + tune['track'] = track + tune['uri'] = uri + return self.xmpp['xep_0163'].publish(tune, + node=UserTune.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user tune information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + tune = UserTune() + return self.xmpp['xep_0163'].publish(tune, + node=UserTune.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0128/__init__.py b/sleekxmpp/plugins/xep_0128/__init__.py index 3c6379a3..27c2cc33 100644 --- a/sleekxmpp/plugins/xep_0128/__init__.py +++ b/sleekxmpp/plugins/xep_0128/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0128.static import StaticExtendedDisco -from sleekxmpp.plugins.xep_0128.extended_disco import xep_0128 +from sleekxmpp.plugins.xep_0128.extended_disco import XEP_0128 + + +register_plugin(XEP_0128) + + +# Retain some backwards compatibility +xep_0128 = XEP_0128 diff --git a/sleekxmpp/plugins/xep_0128/extended_disco.py b/sleekxmpp/plugins/xep_0128/extended_disco.py index 63b3cfee..d49741de 100644 --- a/sleekxmpp/plugins/xep_0128/extended_disco.py +++ b/sleekxmpp/plugins/xep_0128/extended_disco.py @@ -11,13 +11,13 @@ import logging import sleekxmpp from sleekxmpp import Iq from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0004 import Form from sleekxmpp.plugins.xep_0030 import DiscoInfo from sleekxmpp.plugins.xep_0128 import StaticExtendedDisco -class xep_0128(base_plugin): +class XEP_0128(BasePlugin): """ XEP-0128: Service Discovery Extensions @@ -39,11 +39,12 @@ class xep_0128(base_plugin): del_extended_info -- Remove all extensions from a disco#info result. """ + name = 'xep_0128' + description = 'XEP-0128: Service Discovery Extensions' + dependencies = set(['xep_0030', 'xep_0004']) + def plugin_init(self): """Start the XEP-0128 plugin.""" - self.xep = '0128' - self.description = 'Service Discovery Extensions' - self._disco_ops = ['set_extended_info', 'add_extended_info', 'del_extended_info'] @@ -52,7 +53,6 @@ class xep_0128(base_plugin): def post_init(self): """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) self.disco = self.xmpp['xep_0030'] self.static = StaticExtendedDisco(self.disco.static) @@ -76,7 +76,7 @@ class xep_0128(base_plugin): as extended information, replacing any existing extensions. """ - self.disco._run_node_handler('set_extended_info', jid, node, kwargs) + self.disco._run_node_handler('set_extended_info', jid, node, None, kwargs) def add_extended_info(self, jid=None, node=None, **kwargs): """ @@ -88,7 +88,7 @@ class xep_0128(base_plugin): data -- Either a form, or a list of forms to add as extended information. """ - self.disco._run_node_handler('add_extended_info', jid, node, kwargs) + self.disco._run_node_handler('add_extended_info', jid, node, None, kwargs) def del_extended_info(self, jid=None, node=None, **kwargs): """ @@ -98,4 +98,4 @@ class xep_0128(base_plugin): jid -- The JID to modify. node -- The node to modify. """ - self.disco._run_node_handler('del_extended_info', jid, node, kwargs) + self.disco._run_node_handler('del_extended_info', jid, node, None, kwargs) diff --git a/sleekxmpp/plugins/xep_0128/static.py b/sleekxmpp/plugins/xep_0128/static.py index 493d9370..427011c0 100644 --- a/sleekxmpp/plugins/xep_0128/static.py +++ b/sleekxmpp/plugins/xep_0128/static.py @@ -31,42 +31,43 @@ class StaticExtendedDisco(object): """ self.static = static - def set_extended_info(self, jid, node, data): + def set_extended_info(self, jid, node, ifrom, data): """ Replace the extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ - self.del_extended_info(jid, node, data) - self.add_extended_info(jid, node, data) + with self.static.lock: + self.del_extended_info(jid, node, ifrom, data) + self.add_extended_info(jid, node, ifrom, data) - def add_extended_info(self, jid, node, data): + def add_extended_info(self, jid, node, ifrom, data): """ Add additional extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ - self.static.add_node(jid, node) + with self.static.lock: + self.static.add_node(jid, node) - forms = data.get('data', []) - if not isinstance(forms, list): - forms = [forms] + forms = data.get('data', []) + if not isinstance(forms, list): + forms = [forms] - for form in forms: - self.static.nodes[(jid, node)]['info'].append(form) + info = self.static.get_node(jid, node)['info'] + for form in forms: + info.append(form) - def del_extended_info(self, jid, node, data): + def del_extended_info(self, jid, node, ifrom, data): """ Replace the extended identity data for a JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.static.nodes: - return - - info = self.static.nodes[(jid, node)]['info'] - - for form in info['substanza']: - info.xml.remove(form.xml) + with self.static.lock: + if self.static.node_exists(jid, node): + info = self.static.get_node(jid, node)['info'] + for form in info['substanza']: + info.xml.remove(form.xml) diff --git a/sleekxmpp/plugins/xep_0163.py b/sleekxmpp/plugins/xep_0163.py new file mode 100644 index 00000000..5a6df1c8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0163.py @@ -0,0 +1,120 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.base import BasePlugin, register_plugin + + +log = logging.getLogger(__name__) + + +class XEP_0163(BasePlugin): + + """ + XEP-0163: Personal Eventing Protocol (PEP) + """ + + name = 'xep_0163' + description = 'XEP-0163: Personal Eventing Protocol (PEP)' + dependencies = set(['xep_0030', 'xep_0060', 'xep_0115']) + + def register_pep(self, name, stanza): + """ + Setup and configure events and stanza registration for + the given PEP stanza: + + - Add disco feature for the PEP content. + - Register disco interest in the PEP content. + - Map events from the PEP content's namespace to the given name. + + :param str name: The event name prefix to use for PEP events. + :param stanza: The stanza class for the PEP content. + """ + pubsub_stanza = self.xmpp['xep_0060'].stanza + register_stanza_plugin(pubsub_stanza.EventItem, stanza) + + self.add_interest(stanza.namespace) + self.xmpp['xep_0030'].add_feature(stanza.namespace) + self.xmpp['xep_0060'].map_node_event(stanza.namespace, name) + + def add_interest(self, namespace, jid=None): + """ + Mark an interest in a PEP subscription by including a disco + feature with the '+notify' extension. + + Arguments: + namespace -- The base namespace to register as an interest, such + as 'http://jabber.org/protocol/tune'. This may also + be a list of such namespaces. + jid -- Optionally specify the JID. + """ + if not isinstance(namespace, set) and not isinstance(namespace, list): + namespace = [namespace] + + for ns in namespace: + self.xmpp['xep_0030'].add_feature('%s+notify' % ns, + jid=jid) + self.xmpp['xep_0115'].update_caps(jid) + + def remove_interest(self, namespace, jid=None): + """ + Mark an interest in a PEP subscription by including a disco + feature with the '+notify' extension. + + Arguments: + namespace -- The base namespace to remove as an interest, such + as 'http://jabber.org/protocol/tune'. This may also + be a list of such namespaces. + jid -- Optionally specify the JID. + """ + if not isinstance(namespace, set) and not isinstance(namespace, list): + namespace = [namespace] + + for ns in namespace: + self.xmpp['xep_0030'].del_feature(jid=jid, + feature='%s+notify' % namespace) + self.xmpp['xep_0115'].update_caps(jid) + + def publish(self, stanza, node=None, id=None, options=None, ifrom=None, + block=True, callback=None, timeout=None): + """ + Publish a PEP update. + + This is just a (very) thin wrapper around the XEP-0060 publish() + method to set the defaults expected by PEP. + + Arguments: + stanza -- The PEP update stanza to publish. + node -- The node to publish the item to. If not specified, + the stanza's namespace will be used. + id -- Optionally specify the ID of the item. + options -- A form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + if node is None: + node = stanza.namespace + + return self.xmpp['xep_0060'].publish(ifrom, node, + payload=stanza.xml, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + +register_plugin(XEP_0163) diff --git a/sleekxmpp/plugins/xep_0172/__init__.py b/sleekxmpp/plugins/xep_0172/__init__.py new file mode 100644 index 00000000..aa7b9f72 --- /dev/null +++ b/sleekxmpp/plugins/xep_0172/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0172 import stanza +from sleekxmpp.plugins.xep_0172.stanza import UserNick +from sleekxmpp.plugins.xep_0172.user_nick import XEP_0172 + + +register_plugin(XEP_0172) diff --git a/sleekxmpp/plugins/xep_0172/stanza.py b/sleekxmpp/plugins/xep_0172/stanza.py new file mode 100644 index 00000000..110c237b --- /dev/null +++ b/sleekxmpp/plugins/xep_0172/stanza.py @@ -0,0 +1,67 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class UserNick(ElementBase): + + """ + XEP-0172: User Nickname allows the addition of a <nick> element + in several stanza types, including <message> and <presence> stanzas. + + The nickname contained in a <nick> should be the global, friendly or + informal name chosen by the owner of a bare JID. The <nick> element + may be included when establishing communications with new entities, + such as normal XMPP users or MUC services. + + The nickname contained in a <nick> element will not necessarily be + the same as the nickname used in a MUC. + + Example stanzas: + <message to="user@example.com"> + <nick xmlns="http://jabber.org/nick/nick">The User</nick> + <body>...</body> + </message> + + <presence to="otheruser@example.com" type="subscribe"> + <nick xmlns="http://jabber.org/nick/nick">The User</nick> + </presence> + + Stanza Interface: + nick -- A global, friendly or informal name chosen by a user. + + Methods: + setup -- Overrides ElementBase.setup. + get_nick -- Return the nickname in the <nick> element. + set_nick -- Add a <nick> element with the given nickname. + del_nick -- Remove the <nick> element. + """ + + namespace = 'http://jabber.org/protocol/nick' + name = 'nick' + plugin_attrib = name + interfaces = set(('nick',)) + + def set_nick(self, nick): + """ + Add a <nick> element with the given nickname. + + Arguments: + nick -- A human readable, informal name. + """ + self.xml.text = nick + + def get_nick(self): + """Return the nickname in the <nick> element.""" + return self.xml.text + + def del_nick(self): + """Remove the <nick> element.""" + if self.parent is not None: + self.parent().xml.remove(self.xml) diff --git a/sleekxmpp/plugins/xep_0172/user_nick.py b/sleekxmpp/plugins/xep_0172/user_nick.py new file mode 100644 index 00000000..c20c3583 --- /dev/null +++ b/sleekxmpp/plugins/xep_0172/user_nick.py @@ -0,0 +1,86 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza.message import Message +from sleekxmpp.stanza.presence import Presence +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0172 import stanza, UserNick + + +log = logging.getLogger(__name__) + + +class XEP_0172(BasePlugin): + + """ + XEP-0172: User Nickname + """ + + name = 'xep_0172' + description = 'XEP-0172: User Nickname' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, UserNick) + register_stanza_plugin(Presence, UserNick) + self.xmpp['xep_0163'].register_pep('user_nick', UserNick) + + def publish_nick(self, nick=None, options=None, ifrom=None, block=True, + callback=None, timeout=None): + """ + Publish the user's current nick. + + Arguments: + nick -- The user nickname to publish. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + nickname = UserNick() + nickname['nick'] = nick + return self.xmpp['xep_0163'].publish(nickname, + node=UserNick.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user nick information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + nick = UserNick() + return self.xmpp['xep_0163'].publish(nick, + node=UserNick.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0184/__init__.py b/sleekxmpp/plugins/xep_0184/__init__.py new file mode 100644 index 00000000..4b129b6b --- /dev/null +++ b/sleekxmpp/plugins/xep_0184/__init__.py @@ -0,0 +1,19 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0184.stanza import Request, Received +from sleekxmpp.plugins.xep_0184.receipt import XEP_0184 + + +register_plugin(XEP_0184) + + +# Retain some backwards compatibility +xep_0184 = XEP_0184 diff --git a/sleekxmpp/plugins/xep_0184/receipt.py b/sleekxmpp/plugins/xep_0184/receipt.py new file mode 100644 index 00000000..c0086b03 --- /dev/null +++ b/sleekxmpp/plugins/xep_0184/receipt.py @@ -0,0 +1,120 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import Message +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0184 import stanza, Request, Received + + +class XEP_0184(BasePlugin): + + """ + XEP-0184: Message Delivery Receipts + """ + + name = 'xep_0184' + description = 'XEP-0184: Message Delivery Receipts' + dependencies = set(['xep_0030']) + stanza = stanza + + ack_types = ('normal', 'chat', 'headline') + + def plugin_init(self): + self.auto_ack = self.config.get('auto_ack', True) + self.auto_request = self.config.get('auto_request', False) + + register_stanza_plugin(Message, Request) + register_stanza_plugin(Message, Received) + + self.xmpp.add_filter('out', self._filter_add_receipt_request) + + self.xmpp.register_handler( + Callback('Message Receipt', + StanzaPath('message/receipt'), + self._handle_receipt_received)) + + self.xmpp.register_handler( + Callback('Message Receipt Request', + StanzaPath('message/request_receipt'), + self._handle_receipt_request)) + + self.xmpp['xep_0030'].add_feature('urn:xmpp:receipts') + + def ack(self, msg): + """ + Acknowledge a message by sending a receipt. + + Arguments: + msg -- The message to acknowledge. + """ + ack = self.xmpp.Message() + ack['to'] = msg['from'] + ack['from'] = msg['to'] + ack['receipt'] = msg['id'] + ack['id'] = self.xmpp.new_id() + ack.send() + + def _handle_receipt_received(self, msg): + self.xmpp.event('receipt_received', msg) + + def _handle_receipt_request(self, msg): + """ + Auto-ack message receipt requests if ``self.auto_ack`` is ``True``. + + Arguments: + msg -- The incoming message requesting a receipt. + """ + if self.auto_ack: + if msg['type'] in self.ack_types: + if not msg['receipt']: + self.ack(msg) + + def _filter_add_receipt_request(self, stanza): + """ + Auto add receipt requests to outgoing messages, if: + + - ``self.auto_request`` is set to ``True`` + - The message is not for groupchat + - The message does not contain a receipt acknowledgment + - The recipient is a bare JID or, if a full JID, one + that has the ``urn:xmpp:receipts`` feature enabled + + The disco cache is checked if a full JID is specified in + the outgoing message, which may mean a round-trip disco#info + delay for the first message sent to the JID if entity caps + are not used. + """ + + if not self.auto_request: + return stanza + + if not isinstance(stanza, Message): + return stanza + + if stanza['request_receipt']: + return stanza + + if not stanza['type'] in self.ack_types: + return stanza + + if stanza['receipt']: + return stanza + + if stanza['to'].resource: + if not self.xmpp['xep_0030'].supports(stanza['to'], + feature='urn:xmpp:receipts', + cached=True): + return stanza + + stanza['request_receipt'] = True + return stanza diff --git a/sleekxmpp/plugins/xep_0184/stanza.py b/sleekxmpp/plugins/xep_0184/stanza.py new file mode 100644 index 00000000..a7607035 --- /dev/null +++ b/sleekxmpp/plugins/xep_0184/stanza.py @@ -0,0 +1,72 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.stanzabase import ElementBase, ET + + +class Request(ElementBase): + namespace = 'urn:xmpp:receipts' + name = 'request' + plugin_attrib = 'request_receipt' + interfaces = set(('request_receipt',)) + sub_interfaces = interfaces + is_extension = True + + def setup(self, xml=None): + self.xml = ET.Element('') + return True + + def set_request_receipt(self, val): + self.del_request_receipt() + if val: + parent = self.parent() + parent._set_sub_text("{%s}request" % self.namespace, keep=True) + if not parent['id']: + if parent.stream: + parent['id'] = parent.stream.new_id() + + def get_request_receipt(self): + parent = self.parent() + if parent.find("{%s}request" % self.namespace) is not None: + return True + else: + return False + + def del_request_receipt(self): + self.parent()._del_sub("{%s}request" % self.namespace) + + +class Received(ElementBase): + namespace = 'urn:xmpp:receipts' + name = 'received' + plugin_attrib = 'receipt' + interfaces = set(['receipt']) + sub_interfaces = interfaces + is_extension = True + + def setup(self, xml=None): + self.xml = ET.Element('') + return True + + def set_receipt(self, value): + self.del_receipt() + if value: + parent = self.parent() + xml = ET.Element("{%s}received" % self.namespace) + xml.attrib['id'] = value + parent.append(xml) + + def get_receipt(self): + parent = self.parent() + xml = parent.find("{%s}received" % self.namespace) + if xml is not None: + return xml.attrib.get('id', '') + return '' + + def del_receipt(self): + self.parent()._del_sub('{%s}received' % self.namespace) diff --git a/sleekxmpp/plugins/xep_0198/__init__.py b/sleekxmpp/plugins/xep_0198/__init__.py new file mode 100644 index 00000000..db930347 --- /dev/null +++ b/sleekxmpp/plugins/xep_0198/__init__.py @@ -0,0 +1,20 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0198.stanza import Enable, Enabled +from sleekxmpp.plugins.xep_0198.stanza import Resume, Resumed +from sleekxmpp.plugins.xep_0198.stanza import Failed +from sleekxmpp.plugins.xep_0198.stanza import StreamManagement +from sleekxmpp.plugins.xep_0198.stanza import Ack, RequestAck + +from sleekxmpp.plugins.xep_0198.stream_management import XEP_0198 + + +register_plugin(XEP_0198) diff --git a/sleekxmpp/plugins/xep_0198/stanza.py b/sleekxmpp/plugins/xep_0198/stanza.py new file mode 100644 index 00000000..5cf93436 --- /dev/null +++ b/sleekxmpp/plugins/xep_0198/stanza.py @@ -0,0 +1,151 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import Error +from sleekxmpp.xmlstream import ElementBase, StanzaBase + + +class Enable(StanzaBase): + name = 'enable' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['max', 'resume']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_resume(self): + return self._get_attr('resume', 'false').lower() in ('true', '1') + + def set_resume(self, val): + self._del_attr('resume') + self._set_attr('resume', 'true' if val else 'false') + + +class Enabled(StanzaBase): + name = 'enabled' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['id', 'location', 'max', 'resume']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_resume(self): + return self._get_attr('resume', 'false').lower() in ('true', '1') + + def set_resume(self, val): + self._del_attr('resume') + self._set_attr('resume', 'true' if val else 'false') + + +class Resume(StanzaBase): + name = 'resume' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['h', 'previd']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_h(self): + h = self._get_attr('h', None) + if h: + return int(h) + return None + + def set_h(self, val): + self._set_attr('h', str(val)) + + +class Resumed(StanzaBase): + name = 'resumed' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['h', 'previd']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_h(self): + h = self._get_attr('h', None) + if h: + return int(h) + return None + + def set_h(self, val): + self._set_attr('h', str(val)) + + + +class Failed(StanzaBase, Error): + name = 'failed' + namespace = 'urn:xmpp:sm:3' + interfaces = set() + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + +class StreamManagement(ElementBase): + name = 'sm' + namespace = 'urn:xmpp:sm:3' + plugin_attrib = name + interfaces = set(['required', 'optional']) + + def get_required(self): + return self.find('{%s}required' % self.namespace) is not None + + def set_required(self, val): + self.del_required() + if val: + self._set_sub_text('required', '', keep=True) + + def del_required(self): + self._del_sub('required') + + def get_optional(self): + return self.find('{%s}optional' % self.namespace) is not None + + def set_optional(self, val): + self.del_optional() + if val: + self._set_sub_text('optional', '', keep=True) + + def del_optional(self): + self._del_sub('optional') + + +class RequestAck(StanzaBase): + name = 'r' + namespace = 'urn:xmpp:sm:3' + interfaces = set() + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + +class Ack(StanzaBase): + name = 'a' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['h']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_h(self): + h = self._get_attr('h', None) + if h: + return int(h) + return None + + def set_h(self, val): + self._set_attr('h', str(val)) diff --git a/sleekxmpp/plugins/xep_0198/stream_management.py b/sleekxmpp/plugins/xep_0198/stream_management.py new file mode 100644 index 00000000..6ed1ea26 --- /dev/null +++ b/sleekxmpp/plugins/xep_0198/stream_management.py @@ -0,0 +1,266 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import threading +import collections + +from sleekxmpp.stanza import Message, Presence, Iq, StreamFeatures +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback, Waiter +from sleekxmpp.xmlstream.matcher import MatchXPath, MatchMany +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0198 import stanza + + +log = logging.getLogger(__name__) + + +MAX_SEQ = 2**32 + + +class XEP_0198(BasePlugin): + + """ + XEP-0198: Stream Management + """ + + name = 'xep_0198' + description = 'XEP-0198: Stream Management' + dependencies = set() + stanza = stanza + + def plugin_init(self): + """Start the XEP-0198 plugin.""" + + # Only enable stream management for non-components, + # since components do not yet perform feature negotiation. + if self.xmpp.is_component: + return + + #: The stream management ID for the stream. Knowing this value is + #: required in order to do stream resumption. + self.sm_id = self.config.get('sm_id', None) + + #: A counter of handled incoming stanzas, mod 2^32. + self.handled = self.config.get('handled', 0) + + #: A counter of unacked outgoing stanzas, mod 2^32. + self.seq = self.config.get('seq', 0) + + #: The last ack number received from the server. + self.last_ack = self.config.get('last_ack', 0) + + #: The number of stanzas to wait between sending ack requests to + #: the server. Setting this to ``1`` will send an ack request after + #: every sent stanza. Defaults to ``5``. + self.window = self.config.get('window', 5) + + #: Control whether or not the ability to resume the stream will be + #: requested when enabling stream management. Defaults to ``True``. + self.allow_resume = self.config.get('allow_resume', True) + + self.enabled = threading.Event() + self.unacked_queue = collections.deque() + + self.seq_lock = threading.Lock() + self.handled_lock = threading.Lock() + self.ack_lock = threading.Lock() + + register_stanza_plugin(StreamFeatures, stanza.StreamManagement) + self.xmpp.register_stanza(stanza.Enable) + self.xmpp.register_stanza(stanza.Enabled) + self.xmpp.register_stanza(stanza.Resume) + self.xmpp.register_stanza(stanza.Resumed) + self.xmpp.register_stanza(stanza.Ack) + self.xmpp.register_stanza(stanza.RequestAck) + + # Register the feature twice because it may be ordered two + # different ways: enabling after binding and resumption + # before binding. + self.xmpp.register_feature('sm', + self._handle_sm_feature, + restart=True, + order=self.config.get('order', 10100)) + self.xmpp.register_feature('sm', + self._handle_sm_feature, + restart=True, + order=self.config.get('resume_order', 9000)) + + self.xmpp.register_handler( + Callback('Stream Management Enabled', + MatchXPath(stanza.Enabled.tag_name()), + self._handle_enabled, + instream=True)) + + self.xmpp.register_handler( + Callback('Stream Management Resumed', + MatchXPath(stanza.Resumed.tag_name()), + self._handle_resumed, + instream=True)) + + self.xmpp.register_handler( + Callback('Stream Management Failed', + MatchXPath(stanza.Failed.tag_name()), + self._handle_failed, + instream=True)) + + self.xmpp.register_handler( + Callback('Stream Management Ack', + MatchXPath(stanza.Ack.tag_name()), + self._handle_ack, + instream=True)) + + self.xmpp.register_handler( + Callback('Stream Management Request Ack', + MatchXPath(stanza.RequestAck.tag_name()), + self._handle_request_ack, + instream=True)) + + self.xmpp.add_filter('in', self._handle_incoming) + self.xmpp.add_filter('out_sync', self._handle_outgoing) + + self.xmpp.add_event_handler('need_ack', self.request_ack) + + def send_ack(self): + """Send the current ack count to the server.""" + ack = stanza.Ack(self.xmpp) + with self.handled_lock: + ack['h'] = self.handled + ack.send() + + def request_ack(self, e=None): + """Request an ack from the server.""" + req = stanza.RequestAck(self.xmpp) + req.send() + + def _handle_sm_feature(self, features): + """ + Enable or resume stream management. + + If no SM-ID is stored, and resource binding has taken place, + stream management will be enabled. + + If an SM-ID is known, and the server allows resumption, the + previous stream will be resumed. + """ + if 'stream_management' in self.xmpp.features: + # We've already negotiated stream management, + # so no need to do it again. + return False + if not self.sm_id: + if 'bind' in self.xmpp.features: + self.enabled.set() + enable = stanza.Enable(self.xmpp) + enable['resume'] = self.allow_resume + enable.send() + self.handled = 0 + elif self.sm_id and self.allow_resume: + self.enabled.set() + resume = stanza.Resume(self.xmpp) + resume['h'] = self.handled + resume['previd'] = self.sm_id + resume.send(now=True) + + # Wait for a response before allowing stream feature processing + # to continue. The actual result processing will be done in the + # _handle_resumed() or _handle_failed() methods. + waiter = Waiter('resumed_or_failed', + MatchMany([ + MatchXPath(stanza.Resumed.tag_name()), + MatchXPath(stanza.Failed.tag_name())])) + self.xmpp.register_handler(waiter) + result = waiter.wait() + if result is not None and result.name == 'resumed': + return True + return False + + def _handle_enabled(self, stanza): + """Save the SM-ID, if provided. + + Raises an :term:`sm_enabled` event. + """ + self.xmpp.features.add('stream_management') + if stanza['id']: + self.sm_id = stanza['id'] + self.xmpp.event('sm_enabled', stanza) + + def _handle_resumed(self, stanza): + """Finish resuming a stream by resending unacked stanzas. + + Raises a :term:`session_resumed` event. + """ + self.xmpp.features.add('stream_management') + self._handle_ack(stanza) + for id, stanza in self.unacked_queue: + self.xmpp.send(stanza, now=True, use_filters=False) + self.xmpp.session_started_event.set() + self.xmpp.event('session_resumed', stanza) + + def _handle_failed(self, stanza): + """ + Disable and reset any features used since stream management was + requested (tracked stanzas may have been sent during the interval + between the enable request and the enabled response). + + Raises an :term:`sm_failed` event. + """ + self.enabled.clear() + self.unacked_queue.clear() + self.xmpp.event('sm_failed', stanza) + + def _handle_ack(self, ack): + """Process a server ack by freeing acked stanzas from the queue. + + Raises a :term:`stanza_acked` event for each acked stanza. + """ + if ack['h'] == self.last_ack: + return + + with self.ack_lock: + num_acked = (ack['h'] - self.last_ack) % MAX_SEQ + log.debug("Ack: %s, Last Ack: %s, Num Acked: %s, Unacked: %s", + ack['h'], + self.last_ack, + num_acked, + len(self.unacked_queue)) + for x in range(num_acked): + seq, stanza = self.unacked_queue.popleft() + self.xmpp.event('stanza_acked', stanza) + self.last_ack = ack['h'] + + def _handle_request_ack(self, req): + """Handle an ack request by sending an ack.""" + self.send_ack() + + def _handle_incoming(self, stanza): + """Increment the handled counter for each inbound stanza.""" + if not self.enabled.is_set(): + return stanza + + if isinstance(stanza, (Message, Presence, Iq)): + with self.handled_lock: + # Sequence numbers are mod 2^32 + self.handled = (self.handled + 1) % MAX_SEQ + return stanza + + def _handle_outgoing(self, stanza): + """Store outgoing stanzas in a queue to be acked.""" + if not self.enabled.is_set(): + return stanza + + if isinstance(stanza, (Message, Presence, Iq)): + seq = None + with self.seq_lock: + # Sequence numbers are mod 2^32 + self.seq = (self.seq + 1) % MAX_SEQ + seq = self.seq + self.unacked_queue.append((seq, stanza)) + if len(self.unacked_queue) > self.window: + self.xmpp.event('need_ack') + return stanza diff --git a/sleekxmpp/plugins/xep_0199/__init__.py b/sleekxmpp/plugins/xep_0199/__init__.py index 3444fe94..5231a5b5 100644 --- a/sleekxmpp/plugins/xep_0199/__init__.py +++ b/sleekxmpp/plugins/xep_0199/__init__.py @@ -6,5 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0199.stanza import Ping -from sleekxmpp.plugins.xep_0199.ping import xep_0199 +from sleekxmpp.plugins.xep_0199.ping import XEP_0199 + + +register_plugin(XEP_0199) + + +# Backwards compatibility for names +xep_0199 = XEP_0199 +xep_0199.sendPing = xep_0199.send_ping diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py index a0f60532..851e5ae5 100644 --- a/sleekxmpp/plugins/xep_0199/ping.py +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -15,14 +15,14 @@ from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream.handler import Callback -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0199 import stanza, Ping log = logging.getLogger(__name__) -class xep_0199(base_plugin): +class XEP_0199(BasePlugin): """ XEP-0199: XMPP Ping @@ -47,14 +47,15 @@ class xep_0199(base_plugin): round trip time. """ + name = 'xep_0199' + description = 'XEP-0199: XMPP Ping' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """ Start the XEP-0199 plugin. """ - self.description = 'XMPP Ping' - self.xep = '0199' - self.stanza = stanza - self.keepalive = self.config.get('keepalive', False) self.frequency = float(self.config.get('frequency', 300)) self.timeout = self.config.get('timeout', 30) @@ -73,9 +74,6 @@ class xep_0199(base_plugin): self.xmpp.add_event_handler('session_end', self._handle_session_end) - def post_init(self): - """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Ping.namespace) def _handle_keepalive(self, event): @@ -169,7 +167,3 @@ class xep_0199(base_plugin): log.debug("Pong: %s %f", jid, delay) return delay - - -# Backwards compatibility for names -xep_0199.sendPing = xep_0199.send_ping diff --git a/sleekxmpp/plugins/xep_0202/__init__.py b/sleekxmpp/plugins/xep_0202/__init__.py index a34b2376..cdab3665 100644 --- a/sleekxmpp/plugins/xep_0202/__init__.py +++ b/sleekxmpp/plugins/xep_0202/__init__.py @@ -6,7 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin from sleekxmpp.plugins.xep_0202 import stanza from sleekxmpp.plugins.xep_0202.stanza import EntityTime -from sleekxmpp.plugins.xep_0202.time import xep_0202 +from sleekxmpp.plugins.xep_0202.time import XEP_0202 + + +register_plugin(XEP_0202) + + +# Retain some backwards compatibility +xep_0202 = XEP_0202 diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py index 2c6faa4b..ca388c5b 100644 --- a/sleekxmpp/plugins/xep_0202/time.py +++ b/sleekxmpp/plugins/xep_0202/time.py @@ -12,7 +12,7 @@ from sleekxmpp.stanza.iq import Iq from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins import xep_0082
from sleekxmpp.plugins.xep_0202 import stanza
@@ -20,18 +20,19 @@ from sleekxmpp.plugins.xep_0202 import stanza log = logging.getLogger(__name__)
-class xep_0202(base_plugin):
+class XEP_0202(BasePlugin):
"""
XEP-0202: Entity Time
"""
+ name = 'xep_0202'
+ description = 'XEP-0202: Entity Time'
+ dependencies = set(['xep_0030', 'xep_0082'])
+ stanza = stanza
+
def plugin_init(self):
"""Start the XEP-0203 plugin."""
- self.xep = '0202'
- self.description = 'Entity Time'
- self.stanza = stanza
-
self.tz_offset = self.config.get('tz_offset', 0)
# As a default, respond to time requests with the
@@ -48,12 +49,8 @@ class xep_0202(base_plugin): self._handle_time_request))
register_stanza_plugin(Iq, stanza.EntityTime)
- def post_init(self):
- """Handle cross-plugin interactions."""
- base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
-
def _handle_time_request(self, iq):
"""
Respond to a request for the local time.
diff --git a/sleekxmpp/plugins/xep_0203/__init__.py b/sleekxmpp/plugins/xep_0203/__init__.py index 445ccf37..d4d99a6c 100644 --- a/sleekxmpp/plugins/xep_0203/__init__.py +++ b/sleekxmpp/plugins/xep_0203/__init__.py @@ -6,7 +6,16 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0203 import stanza from sleekxmpp.plugins.xep_0203.stanza import Delay -from sleekxmpp.plugins.xep_0203.delay import xep_0203 +from sleekxmpp.plugins.xep_0203.delay import XEP_0203 + + + +register_plugin(XEP_0203) + +# Retain some backwards compatibility +xep_0203 = XEP_0203 diff --git a/sleekxmpp/plugins/xep_0203/delay.py b/sleekxmpp/plugins/xep_0203/delay.py index 8ff14d18..31f31ce3 100644 --- a/sleekxmpp/plugins/xep_0203/delay.py +++ b/sleekxmpp/plugins/xep_0203/delay.py @@ -9,11 +9,11 @@ from sleekxmpp.stanza import Message, Presence from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0203 import stanza -class xep_0203(base_plugin): +class XEP_0203(BasePlugin): """ XEP-0203: Delayed Delivery @@ -26,11 +26,12 @@ class xep_0203(base_plugin): Also see <http://www.xmpp.org/extensions/xep-0203.html>. """ + name = 'xep_0203' + description = 'XEP-0203: Delayed Delivery' + dependencies = set() + stanza = stanza + def plugin_init(self): """Start the XEP-0203 plugin.""" - self.xep = '0203' - self.description = 'Delayed Delivery' - self.stanza = stanza - register_stanza_plugin(Message, stanza.Delay) register_stanza_plugin(Presence, stanza.Delay) diff --git a/sleekxmpp/plugins/xep_0224/__init__.py b/sleekxmpp/plugins/xep_0224/__init__.py index 62f5bf82..1a9d2342 100644 --- a/sleekxmpp/plugins/xep_0224/__init__.py +++ b/sleekxmpp/plugins/xep_0224/__init__.py @@ -6,6 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0224 import stanza from sleekxmpp.plugins.xep_0224.stanza import Attention -from sleekxmpp.plugins.xep_0224.attention import xep_0224 +from sleekxmpp.plugins.xep_0224.attention import XEP_0224 + + +register_plugin(XEP_0224) + + +# Retain some backwards compatibility +xep_0224 = XEP_0224 diff --git a/sleekxmpp/plugins/xep_0224/attention.py b/sleekxmpp/plugins/xep_0224/attention.py index 4a3ff368..6eea5d9d 100644 --- a/sleekxmpp/plugins/xep_0224/attention.py +++ b/sleekxmpp/plugins/xep_0224/attention.py @@ -12,25 +12,26 @@ from sleekxmpp.stanza import Message from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0224 import stanza log = logging.getLogger(__name__) -class xep_0224(base_plugin): +class XEP_0224(BasePlugin): """ XEP-0224: Attention """ + name = 'xep_0224' + description = 'XEP-0224: Attention' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """Start the XEP-0224 plugin.""" - self.xep = '0224' - self.description = 'Attention' - self.stanza = stanza - register_stanza_plugin(Message, stanza.Attention) self.xmpp.register_handler( @@ -38,9 +39,6 @@ class xep_0224(base_plugin): StanzaPath('message/attention'), self._handle_attention)) - def post_init(self): - """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace) def request_attention(self, to, mfrom=None, mbody=''): diff --git a/sleekxmpp/plugins/xep_0249/__init__.py b/sleekxmpp/plugins/xep_0249/__init__.py index e88d87ac..b85f55ce 100644 --- a/sleekxmpp/plugins/xep_0249/__init__.py +++ b/sleekxmpp/plugins/xep_0249/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0249.stanza import Invite -from sleekxmpp.plugins.xep_0249.invite import xep_0249 +from sleekxmpp.plugins.xep_0249.invite import XEP_0249 + + +register_plugin(XEP_0249) + + +# Retain some backwards compatibility +xep_0249 = XEP_0249 diff --git a/sleekxmpp/plugins/xep_0249/invite.py b/sleekxmpp/plugins/xep_0249/invite.py index 95fcb37c..737684f5 100644 --- a/sleekxmpp/plugins/xep_0249/invite.py +++ b/sleekxmpp/plugins/xep_0249/invite.py @@ -10,27 +10,28 @@ import logging import sleekxmpp from sleekxmpp import Message -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.xep_0249 import Invite +from sleekxmpp.plugins.xep_0249 import Invite, stanza log = logging.getLogger(__name__) -class xep_0249(base_plugin): +class XEP_0249(BasePlugin): """ XEP-0249: Direct MUC Invitations """ - def plugin_init(self): - self.xep = "0249" - self.description = "Direct MUC Invitations" - self.stanza = sleekxmpp.plugins.xep_0249.stanza + name = 'xep_0249' + description = 'XEP-0249: Direct MUC Invitations' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): self.xmpp.register_handler( Callback('Direct MUC Invitations', StanzaPath('message/groupchat_invite'), @@ -38,8 +39,6 @@ class xep_0249(base_plugin): register_stanza_plugin(Message, Invite) - def post_init(self): - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Invite.namespace) def _handle_invite(self, msg): diff --git a/sleekxmpp/roster/item.py b/sleekxmpp/roster/item.py index 6f956b31..9cb278a4 100644 --- a/sleekxmpp/roster/item.py +++ b/sleekxmpp/roster/item.py @@ -134,17 +134,22 @@ class RosterItem(object): 'subscription': 'none', 'name': '', 'groups': []} + self._db_state = {} self.load() - def set_backend(self, db=None): + def set_backend(self, db=None, save=True): """ Set the datastore interface object for the roster item. Arguments: - db -- The new datastore interface. + db -- The new datastore interface. + save -- If True, save the existing state to the new + backend datastore. Defaults to True. """ self.db = db + if save: + self.save() self.load() def load(self): @@ -167,16 +172,25 @@ class RosterItem(object): return self._state return None - def save(self): + def save(self, remove=False): """ Save the item's state information to an external datastore, if one has been provided. + + Arguments: + remove -- If True, expunge the item from the datastore. """ self['subscription'] = self._subscription() + if remove: + self._state['removed'] = True if self.db: self.db.save(self.owner, self.jid, self._state, self._db_state) + # Finally, remove the in-memory copy if needed. + if remove: + del self.xmpp.roster[self.owner][self.jid] + def __getitem__(self, key): """Return a state field's value.""" if key in self._state: @@ -482,3 +496,6 @@ class RosterItem(object): a roster reset request. """ self.resources = {} + + def __repr__(self): + return repr(self._state) diff --git a/sleekxmpp/roster/multi.py b/sleekxmpp/roster/multi.py index ee56f2a8..6a60778b 100644 --- a/sleekxmpp/roster/multi.py +++ b/sleekxmpp/roster/multi.py @@ -9,7 +9,6 @@ from sleekxmpp.xmlstream import JID from sleekxmpp.roster import RosterNode - class Roster(object): """ @@ -68,6 +67,8 @@ class Roster(object): """ if isinstance(key, JID): key = key.bare + if key is None: + key = self.xmpp.boundjid.bare if key not in self._rosters: self.add(key) self._rosters[key].auto_authorize = self.auto_authorize @@ -94,18 +95,23 @@ class Roster(object): if node not in self._rosters: self._rosters[node] = RosterNode(self.xmpp, node, self.db) - def set_backend(self, db=None): + def set_backend(self, db=None, save=True): """ Set the datastore interface object for the roster. Arguments: db -- The new datastore interface. + save -- If True, save the existing state to the new + backend datastore. Defaults to True. """ self.db = db - for node in self.db.entries(None, {}): + existing_entries = set(self._rosters) + new_entries = set(self.db.entries(None, {})) + + for node in existing_entries: + self._rosters[node].set_backend(db, save) + for node in new_entries - existing_entries: self.add(node) - for node in self._rosters: - self._rosters[node].set_backend(db) def reset(self): """ @@ -182,3 +188,6 @@ class Roster(object): self._auto_subscribe = value for node in self._rosters: self._rosters[node].auto_subscribe = value + + def __repr__(self): + return repr(self._rosters) diff --git a/sleekxmpp/roster/single.py b/sleekxmpp/roster/single.py index c2c7497d..518afebe 100644 --- a/sleekxmpp/roster/single.py +++ b/sleekxmpp/roster/single.py @@ -57,11 +57,28 @@ class RosterNode(object): self.auto_authorize = True self.auto_subscribe = True self.last_status = None + self._version = '' self._jids = {} if self.db: + if hasattr(self.db, 'version'): + self._version = self.db.version(self.jid) for jid in self.db.entries(self.jid): self.add(jid) + + @property + def version(self): + """Retrieve the roster's version ID.""" + if self.db and hasattr(self.db, 'version'): + self._version = self.db.version(self.jid) + return self._version + + @version.setter + def version(self, version): + """Set the roster's version ID.""" + self._version = version + if self.db and hasattr(self.db, 'set_version'): + self.db.set_version(self.jid, version) def __getitem__(self, key): """ @@ -75,6 +92,17 @@ class RosterNode(object): self.add(key, save=True) return self._jids[key] + def __delitem__(self, key): + """ + Remove a roster item from the local storage. + + To remove an item from the server, use the remove() method. + """ + if isinstance(key, JID): + key = key.bare + if key in self._jids: + del self._jids[key] + def __len__(self): """Return the number of JIDs referenced by the roster.""" return len(self._jids) @@ -101,18 +129,23 @@ class RosterNode(object): """Iterate over the roster items.""" return self._jids.__iter__() - def set_backend(self, db=None): + def set_backend(self, db=None, save=True): """ Set the datastore interface object for the roster node. Arguments: db -- The new datastore interface. + save -- If True, save the existing state to the new + backend datastore. Defaults to True. """ self.db = db - for jid in self.db.entries(self.jid): + existing_entries = set(self._jids) + new_entries = set(self.db.entries(self.jid, {})) + + for jid in existing_entries: + self._jids[jid].set_backend(db, save) + for jid in new_entries - existing_entries: self.add(jid) - for jid in self._jids: - self._jids[jid].set_backend(db) def add(self, jid, name='', groups=None, afrom=False, ato=False, pending_in=False, pending_out=False, whitelisted=False, @@ -144,6 +177,9 @@ class RosterNode(object): """ if isinstance(jid, JID): key = jid.bare + else: + key = jid + state = {'name': name, 'groups': groups or [], 'from': afrom, @@ -152,11 +188,11 @@ class RosterNode(object): 'pending_out': pending_out, 'whitelisted': whitelisted, 'subscription': 'none'} - self._jids[jid] = RosterItem(self.xmpp, jid, self.jid, + self._jids[key] = RosterItem(self.xmpp, jid, self.jid, state=state, db=self.db, roster=self) if save: - self._jids[jid].save() + self._jids[key].save() def subscribe(self, jid): """ @@ -285,3 +321,17 @@ class RosterNode(object): if not self.xmpp.sentpresence: self.xmpp.event('sent_presence') self.xmpp.sentpresence = True + + def send_last_presence(self): + if self.last_status is None: + self.send_presence() + else: + pres = self.last_status + if self.xmpp.is_component: + pres['from'] = self.jid + else: + del pres['from'] + pres.send() + + def __repr__(self): + return repr(self._jids) diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index d985f729..825287ad 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -6,7 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin +from sleekxmpp.xmlstream import ElementBase, ET class Error(ElementBase): diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index f05dad17..47d51b04 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -6,7 +6,6 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Error from sleekxmpp.stanza.rootstanza import RootStanza from sleekxmpp.xmlstream import StanzaBase, ET from sleekxmpp.xmlstream.handler import Waiter, Callback diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py index 19d4d9e2..407802bd 100644 --- a/sleekxmpp/stanza/message.py +++ b/sleekxmpp/stanza/message.py @@ -6,9 +6,8 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Error from sleekxmpp.stanza.rootstanza import RootStanza -from sleekxmpp.xmlstream import StanzaBase, ET +from sleekxmpp.xmlstream import StanzaBase class Message(RootStanza): diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py index 1e23d34f..0e9a5c2b 100644 --- a/sleekxmpp/stanza/nick.py +++ b/sleekxmpp/stanza/nick.py @@ -6,67 +6,12 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Message, Presence -from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin - - -class Nick(ElementBase): - - """ - XEP-0172: User Nickname allows the addition of a <nick> element - in several stanza types, including <message> and <presence> stanzas. - - The nickname contained in a <nick> should be the global, friendly or - informal name chosen by the owner of a bare JID. The <nick> element - may be included when establishing communications with new entities, - such as normal XMPP users or MUC services. - - The nickname contained in a <nick> element will not necessarily be - the same as the nickname used in a MUC. - - Example stanzas: - <message to="user@example.com"> - <nick xmlns="http://jabber.org/nick/nick">The User</nick> - <body>...</body> - </message> - - <presence to="otheruser@example.com" type="subscribe"> - <nick xmlns="http://jabber.org/nick/nick">The User</nick> - </presence> - - Stanza Interface: - nick -- A global, friendly or informal name chosen by a user. - - Methods: - setup -- Overrides ElementBase.setup. - get_nick -- Return the nickname in the <nick> element. - set_nick -- Add a <nick> element with the given nickname. - del_nick -- Remove the <nick> element. - """ - - namespace = 'http://jabber.org/protocol/nick' - name = 'nick' - plugin_attrib = name - interfaces = set(('nick',)) - - def set_nick(self, nick): - """ - Add a <nick> element with the given nickname. - - Arguments: - nick -- A human readable, informal name. - """ - self.xml.text = nick - - def get_nick(self): - """Return the nickname in the <nick> element.""" - return self.xml.text - - def del_nick(self): - """Remove the <nick> element.""" - if self.parent is not None: - self.parent().xml.remove(self.xml) +# The nickname stanza has been moved to its own plugin, but the existing +# references are kept for backwards compatibility. +from sleekxmpp.stanza import Message, Presence +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0172 import UserNick as Nick register_stanza_plugin(Message, Nick) register_stanza_plugin(Presence, Nick) diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py index c8706233..f2dd0968 100644 --- a/sleekxmpp/stanza/presence.py +++ b/sleekxmpp/stanza/presence.py @@ -6,9 +6,8 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.stanza import Error from sleekxmpp.stanza.rootstanza import RootStanza -from sleekxmpp.xmlstream import StanzaBase, ET +from sleekxmpp.xmlstream import StanzaBase class Presence(RootStanza): diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index 2ac47d8b..bb756acb 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -7,8 +7,6 @@ """ import logging -import traceback -import sys from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout from sleekxmpp.stanza import Error diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index 3fcdbebc..4788ba72 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -36,7 +36,30 @@ class Roster(ElementBase): namespace = 'jabber:iq:roster' name = 'query' plugin_attrib = 'roster' - interfaces = set(('items',)) + interfaces = set(('items', 'ver')) + + def get_ver(self): + """ + Ensure handling an empty ver attribute propery. + + The ver attribute is special in that the presence of the + attribute with an empty value is important for boostrapping + roster versioning. + """ + return self.xml.attrib.get('ver', None) + + def set_ver(self, ver): + """ + Ensure handling an empty ver attribute propery. + + The ver attribute is special in that the presence of the + attribute with an empty value is important for boostrapping + roster versioning. + """ + if ver is not None: + self.xml.attrib['ver'] = ver + else: + del self.xml.attrib['ver'] def set_items(self, items): """ @@ -55,20 +78,10 @@ class Roster(ElementBase): """ self.del_items() for jid in items: - ijid = str(jid) - item = ET.Element('{jabber:iq:roster}item', {'jid': ijid}) - if 'subscription' in items[jid]: - item.attrib['subscription'] = items[jid]['subscription'] - if 'name' in items[jid]: - name = items[jid]['name'] - if name is not None: - item.attrib['name'] = name - if 'groups' in items[jid]: - for group in items[jid]['groups']: - groupxml = ET.Element('{jabber:iq:roster}group') - groupxml.text = group - item.append(groupxml) - self.xml.append(item) + item = RosterItem() + item.values = items[jid] + item['jid'] = jid + self.append(item) return self def get_items(self): @@ -83,31 +96,58 @@ class Roster(ElementBase): been assigned. """ items = {} - itemsxml = self.xml.findall('{jabber:iq:roster}item') - if itemsxml is not None: - for itemxml in itemsxml: - item = {} - item['name'] = itemxml.get('name', '') - item['subscription'] = itemxml.get('subscription', '') - item['ask'] = itemxml.get('ask', '') - item['approved'] = itemxml.get('approved', '') - item['groups'] = [] - groupsxml = itemxml.findall('{jabber:iq:roster}group') - if groupsxml is not None: - for groupxml in groupsxml: - item['groups'].append(groupxml.text) - items[itemxml.get('jid')] = item + for item in self['substanzas']: + if isinstance(item, RosterItem): + items[item['jid']] = item.values + # Remove extra JID reference to keep everything + # backward compatible + del items[item['jid']]['jid'] return items def del_items(self): """ Remove all <item> elements from the roster stanza. """ - for child in self.xml.getchildren(): - self.xml.remove(child) + for item in self['substanzas']: + if isinstance(item, RosterItem): + self.xml.remove(item.xml) + + +class RosterItem(ElementBase): + namespace = 'jabber:iq:roster' + name = 'item' + plugin_attrib = 'item' + interfaces = set(('jid', 'name', 'subscription', 'ask', + 'approved', 'groups')) + + def get_jid(self): + return JID(self._get_attr('jid', '')) + + def set_jid(self, jid): + self._set_attr('jid', str(jid)) + + def get_groups(self): + groups = [] + for group in self.xml.findall('{%s}group' % self.namespace): + groups.append(group.text) + return groups + + def set_groups(self, values): + self.del_groups() + for group in values: + group_xml = ET.Element('{%s}group' % self.namespace) + group_xml.text = group + self.xml.append(group_xml) + + def del_groups(self): + for group in self.xml.findall('{%s}group' % self.namespace): + self.xmp.remove(group) + + register_stanza_plugin(Iq, Roster) +register_stanza_plugin(Roster, RosterItem, iterable=True) # To comply with PEP8, method names now use underscores. # Deprecated method names are re-mapped for backwards compatibility. diff --git a/sleekxmpp/stanza/stream_error.py b/sleekxmpp/stanza/stream_error.py index cf59a7fa..5a6dac96 100644 --- a/sleekxmpp/stanza/stream_error.py +++ b/sleekxmpp/stanza/stream_error.py @@ -7,8 +7,7 @@ """ from sleekxmpp.stanza.error import Error -from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream import StanzaBase class StreamError(Error, StanzaBase): diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream_features.py index b800011f..9993c84a 100644 --- a/sleekxmpp/stanza/stream_features.py +++ b/sleekxmpp/stanza/stream_features.py @@ -6,8 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream import ElementBase, StanzaBase, ET -from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream import StanzaBase class StreamFeatures(StanzaBase): diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index dd3df29a..364e5939 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -7,6 +7,7 @@ """ import unittest +from xml.parsers.expat import ExpatError try: import Queue as queue except: @@ -62,8 +63,9 @@ class SleekTest(unittest.TestCase): try: xml = ET.fromstring(xml_string) return xml - except SyntaxError as e: - if 'unbound' in e.msg: + except (SyntaxError, ExpatError) as e: + msg = e.msg if hasattr(e, 'msg') else e.message + if 'unbound' in msg: known_prefixes = { 'stream': 'http://etherx.jabber.org/streams'} diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py index 6af5ffde..d0d3f2ea 100644 --- a/sleekxmpp/thirdparty/mini_dateutil.py +++ b/sleekxmpp/thirdparty/mini_dateutil.py @@ -67,6 +67,7 @@ import re +import math import datetime @@ -240,12 +241,12 @@ except: if frac != None: # ok, fractions of hour? if min == None: - frac, min = _math.modf(frac * 60.0) + frac, min = math.modf(frac * 60.0) min = int(min) # fractions of second? if s == None: - frac, s = _math.modf(frac * 60.0) + frac, s = math.modf(frac * 60.0) s = int(s) # and extract microseconds... diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py index 5cb2ee3d..2044ff80 100644 --- a/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py +++ b/sleekxmpp/thirdparty/suelta/mechanisms/__init__.py @@ -3,3 +3,6 @@ from sleekxmpp.thirdparty.suelta.mechanisms.plain import PLAIN from sleekxmpp.thirdparty.suelta.mechanisms.cram_md5 import CRAM_MD5 from sleekxmpp.thirdparty.suelta.mechanisms.digest_md5 import DIGEST_MD5 from sleekxmpp.thirdparty.suelta.mechanisms.scram_hmac import SCRAM_HMAC +from sleekxmpp.thirdparty.suelta.mechanisms.messenger_oauth2 import X_MESSENGER_OAUTH2 +from sleekxmpp.thirdparty.suelta.mechanisms.facebook_platform import X_FACEBOOK_PLATFORM +from sleekxmpp.thirdparty.suelta.mechanisms.google_token import X_GOOGLE_TOKEN diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py index ba44befe..e07bb883 100644 --- a/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py +++ b/sleekxmpp/thirdparty/suelta/mechanisms/cram_md5.py @@ -33,7 +33,7 @@ class CRAM_MD5(Mechanism): if 'savepass' not in self.values: del self.values['password'] - def process(self, challenge): + def process(self, challenge=None): """ """ if challenge is None: diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py index 5492c553..890f3e24 100644 --- a/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py +++ b/sleekxmpp/thirdparty/suelta/mechanisms/digest_md5.py @@ -1,8 +1,10 @@ import sys import random +import hmac from sleekxmpp.thirdparty.suelta.util import hash, bytes, quote +from sleekxmpp.thirdparty.suelta.util import num_to_bytes, bytes_to_num from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py b/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py new file mode 100644 index 00000000..cb0f09d5 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/facebook_platform.py @@ -0,0 +1,39 @@ +from sleekxmpp.thirdparty.suelta.util import bytes +from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism + +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + + + +class X_FACEBOOK_PLATFORM(Mechanism): + + def __init__(self, sasl, name): + super(X_FACEBOOK_PLATFORM, self).__init__(sasl, name) + self.check_values(['access_token', 'api_key']) + + def process(self, challenge=None): + if challenge is not None: + values = {} + for kv in challenge.split(b'&'): + key, value = kv.split(b'=') + values[key] = value + + resp_data = { + b'method': values[b'method'], + b'v': b'1.0', + b'call_id': b'1.0', + b'nonce': values[b'nonce'], + b'access_token': self.values['access_token'], + b'api_key': self.values['api_key'] + } + resp = '&'.join(['%s=%s' % (k, v) for k, v in resp_data.items()]) + return bytes(resp) + return b'' + + def okay(self): + return True + +register_mechanism('X-FACEBOOK-PLATFORM', 40, X_FACEBOOK_PLATFORM, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py b/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py new file mode 100644 index 00000000..e641bb91 --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/google_token.py @@ -0,0 +1,22 @@ +from sleekxmpp.thirdparty.suelta.util import bytes +from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism +from sleekxmpp.thirdparty.suelta.exceptions import SASLError, SASLCancelled + + + +class X_GOOGLE_TOKEN(Mechanism): + + def __init__(self, sasl, name): + super(X_GOOGLE_TOKEN, self).__init__(sasl, name) + self.check_values(['email', 'access_token']) + + def process(self, challenge=None): + email = bytes(self.values['email']) + token = bytes(self.values['access_token']) + return b'\x00' + email + b'\x00' + token + + def okay(self): + return True + + +register_mechanism('X-GOOGLE-TOKEN', 3, X_GOOGLE_TOKEN, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py b/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py new file mode 100644 index 00000000..f5b0ddec --- /dev/null +++ b/sleekxmpp/thirdparty/suelta/mechanisms/messenger_oauth2.py @@ -0,0 +1,17 @@ +from sleekxmpp.thirdparty.suelta.util import bytes +from sleekxmpp.thirdparty.suelta.sasl import Mechanism, register_mechanism + + +class X_MESSENGER_OAUTH2(Mechanism): + + def __init__(self, sasl, name): + super(X_MESSENGER_OAUTH2, self).__init__(sasl, name) + self.check_values(['access_token']) + + def process(self, challenge=None): + return bytes(self.values['access_token']) + + def okay(self): + return True + +register_mechanism('X-MESSENGER-OAUTH2', 10, X_MESSENGER_OAUTH2, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py index ab17095e..accae54a 100644 --- a/sleekxmpp/thirdparty/suelta/mechanisms/plain.py +++ b/sleekxmpp/thirdparty/suelta/mechanisms/plain.py @@ -58,4 +58,4 @@ class PLAIN(Mechanism): return True -register_mechanism('PLAIN', 1, PLAIN, use_hashes=False) +register_mechanism('PLAIN', 5, PLAIN, use_hashes=False) diff --git a/sleekxmpp/thirdparty/suelta/saslprep.py b/sleekxmpp/thirdparty/suelta/saslprep.py index fe58d58b..8022e1cd 100644 --- a/sleekxmpp/thirdparty/suelta/saslprep.py +++ b/sleekxmpp/thirdparty/suelta/saslprep.py @@ -16,7 +16,7 @@ def saslprep(text, strict=True): if sys.version_info < (3, 0): if type(text) == str: - text = text.decode('us-ascii') + text = text.decode('utf-8') # Mapping: # diff --git a/sleekxmpp/version.py b/sleekxmpp/version.py index 037c6463..543932a5 100644 --- a/sleekxmpp/version.py +++ b/sleekxmpp/version.py @@ -9,5 +9,5 @@ # We don't want to have to import the entire library # just to get the version info for setup.py -__version__ = '1.0' -__version_info__ = (1, 0, 0, '', 0) +__version__ = '1.0.1dev' +__version_info__ = (1, 0, 1, 'dev', 0) diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py index 01ff5d67..899df17c 100644 --- a/sleekxmpp/xmlstream/handler/waiter.py +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -15,7 +15,6 @@ try: except ImportError: import Queue as queue -from sleekxmpp.xmlstream import StanzaBase from sleekxmpp.xmlstream.handler.base import BaseHandler diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py index c91c8fb3..281bf4ee 100644 --- a/sleekxmpp/xmlstream/jid.py +++ b/sleekxmpp/xmlstream/jid.py @@ -139,3 +139,7 @@ class JID(object): def __ne__(self, other): """Two JIDs are considered unequal if they are not equal.""" return not self == other + + def __hash__(self): + """Hash a JID based on the string version of its full JID.""" + return hash(self.full) diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py index 61c5332c..a4c0fda0 100644 --- a/sleekxmpp/xmlstream/matcher/stanzapath.py +++ b/sleekxmpp/xmlstream/matcher/stanzapath.py @@ -10,6 +10,7 @@ """ from sleekxmpp.xmlstream.matcher.base import MatcherBase +from sleekxmpp.xmlstream.stanzabase import fix_ns class StanzaPath(MatcherBase): @@ -18,8 +19,16 @@ class StanzaPath(MatcherBase): The StanzaPath matcher selects stanzas that match a given "stanza path", which is similar to a normal XPath except that it uses the interfaces and plugins of the stanza instead of the actual, underlying XML. + + :param criteria: Object to compare some aspect of a stanza against. """ + def __init__(self, criteria): + self._criteria = fix_ns(criteria, split=True, + propagate_ns=False, + default_ns='jabber:client') + self._raw_criteria = criteria + def match(self, stanza): """ Compare a stanza against a "stanza path". A stanza path is similar to @@ -31,4 +40,4 @@ class StanzaPath(MatcherBase): :param stanza: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` stanza to compare against. """ - return stanza.match(self._criteria) + return stanza.match(self._criteria) or stanza.match(self._raw_criteria) diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py index 4a6f073f..8ec73164 100644 --- a/sleekxmpp/xmlstream/scheduler.py +++ b/sleekxmpp/xmlstream/scheduler.py @@ -161,7 +161,7 @@ class Scheduler(object): else: break for task in cleanup: - x = self.schedule.pop(self.schedule.index(task)) + self.schedule.pop(self.schedule.index(task)) else: updated = True self.schedule_lock.acquire() diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 721181a8..96b4f181 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -14,7 +14,6 @@ import copy import logging -import sys import weakref from xml.etree import cElementTree as ET @@ -77,6 +76,49 @@ def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False): registerStanzaPlugin = register_stanza_plugin +def fix_ns(xpath, split=False, propagate_ns=True, default_ns=''): + """Apply the stanza's namespace to elements in an XPath expression. + + :param string xpath: The XPath expression to fix with namespaces. + :param bool split: Indicates if the fixed XPath should be left as a + list of element names with namespaces. Defaults to + False, which returns a flat string path. + :param bool propagate_ns: Overrides propagating parent element + namespaces to child elements. Useful if + you wish to simply split an XPath that has + non-specified namespaces, and child and + parent namespaces are known not to always + match. Defaults to True. + """ + fixed = [] + # Split the XPath into a series of blocks, where a block + # is started by an element with a namespace. + ns_blocks = xpath.split('{') + for ns_block in ns_blocks: + if '}' in ns_block: + # Apply the found namespace to following elements + # that do not have namespaces. + namespace = ns_block.split('}')[0] + elements = ns_block.split('}')[1].split('/') + else: + # Apply the stanza's namespace to the following + # elements since no namespace was provided. + namespace = default_ns + elements = ns_block.split('/') + + for element in elements: + if element: + # Skip empty entry artifacts from splitting. + if propagate_ns: + tag = '{%s}%s' % (namespace, element) + else: + tag = element + fixed.append(tag) + if split: + return fixed + return '/'.join(fixed) + + class ElementBase(object): """ @@ -309,6 +351,7 @@ class ElementBase(object): if self.xml is None: self.xml = xml + last_xml = self.xml if self.xml is None: # Generate XML from the stanza definition for ename in self.name.split('/'): @@ -345,7 +388,8 @@ class ElementBase(object): """ if attrib not in self.plugins: plugin_class = self.plugin_attrib_map[attrib] - plugin = plugin_class(parent=self) + existing_xml = self.xml.find(plugin_class.tag_name()) + plugin = plugin_class(parent=self, xml=existing_xml) self.plugins[attrib] = plugin if plugin_class in self.plugin_iterables: self.iterables.append(plugin) @@ -759,7 +803,7 @@ class ElementBase(object): may be either a string or a list of element names with attribute checks. """ - if isinstance(xpath, str): + if not isinstance(xpath, list): xpath = self._fix_ns(xpath, split=True, propagate_ns=False) # Extract the tag name and attribute checks for the first XPath node. @@ -917,8 +961,9 @@ class ElementBase(object): Any attribute values will be preserved. """ - for child in self.xml.getchildren(): + for child in list(self.xml): self.xml.remove(child) + for plugin in list(self.plugins.keys()): del self.plugins[plugin] return self @@ -951,46 +996,9 @@ class ElementBase(object): return self def _fix_ns(self, xpath, split=False, propagate_ns=True): - """Apply the stanza's namespace to elements in an XPath expression. - - :param string xpath: The XPath expression to fix with namespaces. - :param bool split: Indicates if the fixed XPath should be left as a - list of element names with namespaces. Defaults to - False, which returns a flat string path. - :param bool propagate_ns: Overrides propagating parent element - namespaces to child elements. Useful if - you wish to simply split an XPath that has - non-specified namespaces, and child and - parent namespaces are known not to always - match. Defaults to True. - """ - fixed = [] - # Split the XPath into a series of blocks, where a block - # is started by an element with a namespace. - ns_blocks = xpath.split('{') - for ns_block in ns_blocks: - if '}' in ns_block: - # Apply the found namespace to following elements - # that do not have namespaces. - namespace = ns_block.split('}')[0] - elements = ns_block.split('}')[1].split('/') - else: - # Apply the stanza's namespace to the following - # elements since no namespace was provided. - namespace = self.namespace - elements = ns_block.split('/') - - for element in elements: - if element: - # Skip empty entry artifacts from splitting. - if propagate_ns: - tag = '{%s}%s' % (namespace, element) - else: - tag = element - fixed.append(tag) - if split: - return fixed - return '/'.join(fixed) + return fix_ns(xpath, split=split, + propagate_ns=propagate_ns, + default_ns=self.namespace) def __eq__(self, other): """Compare the stanza object with another to test for equality. @@ -1251,7 +1259,7 @@ class StanzaBase(ElementBase): stanza sent immediately. Useful for stream initialization. Defaults to ``False``. """ - self.stream.send_raw(self.__str__(), now=now) + self.stream.send(self, now=now) def __copy__(self): """Return a copy of the stanza object that does not share the diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index fb9f91bc..6ba82c37 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -24,7 +24,6 @@ import ssl import sys import threading import time -import types import random import weakref try: @@ -32,10 +31,12 @@ try: except ImportError: import Queue as queue +from xml.parsers.expat import ExpatError + import sleekxmpp from sleekxmpp.thirdparty.statemachine import StateMachine from sleekxmpp.xmlstream import Scheduler, tostring -from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET +from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET, ElementBase from sleekxmpp.xmlstream.handler import Waiter, XMLCallback from sleekxmpp.xmlstream.matcher import MatchXMLMask @@ -80,6 +81,12 @@ SSL_RETRY_MAX = 10 #: Maximum time to delay between connection attempts is one hour. RECONNECT_MAX_DELAY = 600 +#: Maximum number of attempts to connect to the server before quitting +#: and raising a 'connect_failed' event. Setting this to ``None`` will +#: allow infinite reconnection attempts, and using ``0`` will disable +#: reconnections. Defaults to ``None``. +RECONNECT_MAX_ATTEMPTS = None + log = logging.getLogger(__name__) @@ -156,6 +163,12 @@ class XMLStream(object): #: Maximum time to delay between connection attempts is one hour. self.reconnect_max_delay = RECONNECT_MAX_DELAY + #: Maximum number of attempts to connect to the server before + #: quitting and raising a 'connect_failed' event. Setting to + #: ``None`` allows infinite reattempts, while setting it to ``0`` + #: will disable reconnection attempts. Defaults to ``None``. + self.reconnect_max_attempts = RECONNECT_MAX_ATTEMPTS + #: The time in seconds to delay between attempts to resend data #: after an SSL error. self.ssl_retry_max = SSL_RETRY_MAX @@ -254,6 +267,7 @@ class XMLStream(object): #: A queue of string data to be sent over the stream. self.send_queue = queue.Queue() + self.send_queue_lock = threading.Lock() #: A :class:`~sleekxmpp.xmlstream.scheduler.Scheduler` instance for #: executing callbacks in the future based on time delays. @@ -268,6 +282,7 @@ class XMLStream(object): self.__handlers = [] self.__event_handlers = {} self.__event_handlers_lock = threading.Lock() + self.__filters = {'in': [], 'out': [], 'out_sync': []} self._id = 0 self._id_lock = threading.Lock() @@ -355,8 +370,10 @@ class XMLStream(object): use_tls=True, reattempt=True): """Create a new socket and connect to the server. - Setting ``reattempt`` to ``True`` will cause connection attempts to - be made every second until a successful connection is established. + Setting ``reattempt`` to ``True`` will cause connection + attempts to be made with an exponential backoff delay (max of + :attr:`reconnect_max_delay` which defaults to 10 minute) until a + successful connection is established. :param host: The name of the desired server for the connection. :param port: Port to connect to on the server. @@ -381,25 +398,31 @@ class XMLStream(object): if use_tls is not None: self.use_tls = use_tls + # Repeatedly attempt to connect until a successful connection # is established. + attempts = self.reconnect_max_attempts connected = self.state.transition('disconnected', 'connected', - func=self._connect) + func=self._connect, args=(reattempt,)) while reattempt and not connected and not self.stop.is_set(): connected = self.state.transition('disconnected', 'connected', func=self._connect) + if not connected: + if attempts is not None: + attempts -= 1 + if attempts <= 0: + self.event('connection_failed', direct=True) + return False return connected - def _connect(self): + def _connect(self, reattempt=True): self.scheduler.remove('Session timeout check') self.stop.clear() if self.default_domain: self.address = self.pick_dns_answer(self.default_domain, self.address[1]) - self.socket = self.socket_class(Socket.AF_INET, Socket.SOCK_STREAM) - self.configure_socket() - - if self.reconnect_delay is None: + + if self.reconnect_delay is None or not reattempt: delay = 1.0 else: delay = min(self.reconnect_delay * 2, self.reconnect_max_delay) @@ -417,10 +440,33 @@ class XMLStream(object): self.stop.set() return False + try: + # Look for IPv6 addresses, in addition to IPv4 + for res in Socket.getaddrinfo(self.address[0], + int(self.address[1]), + 0, + Socket.SOCK_STREAM): + log.debug("Trying: %s", res[-1]) + af, sock_type, proto, canonical, sock_addr = res + try: + self.socket = self.socket_class(af, sock_type, proto) + break + except Socket.error: + log.debug("Could not open IPv%s socket." % proto) + except Socket.gaierror: + log.warning("Socket could not be opened: no connectivity" + \ + " or wrong IP versions.") + if reattempt: + self.reconnect_delay = delay + return False + + self.configure_socket() + if self.use_proxy: connected = self._connect_proxy() if not connected: - self.reconnect_delay = delay + if reattempt: + self.reconnect_delay = delay return False if self.use_ssl and self.ssl_support: @@ -446,6 +492,12 @@ class XMLStream(object): log.debug("Connecting to %s:%s", *self.address) self.socket.connect(self.address) + if self.use_ssl and self.ssl_support: + cert = self.socket.getpeercert(binary_form=True) + cert = ssl.DER_cert_to_PEM_cert(cert) + log.debug('CERT: %s', cert) + self.event('ssl_cert', cert, direct=True) + self.set_socket(self.socket, ignore=True) #this event is where you should set your application state self.event("connected", direct=True) @@ -453,10 +505,11 @@ class XMLStream(object): return True except Socket.error as serr: error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" - self.event('socket_error', serr) + self.event('socket_error', serr, direct=True) log.error(error_msg, self.address[0], self.address[1], serr.errno, serr.strerror) - self.reconnect_delay = delay + if reattempt: + self.reconnect_delay = delay return False def _connect_proxy(self): @@ -506,7 +559,7 @@ class XMLStream(object): return True except Socket.error as serr: error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" - self.event('socket_error', serr) + self.event('socket_error', serr, direct=True) log.error(error_msg, self.address[0], self.address[1], serr.errno, serr.strerror) return False @@ -550,6 +603,7 @@ class XMLStream(object): :attr:`disconnect_wait`. """ self.state.transition('connected', 'disconnected', + wait=2.0, func=self._disconnect, args=(reconnect, wait)) def _disconnect(self, reconnect=False, wait=None): @@ -577,7 +631,7 @@ class XMLStream(object): self.socket.close() self.filesocket.close() except Socket.error as serr: - self.event('socket_error', serr) + self.event('socket_error', serr, direct=True) finally: #clear your application state self.event("disconnected", direct=True) @@ -590,6 +644,8 @@ class XMLStream(object): self.state.transition('connected', 'disconnected', wait=2.0, func=self._disconnect, args=(True,)) + attempts = self.reconnect_max_attempts + log.debug("connecting...") connected = self.state.transition('disconnected', 'connected', wait=2.0, func=self._connect) @@ -597,6 +653,12 @@ class XMLStream(object): connected = self.state.transition('disconnected', 'connected', wait=2.0, func=self._connect) connected = connected or self.state.ensure('connected') + if not connected: + if attempts is not None: + attempts -= 1 + if attempts <= 0: + self.event('connection_failed', direct=True) + return False return connected def set_socket(self, socket, ignore=False): @@ -674,6 +736,12 @@ class XMLStream(object): else: self.socket = ssl_socket self.socket.do_handshake() + + cert = self.socket.getpeercert(binary_form=True) + cert = ssl.DER_cert_to_PEM_cert(cert) + log.debug('CERT: %s', cert) + self.event('ssl_cert', cert, direct=True) + self.set_socket(self.socket) return True else: @@ -741,7 +809,29 @@ class XMLStream(object): stanza objects, but may still be processed using handlers and matchers. """ - del self.__root_stanza[stanza_class] + self.__root_stanza.remove(stanza_class) + + def add_filter(self, mode, handler, order=None): + """Add a filter for incoming or outgoing stanzas. + + These filters are applied before incoming stanzas are + passed to any handlers, and before outgoing stanzas + are put in the send queue. + + Each filter must accept a single stanza, and return + either a stanza or ``None``. If the filter returns + ``None``, then the stanza will be dropped from being + processed for events or from being sent. + + :param mode: One of ``'in'`` or ``'out'``. + :param handler: The filter function. + :param int order: The position to insert the filter in + the list of active filters. + """ + if order: + self.__filters[mode].insert(order, handler) + else: + self.__filters[mode].append(handler) def add_handler(self, mask, pointer, name=None, disposable=False, threaded=False, filter=False, instream=False): @@ -808,20 +898,44 @@ class XMLStream(object): resolver = dns.resolver.get_default_resolver() self.configure_dns(resolver, domain=domain, port=port) + v4_answers = [] + v6_answers = [] + answers = [] + try: - answers = resolver.query(domain, dns.rdatatype.A) + log.debug("Querying A records for %s" % domain) + v4_answers = resolver.query(domain, dns.rdatatype.A) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): log.warning("No A records for %s", domain) - return [((domain, port), 0, 0)] + v4_answers = [((domain, port), 0, 0)] except dns.exception.Timeout: log.warning("DNS resolution timed out " + \ "for A record of %s", domain) - return [((domain, port), 0, 0)] + v4_answers = [((domain, port), 0, 0)] + else: + for ans in v4_answers: + log.debug("Found A record: %s", ans.address) + answers.append(((ans.address, port), 0, 0)) + + try: + log.debug("Querying AAAA records for %s" % domain) + v6_answers = resolver.query(domain, dns.rdatatype.AAAA) + except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): + log.warning("No AAAA records for %s", domain) + v6_answers = [((domain, port), 0, 0)] + except dns.exception.Timeout: + log.warning("DNS resolution timed out " + \ + "for AAAA record of %s", domain) + v6_answers = [((domain, port), 0, 0)] else: - return [((ans.address, port), 0, 0) for ans in answers] + for ans in v6_answers: + log.debug("Found AAAA record: %s", ans.address) + answers.append(((ans.address, port), 0, 0)) + + return answers else: log.warning("dnspython is not installed -- " + \ - "relying on OS A record resolution") + "relying on OS A/AAAA record resolution") self.configure_dns(None, domain=domain, port=port) return [((domain, port), 0, 0)] @@ -850,6 +964,7 @@ class XMLStream(object): items = [x for x in addresses.keys()] items.sort() + address = (domain, port) picked = random.randint(0, intmax) for item in items: if picked <= item: @@ -857,8 +972,8 @@ class XMLStream(object): break for idx, answer in enumerate(self.dns_answers): if self.dns_answers[0] == address: + self.dns_answers.pop(idx) break - self.dns_answers.pop(idx) log.debug("Trying to connect to %s:%s", *address) return address @@ -971,7 +1086,7 @@ class XMLStream(object): """ return xml - def send(self, data, mask=None, timeout=None, now=False): + def send(self, data, mask=None, timeout=None, now=False, use_filters=True): """A wrapper for :meth:`send_raw()` for sending stanza objects. May optionally block until an expected response is received. @@ -989,18 +1104,40 @@ class XMLStream(object): sending the stanza immediately. Useful mainly for stream initialization stanzas. Defaults to ``False``. + :param bool use_filters: Indicates if outgoing filters should be + applied to the given stanza data. Disabling + filters is useful when resending stanzas. + Defaults to ``True``. """ if timeout is None: timeout = self.response_timeout if hasattr(mask, 'xml'): mask = mask.xml - data = str(data) + + if isinstance(data, ElementBase): + if use_filters: + for filter in self.__filters['out']: + data = filter(data) + if data is None: + return + if mask is not None: log.warning("Use of send mask waiters is deprecated.") wait_for = Waiter("SendWait_%s" % self.new_id(), MatchXMLMask(mask)) self.register_handler(wait_for) - self.send_raw(data, now) + + if isinstance(data, ElementBase): + with self.send_queue_lock: + if use_filters: + for filter in self.__filters['out_sync']: + data = filter(data) + if data is None: + return + str_data = str(data) + self.send_raw(str_data, now) + else: + self.send_raw(data, now) if mask is not None: return wait_for.wait(timeout) @@ -1061,7 +1198,7 @@ class XMLStream(object): if count > 1: log.debug('SENT: %d chunks', count) except Socket.error as serr: - self.event('socket_error', serr) + self.event('socket_error', serr, direct=True) log.warning("Failed to send %s", data) if reconnect is None: reconnect = self.auto_reconnect @@ -1157,12 +1294,11 @@ class XMLStream(object): except SystemExit: log.debug("SystemExit in _process") shutdown = True - except SyntaxError as e: + except (SyntaxError, ExpatError) as e: log.error("Error reading from XML stream.") - shutdown = True self.exception(e) except Socket.error as serr: - self.event('socket_error', serr) + self.event('socket_error', serr, direct=True) log.exception('Socket Error') except Exception as e: if not self.stop.is_set(): @@ -1246,8 +1382,6 @@ class XMLStream(object): :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` stanza to analyze. """ - log.debug("RECV: %s", tostring(xml, xmlns=self.default_ns, - stream=self)) # Apply any preprocessing filters. xml = self.incoming_filter(xml) @@ -1255,6 +1389,14 @@ class XMLStream(object): # stanza type applies, a generic StanzaBase stanza will be used. stanza = self._build_stanza(xml) + for filter in self.__filters['in']: + if stanza is not None: + stanza = filter(stanza) + if stanza is None: + return + + log.debug("RECV: %s", stanza) + # Match the stanza against registered handlers. Handlers marked # to run "in stream" will be executed immediately; the rest will # be queued. @@ -1371,7 +1513,7 @@ class XMLStream(object): """Extract stanzas from the send queue and send them on the stream.""" try: while not self.stop.is_set(): - while not self.stop.is_set and \ + while not self.stop.is_set() and \ not self.session_started_event.is_set(): self.session_started_event.wait(timeout=1) if self.__failed_send_stanza is not None: @@ -1398,9 +1540,7 @@ class XMLStream(object): log.debug('SSL error - max retries reached') self.exception(serr) log.warning("Failed to send %s", data) - if reconnect is None: - reconnect = self.auto_reconnect - self.disconnect(reconnect) + self.disconnect(self.auto_reconnect) log.warning('SSL write error - reattempting') time.sleep(self.ssl_retry_delay) tries += 1 @@ -1408,7 +1548,7 @@ class XMLStream(object): log.debug('SENT: %d chunks', count) self.send_queue.task_done() except Socket.error as serr: - self.event('socket_error', serr) + self.event('socket_error', serr, direct=True) log.warning("Failed to send %s", data) self.__failed_send_stanza = data self.disconnect(self.auto_reconnect) |