diff options
78 files changed, 1651 insertions, 642 deletions
@@ -61,6 +61,7 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0027', 'sleekxmpp/plugins/xep_0030', 'sleekxmpp/plugins/xep_0030/stanza', + 'sleekxmpp/plugins/xep_0033', 'sleekxmpp/plugins/xep_0047', 'sleekxmpp/plugins/xep_0050', 'sleekxmpp/plugins/xep_0054', @@ -72,6 +73,7 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0077', 'sleekxmpp/plugins/xep_0078', 'sleekxmpp/plugins/xep_0080', + 'sleekxmpp/plugins/xep_0084', 'sleekxmpp/plugins/xep_0085', 'sleekxmpp/plugins/xep_0086', 'sleekxmpp/plugins/xep_0092', @@ -90,6 +92,7 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0224', 'sleekxmpp/plugins/xep_0231', 'sleekxmpp/plugins/xep_0249', + 'sleekxmpp/plugins/xep_0258', 'sleekxmpp/features', 'sleekxmpp/features/feature_mechanisms', 'sleekxmpp/features/feature_mechanisms/stanza', diff --git a/sleekxmpp/api.py b/sleekxmpp/api.py index 3261f67f..103de2ff 100644 --- a/sleekxmpp/api.py +++ b/sleekxmpp/api.py @@ -16,24 +16,24 @@ class APIWrapper(object): elif attr == 'settings': return self.api.settings[self.name] elif attr == 'register': - def curried_handler(handler, op, jid=None, node=None, default=False): + def partial(handler, op, jid=None, node=None, default=False): register = getattr(self.api, attr) return register(handler, self.name, op, jid, node, default) - return curried_handler + return partial elif attr == 'register_default': - def curried_handler(handler, op, jid=None, node=None): + def partial(handler, op, jid=None, node=None): return getattr(self.api, attr)(handler, self.name, op) - return curried_handler + return partial elif attr in ('run', 'restore_default', 'unregister'): - def curried_handler(*args, **kwargs): + def partial(*args, **kwargs): return getattr(self.api, attr)(self.name, *args, **kwargs) - return curried_handler + return partial return None def __getitem__(self, attr): - def curried_handler(jid=None, node=None, ifrom=None, args=None): + def partial(jid=None, node=None, ifrom=None, args=None): return self.api.run(self.name, attr, jid, node, ifrom, args) - return curried_handler + return partial class APIRegistry(object): @@ -42,7 +42,7 @@ class APIRegistry(object): self._handlers = {} self._handler_defaults = {} self.xmpp = xmpp - self.settings = {} + self.settings = {} def _setup(self, ctype, op): """Initialize the API callback dictionaries. @@ -138,8 +138,8 @@ class APIRegistry(object): """Register an API callback, with JID+node specificity. The API callback can later be executed based on the - specificity of the provided JID+node combination. - + specificity of the provided JID+node combination. + See :meth:`~ApiRegistry.run` for more details. :param string ctype: The name of the API to use. diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index ae32ebec..aae80168 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -31,6 +31,7 @@ from sleekxmpp.xmlstream import XMLStream, JID from sleekxmpp.xmlstream import ET, register_stanza_plugin from sleekxmpp.xmlstream.matcher import MatchXPath from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.stanzabase import XML_NS from sleekxmpp.features import * from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin @@ -66,7 +67,7 @@ class BaseXMPP(XMLStream): #: An identifier for the stream as given by the server. self.stream_id = None - #: The JabberID (JID) used by this connection. + #: The JabberID (JID) used by this connection. self.boundjid = JID(jid) self._expected_server_name = self.boundjid.host @@ -102,7 +103,7 @@ class BaseXMPP(XMLStream): #: The API registry is a way to process callbacks based on #: JID+node combinations. Each callback in the registry is #: marked with: - #: + #: #: - An API name, e.g. xep_0030 #: - The name of an action, e.g. get_info #: - The JID that will be affected @@ -180,6 +181,8 @@ class BaseXMPP(XMLStream): :param xml: The incoming stream's root element. """ self.stream_id = xml.get('id', '') + self.stream_version = xml.get('version', '') + self.peer_default_lang = xml.get('{%s}lang' % XML_NS, None) def process(self, *args, **kwargs): """Initialize plugins and begin processing the XML stream. @@ -199,7 +202,7 @@ class BaseXMPP(XMLStream): Defaults to ``True``. This does **not** mean that no threads are used at all if ``threaded=False``. - Regardless of these threading options, these threads will + Regardless of these threading options, these threads will always exist: - The event queue processor @@ -272,7 +275,9 @@ class BaseXMPP(XMLStream): def Message(self, *args, **kwargs): """Create a Message stanza associated with this stream.""" - return Message(self, *args, **kwargs) + msg = Message(self, *args, **kwargs) + msg['lang'] = self.default_lang + return msg def Iq(self, *args, **kwargs): """Create an Iq stanza associated with this stream.""" @@ -280,18 +285,20 @@ class BaseXMPP(XMLStream): def Presence(self, *args, **kwargs): """Create a Presence stanza associated with this stream.""" - return Presence(self, *args, **kwargs) + pres = Presence(self, *args, **kwargs) + pres['lang'] = self.default_lang + return pres def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None): """Create a new Iq stanza with a given Id and from JID. :param id: An ideally unique ID value for this stanza thread. Defaults to 0. - :param ifrom: The from :class:`~sleekxmpp.xmlstream.jid.JID` + :param ifrom: The from :class:`~sleekxmpp.xmlstream.jid.JID` to use for this stanza. :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID` for this stanza. - :param itype: The :class:`~sleekxmpp.stanza.iq.Iq`'s type, + :param itype: The :class:`~sleekxmpp.stanza.iq.Iq`'s type, one of: ``'get'``, ``'set'``, ``'result'``, or ``'error'``. :param iquery: Optional namespace for adding a query element. @@ -329,7 +336,7 @@ class BaseXMPP(XMLStream): def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None): """ - Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type + Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'result'`` with the given ID value. :param id: An ideally unique ID value. May use :meth:`new_id()`. @@ -359,10 +366,10 @@ class BaseXMPP(XMLStream): Optionally, a substanza may be given to use as the stanza's payload. - :param sub: Either an + :param sub: Either an :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` stanza object or an - :class:`~xml.etree.ElementTree.Element` XML object + :class:`~xml.etree.ElementTree.Element` XML object to use as the :class:`~sleekxmpp.stanza.iq.Iq`'s payload. :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID` for this stanza. @@ -389,9 +396,9 @@ class BaseXMPP(XMLStream): Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'error'``. :param id: An ideally unique ID value. May use :meth:`new_id()`. - :param type: The type of the error, such as ``'cancel'`` or + :param type: The type of the error, such as ``'cancel'`` or ``'modify'``. Defaults to ``'cancel'``. - :param condition: The error condition. Defaults to + :param condition: The error condition. Defaults to ``'feature-not-implemented'``. :param text: A message describing the cause of the error. :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID` @@ -415,7 +422,7 @@ class BaseXMPP(XMLStream): def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None): """ - Create or modify an :class:`~sleekxmpp.stanza.iq.Iq` stanza + Create or modify an :class:`~sleekxmpp.stanza.iq.Iq` stanza to use the given query namespace. :param iq: Optionally use an existing stanza instead @@ -448,7 +455,7 @@ class BaseXMPP(XMLStream): def make_message(self, mto, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): """ - Create and initialize a new + Create and initialize a new :class:`~sleekxmpp.stanza.message.Message` stanza. :param mto: The recipient of the message. @@ -474,7 +481,7 @@ class BaseXMPP(XMLStream): def make_presence(self, pshow=None, pstatus=None, ppriority=None, pto=None, ptype=None, pfrom=None, pnick=None): """ - Create and initialize a new + Create and initialize a new :class:`~sleekxmpp.stanza.presence.Presence` stanza. :param pshow: The presence's show value. @@ -498,7 +505,7 @@ class BaseXMPP(XMLStream): def send_message(self, mto, mbody, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): """ - Create, initialize, and send a new + Create, initialize, and send a new :class:`~sleekxmpp.stanza.message.Message` stanza. :param mto: The recipient of the message. @@ -518,7 +525,7 @@ class BaseXMPP(XMLStream): def send_presence(self, pshow=None, pstatus=None, ppriority=None, pto=None, pfrom=None, ptype=None, pnick=None): """ - Create, initialize, and send a new + Create, initialize, and send a new :class:`~sleekxmpp.stanza.presence.Presence` stanza. :param pshow: The presence's show value. @@ -529,26 +536,13 @@ class BaseXMPP(XMLStream): :param pfrom: The sender of the presence. :param pnick: Optional nickname of the presence's sender. """ - # Python2.6 chokes on Unicode strings for dict keys. - args = {str('ptype'): ptype, - str('pshow'): pshow, - str('pstatus'): pstatus, - str('ppriority'): ppriority, - str('pnick'): pnick} - - if ptype in ('probe', 'subscribe', 'subscribed', \ - 'unsubscribe', 'unsubscribed'): - args[str('pto')] = pto.bare - - if self.is_component: - self.roster[pfrom].send_presence(**args) - else: - self.client_roster.send_presence(**args) + self.make_presence(pshow, pstatus, ppriority, pto, + ptype, pfrom, pnick).send() def send_presence_subscription(self, pto, pfrom=None, ptype='subscribe', pnick=None): """ - Create, initialize, and send a new + Create, initialize, and send a new :class:`~sleekxmpp.stanza.presence.Presence` stanza of type ``'subscribe'``. @@ -557,10 +551,10 @@ class BaseXMPP(XMLStream): :param ptype: The type of presence, such as ``'subscribe'``. :param pnick: Optional nickname of the presence's sender. """ - self.send_presence(pto=pto, + self.make_presence(ptype=ptype, pfrom=pfrom, - ptype=ptype, - pnick=pnick) + pto=JID(pto).bare, + pnick=pnick).send() @property def jid(self): @@ -749,7 +743,7 @@ class BaseXMPP(XMLStream): return def exception(self, exception): - """Process any uncaught exceptions, notably + """Process any uncaught exceptions, notably :class:`~sleekxmpp.exceptions.IqError` and :class:`~sleekxmpp.exceptions.IqTimeout` exceptions. diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 94ced031..7f606de7 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -54,14 +54,14 @@ class ClientXMPP(BaseXMPP): :param password: The password for the XMPP user account. :param ssl: **Deprecated.** :param plugin_config: A dictionary of plugin configurations. - :param plugin_whitelist: A list of approved plugins that - will be loaded when calling + :param plugin_whitelist: A list of approved plugins that + will be loaded when calling :meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`. :param escape_quotes: **Deprecated.** """ - def __init__(self, jid, password, ssl=False, plugin_config={}, - plugin_whitelist=[], escape_quotes=True, sasl_mech=None): + def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[], + escape_quotes=True, sasl_mech=None, lang='en'): BaseXMPP.__init__(self, jid, 'jabber:client') self.set_jid(jid) @@ -69,15 +69,18 @@ class ClientXMPP(BaseXMPP): self.plugin_config = plugin_config self.plugin_whitelist = plugin_whitelist self.default_port = 5222 + self.default_lang = lang self.credentials = {} self.password = password - self.stream_header = "<stream:stream to='%s' %s %s version='1.0'>" % ( + self.stream_header = "<stream:stream to='%s' %s %s %s %s>" % ( self.boundjid.host, "xmlns:stream='%s'" % self.stream_ns, - "xmlns='%s'" % self.default_ns) + "xmlns='%s'" % self.default_ns, + "xml:lang='%s'" % self.default_lang, + "version='1.0'") self.stream_footer = "</stream:stream>" self.features = set() @@ -186,7 +189,7 @@ class ClientXMPP(BaseXMPP): occurs. Defaults to ``True``. :param timeout: The length of time (in seconds) to wait for a response before continuing if blocking - is used. Defaults to + is used. Defaults to :attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`. :param callback: Optional reference to a stream handler function. Will be executed when the roster is received. @@ -197,7 +200,7 @@ class ClientXMPP(BaseXMPP): def del_roster_item(self, jid): """Remove an item from the roster. - + This is done by setting its subscription status to ``'remove'``. :param jid: The JID of the item to remove. @@ -212,7 +215,7 @@ class ClientXMPP(BaseXMPP): Defaults to ``True``. :param timeout: The length of time (in seconds) to wait for a response before continuing if blocking is used. - Defaults to + Defaults to :attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`. :param callback: Optional reference to a stream handler function. Will be executed when the roster is received. @@ -230,7 +233,7 @@ class ClientXMPP(BaseXMPP): response = iq.send(block, timeout, callback) self.event('roster_received', response) - if block: + if block: self._handle_roster(response) return response @@ -276,7 +279,7 @@ class ClientXMPP(BaseXMPP): roster[jid]['pending_out'] = (item['ask'] == 'subscribe') roster[jid].save(remove=(item['subscription'] == 'remove')) - + self.event("roster_update", iq) if iq['type'] == 'set': resp = self.Iq(stype='result', diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index 33fc882d..d69d8266 100644 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -40,8 +40,8 @@ class ComponentXMPP(BaseXMPP): :param host: The server accepting the component. :param port: The port used to connect to the server. :param plugin_config: A dictionary of plugin configurations. - :param plugin_whitelist: A list of approved plugins that - will be loaded when calling + :param plugin_whitelist: A list of approved plugins that + will be loaded when calling :meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`. :param use_jc_ns: Indicates if the ``'jabber:client'`` namespace should be used instead of the standard @@ -78,7 +78,7 @@ class ComponentXMPP(BaseXMPP): self.add_event_handler('presence_probe', self._handle_probe) - def connect(self, host=None, port=None, use_ssl=False, + def connect(self, host=None, port=None, use_ssl=False, use_tls=False, reattempt=True): """Connect to the server. diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py index 6bac1e40..8036532d 100644 --- a/sleekxmpp/exceptions.py +++ b/sleekxmpp/exceptions.py @@ -69,10 +69,11 @@ class IqTimeout(XMPPError): condition='remote-server-timeout', etype='cancel') - #: The :class:`~sleekxmpp.stanza.iq.Iq` stanza whose response + #: The :class:`~sleekxmpp.stanza.iq.Iq` stanza whose response #: did not arrive before the timeout expired. self.iq = iq + class IqError(XMPPError): """ diff --git a/sleekxmpp/features/__init__.py b/sleekxmpp/features/__init__.py index c63d72bf..1ef1e0cf 100644 --- a/sleekxmpp/features/__init__.py +++ b/sleekxmpp/features/__init__.py @@ -7,9 +7,9 @@ """ __all__ = [ - 'feature_starttls', - 'feature_mechanisms', - 'feature_bind', + 'feature_starttls', + 'feature_mechanisms', + 'feature_bind', 'feature_session', 'feature_rosterver' ] diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py index 69769507..8b9d18b6 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/auth.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py @@ -23,7 +23,7 @@ class Auth(StanzaBase): interfaces = set(('mechanism', 'value')) plugin_attrib = name - #: Some SASL mechs require sending values as is, + #: Some SASL mechs require sending values as is, #: without converting base64. plain_mechs = set(['X-MESSENGER-OAUTH2']) diff --git a/sleekxmpp/features/feature_mechanisms/stanza/failure.py b/sleekxmpp/features/feature_mechanisms/stanza/failure.py index 5dd0de56..b9f32605 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/failure.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/failure.py @@ -47,7 +47,7 @@ class Failure(StanzaBase): def get_condition(self): """Return the condition element's name.""" - for child in self.xml.getchildren(): + for child in self.xml: if "{%s}" % self.namespace in child.tag: cond = child.tag.split('}', 1)[-1] if cond in self.conditions: @@ -68,7 +68,7 @@ class Failure(StanzaBase): def del_condition(self): """Remove the condition element.""" - for child in self.xml.getchildren(): + for child in self.xml: if "{%s}" % self.condition_ns in child.tag: tag = child.tag.split('}', 1)[-1] if tag in self.conditions: diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index a4be9e65..0fbebf33 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -33,6 +33,7 @@ __all__ = [ # 'xep_0078', # Non-SASL auth. Don't automatically load 'xep_0080', # User Location 'xep_0082', # XMPP Date and Time Profiles + 'xep_0084', # User Avatar 'xep_0085', # Chat State Notifications 'xep_0086', # Legacy Error Codes 'xep_0092', # Software Version @@ -49,7 +50,10 @@ __all__ = [ 'xep_0199', # Ping 'xep_0202', # Entity Time 'xep_0203', # Delayed Delivery + 'xep_0222', # Persistent Storage of Public Data via Pubsub + 'xep_0223', # Persistent Storage of Private Data via Pubsub 'xep_0224', # Attention 'xep_0231', # Bits of Binary 'xep_0249', # Direct MUC Invitations + 'xep_0258', # Security Labels in XMPP ] diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index 9a7e1b19..337db2db 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -31,10 +31,10 @@ log = logging.getLogger(__name__) PLUGIN_REGISTRY = {} #: In order to do cascading plugin disabling, reverse dependencies -#: must be tracked. +#: must be tracked. PLUGIN_DEPENDENTS = {} -#: Only allow one thread to manipulate the plugin registry at a time. +#: Only allow one thread to manipulate the plugin registry at a time. REGISTRY_LOCK = threading.RLock() @@ -75,7 +75,7 @@ def load_plugin(name, module=None): 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 + :param str module: The name of the base module to search for the plugin. """ try: @@ -84,7 +84,7 @@ def load_plugin(name, module=None): module = 'sleekxmpp.plugins.%s' % name __import__(module) mod = sys.modules[module] - except: + except ImportError: module = 'sleekxmpp.features.%s' % name __import__(module) mod = sys.modules[module] @@ -103,11 +103,11 @@ def load_plugin(name, module=None): # we can work around dependency issues. plugin.old_style = True register_plugin(plugin, name) - except: + except ImportError: log.exception("Unable to load plugin: %s", name) -class PluginManager(object): +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 @@ -181,7 +181,7 @@ class PluginManager(object): 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. @@ -292,7 +292,7 @@ class BasePlugin(object): def post_init(self): """Initialize any cross-plugin state. - + Only needed if the plugin has circular dependencies. """ pass diff --git a/sleekxmpp/plugins/xep_0027/gpg.py b/sleekxmpp/plugins/xep_0027/gpg.py index ccc7c400..7cc128bd 100644 --- a/sleekxmpp/plugins/xep_0027/gpg.py +++ b/sleekxmpp/plugins/xep_0027/gpg.py @@ -81,7 +81,8 @@ class XEP_0027(BasePlugin): def _sign_presence(self, stanza): if isinstance(stanza, Presence): - if stanza['type'] == 'available' or stanza['type'] in Presence.showtypes: + if stanza['type'] == 'available' or \ + stanza['type'] in Presence.showtypes: stanza['signed'] = stanza['status'] return stanza diff --git a/sleekxmpp/plugins/xep_0027/stanza.py b/sleekxmpp/plugins/xep_0027/stanza.py index 927693ad..3170ca6e 100644 --- a/sleekxmpp/plugins/xep_0027/stanza.py +++ b/sleekxmpp/plugins/xep_0027/stanza.py @@ -51,6 +51,3 @@ class Encrypted(ElementBase): if self.xml.text: return xmpp['xep_0027'].decrypt(self.xml.text, parent['to']) return None - - - diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index a66ab935..18c1dba2 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -339,8 +339,8 @@ class XEP_0030(BasePlugin): if local: log.debug("Looking up local disco#info data " + \ "for %s, node %s.", jid, node) - info = self.api['get_info'](jid, node, - kwargs.get('ifrom', None), + info = self.api['get_info'](jid, node, + kwargs.get('ifrom', None), kwargs) info = self._fix_default_info(info) return self._wrap(kwargs.get('ifrom', None), jid, info) @@ -348,8 +348,8 @@ class XEP_0030(BasePlugin): if cached: log.debug("Looking up cached disco#info data " + \ "for %s, node %s.", jid, node) - info = self.api['get_cached_info'](jid, node, - kwargs.get('ifrom', None), + info = self.api['get_cached_info'](jid, node, + kwargs.get('ifrom', None), kwargs) if info is not None: return self._wrap(kwargs.get('ifrom', None), jid, info) @@ -405,8 +405,8 @@ class XEP_0030(BasePlugin): Otherwise the parameter is ignored. """ if local or jid is None: - items = self.api['get_items'](jid, node, - kwargs.get('ifrom', None), + items = self.api['get_items'](jid, node, + kwargs.get('ifrom', None), kwargs) return self._wrap(kwargs.get('ifrom', None), jid, items) diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py deleted file mode 100644 index feef5a13..00000000 --- a/sleekxmpp/plugins/xep_0033.py +++ /dev/null @@ -1,167 +0,0 @@ -""" - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 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.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')) - - 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 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 delBcc(self): - self.delAddresses('bcc') - - def delCc(self): - self.delAddresses('cc') - - def delNoreply(self): - self.delAddresses('noreply') - - def delReplyroom(self): - self.delAddresses('replyroom') - - def delReplyto(self): - self.delAddresses('replyto') - - def delTo(self): - self.delAddresses('to') - - # -------------------------------------------------------------- - - def getBcc(self): - return self.getAddresses('bcc') - - def getCc(self): - return self.getAddresses('cc') - - def getNoreply(self): - return self.getAddresses('noreply') - - def getReplyroom(self): - return self.getAddresses('replyroom') - - def getReplyto(self): - return self.getAddresses('replyto') - - def getTo(self): - return self.getAddresses('to') - - # -------------------------------------------------------------- - - def setBcc(self, addresses): - self.setAddresses(addresses, 'bcc') - - def setCc(self, addresses): - self.setAddresses(addresses, 'cc') - - def setNoreply(self, addresses): - self.setAddresses(addresses, 'noreply') - - def setReplyroom(self, addresses): - self.setAddresses(addresses, 'replyroom') - - def setReplyto(self, addresses): - self.setAddresses(addresses, 'replyto') - - 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(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_0033/__init__.py b/sleekxmpp/plugins/xep_0033/__init__.py new file mode 100644 index 00000000..ba8152c4 --- /dev/null +++ b/sleekxmpp/plugins/xep_0033/__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_0033 import stanza +from sleekxmpp.plugins.xep_0033.stanza import Addresses, Address +from sleekxmpp.plugins.xep_0033.addresses import XEP_0033 + + +register_plugin(XEP_0033) + +# Retain some backwards compatibility +xep_0033 = XEP_0033 +Addresses.addAddress = Addresses.add_address diff --git a/sleekxmpp/plugins/xep_0033/addresses.py b/sleekxmpp/plugins/xep_0033/addresses.py new file mode 100644 index 00000000..78b9fbb5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0033/addresses.py @@ -0,0 +1,32 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 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, Presence +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0033 import stanza, Addresses + + +class XEP_0033(BasePlugin): + + """ + XEP-0033: Extended Stanza Addressing + """ + + name = 'xep_0033' + description = 'XEP-0033: Extended Stanza Addressing' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + self.xmpp['xep_0030'].add_feature(Addresses.namespace) + + register_stanza_plugin(Message, Addresses) + register_stanza_plugin(Presence, Addresses) diff --git a/sleekxmpp/plugins/xep_0033/stanza.py b/sleekxmpp/plugins/xep_0033/stanza.py new file mode 100644 index 00000000..1ff9fb20 --- /dev/null +++ b/sleekxmpp/plugins/xep_0033/stanza.py @@ -0,0 +1,131 @@ +""" + 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 JID, ElementBase, ET, register_stanza_plugin + + +class Addresses(ElementBase): + + name = 'addresses' + namespace = 'http://jabber.org/protocol/address' + plugin_attrib = 'addresses' + interfaces = set() + + def add_address(self, atype='to', jid='', node='', uri='', + desc='', delivered=False): + addr = Address(parent=self) + addr['type'] = atype + addr['jid'] = jid + addr['node'] = node + addr['uri'] = uri + addr['desc'] = desc + addr['delivered'] = delivered + + return addr + + # Additional methods for manipulating sets of addresses + # based on type are generated below. + + +class Address(ElementBase): + + name = 'address' + namespace = 'http://jabber.org/protocol/address' + plugin_attrib = 'address' + interfaces = set(['type', 'jid', 'node', 'uri', 'desc', 'delivered']) + + address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + + def get_jid(self): + return JID(self._get_attr('jid')) + + def set_jid(self, value): + self._set_attr('jid', str(value)) + + def get_delivered(self): + value = self._get_attr('delivered', False) + return value and value.lower() in ('true', '1') + + def set_delivered(self, delivered): + if delivered: + self._set_attr('delivered', 'true') + else: + del self['delivered'] + + def set_uri(self, uri): + if uri: + del self['jid'] + del self['node'] + self._set_attr('uri', uri) + else: + self._del_attr('uri') + + +# ===================================================================== +# Auto-generate address type filters for the Addresses class. + +def _addr_filter(atype): + def _type_filter(addr): + if isinstance(addr, Address): + if atype == 'all' or addr['type'] == atype: + return True + return False + return _type_filter + + +def _build_methods(atype): + + def get_multi(self): + return list(filter(_addr_filter(atype), self)) + + def set_multi(self, value): + del self[atype] + for addr in value: + + # Support assigning dictionary versions of addresses + # instead of full Address objects. + if not isinstance(addr, Address): + if atype != 'all': + addr['type'] = atype + elif 'atype' in addr and 'type' not in addr: + addr['type'] = addr['atype'] + addrObj = Address() + addrObj.values = addr + addr = addrObj + + self.append(addr) + + def del_multi(self): + res = list(filter(_addr_filter(atype), self)) + for addr in res: + self.iterables.remove(addr) + self.xml.remove(addr.xml) + + return get_multi, set_multi, del_multi + + +for atype in ('all', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to'): + get_multi, set_multi, del_multi = _build_methods(atype) + + Addresses.interfaces.add(atype) + setattr(Addresses, "get_%s" % atype, get_multi) + setattr(Addresses, "set_%s" % atype, set_multi) + setattr(Addresses, "del_%s" % atype, del_multi) + + # To retain backwards compatibility: + setattr(Addresses, "get%s" % atype.title(), get_multi) + setattr(Addresses, "set%s" % atype.title(), set_multi) + setattr(Addresses, "del%s" % atype.title(), del_multi) + if atype == 'all': + Addresses.interfaces.add('addresses') + setattr(Addresses, "getAddresses", get_multi) + setattr(Addresses, "setAddresses", set_multi) + setattr(Addresses, "delAddresses", del_multi) + + +register_stanza_plugin(Addresses, Address, iterable=True) diff --git a/sleekxmpp/plugins/xep_0050/stanza.py b/sleekxmpp/plugins/xep_0050/stanza.py index 31a4a5d5..2367c77b 100644 --- a/sleekxmpp/plugins/xep_0050/stanza.py +++ b/sleekxmpp/plugins/xep_0050/stanza.py @@ -110,14 +110,14 @@ class Command(ElementBase): """ Return the set of allowable next actions. """ - actions = [] + actions = set() actions_xml = self.find('{%s}actions' % self.namespace) if actions_xml is not None: for action in self.next_actions: action_xml = actions_xml.find('{%s}%s' % (self.namespace, action)) if action_xml is not None: - actions.append(action) + actions.add(action) return actions def del_actions(self): diff --git a/sleekxmpp/plugins/xep_0054/stanza.py b/sleekxmpp/plugins/xep_0054/stanza.py index 13865bb5..75b69d3e 100644 --- a/sleekxmpp/plugins/xep_0054/stanza.py +++ b/sleekxmpp/plugins/xep_0054/stanza.py @@ -72,6 +72,7 @@ class Nickname(ElementBase): name = 'NICKNAME' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'nicknames' interfaces = set([name]) is_extension = True @@ -94,6 +95,7 @@ class Email(ElementBase): name = 'EMAIL' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'emails' interfaces = set(['HOME', 'WORK', 'INTERNET', 'PREF', 'X400', 'USERID']) sub_interfaces = set(['USERID']) bool_interfaces = set(['HOME', 'WORK', 'INTERNET', 'PREF', 'X400']) @@ -103,8 +105,9 @@ class Address(ElementBase): name = 'ADR' namespace = 'vcard-temp' plugin_attrib = name - interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INTL', - 'PREF', 'POBOX', 'EXTADD', 'STREET', 'LOCALITY', + plugin_multi_attrib = 'addresses' + interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INTL', + 'PREF', 'POBOX', 'EXTADD', 'STREET', 'LOCALITY', 'REGION', 'PCODE', 'CTRY']) sub_interfaces = set(['POBOX', 'EXTADD', 'STREET', 'LOCALITY', 'REGION', 'PCODE', 'CTRY']) @@ -115,12 +118,13 @@ class Telephone(ElementBase): name = 'TEL' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'telephone_numbers' interfaces = set(['HOME', 'WORK', 'VOICE', 'FAX', 'PAGER', 'MSG', 'CELL', 'VIDEO', 'BBS', 'MODEM', 'ISDN', 'PCS', 'PREF', 'NUMBER']) sub_interfaces = set(['NUMBER']) - bool_interfaces = set(['HOME', 'WORK', 'VOICE', 'FAX', 'PAGER', - 'MSG', 'CELL', 'VIDEO', 'BBS', 'MODEM', + bool_interfaces = set(['HOME', 'WORK', 'VOICE', 'FAX', 'PAGER', + 'MSG', 'CELL', 'VIDEO', 'BBS', 'MODEM', 'ISDN', 'PCS', 'PREF']) def setup(self, xml=None): @@ -138,9 +142,10 @@ class Label(ElementBase): name = 'LABEL' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'labels' interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INT', 'PREF', 'lines']) - bool_interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', + bool_interfaces = set(['HOME', 'WORK', 'POSTAL', 'PARCEL', 'DOM', 'INT', 'PREF']) def add_line(self, value): @@ -171,6 +176,7 @@ class Geo(ElementBase): name = 'GEO' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'geolocations' interfaces = set(['LAT', 'LON']) sub_interfaces = interfaces @@ -179,6 +185,7 @@ class Org(ElementBase): name = 'ORG' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'organizations' interfaces = set(['ORGNAME', 'ORGUNIT', 'orgunits']) sub_interfaces = set(['ORGNAME', 'ORGUNIT']) @@ -210,6 +217,7 @@ class Photo(ElementBase): name = 'PHOTO' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'photos' interfaces = set(['TYPE', 'EXTVAL']) sub_interfaces = interfaces @@ -218,14 +226,16 @@ class Logo(ElementBase): name = 'LOGO' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'logos' interfaces = set(['TYPE', 'EXTVAL']) sub_interfaces = interfaces class Sound(ElementBase): - name = 'LOGO' + name = 'SOUND' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'sounds' interfaces = set(['PHONETC', 'EXTVAL']) sub_interfaces = interfaces @@ -264,6 +274,7 @@ class Classification(ElementBase): name = 'CLASS' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'classifications' interfaces = set(['PUBLIC', 'PRIVATE', 'CONFIDENTIAL']) bool_interfaces = interfaces @@ -272,6 +283,7 @@ class Categories(ElementBase): name = 'CATEGORIES' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'categories' interfaces = set([name]) is_extension = True @@ -301,6 +313,7 @@ class Birthday(ElementBase): name = 'BDAY' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'birthdays' interfaces = set([name]) is_extension = True @@ -319,6 +332,7 @@ class Rev(ElementBase): name = 'REV' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'revision_dates' interfaces = set([name]) is_extension = True @@ -337,6 +351,7 @@ class Title(ElementBase): name = 'TITLE' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'titles' interfaces = set([name]) is_extension = True @@ -351,6 +366,7 @@ class Role(ElementBase): name = 'ROLE' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'roles' interfaces = set([name]) is_extension = True @@ -365,6 +381,7 @@ class Note(ElementBase): name = 'NOTE' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'notes' interfaces = set([name]) is_extension = True @@ -379,6 +396,7 @@ class Desc(ElementBase): name = 'DESC' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'descriptions' interfaces = set([name]) is_extension = True @@ -393,6 +411,7 @@ class URL(ElementBase): name = 'URL' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'urls' interfaces = set([name]) is_extension = True @@ -407,6 +426,7 @@ class UID(ElementBase): name = 'UID' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'uids' interfaces = set([name]) is_extension = True @@ -421,6 +441,7 @@ class ProdID(ElementBase): name = 'PRODID' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'product_ids' interfaces = set([name]) is_extension = True @@ -435,6 +456,7 @@ class Mailer(ElementBase): name = 'MAILER' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'mailers' interfaces = set([name]) is_extension = True @@ -449,6 +471,7 @@ class SortString(ElementBase): name = 'SORT-STRING' namespace = 'vcard-temp' plugin_attrib = 'SORT_STRING' + plugin_multi_attrib = 'sort_strings' interfaces = set([name]) is_extension = True @@ -463,6 +486,7 @@ class Agent(ElementBase): name = 'AGENT' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'agents' interfaces = set(['EXTVAL']) sub_interfaces = interfaces @@ -471,6 +495,7 @@ class JabberID(ElementBase): name = 'JABBERID' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'jids' interfaces = set([name]) is_extension = True @@ -485,11 +510,12 @@ class TimeZone(ElementBase): name = 'TZ' namespace = 'vcard-temp' plugin_attrib = name + plugin_multi_attrib = 'timezones' interfaces = set([name]) is_extension = True def set_tz(self, value): - time = xep_0082.time(offset=value) + time = xep_0082.time(offset=value) if time[-1] == 'Z': self.xml.text = 'Z' else: diff --git a/sleekxmpp/plugins/xep_0054/vcard_temp.py b/sleekxmpp/plugins/xep_0054/vcard_temp.py index 2c462037..672f948a 100644 --- a/sleekxmpp/plugins/xep_0054/vcard_temp.py +++ b/sleekxmpp/plugins/xep_0054/vcard_temp.py @@ -53,7 +53,7 @@ class XEP_0054(BasePlugin): def make_vcard(self): return VCardTemp() - def get_vcard(self, jid=None, ifrom=None, local=False, cached=False, + def get_vcard(self, jid=None, ifrom=None, local=False, cached=False, block=True, callback=None, timeout=None): if self.xmpp.is_component and jid.domain == self.xmpp.boundjid.domain: local = True @@ -84,12 +84,12 @@ class XEP_0054(BasePlugin): iq.enable('vcard_temp') vcard = iq.send(block=block, callback=callback, timeout=timeout) - + if block: self.api['set_vcard'](vcard['from'], args=vcard['vcard_temp']) return vcard - def publish_vcard(self, vcard=None, jid=None, block=True, ifrom=None, + def publish_vcard(self, vcard=None, jid=None, block=True, ifrom=None, callback=None, timeout=None): if self.xmpp.is_component: self.api['set_vcard'](jid, None, ifrom, vcard) @@ -107,7 +107,7 @@ class XEP_0054(BasePlugin): self.api['set_vcard'](jid=iq['from'], args=iq['vcard_temp']) return elif iq['type'] == 'get': - vcard = self.api['get_vard'](iq['from'].bare) + vcard = self.api['get_vcard'](iq['from'].bare) if isinstance(vcard, Iq): vcard.send() else: diff --git a/sleekxmpp/plugins/xep_0059/stanza.py b/sleekxmpp/plugins/xep_0059/stanza.py index 7c637d0b..48f5c8a0 100644 --- a/sleekxmpp/plugins/xep_0059/stanza.py +++ b/sleekxmpp/plugins/xep_0059/stanza.py @@ -74,7 +74,7 @@ class Set(ElementBase): if fi is not None: if val: fi.attrib['index'] = val - else: + elif 'index' in fi.attrib: del fi.attrib['index'] elif val: fi = ET.Element("{%s}first" % (self.namespace)) @@ -93,7 +93,7 @@ class Set(ElementBase): def set_before(self, val): b = self.xml.find("{%s}before" % (self.namespace)) - if b is None and val == True: + if b is None and val is True: self._set_sub_text('{%s}before' % self.namespace, '', True) else: self._set_sub_text('{%s}before' % self.namespace, val) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py index 004f0a02..b2fe3010 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py @@ -77,12 +77,12 @@ class Item(ElementBase): self.append(value) def get_payload(self): - childs = self.xml.getchildren() + childs = list(self.xml) if len(childs) > 0: return childs[0] def del_payload(self): - for child in self.xml.getchildren(): + for child in self.xml: self.xml.remove(child) @@ -254,12 +254,12 @@ class PubsubState(ElementBase): self.xml.append(value) def get_payload(self): - childs = self.xml.getchildren() + childs = list(self.xml) if len(childs) > 0: return childs[0] def del_payload(self): - for child in self.xml.getchildren(): + for child in self.xml: self.xml.remove(child) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py index aeaeefe0..59cf1a50 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py @@ -33,7 +33,7 @@ class PubsubErrorCondition(ElementBase): def get_condition(self): """Return the condition element's name.""" - for child in self.parent().xml.getchildren(): + for child in self.parent().xml: if "{%s}" % self.condition_ns in child.tag: cond = child.tag.split('}', 1)[-1] if cond in self.conditions: @@ -55,7 +55,7 @@ class PubsubErrorCondition(ElementBase): def del_condition(self): """Remove the condition element.""" - for child in self.parent().xml.getchildren(): + for child in self.parent().xml: if "{%s}" % self.condition_ns in child.tag: tag = child.tag.split('}', 1)[-1] if tag in self.conditions: diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py index c0d4929e..32f217fa 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py @@ -31,12 +31,12 @@ class EventItem(ElementBase): self.xml.append(value) def get_payload(self): - childs = self.xml.getchildren() + childs = list(self.xml) if len(childs) > 0: return childs[0] def del_payload(self): - for child in self.xml.getchildren(): + for child in self.xml: self.xml.remove(child) diff --git a/sleekxmpp/plugins/xep_0078/stanza.py b/sleekxmpp/plugins/xep_0078/stanza.py index 86ba09ad..c8b26071 100644 --- a/sleekxmpp/plugins/xep_0078/stanza.py +++ b/sleekxmpp/plugins/xep_0078/stanza.py @@ -39,5 +39,3 @@ class AuthFeature(ElementBase): interfaces = set() plugin_tag_map = {} plugin_attrib_map = {} - - diff --git a/sleekxmpp/plugins/xep_0080/geoloc.py b/sleekxmpp/plugins/xep_0080/geoloc.py index 20dde4dd..28c69a2d 100644 --- a/sleekxmpp/plugins/xep_0080/geoloc.py +++ b/sleekxmpp/plugins/xep_0080/geoloc.py @@ -40,33 +40,33 @@ class XEP_0080(BasePlugin): 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 + 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 + 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 + 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 + 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, + 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 + text -- A catch-all element that captures any other information about the location. - timestamp -- UTC timestamp specifying the moment when the + timestamp -- UTC timestamp specifying the moment when the reading was taken. uri -- A URI or URL pointing to information about the location. @@ -115,7 +115,7 @@ class XEP_0080(BasePlugin): be executed when a reply stanza is received. """ geoloc = Geoloc() - return self.xmpp['xep_0163'].publish(geoloc, + return self.xmpp['xep_0163'].publish(geoloc, ifrom=ifrom, block=block, callback=callback, diff --git a/sleekxmpp/plugins/xep_0080/stanza.py b/sleekxmpp/plugins/xep_0080/stanza.py index a83a8b1b..8f466516 100644 --- a/sleekxmpp/plugins/xep_0080/stanza.py +++ b/sleekxmpp/plugins/xep_0080/stanza.py @@ -31,33 +31,33 @@ class Geoloc(ElementBase): 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 + 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 + 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 + 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 + 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, + 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 + text -- A catch-all element that captures any other information about the location. - timestamp -- UTC timestamp specifying the moment when the + timestamp -- UTC timestamp specifying the moment when the reading was taken. uri -- A URI or URL pointing to information about the location. @@ -65,10 +65,10 @@ class Geoloc(ElementBase): 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', + 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 @@ -88,7 +88,7 @@ class Geoloc(ElementBase): """ self._set_sub_text('accuracy', text=str(accuracy)) return self - + def get_accuracy(self): """ Return the value of the <accuracy> element as an integer. @@ -111,7 +111,7 @@ class Geoloc(ElementBase): """ self._set_sub_text('alt', text=str(alt)) return self - + def get_alt(self): """ Return the value of the <alt> element as an integer. @@ -130,8 +130,8 @@ class Geoloc(ElementBase): 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 + 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)) @@ -155,7 +155,7 @@ class Geoloc(ElementBase): Set the value of the <error> element. Arguments: - error -- Horizontal GPS error in arc minutes; this + error -- Horizontal GPS error in arc minutes; this element is deprecated in favor of <accuracy/> """ self._set_sub_text('error', text=str(error)) @@ -183,7 +183,7 @@ class Geoloc(ElementBase): """ self._set_sub_text('lat', text=str(lat)) return self - + def get_lat(self): """ Return the value of the <lat> element as a float. @@ -196,7 +196,7 @@ class Geoloc(ElementBase): return float(p) except ValueError: return None - + def set_lon(self, lon): """ Set the value of the <lon> element. @@ -225,12 +225,12 @@ class Geoloc(ElementBase): Set the value of the <speed> element. Arguments: - speed -- The speed at which the entity is moving, + 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. diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py index 96eb331a..02571fa7 100644 --- a/sleekxmpp/plugins/xep_0082.py +++ b/sleekxmpp/plugins/xep_0082.py @@ -42,6 +42,7 @@ def format_date(time_obj): time_obj = time_obj.date() return time_obj.isoformat() + def format_time(time_obj): """ Return a formatted string version of a time object. @@ -60,6 +61,7 @@ def format_time(time_obj): return '%sZ' % timestamp return timestamp + def format_datetime(time_obj): """ Return a formatted string version of a datetime object. @@ -76,6 +78,7 @@ def format_datetime(time_obj): return '%sZ' % timestamp return timestamp + def date(year=None, month=None, day=None, obj=False): """ Create a date only timestamp for the given instant. @@ -98,9 +101,10 @@ def date(year=None, month=None, day=None, obj=False): day = today.day value = dt.date(year, month, day) if obj: - return value + return value return format_date(value) + def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False): """ Create a time only timestamp for the given instant. @@ -136,6 +140,7 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False): return value return format_time(value) + def datetime(year=None, month=None, day=None, hour=None, min=None, sec=None, micro=None, offset=None, separators=True, obj=False): @@ -181,7 +186,7 @@ def datetime(year=None, month=None, day=None, hour=None, value = dt.datetime(year, month, day, hour, min, sec, micro, offset) if obj: - return value + return value return format_datetime(value) diff --git a/sleekxmpp/plugins/xep_0084/__init__.py b/sleekxmpp/plugins/xep_0084/__init__.py new file mode 100644 index 00000000..6b87573f --- /dev/null +++ b/sleekxmpp/plugins/xep_0084/__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_0084 import stanza +from sleekxmpp.plugins.xep_0084.stanza import Data, MetaData +from sleekxmpp.plugins.xep_0084.avatar import XEP_0084 + + +register_plugin(XEP_0084) diff --git a/sleekxmpp/plugins/xep_0084/avatar.py b/sleekxmpp/plugins/xep_0084/avatar.py new file mode 100644 index 00000000..14ab7d97 --- /dev/null +++ b/sleekxmpp/plugins/xep_0084/avatar.py @@ -0,0 +1,101 @@ +""" + 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 hashlib +import logging + +from sleekxmpp import Iq +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0084 import stanza, Data, MetaData + + +log = logging.getLogger(__name__) + + +class XEP_0084(BasePlugin): + + name = 'xep_0084' + description = 'XEP-0084: User Avatar' + dependencies = set(['xep_0163', 'xep_0060']) + stanza = stanza + + def plugin_init(self): + self.xmpp['xep_0163'].register_pep('avatar_metadata', MetaData) + + pubsub_stanza = self.xmpp['xep_0060'].stanza + register_stanza_plugin(pubsub_stanza.Item, Data) + register_stanza_plugin(pubsub_stanza.EventItem, Data) + + self.xmpp['xep_0060'].map_node_event(Data.namespace, 'avatar_data') + + def retrieve_avatar(self, jid, id, url=None, ifrom=None, block=True, + callback=None, timeout=None): + return self.xmpp['xep_0060'].get_item(jid, Data.namespace, id, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def publish_avatar(self, data, ifrom=None, block=True, callback=None, + timeout=None): + payload = Data() + payload['value'] = data + return self.xmpp['xep_0163'].publish(payload, + node=Data.namespace, + id=hashlib.sha1(data).hexdigest(), + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def publish_avatar_metadata(self, items=None, pointers=None, + ifrom=None, block=True, + callback=None, timeout=None): + metadata = MetaData() + if items is None: + items = [] + for info in items: + metadata.add_info(info['id'], info['type'], info['bytes'], + height=info.get('height', ''), + width=info.get('width', ''), + url=info.get('url', '')) + for pointer in pointers: + metadata.add_pointer(pointer) + + return self.xmpp['xep_0163'].publish(payload, + node=Data.namespace, + id=hashlib.sha1(data).hexdigest(), + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing avatar metadata 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. + """ + metadata = MetaData() + return self.xmpp['xep_0163'].publish(metadata, + node=MetaData.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0084/stanza.py b/sleekxmpp/plugins/xep_0084/stanza.py new file mode 100644 index 00000000..1b204471 --- /dev/null +++ b/sleekxmpp/plugins/xep_0084/stanza.py @@ -0,0 +1,78 @@ +""" + 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 base64 import b64encode, b64decode +from sleekxmpp.thirdparty.suelta.util import bytes + +from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin + + +class Data(ElementBase): + name = 'data' + namespace = 'urn:xmpp:avatar:data' + plugin_attrib = 'avatar_data' + interfaces = set(['value']) + + def get_value(self): + if self.xml.text: + return b64decode(bytes(self.xml.text)) + return '' + + def set_value(self, value): + if value: + self.xml.text = b64encode(bytes(value)) + else: + self.xml.text = '' + + def del_value(self): + self.xml.text = '' + + +class MetaData(ElementBase): + name = 'metadata' + namespace = 'urn:xmpp:avatar:metadata' + plugin_attrib = 'avatar_metadata' + interfaces = set() + + def add_info(self, id, itype, ibytes, height=None, width=None, url=None): + info = Info() + info.values = {'id': id, + 'type': itype, + 'bytes': ibytes, + 'height': height, + 'width': width, + 'url': url} + self.append(info) + + def add_pointer(self, xml): + if not isinstance(xml, Pointer): + pointer = Pointer() + pointer.append(xml) + self.append(pointer) + else: + self.append(xml) + + +class Info(ElementBase): + name = 'info' + namespace = 'urn:xmpp:avatar:metadata' + plugin_attrib = 'info' + plugin_multi_attrib = 'items' + interfaces = set(['bytes', 'height', 'id', 'type', 'url', 'width']) + + +class Pointer(ElementBase): + name = 'pointer' + namespace = 'urn:xmpp:avatar:metadata' + plugin_attrib = 'pointer' + plugin_multi_attrib = 'pointers' + interfaces = set() + + +register_stanza_plugin(MetaData, Info, iterable=True) +register_stanza_plugin(MetaData, Pointer, iterable=True) diff --git a/sleekxmpp/plugins/xep_0086/stanza.py b/sleekxmpp/plugins/xep_0086/stanza.py index 6554d249..d4909806 100644 --- a/sleekxmpp/plugins/xep_0086/stanza.py +++ b/sleekxmpp/plugins/xep_0086/stanza.py @@ -47,28 +47,28 @@ class LegacyError(ElementBase): interfaces = set(('condition',)) overrides = ['set_condition'] - error_map = {'bad-request': ('modify','400'), - 'conflict': ('cancel','409'), - 'feature-not-implemented': ('cancel','501'), - 'forbidden': ('auth','403'), - 'gone': ('modify','302'), - 'internal-server-error': ('wait','500'), - 'item-not-found': ('cancel','404'), - 'jid-malformed': ('modify','400'), - 'not-acceptable': ('modify','406'), - 'not-allowed': ('cancel','405'), - 'not-authorized': ('auth','401'), - 'payment-required': ('auth','402'), - 'recipient-unavailable': ('wait','404'), - 'redirect': ('modify','302'), - 'registration-required': ('auth','407'), - 'remote-server-not-found': ('cancel','404'), - 'remote-server-timeout': ('wait','504'), - 'resource-constraint': ('wait','500'), - 'service-unavailable': ('cancel','503'), - 'subscription-required': ('auth','407'), - 'undefined-condition': (None,'500'), - 'unexpected-request': ('wait','400')} + error_map = {'bad-request': ('modify', '400'), + 'conflict': ('cancel', '409'), + 'feature-not-implemented': ('cancel', '501'), + 'forbidden': ('auth', '403'), + 'gone': ('modify', '302'), + 'internal-server-error': ('wait', '500'), + 'item-not-found': ('cancel', '404'), + 'jid-malformed': ('modify', '400'), + 'not-acceptable': ('modify', '406'), + 'not-allowed': ('cancel', '405'), + 'not-authorized': ('auth', '401'), + 'payment-required': ('auth', '402'), + 'recipient-unavailable': ('wait', '404'), + 'redirect': ('modify', '302'), + 'registration-required': ('auth', '407'), + 'remote-server-not-found': ('cancel', '404'), + 'remote-server-timeout': ('wait', '504'), + 'resource-constraint': ('wait', '500'), + 'service-unavailable': ('cancel', '503'), + 'subscription-required': ('auth', '407'), + 'undefined-condition': (None, '500'), + 'unexpected-request': ('wait', '400')} def setup(self, xml): """Don't create XML for the plugin.""" diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index c6223c10..5e84b2ff 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -79,5 +79,7 @@ class XEP_0092(BasePlugin): result = iq.send() if result and result['type'] != 'error': - return result['software_version'].values + values = result['software_version'].values + del values['lang'] + return values return False diff --git a/sleekxmpp/plugins/xep_0107/user_mood.py b/sleekxmpp/plugins/xep_0107/user_mood.py index 11aaace4..95e17d45 100644 --- a/sleekxmpp/plugins/xep_0107/user_mood.py +++ b/sleekxmpp/plugins/xep_0107/user_mood.py @@ -34,7 +34,7 @@ class XEP_0107(BasePlugin): register_stanza_plugin(Message, UserMood) self.xmpp['xep_0163'].register_pep('user_mood', UserMood) - def publish_mood(self, value=None, text=None, options=None, + def publish_mood(self, value=None, text=None, options=None, ifrom=None, block=True, callback=None, timeout=None): """ Publish the user's current mood. @@ -79,7 +79,7 @@ class XEP_0107(BasePlugin): be executed when a reply stanza is received. """ mood = UserMood() - return self.xmpp['xep_0163'].publish(mood, + return self.xmpp['xep_0163'].publish(mood, node=UserMood.namespace, ifrom=ifrom, block=block, diff --git a/sleekxmpp/plugins/xep_0108/stanza.py b/sleekxmpp/plugins/xep_0108/stanza.py index 4dc18f43..4650160a 100644 --- a/sleekxmpp/plugins/xep_0108/stanza.py +++ b/sleekxmpp/plugins/xep_0108/stanza.py @@ -21,7 +21,7 @@ class UserActivity(ElementBase): 'talking', 'traveling', 'undefined', 'working']) specific = set(['at_the_spa', 'brushing_teeth', 'buying_groceries', 'cleaning', 'coding', 'commuting', 'cooking', 'cycling', - 'dancing', 'day_off', 'doing_maintenance', + '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', @@ -31,11 +31,11 @@ class UserActivity(ElementBase): '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', + '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', + 'taking_a_bath', 'taking_a_shower', 'thinking', 'walking', 'walking_the_dog', 'watching_a_movie', 'watching_tv', 'working_out', 'writing']) @@ -46,7 +46,7 @@ class UserActivity(ElementBase): 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: diff --git a/sleekxmpp/plugins/xep_0108/user_activity.py b/sleekxmpp/plugins/xep_0108/user_activity.py index 43270486..cd4f48d1 100644 --- a/sleekxmpp/plugins/xep_0108/user_activity.py +++ b/sleekxmpp/plugins/xep_0108/user_activity.py @@ -29,7 +29,7 @@ class XEP_0108(BasePlugin): def plugin_init(self): self.xmpp['xep_0163'].register_pep('user_activity', UserActivity) - def publish_activity(self, general, specific=None, text=None, options=None, + 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. @@ -76,7 +76,7 @@ class XEP_0108(BasePlugin): be executed when a reply stanza is received. """ activity = UserActivity() - return self.xmpp['xep_0163'].publish(activity, + return self.xmpp['xep_0163'].publish(activity, node=UserActivity.namespace, ifrom=ifrom, block=block, diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index 5e5d2320..b0cba42d 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -35,7 +35,7 @@ class XEP_0115(BasePlugin): stanza = stanza def plugin_init(self): - self.hashes = {'sha-1': hashlib.sha1, + self.hashes = {'sha-1': hashlib.sha1, 'sha1': hashlib.sha1, 'md5': hashlib.md5} @@ -124,7 +124,7 @@ class XEP_0115(BasePlugin): 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']) @@ -132,7 +132,7 @@ class XEP_0115(BasePlugin): return except XMPPError: return - + log.debug("New caps verification string: %s", pres['caps']['ver']) try: node = '%s#%s' % (pres['caps']['node'], pres['caps']['ver']) @@ -140,7 +140,7 @@ class XEP_0115(BasePlugin): if isinstance(caps, Iq): caps = caps['disco_info'] - + if self._validate_caps(caps, pres['caps']['hash'], pres['caps']['ver']): self.assign_verstring(pres['from'], pres['caps']['ver']) @@ -173,7 +173,8 @@ class XEP_0115(BasePlugin): 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") + log.debug("Duplicated FORM_TYPE values, " + \ + "invalid for caps") return False if len(f_type) > 1: @@ -183,7 +184,8 @@ class XEP_0115(BasePlugin): return False if stanza['fields']['FORM_TYPE']['type'] != 'hidden': - log.debug("Field FORM_TYPE type not 'hidden', ignoring form for caps") + 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") @@ -212,7 +214,7 @@ class XEP_0115(BasePlugin): identities = sorted(('/'.join(i) for i in identities)) features = sorted(info['features']) - + S += '<'.join(identities) + '<' S += '<'.join(features) + '<' @@ -254,7 +256,7 @@ class XEP_0115(BasePlugin): info = info['disco_info'] ver = self.generate_verstring(info, self.hash) self.xmpp['xep_0030'].set_info( - jid=jid, + jid=jid, node='%s#%s' % (self.caps_node, ver), info=info) self.cache_caps(ver, info) diff --git a/sleekxmpp/plugins/xep_0115/static.py b/sleekxmpp/plugins/xep_0115/static.py index a0a8fb23..f83c244c 100644 --- a/sleekxmpp/plugins/xep_0115/static.py +++ b/sleekxmpp/plugins/xep_0115/static.py @@ -69,7 +69,7 @@ class StaticCaps(object): return True try: - info = self.disco.get_info(jid=jid, node=node, + 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'] @@ -99,7 +99,7 @@ class StaticCaps(object): be skipped, even if a result has already been cached. Defaults to false. """ - identity = (data.get('category', None), + identity = (data.get('category', None), data.get('itype', None), data.get('lang', None)) @@ -114,7 +114,7 @@ class StaticCaps(object): return True try: - info = self.disco.get_info(jid=jid, node=node, + 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']) diff --git a/sleekxmpp/plugins/xep_0118/stanza.py b/sleekxmpp/plugins/xep_0118/stanza.py index 80e0358a..3fdab284 100644 --- a/sleekxmpp/plugins/xep_0118/stanza.py +++ b/sleekxmpp/plugins/xep_0118/stanza.py @@ -14,7 +14,7 @@ class UserTune(ElementBase): name = 'tune' namespace = 'http://jabber.org/protocol/tune' plugin_attrib = 'tune' - interfaces = set(['artist', 'length', 'rating', 'source', + interfaces = set(['artist', 'length', 'rating', 'source', 'title', 'track', 'uri']) sub_interfaces = interfaces diff --git a/sleekxmpp/plugins/xep_0118/user_tune.py b/sleekxmpp/plugins/xep_0118/user_tune.py index c848eaa8..53a4f51a 100644 --- a/sleekxmpp/plugins/xep_0118/user_tune.py +++ b/sleekxmpp/plugins/xep_0118/user_tune.py @@ -30,7 +30,7 @@ class XEP_0118(BasePlugin): 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, + title=None, track=None, uri=None, options=None, ifrom=None, block=True, callback=None, timeout=None): """ Publish the user's current tune. @@ -61,7 +61,7 @@ class XEP_0118(BasePlugin): tune['title'] = title tune['track'] = track tune['uri'] = uri - return self.xmpp['xep_0163'].publish(tune, + return self.xmpp['xep_0163'].publish(tune, node=UserTune.namespace, options=options, ifrom=ifrom, @@ -84,7 +84,7 @@ class XEP_0118(BasePlugin): be executed when a reply stanza is received. """ tune = UserTune() - return self.xmpp['xep_0163'].publish(tune, + return self.xmpp['xep_0163'].publish(tune, node=UserTune.namespace, ifrom=ifrom, block=block, diff --git a/sleekxmpp/plugins/xep_0153/vcard_avatar.py b/sleekxmpp/plugins/xep_0153/vcard_avatar.py index 2cc2f15a..3f36d135 100644 --- a/sleekxmpp/plugins/xep_0153/vcard_avatar.py +++ b/sleekxmpp/plugins/xep_0153/vcard_avatar.py @@ -45,7 +45,7 @@ class XEP_0153(BasePlugin): self.api.register(self._set_hash, 'set_hash', default=True) self.api.register(self._get_hash, 'get_hash', default=True) - def set_avatar(self, jid=None, avatar=None, mtype=None, block=True, + def set_avatar(self, jid=None, avatar=None, mtype=None, block=True, timeout=None, callback=None): vcard = self.xmpp['xep_0054'].get_vcard(jid, cached=True) vcard = vcard['vcard_temp'] @@ -69,7 +69,7 @@ class XEP_0153(BasePlugin): own_jid = (jid.bare == self.xmpp.boundjid.bare) if self.xmpp.is_component: own_jid = (jid.domain == self.xmpp.boundjid.domain) - + if jid is not None: jid = jid.bare self.api['set_hash'](jid, args=None) @@ -77,7 +77,7 @@ class XEP_0153(BasePlugin): self.xmpp.roster[jid].send_last_presence() iq = self.xmpp['xep_0054'].get_vcard( - jid=jid, + jid=jid, ifrom=self.xmpp.boundjid) data = iq['vcard_temp']['PHOTO']['BINVAL'] if not data: diff --git a/sleekxmpp/plugins/xep_0163.py b/sleekxmpp/plugins/xep_0163.py index 5a6df1c8..43d3ad3a 100644 --- a/sleekxmpp/plugins/xep_0163.py +++ b/sleekxmpp/plugins/xep_0163.py @@ -56,7 +56,7 @@ class XEP_0163(BasePlugin): jid -- Optionally specify the JID. """ if not isinstance(namespace, set) and not isinstance(namespace, list): - namespace = [namespace] + namespace = [namespace] for ns in namespace: self.xmpp['xep_0030'].add_feature('%s+notify' % ns, @@ -75,7 +75,7 @@ class XEP_0163(BasePlugin): jid -- Optionally specify the JID. """ if not isinstance(namespace, set) and not isinstance(namespace, list): - namespace = [namespace] + namespace = [namespace] for ns in namespace: self.xmpp['xep_0030'].del_feature(jid=jid, @@ -109,6 +109,7 @@ class XEP_0163(BasePlugin): node = stanza.namespace return self.xmpp['xep_0060'].publish(ifrom, node, + id=id, payload=stanza.xml, options=options, ifrom=ifrom, diff --git a/sleekxmpp/plugins/xep_0172/user_nick.py b/sleekxmpp/plugins/xep_0172/user_nick.py index c20c3583..324407c3 100644 --- a/sleekxmpp/plugins/xep_0172/user_nick.py +++ b/sleekxmpp/plugins/xep_0172/user_nick.py @@ -78,7 +78,7 @@ class XEP_0172(BasePlugin): be executed when a reply stanza is received. """ nick = UserNick() - return self.xmpp['xep_0163'].publish(nick, + return self.xmpp['xep_0163'].publish(nick, node=UserNick.namespace, ifrom=ifrom, block=block, diff --git a/sleekxmpp/plugins/xep_0184/receipt.py b/sleekxmpp/plugins/xep_0184/receipt.py index c0086b03..83d89269 100644 --- a/sleekxmpp/plugins/xep_0184/receipt.py +++ b/sleekxmpp/plugins/xep_0184/receipt.py @@ -100,13 +100,13 @@ class XEP_0184(BasePlugin): 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 diff --git a/sleekxmpp/plugins/xep_0198/stanza.py b/sleekxmpp/plugins/xep_0198/stanza.py index 5cf93436..6461d766 100644 --- a/sleekxmpp/plugins/xep_0198/stanza.py +++ b/sleekxmpp/plugins/xep_0198/stanza.py @@ -82,7 +82,6 @@ class Resumed(StanzaBase): self._set_attr('h', str(val)) - class Failed(StanzaBase, Error): name = 'failed' namespace = 'urn:xmpp:sm:3' @@ -106,7 +105,7 @@ class StreamManagement(ElementBase): self.del_required() if val: self._set_sub_text('required', '', keep=True) - + def del_required(self): self._del_sub('required') @@ -117,7 +116,7 @@ class StreamManagement(ElementBase): self.del_optional() if val: self._set_sub_text('optional', '', keep=True) - + def del_optional(self): self._del_sub('optional') diff --git a/sleekxmpp/plugins/xep_0198/stream_management.py b/sleekxmpp/plugins/xep_0198/stream_management.py index 7045ad21..05d5856f 100644 --- a/sleekxmpp/plugins/xep_0198/stream_management.py +++ b/sleekxmpp/plugins/xep_0198/stream_management.py @@ -21,7 +21,7 @@ from sleekxmpp.plugins.xep_0198 import stanza log = logging.getLogger(__name__) -MAX_SEQ = 2**32 +MAX_SEQ = 2 ** 32 class XEP_0198(BasePlugin): @@ -69,7 +69,7 @@ class XEP_0198(BasePlugin): self.enabled = threading.Event() self.unacked_queue = collections.deque() - + self.seq_lock = threading.Lock() self.handled_lock = threading.Lock() self.ack_lock = threading.Lock() @@ -197,7 +197,7 @@ class XEP_0198(BasePlugin): def _handle_enabled(self, stanza): """Save the SM-ID, if provided. - + Raises an :term:`sm_enabled` event. """ self.xmpp.features.add('stream_management') @@ -231,7 +231,7 @@ class XEP_0198(BasePlugin): 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: @@ -243,10 +243,10 @@ class XEP_0198(BasePlugin): log.debug("Ack: %s, Last Ack: %s, " + \ "Unacked: %s, Num Acked: %s, " + \ "Remaining: %s", - ack['h'], - self.last_ack, + ack['h'], + self.last_ack, num_unacked, - num_acked, + num_acked, num_unacked - num_acked) for x in range(num_acked): seq, stanza = self.unacked_queue.popleft() diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py index ca388c5b..319a9bc5 100644 --- a/sleekxmpp/plugins/xep_0202/time.py +++ b/sleekxmpp/plugins/xep_0202/time.py @@ -40,8 +40,12 @@ class XEP_0202(BasePlugin): # custom function can be supplied which accepts
# the JID of the entity to query for the time.
self.local_time = self.config.get('local_time', None)
+
+ def default_local_time(jid):
+ return xep_0082.datetime(offset=self.tz_offset)
+
if not self.local_time:
- self.local_time = lambda x: xep_0082.datetime(offset=self.tz_offset)
+ self.local_time = default_local_time
self.xmpp.registerHandler(
Callback('Entity Time',
diff --git a/sleekxmpp/plugins/xep_0203/__init__.py b/sleekxmpp/plugins/xep_0203/__init__.py index d4d99a6c..a95ead7e 100644 --- a/sleekxmpp/plugins/xep_0203/__init__.py +++ b/sleekxmpp/plugins/xep_0203/__init__.py @@ -13,9 +13,7 @@ from sleekxmpp.plugins.xep_0203.stanza import Delay 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_0222.py b/sleekxmpp/plugins/xep_0222.py new file mode 100644 index 00000000..724ef968 --- /dev/null +++ b/sleekxmpp/plugins/xep_0222.py @@ -0,0 +1,126 @@ +""" + 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.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.base import BasePlugin, register_plugin + + +log = logging.getLogger(__name__) + + +class XEP_0222(BasePlugin): + + """ + XEP-0222: Persistent Storage of Public Data via PubSub + """ + + name = 'xep_0222' + description = 'XEP-0222: Persistent Storage of Private Data via PubSub' + dependencies = set(['xep_0163', 'xep_0060', 'xep_0004']) + + profile = {'pubsub#persist_items': True, + 'pubsub#send_last_published_item': 'never'} + + def configure(self, node): + """ + Update a node's configuration to match the public storage profile. + """ + config = self.xmpp['xep_0004'].Form() + config['type'] = 'submit' + + for field, value in self.profile.items(): + config.add_field(var=field, value=value) + + return self.xmpp['xep_0060'].set_node_config(None, node, config, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def store(self, stanza, node=None, id=None, ifrom=None, options=None, + block=True, callback=None, timeout=None): + """ + Store public data via PEP. + + This is just a (very) thin wrapper around the XEP-0060 publish() + method to set the defaults expected by PEP. + + Arguments: + stanza -- The private content to store. + node -- The node to publish the content to. If not specified, + the stanza's namespace will be used. + id -- Optionally specify the ID of the item. + options -- Publish options to use, which will be modified to + fit the persistent storage option profile. + 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 not options: + options = self.xmpp['xep_0004'].stanza.Form() + options['type'] = 'submit' + options.add_field( + var='FORM_TYPE', + ftype='hidden', + value='http://jabber.org/protocol/pubsub#publish-options') + + for field, value in self.profile.items(): + if field not in options.fields: + options.add_field(var=field) + options.fields[field]['value'] = value + + return self.xmpp['xep_0163'].publish(stanza, node, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def retrieve(self, node, id=None, item_ids=None, ifrom=None, + block=True, callback=None, timeout=None): + """ + Retrieve public data via PEP. + + This is just a (very) thin wrapper around the XEP-0060 publish() + method to set the defaults expected by PEP. + + Arguments: + node -- The node to retrieve content from. + id -- Optionally specify the ID of the item. + item_ids -- Specify a group of IDs. If id is also specified, it + will be included in item_ids. + 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 item_ids is None: + item_ids = [] + if id is not None: + item_ids.append(id) + + return self.xmpp['xep_0060'].get_items(None, node, + item_ids=item_ids, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + +register_plugin(XEP_0222) diff --git a/sleekxmpp/plugins/xep_0223.py b/sleekxmpp/plugins/xep_0223.py new file mode 100644 index 00000000..ab99f277 --- /dev/null +++ b/sleekxmpp/plugins/xep_0223.py @@ -0,0 +1,126 @@ +""" + 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.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.base import BasePlugin, register_plugin + + +log = logging.getLogger(__name__) + + +class XEP_0223(BasePlugin): + + """ + XEP-0223: Persistent Storage of Private Data via PubSub + """ + + name = 'xep_0223' + description = 'XEP-0223: Persistent Storage of Private Data via PubSub' + dependencies = set(['xep_0163', 'xep_0060', 'xep_0004']) + + profile = {'pubsub#persist_items': True, + 'pubsub#send_last_published_item': 'never'} + + def configure(self, node): + """ + Update a node's configuration to match the public storage profile. + """ + config = self.xmpp['xep_0004'].Form() + config['type'] = 'submit' + + for field, value in self.profile.items(): + config.add_field(var=field, value=value) + + return self.xmpp['xep_0060'].set_node_config(None, node, config, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def store(self, stanza, node=None, id=None, ifrom=None, options=None, + block=True, callback=None, timeout=None): + """ + Store private data via PEP. + + This is just a (very) thin wrapper around the XEP-0060 publish() + method to set the defaults expected by PEP. + + Arguments: + stanza -- The private content to store. + node -- The node to publish the content to. If not specified, + the stanza's namespace will be used. + id -- Optionally specify the ID of the item. + options -- Publish options to use, which will be modified to + fit the persistent storage option profile. + 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 not options: + options = self.xmpp['xep_0004'].stanza.Form() + options['type'] = 'submit' + options.add_field( + var='FORM_TYPE', + ftype='hidden', + value='http://jabber.org/protocol/pubsub#publish-options') + + for field, value in self.profile.items(): + if field not in options.fields: + options.add_field(var=field) + options.fields[field]['value'] = value + + return self.xmpp['xep_0163'].publish(stanza, node, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def retrieve(self, node, id=None, item_ids=None, ifrom=None, + block=True, callback=None, timeout=None): + """ + Retrieve private data via PEP. + + This is just a (very) thin wrapper around the XEP-0060 publish() + method to set the defaults expected by PEP. + + Arguments: + node -- The node to retrieve content from. + id -- Optionally specify the ID of the item. + item_ids -- Specify a group of IDs. If id is also specified, it + will be included in item_ids. + 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 item_ids is None: + item_ids = [] + if id is not None: + item_ids.append(id) + + return self.xmpp['xep_0060'].get_items(None, node, + item_ids=item_ids, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + +register_plugin(XEP_0223) diff --git a/sleekxmpp/plugins/xep_0231/__init__.py b/sleekxmpp/plugins/xep_0231/__init__.py index 6a70cc07..2861d67b 100644 --- a/sleekxmpp/plugins/xep_0231/__init__.py +++ b/sleekxmpp/plugins/xep_0231/__init__.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2012 Nathanael C. Fritz, + Copyright (C) 2012 Nathanael C. Fritz, Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> This file is part of SleekXMPP. diff --git a/sleekxmpp/plugins/xep_0231/bob.py b/sleekxmpp/plugins/xep_0231/bob.py index 011a1952..f411a8f7 100644 --- a/sleekxmpp/plugins/xep_0231/bob.py +++ b/sleekxmpp/plugins/xep_0231/bob.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2012 Nathanael C. Fritz, + Copyright (C) 2012 Nathanael C. Fritz, Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> This file is part of SleekXMPP. @@ -58,7 +58,6 @@ class XEP_0231(BasePlugin): self.api.register(self._set_bob, 'set_bob', default=True) self.api.register(self._del_bob, 'del_bob', default=True) - def set_bob(self, data, mtype, cid=None, max_age=None): if cid is None: cid = 'sha1+%s@bob.xmpp.org' % hashlib.sha1(data).hexdigest() @@ -73,7 +72,7 @@ class XEP_0231(BasePlugin): return cid - def get_bob(self, jid=None, cid=None, cached=True, ifrom=None, + def get_bob(self, jid=None, cid=None, cached=True, ifrom=None, block=True, timeout=None, callback=None): if cached: data = self.api['get_bob'](None, None, ifrom, args=cid) @@ -112,7 +111,7 @@ class XEP_0231(BasePlugin): iq.send() def _handle_bob(self, stanza): - self.api['set_bob'](stanza['from'], None, + self.api['set_bob'](stanza['from'], None, stanza['to'], args=stanza['bob']) self.xmpp.event('bob', stanza) diff --git a/sleekxmpp/plugins/xep_0231/stanza.py b/sleekxmpp/plugins/xep_0231/stanza.py index 13d7a5db..a51f5a03 100644 --- a/sleekxmpp/plugins/xep_0231/stanza.py +++ b/sleekxmpp/plugins/xep_0231/stanza.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2012 Nathanael C. Fritz, + Copyright (C) 2012 Nathanael C. Fritz, Emmanuel Gil Peyrot <linkmauve@linkmauve.fr> This file is part of SleekXMPP. diff --git a/sleekxmpp/plugins/xep_0258/__init__.py b/sleekxmpp/plugins/xep_0258/__init__.py new file mode 100644 index 00000000..516a3706 --- /dev/null +++ b/sleekxmpp/plugins/xep_0258/__init__.py @@ -0,0 +1,18 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 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_0258 import stanza +from sleekxmpp.plugins.xep_0258.stanza import SecurityLabel, Label +from sleekxmpp.plugins.xep_0258.stanza import DisplayMarking, EquivalentLabel +from sleekxmpp.plugins.xep_0258.stanza import ESSLabel, Catalog, CatalogItem +from sleekxmpp.plugins.xep_0258.security_labels import XEP_0258 + + +register_plugin(XEP_0258) diff --git a/sleekxmpp/plugins/xep_0258/security_labels.py b/sleekxmpp/plugins/xep_0258/security_labels.py new file mode 100644 index 00000000..e0426f32 --- /dev/null +++ b/sleekxmpp/plugins/xep_0258/security_labels.py @@ -0,0 +1,40 @@ +""" + 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 import Iq, Message +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0258 import stanza, SecurityLabel, Catalog + + +log = logging.getLogger(__name__) + + +class XEP_0258(BasePlugin): + + name = 'xep_0258' + description = 'XEP-0258: Security Labels in XMPP' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + self.xmpp['xep_0030'].add_feature(SecurityLabel.namespace) + + register_stanza_plugin(Message, SecurityLabel) + register_stanza_plugin(Iq, Catalog) + + def get_catalog(self, jid, ifrom=None, block=True, + callback=None, timeout=None): + iq = self.xmpp.Iq() + iq['to'] = jid + iq['from'] = ifrom + iq['type'] = 'get' + iq.enable('security_label_catalog') + return iq.send(block=block, callback=callback, timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0258/stanza.py b/sleekxmpp/plugins/xep_0258/stanza.py new file mode 100644 index 00000000..4d828a46 --- /dev/null +++ b/sleekxmpp/plugins/xep_0258/stanza.py @@ -0,0 +1,142 @@ +""" + 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 base64 import b64encode, b64decode + +from sleekxmpp.thirdparty.suelta.util import bytes + +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin + + +class SecurityLabel(ElementBase): + name = 'securitylabel' + namespace = 'urn:xmpp:sec-label:0' + plugin_attrib = 'security_label' + + def add_equivalent(self, label): + equiv = EquivalentLabel(parent=self) + equiv.append(label) + return equiv + + +class Label(ElementBase): + name = 'label' + namespace = 'urn:xmpp:sec-label:0' + plugin_attrib = 'label' + + +class DisplayMarking(ElementBase): + name = 'displaymarking' + namespace = 'urn:xmpp:sec-label:0' + plugin_attrib = 'display_marking' + interfaces = set(['fgcolor', 'bgcolor', 'value']) + + def get_fgcolor(self): + return self._get_attr('fgcolor', 'black') + + def get_bgcolor(self): + return self._get_attr('fgcolor', 'white') + + def get_value(self): + return self.xml.text + + def set_value(self, value): + self.xml.text = value + + def del_value(self): + self.xml.text = '' + + +class EquivalentLabel(ElementBase): + name = 'equivalentlabel' + namespace = 'urn:xmpp:sec-label:0' + plugin_attrib = 'equivalent_label' + plugin_multi_attrib = 'equivalent_labels' + + +class Catalog(ElementBase): + name = 'catalog' + namespace = 'urn:xmpp:sec-label:catalog:2' + plugin_attrib = 'security_label_catalog' + interfaces = set(['to', 'from', 'name', 'desc', 'id', 'size', 'restrict']) + + def get_to(self): + return JID(self._get_attr('to')) + pass + + def set_to(self, value): + return self._set_attr('to', str(value)) + + def get_from(self): + return JID(self._get_attr('from')) + + def set_from(self, value): + return self._set_attr('from', str(value)) + + def get_restrict(self): + value = self._get_attr('restrict', '') + if value and value.lower() in ('true', '1'): + return True + return False + + def set_restrict(self, value): + self._del_attr('restrict') + if value: + self._set_attr('restrict', 'true') + elif value is False: + self._set_attr('restrict', 'false') + + +class CatalogItem(ElementBase): + name = 'catalog' + namespace = 'urn:xmpp:sec-label:catalog:2' + plugin_attrib = 'item' + plugin_multi_attrib = 'items' + interfaces = set(['selector', 'default']) + + def get_default(self): + value = self._get_attr('default', '') + if value.lower() in ('true', '1'): + return True + return False + + def set_default(self, value): + self._del_attr('default') + if value: + self._set_attr('default', 'true') + elif value is False: + self._set_attr('default', 'false') + + +class ESSLabel(ElementBase): + name = 'esssecuritylabel' + namespace = 'urn:xmpp:sec-label:ess:0' + plugin_attrib = 'ess' + interfaces = set(['value']) + + def get_value(self): + if self.xml.text: + return b64decode(bytes(self.xml.text)) + return '' + + def set_value(self, value): + self.xml.text = '' + if value: + self.xml.text = b64encode(bytes(value)) + + def del_value(self): + self.xml.text = '' + + +register_stanza_plugin(Catalog, CatalogItem, iterable=True) +register_stanza_plugin(CatalogItem, SecurityLabel) +register_stanza_plugin(EquivalentLabel, ESSLabel) +register_stanza_plugin(Label, ESSLabel) +register_stanza_plugin(SecurityLabel, DisplayMarking) +register_stanza_plugin(SecurityLabel, EquivalentLabel, iterable=True) +register_stanza_plugin(SecurityLabel, Label) diff --git a/sleekxmpp/roster/item.py b/sleekxmpp/roster/item.py index 6ed1e42d..6e9c0d01 100644 --- a/sleekxmpp/roster/item.py +++ b/sleekxmpp/roster/item.py @@ -307,34 +307,29 @@ class RosterItem(object): p['from'] = self.owner p.send() - def send_presence(self, ptype=None, pshow=None, pstatus=None, - ppriority=None, pnick=None): + def send_presence(self, **kwargs): """ Create, initialize, and send a Presence stanza. + If no recipient is specified, send the presence immediately. + Otherwise, forward the send request to the recipient's roster + entry for processing. + Arguments: pshow -- The presence's show value. pstatus -- The presence's status message. ppriority -- This connections' priority. + pto -- The recipient of a directed presence. + pfrom -- The sender of a directed presence, which should + be the owner JID plus resource. ptype -- The type of presence, such as 'subscribe'. pnick -- Optional nickname of the presence's sender. """ - p = self.xmpp.make_presence(pshow=pshow, - pstatus=pstatus, - ppriority=ppriority, - ptype=ptype, - pnick=pnick, - pto=self.jid) - if self.xmpp.is_component: - p['from'] = self.owner - if p['type'] in p.showtypes or \ - p['type'] in ['available', 'unavailable']: - self.last_status = p - p.send() - - if not self.xmpp.sentpresence: - self.xmpp.event('sent_presence') - self.xmpp.sentpresence = True + if self.xmpp.is_component and not kwargs.get('pfrom', ''): + kwargs['pfrom'] = self.owner + if not kwargs.get('pto', ''): + kwargs['pto'] = self.jid + self.xmpp.send_presence(**kwargs) def send_last_presence(self): if self.last_status is None: diff --git a/sleekxmpp/roster/multi.py b/sleekxmpp/roster/multi.py index 6a60778b..9a04aebb 100644 --- a/sleekxmpp/roster/multi.py +++ b/sleekxmpp/roster/multi.py @@ -6,9 +6,11 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.stanza import Presence from sleekxmpp.xmlstream import JID from sleekxmpp.roster import RosterNode + class Roster(object): """ @@ -55,6 +57,33 @@ class Roster(object): for node in self.db.entries(None, {}): self.add(node) + self.xmpp.add_filter('out', self._save_last_status) + + def _save_last_status(self, stanza): + + if isinstance(stanza, Presence): + sfrom = stanza['from'].full + sto = stanza['to'].full + + if not sfrom: + sfrom = self.xmpp.boundjid + + if stanza['type'] in stanza.showtypes or \ + stanza['type'] in ('available', 'unavailable'): + if sto: + self[sfrom][sto].last_status = stanza + else: + self[sfrom].last_status = stanza + with self[sfrom]._last_status_lock: + for jid in self[sfrom]: + self[sfrom][jid].last_status = None + + if not self.xmpp.sentpresence: + self.xmpp.event('sent_presence') + self.xmpp.sentpresence = True + + return stanza + def __getitem__(self, key): """ Return the roster node for a JID. @@ -121,29 +150,27 @@ class Roster(object): for node in self: self[node].reset() - def send_presence(self, pshow=None, pstatus=None, ppriority=None, - pto=None, pfrom=None, ptype=None, pnick=None): + def send_presence(self, **kwargs): """ Create, initialize, and send a Presence stanza. - Forwards the send request to the appropriate roster to - perform the actual sending. + If no recipient is specified, send the presence immediately. + Otherwise, forward the send request to the recipient's roster + entry for processing. Arguments: pshow -- The presence's show value. pstatus -- The presence's status message. ppriority -- This connections' priority. pto -- The recipient of a directed presence. + pfrom -- The sender of a directed presence, which should + be the owner JID plus resource. ptype -- The type of presence, such as 'subscribe'. - pfrom -- The sender of the presence. pnick -- Optional nickname of the presence's sender. """ - self[pfrom].send_presence(ptype=ptype, - pshow=pshow, - pstatus=pstatus, - ppriority=ppriority, - pnick=pnick, - pto=pto) + if self.xmpp.is_component and not kwargs.get('pfrom', ''): + kwargs['pfrom'] = self.jid + self.xmpp.send_presence(**kwargs) @property def auto_authorize(self): diff --git a/sleekxmpp/roster/single.py b/sleekxmpp/roster/single.py index 048b091e..f8c9c781 100644 --- a/sleekxmpp/roster/single.py +++ b/sleekxmpp/roster/single.py @@ -6,6 +6,8 @@ See the file LICENSE for copying permission. """ +import threading + from sleekxmpp.xmlstream import JID from sleekxmpp.roster import RosterItem @@ -59,13 +61,14 @@ class RosterNode(object): self.last_status = None self._version = '' self._jids = {} + self._last_status_lock = threading.Lock() 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.""" @@ -146,7 +149,7 @@ class RosterNode(object): self.db = db 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: @@ -291,8 +294,7 @@ class RosterNode(object): for jid in self: self[jid].reset() - def send_presence(self, ptype=None, pshow=None, pstatus=None, - ppriority=None, pnick=None, pto=None): + def send_presence(self, **kwargs): """ Create, initialize, and send a Presence stanza. @@ -305,27 +307,14 @@ class RosterNode(object): pstatus -- The presence's status message. ppriority -- This connections' priority. pto -- The recipient of a directed presence. + pfrom -- The sender of a directed presence, which should + be the owner JID plus resource. ptype -- The type of presence, such as 'subscribe'. + pnick -- Optional nickname of the presence's sender. """ - if pto: - self[pto].send_presence(ptype, pshow, pstatus, - ppriority, pnick) - else: - p = self.xmpp.make_presence(pshow=pshow, - pstatus=pstatus, - ppriority=ppriority, - ptype=ptype, - pnick=pnick) - if self.xmpp.is_component: - p['from'] = self.jid - if p['type'] in p.showtypes or \ - p['type'] in ['available', 'unavailable']: - self.last_status = p - p.send() - - if not self.xmpp.sentpresence: - self.xmpp.event('sent_presence') - self.xmpp.sentpresence = True + if self.xmpp.is_component and not kwargs.get('pfrom', ''): + kwargs['pfrom'] = self.jid + self.xmpp.send_presence(**kwargs) def send_last_presence(self): if self.last_status is None: diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index 825287ad..60bc65bc 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -51,7 +51,8 @@ class Error(ElementBase): namespace = 'jabber:client' name = 'error' plugin_attrib = 'error' - interfaces = set(('code', 'condition', 'text', 'type')) + interfaces = set(('code', 'condition', 'text', 'type', + 'gone', 'redirect')) sub_interfaces = set(('text',)) plugin_attrib_map = {} plugin_tag_map = {} @@ -88,7 +89,7 @@ class Error(ElementBase): def get_condition(self): """Return the condition element's name.""" - for child in self.xml.getchildren(): + for child in self.xml: if "{%s}" % self.condition_ns in child.tag: cond = child.tag.split('}', 1)[-1] if cond in self.conditions: @@ -109,7 +110,7 @@ class Error(ElementBase): def del_condition(self): """Remove the condition element.""" - for child in self.xml.getchildren(): + for child in self.xml: if "{%s}" % self.condition_ns in child.tag: tag = child.tag.split('}', 1)[-1] if tag in self.conditions: @@ -135,6 +136,33 @@ class Error(ElementBase): self._del_sub('{%s}text' % self.condition_ns) return self + def get_gone(self): + return self._get_sub_text('{%s}gone' % self.condition_ns, '') + + def get_redirect(self): + return self._get_sub_text('{%s}redirect' % self.condition_ns, '') + + def set_gone(self, value): + if value: + del self['condition'] + return self._set_sub_text('{%s}gone' % self.condition_ns, value) + elif self['condition'] == 'gone': + del self['condition'] + + def set_redirect(self, value): + if value: + del self['condition'] + ns = self.condition_ns + return self._set_sub_text('{%s}redirect' % ns, value) + elif self['condition'] == 'redirect': + del self['condition'] + + def del_gone(self): + self._del_sub('{%s}gone' % self.condition_ns) + + def del_redirect(self): + self._del_sub('{%s}redirect' % self.condition_ns) + # To comply with PEP8, method names now use underscores. # Deprecated method names are re-mapped for backwards compatibility. diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index 47d51b04..f45b3c67 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -122,7 +122,7 @@ class Iq(RootStanza): def get_query(self): """Return the namespace of the <query> element.""" - for child in self.xml.getchildren(): + for child in self.xml: if child.tag.endswith('query'): ns = child.tag.split('}')[0] if '{' in ns: @@ -132,7 +132,7 @@ class Iq(RootStanza): def del_query(self): """Remove the <query> element.""" - for child in self.xml.getchildren(): + for child in self.xml: if child.tag.endswith('query'): self.xml.remove(child) return self diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index bb756acb..a7c2b218 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -78,7 +78,8 @@ class RootStanza(StanzaBase): self['error']['type'] = 'cancel' self.send() # log the error - log.exception('Error handling {%s}%s stanza' , self.namespace, self.name) + log.exception('Error handling {%s}%s stanza', + self.namespace, self.name) # Finally raise the exception to a global exception handler self.stream.exception(e) diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index 253c2aea..a415c482 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -47,7 +47,7 @@ class Roster(ElementBase): roster versioning. """ return self.xml.attrib.get('ver', None) - + def set_ver(self, ver): """ Ensure handling an empty ver attribute propery. @@ -101,7 +101,8 @@ class Roster(ElementBase): items[item['jid']] = item.values # Remove extra JID reference to keep everything # backward compatible - del items[item['jid']]['jid'] + del items[item['jid']]['jid'] + del items[item['jid']]['lang'] return items def del_items(self): diff --git a/sleekxmpp/stanza/stream_error.py b/sleekxmpp/stanza/stream_error.py index 5a6dac96..ed0078c9 100644 --- a/sleekxmpp/stanza/stream_error.py +++ b/sleekxmpp/stanza/stream_error.py @@ -54,7 +54,7 @@ class StreamError(Error, StanzaBase): """ namespace = 'http://etherx.jabber.org/streams' - interfaces = set(('condition', 'text')) + interfaces = set(('condition', 'text', 'see_other_host')) conditions = set(( 'bad-format', 'bad-namespace-prefix', 'conflict', 'connection-timeout', 'host-gone', 'host-unknown', @@ -66,3 +66,18 @@ class StreamError(Error, StanzaBase): 'unsupported-feature', 'unsupported-stanza-type', 'unsupported-version')) condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams' + + def get_see_other_host(self): + ns = self.condition_ns + return self._get_sub_text('{%s}see-other-host' % ns, '') + + def set_see_other_host(self, value): + if value: + del self['condition'] + ns = self.condition_ns + return self._set_sub_text('{%s}see-other-host' % ns, value) + elif self['condition'] == 'see-other-host': + del self['condition'] + + def del_see_other_host(self): + self._del_sub('{%s}see-other-host' % self.condition_ns) diff --git a/sleekxmpp/stanza/stream_features.py b/sleekxmpp/stanza/stream_features.py index 9993c84a..e487721e 100644 --- a/sleekxmpp/stanza/stream_features.py +++ b/sleekxmpp/stanza/stream_features.py @@ -6,6 +6,7 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.thirdparty import OrderedDict from sleekxmpp.xmlstream import StanzaBase @@ -28,7 +29,10 @@ class StreamFeatures(StanzaBase): def get_features(self): """ """ - return self.plugins + features = OrderedDict() + for (name, lang), plugin in self.plugins.items(): + features[name] = plugin + return features def set_features(self, value): """ diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index ba79dce8..cac99f77 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -76,7 +76,7 @@ class SleekTest(unittest.TestCase): known_prefixes[prefix], xml_string) xml = self.parse_xml(xml_string) - xml = xml.getchildren()[0] + xml = list(xml)[0] return xml else: self.fail("XML data was mal-formed:\n%s" % xml_string) @@ -333,6 +333,8 @@ class SleekTest(unittest.TestCase): # Remove unique ID prefix to make it easier to test self.xmpp._id_prefix = '' self.xmpp._disconnect_wait_for_threads = False + self.xmpp.default_lang = None + self.xmpp.peer_default_lang = None # We will use this to wait for the session_start event # for live connections. @@ -386,6 +388,7 @@ class SleekTest(unittest.TestCase): sid='', stream_ns="http://etherx.jabber.org/streams", default_ns="jabber:client", + default_lang="en", version="1.0", xml_header=True): """ @@ -413,6 +416,8 @@ class SleekTest(unittest.TestCase): parts.append('from="%s"' % sfrom) if sid: parts.append('id="%s"' % sid) + if default_lang: + parts.append('xml:lang="%s"' % default_lang) parts.append('version="%s"' % version) parts.append('xmlns:stream="%s"' % stream_ns) parts.append('xmlns="%s"' % default_ns) @@ -512,9 +517,9 @@ class SleekTest(unittest.TestCase): if '{%s}lang' % xml_ns in recv_xml.attrib: del recv_xml.attrib['{%s}lang' % xml_ns] - if recv_xml.getchildren: + if list(recv_xml): # We received more than just the header - for xml in recv_xml.getchildren(): + for xml in recv_xml: self.xmpp.socket.recv_data(tostring(xml)) attrib = recv_xml.attrib @@ -564,6 +569,7 @@ class SleekTest(unittest.TestCase): sid='', stream_ns="http://etherx.jabber.org/streams", default_ns="jabber:client", + default_lang="en", version="1.0", xml_header=False, timeout=1): @@ -585,6 +591,7 @@ class SleekTest(unittest.TestCase): header = self.make_header(sto, sfrom, sid, stream_ns=stream_ns, default_ns=default_ns, + default_lang=default_lang, version=version, xml_header=xml_header) sent_header = self.xmpp.socket.next_sent(timeout) @@ -691,7 +698,7 @@ class SleekTest(unittest.TestCase): if xml.tag.startswith('{'): return xml.tag = '{%s}%s' % (ns, xml.tag) - for child in xml.getchildren(): + for child in xml: self.fix_namespaces(child, ns) def compare(self, xml, *other): @@ -734,7 +741,7 @@ class SleekTest(unittest.TestCase): return False # Step 4: Check children count - if len(xml.getchildren()) != len(other.getchildren()): + if len(list(xml)) != len(list(other)): return False # Step 5: Recursively check children diff --git a/sleekxmpp/xmlstream/cert.py b/sleekxmpp/xmlstream/cert.py index b2711e8e..339f872d 100644 --- a/sleekxmpp/xmlstream/cert.py +++ b/sleekxmpp/xmlstream/cert.py @@ -7,11 +7,12 @@ try: from pyasn1.type.univ import Any, ObjectIdentifier, OctetString from pyasn1.type.char import BMPString, IA5String, UTF8String from pyasn1.type.useful import GeneralizedTime - from pyasn1_modules.rfc2459 import Certificate, DirectoryString, SubjectAltName, GeneralNames, GeneralName + from pyasn1_modules.rfc2459 import (Certificate, DirectoryString, + SubjectAltName, GeneralNames, + GeneralName) from pyasn1_modules.rfc2459 import id_ce_subjectAltName as SUBJECT_ALT_NAME from pyasn1_modules.rfc2459 import id_at_commonName as COMMON_NAME - XMPP_ADDR = ObjectIdentifier('1.3.6.1.5.5.7.8.5') SRV_NAME = ObjectIdentifier('1.3.6.1.5.5.7.8.7') @@ -149,7 +150,7 @@ def verify(expected, raw_cert): expected_wild = expected[expected.index('.'):] expected_srv = '_xmpp-client.%s' % expected - for name in cert_names['XMPPAddr']: + for name in cert_names['XMPPAddr']: if name == expected: return True for name in cert_names['SRV']: diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py index 59dcb306..01c1991a 100644 --- a/sleekxmpp/xmlstream/handler/base.py +++ b/sleekxmpp/xmlstream/handler/base.py @@ -49,7 +49,7 @@ class BaseHandler(object): def match(self, xml): """Compare a stanza or XML object with the handler's matcher. - :param xml: An XML or + :param xml: An XML or :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object """ return self._matcher.match(xml) @@ -73,7 +73,7 @@ class BaseHandler(object): self._payload = payload def check_delete(self): - """Check if the handler should be removed from the list + """Check if the handler should be removed from the list of stream handlers. """ return self._destroy diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index 37f53335..7e3388f1 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -33,7 +33,7 @@ class Callback(BaseHandler): :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase` derived object for matching stanza objects. :param pointer: The function to execute during callback. - :param bool thread: **DEPRECATED.** Remains only for + :param bool thread: **DEPRECATED.** Remains only for backwards compatibility. :param bool once: Indicates if the handler should be used only once. Defaults to False. diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index 7977e767..a0568f08 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -34,9 +34,9 @@ class MatchXMLMask(MatcherBase): <message xmlns="jabber:client"><body /></message> - Use of XMLMask is discouraged, and - :class:`~sleekxmpp.xmlstream.matcher.xpath.MatchXPath` or - :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` + Use of XMLMask is discouraged, and + :class:`~sleekxmpp.xmlstream.matcher.xpath.MatchXPath` or + :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` should be used instead. The use of namespaces in the mask comparison is controlled by @@ -151,8 +151,8 @@ class MatchXMLMask(MatcherBase): """ tag = tag.split('}')[-1] try: - children = [c.tag.split('}')[-1] for c in xml.getchildren()] + children = [c.tag.split('}')[-1] for c in xml] index = children.index(tag) except ValueError: return None - return xml.getchildren()[index] + return list(xml)[index] diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index b6af0609..3f03e68e 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -77,10 +77,10 @@ class MatchXPath(MatcherBase): # Skip empty tag name artifacts from the cleanup phase. continue - children = [c.tag.split('}')[-1] for c in xml.getchildren()] + children = [c.tag.split('}')[-1] for c in xml] try: index = children.index(tag) except ValueError: return False - xml = xml.getchildren()[index] + xml = list(xml)[index] return True diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py index cf47c164..70e36f24 100644 --- a/sleekxmpp/xmlstream/scheduler.py +++ b/sleekxmpp/xmlstream/scheduler.py @@ -57,7 +57,7 @@ class Task(object): #: The keyword arguments to pass to :attr:`callback`. self.kwargs = kwargs or {} - + #: Indicates if the task should repeat after executing, #: using the same :attr:`seconds` delay. self.repeat = repeat @@ -103,7 +103,7 @@ class Scheduler(object): def __init__(self, parentstop=None): #: A queue for storing tasks self.addq = queue.Queue() - + #: A list of tasks in order of execution time. self.schedule = [] diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 5d572437..64e00626 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -12,6 +12,8 @@ :license: MIT, see LICENSE for more details """ +from __future__ import with_statement, unicode_literals + import copy import logging import weakref @@ -29,6 +31,9 @@ log = logging.getLogger(__name__) XML_TYPE = type(ET.Element('xml')) +XML_NS = 'http://www.w3.org/XML/1998/namespace' + + def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False): """ Associate a stanza object as a plugin for another stanza. @@ -40,7 +45,7 @@ def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False): substanzas for the parent, using ``parent['substanzas']``. If the attribute ``plugin_multi_attrib`` was defined for the plugin, then the substanza set can be filtered to only instances of the plugin - class. For example, given a plugin class ``Foo`` with + class. For example, given a plugin class ``Foo`` with ``plugin_multi_attrib = 'foos'`` then:: parent['foos'] @@ -94,6 +99,14 @@ def multifactory(stanza, plugin_attrib): """ Returns a ElementBase class for handling reoccuring child stanzas """ + + def plugin_filter(self): + return lambda x: isinstance(x, self._multistanza) + + def plugin_lang_filter(self, lang): + return lambda x: isinstance(x, self._multistanza) and \ + x['lang'] == lang + class Multi(ElementBase): """ Template class for multifactory @@ -101,28 +114,45 @@ def multifactory(stanza, plugin_attrib): def setup(self, xml=None): self.xml = ET.Element('') - def get_multi(self): + def get_multi(self, lang=None): parent = self.parent() - res = filter(lambda sub: isinstance(sub, self._multistanza), parent) + if not lang or lang == '*': + res = filter(plugin_filter(self), parent) + else: + res = filter(plugin_filter(self, lang), parent) return list(res) - def set_multi(self, val): + def set_multi(self, val, lang=None): parent = self.parent() - del parent[self.plugin_attrib] + del_multi = getattr(self, 'del_%s' % plugin_attrib) + del_multi(lang) for sub in val: parent.append(sub) - def del_multi(self): + def del_multi(self, lang=None): parent = self.parent() - res = filter(lambda sub: isinstance(sub, self._multistanza), parent) - for stanza in list(res): - parent.iterables.remove(stanza) - parent.xml.remove(stanza.xml) + if not lang or lang == '*': + res = filter(plugin_filter(self), parent) + else: + res = filter(plugin_filter(self, lang), parent) + res = list(res) + if not res: + del parent.plugins[(plugin_attrib, None)] + parent.loaded_plugins.remove(plugin_attrib) + try: + parent.xml.remove(self.xml) + except: + pass + else: + for stanza in list(res): + parent.iterables.remove(stanza) + parent.xml.remove(stanza.xml) Multi.is_extension = True Multi.plugin_attrib = plugin_attrib Multi._multistanza = stanza - Multi.interfaces = (plugin_attrib,) + Multi.interfaces = set([plugin_attrib]) + Multi.lang_interfaces = set([plugin_attrib]) setattr(Multi, "get_%s" % plugin_attrib, get_multi) setattr(Multi, "set_%s" % plugin_attrib, set_multi) setattr(Multi, "del_%s" % plugin_attrib, del_multi) @@ -231,8 +261,10 @@ class ElementBase(object): directly from the parent stanza, as shown below, but retrieving information will require all interfaces to be used, as so:: - >>> message['custom'] = 'bar' # Same as using message['custom']['custom'] - >>> message['custom']['custom'] # Must use all interfaces + >>> # Same as using message['custom']['custom'] + >>> message['custom'] = 'bar' + >>> # Must use all interfaces + >>> message['custom']['custom'] 'bar' If the plugin sets :attr:`is_extension` to ``True``, then both setting @@ -245,13 +277,13 @@ class ElementBase(object): :param xml: Initialize the stanza object with an existing XML object. - :param parent: Optionally specify a parent stanza object will will + :param parent: Optionally specify a parent stanza object will contain this substanza. """ #: The XML tag name of the element, not including any namespace - #: prefixes. For example, an :class:`ElementBase` object for ``<message />`` - #: would use ``name = 'message'``. + #: prefixes. For example, an :class:`ElementBase` object for + #: ``<message />`` would use ``name = 'message'``. name = 'stanza' #: The XML namespace for the element. Given ``<foo xmlns="bar" />``, @@ -289,14 +321,17 @@ class ElementBase(object): #: subelements of the underlying XML object. Using this set, the text #: of these subelements may be set, retrieved, or removed without #: needing to define custom methods. - sub_interfaces = tuple() + sub_interfaces = set() #: A subset of :attr:`interfaces` which maps the presence of #: subelements to boolean values. Using this set allows for quickly #: checking for the existence of empty subelements like ``<required />``. #: #: .. versionadded:: 1.1 - bool_interfaces = tuple() + bool_interfaces = set() + + #: .. versionadded:: 1.1.2 + lang_interfaces = set() #: In some cases you may wish to override the behaviour of one of the #: parent stanza's interfaces. The ``overrides`` list specifies the @@ -363,7 +398,7 @@ class ElementBase(object): subitem = set() #: The default XML namespace: ``http://www.w3.org/XML/1998/namespace``. - xml_ns = 'http://www.w3.org/XML/1998/namespace' + xml_ns = XML_NS def __init__(self, xml=None, parent=None): self._index = 0 @@ -375,6 +410,7 @@ class ElementBase(object): #: An ordered dictionary of plugin stanzas, mapped by their #: :attr:`plugin_attrib` value. self.plugins = OrderedDict() + self.loaded_plugins = set() #: A list of child stanzas whose class is included in #: :attr:`plugin_iterables`. @@ -385,6 +421,12 @@ class ElementBase(object): #: ``'{namespace}elementname'``. self.tag = self.tag_name() + if 'lang' not in self.interfaces: + if isinstance(self.interfaces, tuple): + self.interfaces += ('lang',) + else: + self.interfaces.add('lang') + #: A :class:`weakref.weakref` to the parent stanza, if there is one. #: If not, then :attr:`parent` is ``None``. self.parent = None @@ -403,13 +445,12 @@ class ElementBase(object): return # Initialize values using provided XML - for child in self.xml.getchildren(): + for child in self.xml: if child.tag in self.plugin_tag_map: plugin_class = self.plugin_tag_map[child.tag] - plugin = plugin_class(child, self) - self.plugins[plugin.plugin_attrib] = plugin - if plugin_class in self.plugin_iterables: - self.iterables.append(plugin) + self.init_plugin(plugin_class.plugin_attrib, + existing_xml=child, + reuse=False) def setup(self, xml=None): """Initialize the stanza's XML contents. @@ -443,7 +484,7 @@ class ElementBase(object): # We did not generate XML return False - def enable(self, attrib): + def enable(self, attrib, lang=None): """Enable and initialize a stanza plugin. Alias for :meth:`init_plugin`. @@ -451,24 +492,67 @@ class ElementBase(object): :param string attrib: The :attr:`plugin_attrib` value of the plugin to enable. """ - return self.init_plugin(attrib) + return self.init_plugin(attrib, lang) - def init_plugin(self, attrib): + def _get_plugin(self, name, lang=None): + if lang is None: + lang = self.get_lang() + + if name not in self.plugin_attrib_map: + return None + + plugin_class = self.plugin_attrib_map[name] + + if plugin_class.is_extension: + if (name, None) in self.plugins: + return self.plugins[(name, None)] + else: + return self.init_plugin(name, lang) + else: + if (name, lang) in self.plugins: + return self.plugins[(name, lang)] + else: + return self.init_plugin(name, lang) + + def init_plugin(self, attrib, lang=None, existing_xml=None, reuse=True): """Enable and initialize a stanza plugin. :param string attrib: The :attr:`plugin_attrib` value of the plugin to enable. """ - if attrib not in self.plugins: - plugin_class = self.plugin_attrib_map[attrib] + if lang is None: + lang = self.get_lang() + + plugin_class = self.plugin_attrib_map[attrib] + + if plugin_class.is_extension and (attrib, None) in self.plugins: + return self.plugins[(attrib, None)] + if reuse and (attrib, lang) in self.plugins: + return self.plugins[(attrib, lang)] + + if existing_xml is None: 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) - if plugin_class.plugin_multi_attrib: - self.init_plugin(plugin_class.plugin_multi_attrib) - return self + + if existing_xml is not None: + if existing_xml.attrib.get('{%s}lang' % XML_NS, '') != lang: + existing_xml = None + + plugin = plugin_class(parent=self, xml=existing_xml) + + if plugin.is_extension: + self.plugins[(attrib, None)] = plugin + else: + plugin['lang'] = lang + self.plugins[(attrib, lang)] = plugin + + if plugin_class in self.plugin_iterables: + self.iterables.append(plugin) + if plugin_class.plugin_multi_attrib: + self.init_plugin(plugin_class.plugin_multi_attrib) + + self.loaded_plugins.add(attrib) + + return plugin def _get_stanza_values(self): """Return A JSON/dictionary version of the XML content @@ -492,8 +576,14 @@ class ElementBase(object): values = {} for interface in self.interfaces: values[interface] = self[interface] + if interface in self.lang_interfaces: + values['%s|*' % interface] = self['%s|*' % interface] for plugin, stanza in self.plugins.items(): - values[plugin] = stanza.values + lang = stanza['lang'] + if lang: + values['%s|%s' % (plugin, lang)] = stanza.values + else: + values[plugin[0]] = stanza.values if self.iterables: iterables = [] for stanza in self.iterables: @@ -517,6 +607,11 @@ class ElementBase(object): p in self.plugin_iterables] for interface, value in values.items(): + full_interface = interface + interface_lang = ('%s|' % interface).split('|') + interface = interface_lang[0] + lang = interface_lang[1] or self.get_lang() + if interface == 'substanzas': # Remove existing substanzas for stanza in self.iterables: @@ -535,12 +630,12 @@ class ElementBase(object): self.iterables.append(sub) break elif interface in self.interfaces: - self[interface] = value + self[full_interface] = value elif interface in self.plugin_attrib_map: if interface not in iterable_interfaces: - if interface not in self.plugins: - self.init_plugin(interface) - self.plugins[interface].values = value + plugin = self._get_plugin(interface, lang) + if plugin: + plugin.values = value return self def __getitem__(self, attrib): @@ -572,6 +667,15 @@ class ElementBase(object): :param string attrib: The name of the requested stanza interface. """ + full_attrib = attrib + attrib_lang = ('%s|' % attrib).split('|') + attrib = attrib_lang[0] + lang = attrib_lang[1] or '' + + kwargs = {} + if lang and attrib in self.lang_interfaces: + kwargs['lang'] = lang + if attrib == 'substanzas': return self.iterables elif attrib in self.interfaces: @@ -579,32 +683,31 @@ class ElementBase(object): get_method2 = "get%s" % attrib.title() if self.plugin_overrides: - plugin = self.plugin_overrides.get(get_method, None) - if plugin: - if plugin not in self.plugins: - self.init_plugin(plugin) - handler = getattr(self.plugins[plugin], get_method, None) - if handler: - return handler() + name = self.plugin_overrides.get(get_method, None) + if name: + plugin = self._get_plugin(name, lang) + if plugin: + handler = getattr(plugin, get_method, None) + if handler: + return handler(**kwargs) if hasattr(self, get_method): - return getattr(self, get_method)() + return getattr(self, get_method)(**kwargs) elif hasattr(self, get_method2): - return getattr(self, get_method2)() + return getattr(self, get_method2)(**kwargs) else: if attrib in self.sub_interfaces: - return self._get_sub_text(attrib) + return self._get_sub_text(attrib, lang=lang) elif attrib in self.bool_interfaces: elem = self.xml.find('{%s}%s' % (self.namespace, attrib)) return elem is not None else: return self._get_attr(attrib) elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: - self.init_plugin(attrib) - if self.plugins[attrib].is_extension: - return self.plugins[attrib][attrib] - return self.plugins[attrib] + plugin = self._get_plugin(attrib, lang) + if plugin and plugin.is_extension: + return plugin[full_attrib] + return plugin else: return '' @@ -640,41 +743,58 @@ class ElementBase(object): :param string attrib: The name of the stanza interface to modify. :param value: The new value of the stanza interface. """ + full_attrib = attrib + attrib_lang = ('%s|' % attrib).split('|') + attrib = attrib_lang[0] + lang = attrib_lang[1] or '' + + kwargs = {} + if lang and attrib in self.lang_interfaces: + kwargs['lang'] = lang + if attrib in self.interfaces: if value is not None: set_method = "set_%s" % attrib.lower() set_method2 = "set%s" % attrib.title() if self.plugin_overrides: - plugin = self.plugin_overrides.get(set_method, None) - if plugin: - if plugin not in self.plugins: - self.init_plugin(plugin) - handler = getattr(self.plugins[plugin], - set_method, None) - if handler: - return handler(value) + name = self.plugin_overrides.get(set_method, None) + if name: + plugin = self._get_plugin(name, lang) + if plugin: + handler = getattr(plugin, set_method, None) + if handler: + return handler(value, **kwargs) if hasattr(self, set_method): - getattr(self, set_method)(value,) + getattr(self, set_method)(value, **kwargs) elif hasattr(self, set_method2): - getattr(self, set_method2)(value,) + getattr(self, set_method2)(value, **kwargs) else: if attrib in self.sub_interfaces: - return self._set_sub_text(attrib, text=value) + if lang == '*': + return self._set_all_sub_text(attrib, + value, + lang='*') + return self._set_sub_text(attrib, text=value, + lang=lang) elif attrib in self.bool_interfaces: if value: - return self._set_sub_text(attrib, '', keep=True) + return self._set_sub_text(attrib, '', + keep=True, + lang=lang) else: - return self._set_sub_text(attrib, '', keep=False) + return self._set_sub_text(attrib, '', + keep=False, + lang=lang) else: self._set_attr(attrib, value) else: self.__delitem__(attrib) elif attrib in self.plugin_attrib_map: - if attrib not in self.plugins: - self.init_plugin(attrib) - self.plugins[attrib][attrib] = value + plugin = self._get_plugin(attrib, lang) + if plugin: + plugin[full_attrib] = value return self def __delitem__(self, attrib): @@ -709,40 +829,53 @@ class ElementBase(object): :param attrib: The name of the affected stanza interface. """ + full_attrib = attrib + attrib_lang = ('%s|' % attrib).split('|') + attrib = attrib_lang[0] + lang = attrib_lang[1] or '' + + kwargs = {} + if lang and attrib in self.lang_interfaces: + kwargs['lang'] = lang + if attrib in self.interfaces: del_method = "del_%s" % attrib.lower() del_method2 = "del%s" % attrib.title() if self.plugin_overrides: - plugin = self.plugin_overrides.get(del_method, None) - if plugin: - if plugin not in self.plugins: - self.init_plugin(plugin) - handler = getattr(self.plugins[plugin], del_method, None) - if handler: - return handler() + name = self.plugin_overrides.get(del_method, None) + if name: + plugin = self._get_plugin(attrib, lang) + if plugin: + handler = getattr(plugin, del_method, None) + if handler: + return handler(**kwargs) if hasattr(self, del_method): - getattr(self, del_method)() + getattr(self, del_method)(**kwargs) elif hasattr(self, del_method2): - getattr(self, del_method2)() + getattr(self, del_method2)(**kwargs) else: if attrib in self.sub_interfaces: - return self._del_sub(attrib) + return self._del_sub(attrib, lang=lang) elif attrib in self.bool_interfaces: - return self._del_sub(attrib) + return self._del_sub(attrib, lang=lang) else: self._del_attr(attrib) elif attrib in self.plugin_attrib_map: - if attrib in self.plugins: - xml = self.plugins[attrib].xml - if self.plugins[attrib].is_extension: - del self.plugins[attrib][attrib] - del self.plugins[attrib] - try: - self.xml.remove(xml) - except: - pass + plugin = self._get_plugin(attrib, lang) + if not plugin: + return self + if plugin.is_extension: + del plugin[full_attrib] + del self.plugins[(attrib, None)] + else: + del self.plugins[(attrib, lang)] + self.loaded_plugins.remove(attrib) + try: + self.xml.remove(plugin.xml) + except: + pass return self def _set_attr(self, name, value): @@ -781,7 +914,7 @@ class ElementBase(object): """ return self.xml.attrib.get(name, default) - def _get_sub_text(self, name, default=''): + def _get_sub_text(self, name, default='', lang=None): """Return the text contents of a sub element. In case the element does not exist, or it has no textual content, @@ -793,13 +926,38 @@ class ElementBase(object): not exists. An empty string is returned otherwise. """ name = self._fix_ns(name) - stanza = self.xml.find(name) - if stanza is None or stanza.text is None: + if lang == '*': + return self._get_all_sub_text(name, default, None) + + default_lang = self.get_lang() + if not lang: + lang = default_lang + + stanzas = self.xml.findall(name) + if not stanzas: return default - else: - return stanza.text + for stanza in stanzas: + if stanza.attrib.get('{%s}lang' % XML_NS, default_lang) == lang: + if stanza.text is None: + return default + return stanza.text + return default + + def _get_all_sub_text(self, name, default='', lang=None): + name = self._fix_ns(name) - def _set_sub_text(self, name, text=None, keep=False): + default_lang = self.get_lang() + results = OrderedDict() + stanzas = self.xml.findall(name) + if stanzas: + for stanza in stanzas: + stanza_lang = stanza.attrib.get('{%s}lang' % XML_NS, + default_lang) + if not lang or lang == '*' or stanza_lang == lang: + results[stanza_lang] = stanza.text + return results + + def _set_sub_text(self, name, text=None, keep=False, lang=None): """Set the text contents of a sub element. In case the element does not exist, a element will be created, @@ -817,9 +975,14 @@ class ElementBase(object): """ path = self._fix_ns(name, split=True) element = self.xml.find(name) + parent = self.xml + + default_lang = self.get_lang() + if lang is None: + lang = default_lang if not text and not keep: - return self._del_sub(name) + return self._del_sub(name, lang=lang) if element is None: # We need to add the element. If the provided name was @@ -833,14 +996,31 @@ class ElementBase(object): element = self.xml.find("/".join(walked)) if element is None: element = ET.Element(ename) + if lang: + element.attrib['{%s}lang' % XML_NS] = lang last_xml.append(element) + parent = last_xml last_xml = element element = last_xml + if element.attrib.get('{%s}lang' % XML_NS, default_lang) != lang: + element = ET.Element(ename) + if lang: + element.attrib['{%s}lang' % XML_NS] = lang + parent.append(element) + element.text = text return element - def _del_sub(self, name, all=False): + def _set_all_sub_text(self, name, values, keep=False, lang=None): + self._del_sub(name, lang) + for value_lang, value in values.items(): + if not lang or lang == '*' or value_lang == lang: + self._set_sub_text(name, text=value, + keep=keep, + lang=value_lang) + + def _del_sub(self, name, all=False, lang=None): """Remove sub elements that match the given name or XPath. If the element is in a path, then any parent elements that become @@ -854,6 +1034,10 @@ class ElementBase(object): path = self._fix_ns(name, split=True) original_target = path[-1] + default_lang = self.get_lang() + if not lang: + lang = default_lang + for level, _ in enumerate(path): # Generate the paths to the target elements and their parent. element_path = "/".join(path[:len(path) - level]) @@ -866,11 +1050,13 @@ class ElementBase(object): if parent is None: parent = self.xml for element in elements: - if element.tag == original_target or \ - not element.getchildren(): + if element.tag == original_target or not list(element): # Only delete the originally requested elements, and # any parent elements that have become empty. - parent.remove(element) + elem_lang = element.attrib.get('{%s}lang' % XML_NS, + default_lang) + if lang == '*' or elem_lang == lang: + parent.remove(element) if not all: # If we don't want to delete elements up the tree, stop # after deleting the first level of elements. @@ -903,7 +1089,7 @@ class ElementBase(object): attributes = components[1:] if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \ - tag not in self.plugins and tag not in self.plugin_attrib: + tag not in self.loaded_plugins and tag not in self.plugin_attrib: # The requested tag is not in this stanza, so no match. return False @@ -932,10 +1118,12 @@ class ElementBase(object): if not matched_substanzas and len(xpath) > 1: # Convert {namespace}tag@attribs to just tag next_tag = xpath[1].split('@')[0].split('}')[-1] - if next_tag in self.plugins: - return self.plugins[next_tag].match(xpath[1:]) - else: - return False + langs = [name[1] for name in self.plugins if name[0] == next_tag] + for lang in langs: + plugin = self._get_plugin(next_tag, lang) + if plugin and plugin.match(xpath[1:]): + return True + return False # Everything matched. return True @@ -995,7 +1183,7 @@ class ElementBase(object): """ out = [] out += [x for x in self.interfaces] - out += [x for x in self.plugins] + out += [x for x in self.loaded_plugins] if self.iterables: out.append('substanzas') return out @@ -1075,6 +1263,23 @@ class ElementBase(object): """ return "{%s}%s" % (cls.namespace, cls.name) + def get_lang(self): + result = self.xml.attrib.get('{%s}lang' % XML_NS, '') + if not result and self.parent and self.parent(): + return self.parent()['lang'] + return result + + def set_lang(self, lang): + self.del_lang() + attr = '{%s}lang' % XML_NS + if lang: + self.xml.attrib[attr] = lang + + def del_lang(self): + attr = '{%s}lang' % XML_NS + if attr in self.xml.attrib: + del self.xml.attrib[attr] + @property def attrib(self): """Return the stanza object itself. @@ -1090,8 +1295,8 @@ class ElementBase(object): return self def _fix_ns(self, xpath, split=False, propagate_ns=True): - return fix_ns(xpath, split=split, - propagate_ns=propagate_ns, + return fix_ns(xpath, split=split, + propagate_ns=propagate_ns, default_ns=self.namespace) def __eq__(self, other): @@ -1219,6 +1424,8 @@ class StanzaBase(ElementBase): :param sfrom: Optional string or :class:`sleekxmpp.xmlstream.JID` object of the sender's JID. :param string sid: Optional ID value for the stanza. + :param parent: Optionally specify a parent stanza object will + contain this substanza. """ #: The default XMPP client namespace @@ -1233,11 +1440,11 @@ class StanzaBase(ElementBase): types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) def __init__(self, stream=None, xml=None, stype=None, - sto=None, sfrom=None, sid=None): + sto=None, sfrom=None, sid=None, parent=None): self.stream = stream if stream is not None: self.namespace = stream.default_ns - ElementBase.__init__(self, xml) + ElementBase.__init__(self, xml, parent) if stype is not None: self['type'] = stype if sto is not None: @@ -1285,7 +1492,7 @@ class StanzaBase(ElementBase): def get_payload(self): """Return a list of XML objects contained in the stanza.""" - return self.xml.getchildren() + return list(self.xml) def set_payload(self, value): """Add XML content to the stanza. diff --git a/sleekxmpp/xmlstream/tostring.py b/sleekxmpp/xmlstream/tostring.py index 8e729f79..2480f9b2 100644 --- a/sleekxmpp/xmlstream/tostring.py +++ b/sleekxmpp/xmlstream/tostring.py @@ -13,14 +13,19 @@ :license: MIT, see LICENSE for more details """ +from __future__ import unicode_literals + import sys if sys.version_info < (3, 0): import types +XML_NS = 'http://www.w3.org/XML/1998/namespace' + + def tostring(xml=None, xmlns='', stanza_ns='', stream=None, - outbuffer='', top_level=False): + outbuffer='', top_level=False, open_only=False): """Serialize an XML object to a Unicode string. If namespaces are provided using ``xmlns`` or ``stanza_ns``, then @@ -88,6 +93,13 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, output.append(' %s:%s="%s"' % (mapped_ns, attrib, value)) + elif attrib_ns == XML_NS: + output.append(' xml:%s="%s"' % (attrib, value)) + + if open_only: + # Only output the opening tag, regardless of content. + output.append(">") + return ''.join(output) if len(xml) or xml.text: # If there are additional child elements to serialize. @@ -95,7 +107,7 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, if xml.text: output.append(xml_escape(xml.text)) if len(xml): - for child in xml.getchildren(): + for child in xml: output.append(tostring(child, tag_xmlns, stanza_ns, stream)) output.append("</%s>" % tag_name) elif xml.text: diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 40ae38c9..4e7b4050 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -55,7 +55,7 @@ RESPONSE_TIMEOUT = 30 WAIT_TIMEOUT = 0.1 #: The number of threads to use to handle XML stream events. This is not the -#: same as the number of custom event handling threads. +#: same as the number of custom event handling threads. #: :data:`HANDLER_THREADS` must be at least 1. For Python implementations #: with a GIL, this should be left at 1, but for implemetnations without #: a GIL increasing this value can provide better performance. @@ -124,7 +124,7 @@ class XMLStream(object): self.ssl_support = SSL_SUPPORT #: Most XMPP servers support TLSv1, but OpenFire in particular - #: does not work well with it. For OpenFire, set + #: does not work well with it. For OpenFire, set #: :attr:`ssl_version` to use ``SSLv23``:: #: #: import ssl @@ -134,30 +134,30 @@ class XMLStream(object): #: Path to a file containing certificates for verifying the #: server SSL certificate. A non-``None`` value will trigger #: certificate checking. - #: + #: #: .. note:: #: #: On Mac OS X, certificates in the system keyring will #: be consulted, even if they are not in the provided file. self.ca_certs = None - #: The time in seconds to wait for events from the event queue, + #: The time in seconds to wait for events from the event queue, #: and also the time between checks for the process stop signal. self.wait_timeout = WAIT_TIMEOUT - #: The time in seconds to wait before timing out waiting + #: The time in seconds to wait before timing out waiting #: for response stanzas. self.response_timeout = RESPONSE_TIMEOUT - #: The current amount to time to delay attempting to reconnect. + #: The current amount to time to delay attempting to reconnect. #: This value doubles (with some jitter) with each failed #: connection attempt up to :attr:`reconnect_max_delay` seconds. self.reconnect_delay = None - + #: 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 + #: 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``. @@ -178,16 +178,16 @@ class XMLStream(object): #: The default port to return when querying DNS records. self.default_port = int(port) - - #: The domain to try when querying DNS records. + + #: The domain to try when querying DNS records. self.default_domain = '' #: The expected name of the server, for validation. self._expected_server_name = '' - + #: The desired, or actual, address of the connected server. self.address = (host, int(port)) - + #: A file-like wrapper for the socket for use with the #: :mod:`~xml.etree.ElementTree` module. self.filesocket = None @@ -223,6 +223,9 @@ class XMLStream(object): #: stream wrapper itself. self.default_ns = '' + self.default_lang = None + self.peer_default_lang = None + #: The namespace of the enveloping stream element. self.stream_ns = '' @@ -255,7 +258,7 @@ class XMLStream(object): #: and data is sent immediately over the wire. self.session_started_event = threading.Event() - #: The default time in seconds to wait for a session to start + #: The default time in seconds to wait for a session to start #: after connecting before reconnecting and trying again. self.session_timeout = 45 @@ -414,12 +417,12 @@ 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, args=(reattempt,)) + 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) @@ -434,7 +437,7 @@ class XMLStream(object): def _connect(self, reattempt=True): self.scheduler.remove('Session timeout check') self.stop.clear() - + if self.reconnect_delay is None or not reattempt: delay = 1.0 else: @@ -480,7 +483,7 @@ class XMLStream(object): if self.use_proxy: connected = self._connect_proxy() if not connected: - if reattempt: + if reattempt: self.reconnect_delay = delay return False @@ -517,7 +520,8 @@ class XMLStream(object): except (Socket.error, ssl.SSLError): log.error('CERT: Invalid certificate trust chain.') if not self.event_handled('ssl_invalid_chain'): - self.disconnect(self.auto_reconnect, send_close=False) + self.disconnect(self.auto_reconnect, + send_close=False) else: self.event('ssl_invalid_chain', direct=True) return False @@ -525,7 +529,7 @@ class XMLStream(object): self._der_cert = self.socket.getpeercert(binary_form=True) pem_cert = ssl.DER_cert_to_PEM_cert(self._der_cert) log.debug('CERT: %s', pem_cert) - + self.event('ssl_cert', pem_cert, direct=True) try: cert.verify(self._expected_server_name, self._der_cert) @@ -534,7 +538,9 @@ class XMLStream(object): if not self.event_handled('ssl_invalid_cert'): self.disconnect(send_close=False) else: - self.event('ssl_invalid_cert', pem_cert, direct=True) + self.event('ssl_invalid_cert', + pem_cert, + direct=True) self.set_socket(self.socket, ignore=True) #this event is where you should set your application state @@ -627,7 +633,7 @@ class XMLStream(object): If the disconnect should take place after all items in the send queue have been sent, use ``wait=True``. - + .. warning:: If you are constantly adding items to the queue @@ -648,7 +654,7 @@ class XMLStream(object): """ self.state.transition('connected', 'disconnected', wait=2.0, - func=self._disconnect, + func=self._disconnect, args=(reconnect, wait, send_close)) def _disconnect(self, reconnect=False, wait=None, send_close=True): @@ -702,16 +708,18 @@ class XMLStream(object): """Reset the stream's state and reconnect to the server.""" log.debug("reconnecting...") if self.state.ensure('connected'): - self.state.transition('connected', 'disconnected', + self.state.transition('connected', 'disconnected', wait=2.0, - func=self._disconnect, + func=self._disconnect, args=(True, wait, send_close)) attempts = self.reconnect_max_attempts log.debug("connecting...") connected = self.state.transition('disconnected', 'connected', - wait=2.0, func=self._connect, args=(reattempt,)) + wait=2.0, + func=self._connect, + args=(reattempt,)) while reattempt and not connected and not self.stop.is_set(): connected = self.state.transition('disconnected', 'connected', wait=2.0, func=self._connect) @@ -759,8 +767,8 @@ class XMLStream(object): """ Configure and set options for a :class:`~dns.resolver.Resolver` instance, and other DNS related tasks. For example, you - can also check :meth:`~socket.socket.getaddrinfo` to see - if you need to call out to ``libresolv.so.2`` to + can also check :meth:`~socket.socket.getaddrinfo` to see + if you need to call out to ``libresolv.so.2`` to run ``res_init()``. Meant to be overridden. @@ -814,7 +822,7 @@ class XMLStream(object): log.debug('CERT: %s', pem_cert) self.event('ssl_cert', pem_cert, direct=True) - try: + try: cert.verify(self._expected_server_name, self._der_cert) except cert.CertificateError as err: log.error(err.message) @@ -874,8 +882,8 @@ class XMLStream(object): self.schedule('Whitespace Keepalive', self.whitespace_keepalive_interval, self.send_raw, - args = (' ',), - kwargs = {'now': True}, + args=(' ',), + kwargs={'now': True}, repeat=True) def _remove_schedules(self, event): @@ -884,7 +892,7 @@ class XMLStream(object): self.scheduler.remove('Certificate Expiration') def start_stream_handler(self, xml): - """Perform any initialization actions, such as handshakes, + """Perform any initialization actions, such as handshakes, once the stream header has been sent. Meant to be overridden. @@ -892,8 +900,8 @@ class XMLStream(object): pass def register_stanza(self, stanza_class): - """Add a stanza object class as a known root stanza. - + """Add a stanza object class as a known root stanza. + A root stanza is one that appears as a direct child of the stream's root element. @@ -910,8 +918,8 @@ class XMLStream(object): self.__root_stanza.append(stanza_class) def remove_stanza(self, stanza_class): - """Remove a stanza from being a known root stanza. - + """Remove a stanza from being a known root stanza. + A root stanza is one that appears as a direct child of the stream's root element. @@ -976,8 +984,9 @@ class XMLStream(object): """Add a stream event handler that will be executed when a matching stanza is received. - :param handler: The :class:`~sleekxmpp.xmlstream.handler.base.BaseHandler` - derived object to execute. + :param handler: + The :class:`~sleekxmpp.xmlstream.handler.base.BaseHandler` + derived object to execute. """ if handler.stream is None: self.__handlers.append(handler) @@ -1004,11 +1013,12 @@ class XMLStream(object): """ if port is None: port = self.default_port - + resolver = default_resolver() self.configure_dns(resolver, domain=domain, port=port) - return resolve(domain, port, service=self.dns_service, resolver=resolver) + return resolve(domain, port, service=self.dns_service, + resolver=resolver) def pick_dns_answer(self, domain, port=None): """Pick a server and port from DNS answers. @@ -1026,7 +1036,7 @@ class XMLStream(object): return self.dns_answers.next() else: return next(self.dns_answers) - + def add_event_handler(self, name, pointer, threaded=False, disposable=False): """Add a custom event handler that will be executed whenever @@ -1141,9 +1151,9 @@ class XMLStream(object): May optionally block until an expected response is received. - :param data: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + :param data: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` stanza to send on the stream. - :param mask: **DEPRECATED** + :param mask: **DEPRECATED** An XML string snippet matching the structure of the expected response. Execution will block in this thread until the response is received @@ -1195,9 +1205,9 @@ class XMLStream(object): """Send an XML object on the stream, and optionally wait for a response. - :param data: The :class:`~xml.etree.ElementTree.Element` XML object + :param data: The :class:`~xml.etree.ElementTree.Element` XML object to send on the stream. - :param mask: **DEPRECATED** + :param mask: **DEPRECATED** An XML string snippet matching the structure of the expected response. Execution will block in this thread until the response is received @@ -1237,14 +1247,15 @@ class XMLStream(object): count += 1 except ssl.SSLError as serr: if tries >= self.ssl_retry_max: - log.debug('SSL error - max retries reached') + 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 if not self.stop.is_set(): - self.disconnect(reconnect, send_close=False) - log.warning('SSL write error - reattempting') + self.disconnect(reconnect, + send_close=False) + log.warning('SSL write error: retrying') if not self.stop.is_set(): time.sleep(self.ssl_retry_delay) tries += 1 @@ -1299,7 +1310,7 @@ class XMLStream(object): def _wait_for_threads(self): with self.__thread_cond: if self.__thread_count != 0: - log.debug("Waiting for %s threads to exit." % + log.debug("Waiting for %s threads to exit." % self.__thread_count) name = threading.current_thread().name if name in self.__thread: @@ -1331,7 +1342,7 @@ class XMLStream(object): Defaults to ``True``. This does **not** mean that no threads are used at all if ``threaded=False``. - Regardless of these threading options, these threads will + Regardless of these threading options, these threads will always exist: - The event queue processor @@ -1421,7 +1432,7 @@ class XMLStream(object): def __read_xml(self): """Parse the incoming XML stream - + Stream events are raised for each received stanza. """ depth = 0 @@ -1431,6 +1442,10 @@ class XMLStream(object): if depth == 0: # We have received the start of the root element. root = xml + log.debug('RECV: %s', tostring(root, xmlns=self.default_ns, + stream=self, + top_level=True, + open_only=True)) # Perform any stream initialization actions, such # as handshakes. self.stream_end_event.clear() @@ -1461,10 +1476,10 @@ class XMLStream(object): """Create a stanza object from a given XML object. If a specialized stanza type is not found for the XML, then - a generic :class:`~sleekxmpp.xmlstream.stanzabase.StanzaBase` + a generic :class:`~sleekxmpp.xmlstream.stanzabase.StanzaBase` stanza will be returned. - :param xml: The :class:`~xml.etree.ElementTree.Element` XML object + :param xml: The :class:`~xml.etree.ElementTree.Element` XML object to convert into a stanza object. :param default_ns: Optional default namespace to use instead of the stream's current default namespace. @@ -1478,6 +1493,8 @@ class XMLStream(object): stanza_type = stanza_class break stanza = stanza_type(self, xml) + if stanza['lang'] is None and self.peer_default_lang: + stanza['lang'] = self.peer_default_lang return stanza def __spawn_event(self, xml): @@ -1647,12 +1664,13 @@ class XMLStream(object): count += 1 except ssl.SSLError as serr: if tries >= self.ssl_retry_max: - log.debug('SSL error - max retries reached') + log.debug('SSL error: max retries reached') self.exception(serr) log.warning("Failed to send %s", data) if not self.stop.is_set(): - self.disconnect(self.auto_reconnect, send_close=False) - log.warning('SSL write error - reattempting') + self.disconnect(self.auto_reconnect, + send_close=False) + log.warning('SSL write error: retrying') if not self.stop.is_set(): time.sleep(self.ssl_retry_delay) tries += 1 diff --git a/tests/test_stanza_element.py b/tests/test_stanza_element.py index 09093003..1b47e733 100644 --- a/tests/test_stanza_element.py +++ b/tests/test_stanza_element.py @@ -64,14 +64,18 @@ class TestElementBase(SleekTest): stanza.append(substanza) values = stanza.getStanzaValues() - expected = {'bar': 'a', + expected = {'lang': '', + 'bar': 'a', 'baz': '', - 'foo2': {'bar': '', + 'foo2': {'lang': '', + 'bar': '', 'baz': 'b'}, 'substanzas': [{'__childtag__': '{foo}foo2', + 'lang': '', 'bar': '', 'baz': 'b'}, {'__childtag__': '{foo}subfoo', + 'lang': '', 'bar': 'c', 'baz': ''}]} self.failUnless(values == expected, @@ -555,12 +559,12 @@ class TestElementBase(SleekTest): stanza = TestStanza() - self.failUnless(set(stanza.keys()) == set(('bar', 'baz')), + self.failUnless(set(stanza.keys()) == set(('lang', 'bar', 'baz')), "Returned set of interface keys does not match expected.") stanza.enable('qux') - self.failUnless(set(stanza.keys()) == set(('bar', 'baz', 'qux')), + self.failUnless(set(stanza.keys()) == set(('lang', 'bar', 'baz', 'qux')), "Incorrect set of interface and plugin keys.") def testGet(self): diff --git a/tests/test_stanza_xep_0050.py b/tests/test_stanza_xep_0050.py index ae584de4..e02e86c3 100644 --- a/tests/test_stanza_xep_0050.py +++ b/tests/test_stanza_xep_0050.py @@ -49,7 +49,7 @@ class TestAdHocCommandStanzas(SleekTest): iq['command']['actions'] = ['prev', 'next'] results = iq['command']['actions'] - expected = ['prev', 'next'] + expected = set(['prev', 'next']) self.assertEqual(results, expected, "Incorrect next actions: %s" % results) |