diff options
Diffstat (limited to 'sleekxmpp/plugins')
81 files changed, 4459 insertions, 640 deletions
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index c0b1121b..c374f27b 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -5,9 +5,46 @@ See the file LICENSE for copying permission. """ -__all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', - 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082', - 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199', - 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify'] -# Don't automatically load xep_0078 +from sleekxmpp.plugins.base import PluginManager, PluginNotFound, BasePlugin +from sleekxmpp.plugins.base import register_plugin, load_plugin + + +__all__ = [ + # Non-standard + 'gmail_notify', # Gmail searching and notifications + + # XEPS + 'xep_0004', # Data Forms + 'xep_0009', # Jabber-RPC + 'xep_0012', # Last Activity + 'xep_0030', # Service Discovery + 'xep_0033', # Extended Stanza Addresses + 'xep_0045', # Multi-User Chat (Client) + 'xep_0047', # In-Band Bytestreams + 'xep_0050', # Ad-hoc Commands + 'xep_0059', # Result Set Management + 'xep_0060', # Pubsub (Client) + 'xep_0066', # Out of Band Data + 'xep_0077', # In-Band Registration +# 'xep_0078', # Non-SASL auth. Don't automatically load + 'xep_0080', # User Location + 'xep_0082', # XMPP Date and Time Profiles + 'xep_0085', # Chat State Notifications + 'xep_0086', # Legacy Error Codes + 'xep_0092', # Software Version + 'xep_0107', # User Mood + 'xep_0108', # User Activity + 'xep_0115', # Entity Capabilities + 'xep_0118', # User Tune + 'xep_0128', # Extended Service Discovery + 'xep_0163', # Personal Eventing Protocol + 'xep_0172', # User Nickname + 'xep_0184', # Message Receipts + 'xep_0198', # Stream Management + 'xep_0199', # Ping + 'xep_0202', # Entity Time + 'xep_0203', # Delayed Delivery + 'xep_0224', # Attention + 'xep_0249', # Direct MUC Invitations +] diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index 561421d8..f08023ba 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -1,91 +1,293 @@ +# -*- encoding: utf-8 -*- + """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.plugins.base + ~~~~~~~~~~~~~~~~~~~~~~ + + This module provides XMPP functionality that + is specific to client connections. + + Part of SleekXMPP: The Sleek XMPP Library - See the file LICENSE for copying permission. + :copyright: (c) 2012 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ +import sys +import logging +import threading + + +log = logging.getLogger(__name__) + + +#: Associate short string names of plugins with implementations. The +#: plugin names are based on the spec used by the plugin, such as +#: `'xep_0030'` for a plugin that implements XEP-0030. +PLUGIN_REGISTRY = {} + +#: In order to do cascading plugin disabling, reverse dependencies +#: must be tracked. +PLUGIN_DEPENDENTS = {} + +#: Only allow one thread to manipulate the plugin registry at a time. +REGISTRY_LOCK = threading.RLock() -class base_plugin(object): +class PluginNotFound(Exception): + """Raised if an unknown plugin is accessed.""" + + +def register_plugin(impl, name=None): + """Add a new plugin implementation to the registry. + + :param class impl: The plugin class. + + The implementation class must provide a :attr:`~BasePlugin.name` + value that will be used as a short name for enabling and disabling + the plugin. The name should be based on the specification used by + the plugin. For example, a plugin implementing XEP-0030 would be + named `'xep_0030'`. """ - The base_plugin class serves as a base for user created plugins - that provide support for existing or experimental XEPS. - - Each plugin has a dictionary for configuration options, as well - as a name and description. - - The lifecycle of a plugin is: - 1. The plugin is instantiated during registration. - 2. Once the XML stream begins processing, the method - plugin_init() is called (if the plugin is configured - as enabled with {'enable': True}). - 3. After all plugins have been initialized, the - method post_init() is called. - - Recommended event handlers: - session_start -- Plugins which require the use of the current - bound JID SHOULD wait for the session_start - event to perform any initialization (or - resetting). This is a transitive recommendation, - plugins that use other plugins which use the - bound JID should also wait for session_start - before making such calls. - session_end -- If the plugin keeps any per-session state, - such as joined MUC rooms, such state SHOULD - be cleared when the session_end event is raised. - - Attributes: - xep -- The XEP number the plugin implements, if any. - description -- A short description of the plugin, typically - the long name of the implemented XEP. - xmpp -- The main SleekXMPP instance. - config -- A dictionary of custom configuration values. - The value 'enable' is special and controls - whether or not the plugin is initialized - after registration. - post_initted -- Executed after all plugins have been initialized - to handle any cross-plugin interactions, such as - registering service discovery items. - enable -- Indicates that the plugin is enabled for use and - will be initialized after registration. - - Methods: - plugin_init -- Initialize the plugin state. - post_init -- Handle any cross-plugin concerns. + if name is None: + name = impl.name + with REGISTRY_LOCK: + PLUGIN_REGISTRY[name] = impl + if name not in PLUGIN_DEPENDENTS: + PLUGIN_DEPENDENTS[name] = set() + for dep in impl.dependencies: + if dep not in PLUGIN_DEPENDENTS: + PLUGIN_DEPENDENTS[dep] = set() + PLUGIN_DEPENDENTS[dep].add(name) + + +def load_plugin(name, module=None): + """Find and import a plugin module so that it can be registered. + + This function is called to import plugins that have selected for + enabling, but no matching registered plugin has been found. + + :param str name: The name of the plugin. It is expected that + plugins are in packages matching their name, + even though the plugin class name does not + have to match. + :param str module: The name of the base module to search + for the plugin. """ + try: + if not module: + try: + module = 'sleekxmpp.plugins.%s' % name + __import__(module) + mod = sys.modules[module] + except: + module = 'sleekxmpp.features.%s' % name + __import__(module) + mod = sys.modules[module] + else: + __import__(module) + mod = sys.modules[module] + # Add older style plugins to the registry. + if hasattr(mod, name): + plugin = getattr(mod, name) + if hasattr(plugin, 'xep') or hasattr(plugin, 'rfc'): + plugin.name = name + # Mark the plugin as an older style plugin so + # we can work around dependency issues. + plugin.old_style = True + register_plugin(plugin, name) + except: + log.exception("Unable to load plugin: %s", name) + + +class PluginManager(object): def __init__(self, xmpp, config=None): + #: We will track all enabled plugins in a set so that we + #: can enable plugins in batches and pull in dependencies + #: without problems. + self._enabled = set() + + #: Maintain references to active plugins. + self._plugins = {} + + self._plugin_lock = threading.RLock() + + #: Globally set default plugin configuration. This will + #: be used for plugins that are auto-enabled through + #: dependency loading. + self.config = config if config else {} + + self.xmpp = xmpp + + def register(self, plugin, enable=True): + """Register a new plugin, and optionally enable it. + + :param class plugin: The implementation class of the plugin + to register. + :param bool enable: If ``True``, immediately enable the + plugin after registration. """ - Instantiate a new plugin and store the given configuration. + register_plugin(plugin) + if enable: + self.enable(plugin.name) + + def enable(self, name, config=None, enabled=None): + """Enable a plugin, including any dependencies. - Arguments: - xmpp -- The main SleekXMPP instance. - config -- A dictionary of configuration values. + :param string name: The short name of the plugin. + :param dict config: Optional settings dictionary for + configuring plugin behaviour. """ + top_level = False + if enabled is None: + enabled = set() + + with self._plugin_lock: + if name not in self._enabled: + enabled.add(name) + self._enabled.add(name) + if not self.registered(name): + load_plugin(name) + + plugin_class = PLUGIN_REGISTRY.get(name, None) + if not plugin_class: + raise PluginNotFound(name) + + if config is None: + config = self.config.get(name, None) + + plugin = plugin_class(self.xmpp, config) + self._plugins[name] = plugin + for dep in plugin.dependencies: + self.enable(dep, enabled=enabled) + plugin.plugin_init() + log.debug("Loaded Plugin: %s", plugin.description) + + if top_level: + for name in enabled: + if hasattr(self.plugins[name], 'old_style'): + # Older style plugins require post_init() + # to run just before stream processing begins, + # so we don't call it here. + pass + self.plugins[name].post_init() + + def enable_all(self, names=None, config=None): + """Enable all registered plugins. + + :param list names: A list of plugin names to enable. If + none are provided, all registered plugins + will be enabled. + :param dict config: A dictionary mapping plugin names to + configuration dictionaries, as used by + :meth:`~PluginManager.enable`. + """ + names = names if names else PLUGIN_REGISTRY.keys() if config is None: config = {} - self.xep = None - self.rfc = None - self.description = 'Base Plugin' - self.xmpp = xmpp - self.config = config - self.post_inited = False - self.enable = config.get('enable', True) - if self.enable: - self.plugin_init() + for name in names: + self.enable(name, config.get(name, {})) - def plugin_init(self): + def enabled(self, name): + """Check if a plugin has been enabled. + + :param string name: The name of the plugin to check. + :return: boolean + """ + return name in self._enabled + + def registered(self, name): + """Check if a plugin has been registered. + + :param string name: The name of the plugin to check. + :return: boolean + """ + return name in PLUGIN_REGISTRY + + def disable(self, name, _disabled=None): + """Disable a plugin, including any dependent upon it. + + :param string name: The name of the plugin to disable. + :param set _disabled: Private set used to track the + disabled status of plugins during + the cascading process. + """ + if _disabled is None: + _disabled = set() + with self._plugin_lock: + if name not in _disabled and name in self._enabled: + _disabled.add(name) + plugin = self._plugins.get(name, None) + if plugin is None: + raise PluginNotFound(name) + for dep in PLUGIN_DEPENDENTS[name]: + self.disable(dep, _disabled) + plugin.plugin_end() + if name in self._enabled: + self._enabled.remove(name) + del self._plugins[name] + + def __keys__(self): + """Return the set of enabled plugins.""" + return self._plugins.keys() + + def __getitem__(self, name): """ - Initialize plugin state, such as registering any stream or - event handlers, or new stanza types. + Allow plugins to be accessed through the manager as if + it were a dictionary. """ + plugin = self._plugins.get(name, None) + if plugin is None: + raise PluginNotFound(name) + return plugin + + def __iter__(self): + """Return an iterator over the set of enabled plugins.""" + return self._plugins.__iter__() + + def __len__(self): + """Return the number of enabled plugins.""" + return len(self._plugins) + + +class BasePlugin(object): + + #: A short name for the plugin based on the implemented specification. + #: For example, a plugin for XEP-0030 would use `'xep_0030'`. + name = '' + + #: A longer name for the plugin, describing its purpose. For example, + #: a plugin for XEP-0030 would use `'Service Discovery'` as its + #: description value. + description = '' + + #: Some plugins may depend on others in order to function properly. + #: Any plugin names included in :attr:`~BasePlugin.dependencies` will + #: be initialized as needed if this plugin is enabled. + dependencies = set() + + def __init__(self, xmpp, config=None): + self.xmpp = xmpp + + #: A plugin's behaviour may be configurable, in which case those + #: configuration settings will be provided as a dictionary. + self.config = config if config is not None else {} + + def plugin_init(self): + """Initialize plugin state, such as registering event handlers.""" + pass + + def plugin_end(self): + """Cleanup plugin state, and prepare for plugin removal.""" pass def post_init(self): + """Initialize any cross-plugin state. + + Only needed if the plugin has circular dependencies. """ - Perform any cross-plugin interactions, such as registering - service discovery identities or items. - """ - self.post_inited = True + pass + + +base_plugin = BasePlugin diff --git a/sleekxmpp/plugins/xep_0004/__init__.py b/sleekxmpp/plugins/xep_0004/__init__.py index aad4e15f..2cd18ec8 100644 --- a/sleekxmpp/plugins/xep_0004/__init__.py +++ b/sleekxmpp/plugins/xep_0004/__init__.py @@ -6,6 +6,17 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0004.stanza import Form from sleekxmpp.plugins.xep_0004.stanza import FormField, FieldOption -from sleekxmpp.plugins.xep_0004.dataforms import xep_0004 +from sleekxmpp.plugins.xep_0004.dataforms import XEP_0004 + + +register_plugin(XEP_0004) + + +# Retain some backwards compatibility +xep_0004 = XEP_0004 +xep_0004.makeForm = xep_0004.make_form +xep_0004.buildForm = xep_0004.build_form diff --git a/sleekxmpp/plugins/xep_0004/dataforms.py b/sleekxmpp/plugins/xep_0004/dataforms.py index 5414be5c..1097bd29 100644 --- a/sleekxmpp/plugins/xep_0004/dataforms.py +++ b/sleekxmpp/plugins/xep_0004/dataforms.py @@ -6,29 +6,27 @@ See the file LICENSE for copying permission. """ -import copy - -from sleekxmpp.thirdparty import OrderedDict - from sleekxmpp import Message -from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0004 import stanza from sleekxmpp.plugins.xep_0004.stanza import Form, FormField, FieldOption -class xep_0004(base_plugin): +class XEP_0004(BasePlugin): + """ XEP-0004: Data Forms """ - def plugin_init(self): - self.xep = '0004' - self.description = 'Data Forms' - self.stanza = stanza + name = 'xep_0004' + description = 'XEP-0004: Data Forms' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): self.xmpp.registerHandler( Callback('Data Form', StanzaPath('message/form'), @@ -38,6 +36,8 @@ class xep_0004(base_plugin): register_stanza_plugin(Form, FormField, iterable=True) register_stanza_plugin(Message, Form) + self.xmpp['xep_0030'].add_feature('jabber:x:data') + def make_form(self, ftype='form', title='', instructions=''): f = Form() f['type'] = ftype @@ -45,16 +45,8 @@ class xep_0004(base_plugin): f['instructions'] = instructions return f - def post_init(self): - base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') - def handle_form(self, message): self.xmpp.event("message_xform", message) def build_form(self, xml): return Form(xml=xml) - - -xep_0004.makeForm = xep_0004.make_form -xep_0004.buildForm = xep_0004.build_form diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py index 8131233c..1e175966 100644 --- a/sleekxmpp/plugins/xep_0004/stanza/field.py +++ b/sleekxmpp/plugins/xep_0004/stanza/field.py @@ -79,19 +79,21 @@ class FormField(ElementBase): reqXML = self.xml.find('{%s}required' % self.namespace) return reqXML is not None - def get_value(self): + def get_value(self, convert=True): valsXML = self.xml.findall('{%s}value' % self.namespace) if len(valsXML) == 0: return None elif self._type == 'boolean': - return valsXML[0].text in self.true_values + if convert: + return valsXML[0].text in self.true_values + return valsXML[0].text elif self._type in self.multi_value_types or len(valsXML) > 1: values = [] for valXML in valsXML: if valXML.text is None: valXML.text = '' values.append(valXML.text) - if self._type == 'text-multi': + if self._type == 'text-multi' and convert: values = "\n".join(values) return values else: @@ -136,6 +138,8 @@ class FormField(ElementBase): valXML.text = '0' self.xml.append(valXML) elif self._type in self.multi_value_types or self._type in ('', None): + if isinstance(value, bool): + value = [value] if not isinstance(value, list): value = value.replace('\r', '') value = value.split('\n') diff --git a/sleekxmpp/plugins/xep_0009/__init__.py b/sleekxmpp/plugins/xep_0009/__init__.py index 2cd14170..0ce3cf2c 100644 --- a/sleekxmpp/plugins/xep_0009/__init__.py +++ b/sleekxmpp/plugins/xep_0009/__init__.py @@ -6,6 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0009 import stanza -from sleekxmpp.plugins.xep_0009.rpc import xep_0009 +from sleekxmpp.plugins.xep_0009.rpc import XEP_0009 from sleekxmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse + + +register_plugin(XEP_0009) + + +# Retain some backwards compatibility +xep_0009 = XEP_0009 diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py index 892fa67a..5418626b 100644 --- a/sleekxmpp/plugins/xep_0009/binding.py +++ b/sleekxmpp/plugins/xep_0009/binding.py @@ -6,7 +6,7 @@ See the file LICENSE for copying permission. """ -from xml.etree import cElementTree as ET +from sleekxmpp.xmlstream import ET import base64 import logging import time diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py index 4f749f30..4e1c538b 100644 --- a/sleekxmpp/plugins/xep_0009/rpc.py +++ b/sleekxmpp/plugins/xep_0009/rpc.py @@ -6,28 +6,28 @@ See the file LICENSE for copying permission.
"""
-from sleekxmpp.plugins import base
-from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
-from sleekxmpp.stanza.iq import Iq
-from sleekxmpp.xmlstream.handler.callback import Callback
-from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
-from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
-from xml.etree import cElementTree as ET
import logging
+from sleekxmpp import Iq
+from sleekxmpp.xmlstream import ET, register_stanza_plugin
+from sleekxmpp.xmlstream.handler import Callback
+from sleekxmpp.xmlstream.matcher import MatchXPath
+from sleekxmpp.plugins import BasePlugin
+from sleekxmpp.plugins.xep_0009 import stanza
+from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
log = logging.getLogger(__name__)
+class XEP_0009(BasePlugin):
-class xep_0009(base.base_plugin):
+ name = 'xep_0009'
+ description = 'XEP-0009: Jabber-RPC'
+ dependencies = set(['xep_0030'])
+ stanza = stanza
def plugin_init(self):
- self.xep = '0009'
- self.description = 'Jabber-RPC'
- #self.stanza = sleekxmpp.plugins.xep_0009.stanza
-
register_stanza_plugin(Iq, RPCQuery)
register_stanza_plugin(RPCQuery, MethodCall)
register_stanza_plugin(RPCQuery, MethodResponse)
@@ -51,10 +51,8 @@ class xep_0009(base.base_plugin): self.xmpp.add_event_handler('error', self._handle_error)
#self.activeCalls = []
- def post_init(self):
- base.base_plugin.post_init(self)
- self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
- self.xmpp.plugin['xep_0030'].add_identity('automation','rpc')
+ self.xmpp['xep_0030'].add_feature('jabber:iq:rpc')
+ self.xmpp['xep_0030'].add_identity('automation','rpc')
def make_iq_method_call(self, pto, pmethod, params):
iq = self.xmpp.makeIqSet()
@@ -218,4 +216,3 @@ class xep_0009(base.base_plugin): def _extract_method(self, stanza):
xml = ET.fromstring("%s" % stanza)
return xml.find("./methodCall/methodName").text
-
diff --git a/sleekxmpp/plugins/xep_0012.py b/sleekxmpp/plugins/xep_0012.py index c5532bd4..01fb60a8 100644 --- a/sleekxmpp/plugins/xep_0012.py +++ b/sleekxmpp/plugins/xep_0012.py @@ -9,11 +9,11 @@ from datetime import datetime
import logging
-from . import base
-from .. stanza.iq import Iq
-from .. xmlstream.handler.callback import Callback
-from .. xmlstream.matcher.xpath import MatchXPath
-from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
+from sleekxmpp.plugins import BasePlugin, register_plugin
+from sleekxmpp import Iq
+from sleekxmpp.xmlstream.handler.callback import Callback
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+from sleekxmpp.xmlstream import ElementBase, ET, JID, register_stanza_plugin
log = logging.getLogger(__name__)
@@ -40,12 +40,18 @@ class LastActivity(ElementBase): def del_status(self):
self.xml.text = ''
-class xep_0012(base.base_plugin):
+
+class XEP_0012(BasePlugin):
+
"""
XEP-0012 Last Activity
"""
+
+ name = 'xep_0012'
+ description = 'XEP-0012: Last Activity'
+ dependencies = set(['xep_0030'])
+
def plugin_init(self):
- self.description = "Last Activity"
self.xep = "0012"
self.xmpp.registerHandler(
@@ -57,9 +63,6 @@ class xep_0012(base.base_plugin): self.xmpp.add_event_handler('last_activity_request', self.handle_last_activity)
-
- def post_init(self):
- base.base_plugin.post_init(self)
if self.xmpp.is_component:
# We are a component, so we track the uptime
self.xmpp.add_event_handler("session_start", self._reset_uptime)
@@ -113,3 +116,7 @@ class xep_0012(base.base_plugin): id = iq.get('id')
result = iq.send()
return result['last_activity']['seconds']
+
+
+xep_0012 = XEP_0012
+register_plugin(XEP_0012)
diff --git a/sleekxmpp/plugins/xep_0030/__init__.py b/sleekxmpp/plugins/xep_0030/__init__.py index 2e183852..0d1de65b 100644 --- a/sleekxmpp/plugins/xep_0030/__init__.py +++ b/sleekxmpp/plugins/xep_0030/__init__.py @@ -6,7 +6,18 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0030 import stanza from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems from sleekxmpp.plugins.xep_0030.static import StaticDisco -from sleekxmpp.plugins.xep_0030.disco import xep_0030 +from sleekxmpp.plugins.xep_0030.disco import XEP_0030 + + +register_plugin(XEP_0030) + +# Retain some backwards compatibility +xep_0030 = XEP_0030 +XEP_0030.getInfo = XEP_0030.get_info +XEP_0030.getItems = XEP_0030.get_items +XEP_0030.make_static = XEP_0030.restore_defaults diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 53086d4e..a5e8fd1c 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -8,20 +8,19 @@ import logging -import sleekxmpp from sleekxmpp import Iq -from sleekxmpp.exceptions import XMPPError -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID -from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems +from sleekxmpp.plugins.xep_0030 import StaticDisco log = logging.getLogger(__name__) -class xep_0030(base_plugin): +class XEP_0030(BasePlugin): """ XEP-0030: Service Discovery @@ -85,14 +84,15 @@ class xep_0030(base_plugin): add_item -- """ + name = 'xep_0030' + description = 'XEP-0030: Service Discovery' + dependencies = set() + stanza = stanza + def plugin_init(self): """ Start the XEP-0030 plugin. """ - self.xep = '0030' - self.description = 'Service Discovery' - self.stanza = sleekxmpp.plugins.xep_0030.stanza - self.xmpp.register_handler( Callback('Disco Info', StanzaPath('iq/disco_info'), @@ -106,25 +106,23 @@ class xep_0030(base_plugin): register_stanza_plugin(Iq, DiscoInfo) register_stanza_plugin(Iq, DiscoItems) - self.static = StaticDisco(self.xmpp) + self.static = StaticDisco(self.xmpp, self) + + self.use_cache = self.config.get('use_cache', True) + self.wrap_results = self.config.get('wrap_results', False) + + self._disco_ops = [ + 'get_info', 'set_info', 'set_identities', 'set_features', + 'get_items', 'set_items', 'del_items', 'add_identity', + 'del_identity', 'add_feature', 'del_feature', 'add_item', + 'del_item', 'del_identities', 'del_features', 'cache_info', + 'get_cached_info', 'supports', 'has_identity'] - self._disco_ops = ['get_info', 'set_identities', 'set_features', - 'get_items', 'set_items', 'del_items', - 'add_identity', 'del_identity', 'add_feature', - 'del_feature', 'add_item', 'del_item', - 'del_identities', 'del_features'] self.default_handlers = {} self._handlers = {} for op in self._disco_ops: self._add_disco_op(op, getattr(self.static, op)) - def post_init(self): - """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) - if 'xep_0059' in self.xmpp.plugin: - register_stanza_plugin(DiscoItems, - self.xmpp['xep_0059'].stanza.Set) - def _add_disco_op(self, op, default_handler): self.default_handlers[op] = default_handler self._handlers[op] = {'global': default_handler, @@ -237,7 +235,78 @@ class xep_0030(base_plugin): self.del_node_handler(op, jid, node) self.set_node_handler(op, jid, node, self.default_handlers[op]) - def get_info(self, jid=None, node=None, local=False, **kwargs): + def supports(self, jid=None, node=None, feature=None, local=False, + cached=True, ifrom=None): + """ + Check if a JID supports a given feature. + + Return values: + True -- The feature is supported + False -- The feature is not listed as supported + None -- Nothing could be found due to a timeout + + Arguments: + jid -- Request info from this JID. + node -- The particular node to query. + feature -- The name of the feature to check. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + ifrom -- Specifiy the sender's JID. + """ + data = {'feature': feature, + 'local': local, + 'cached': cached} + return self._run_node_handler('supports', jid, node, ifrom, data) + + def has_identity(self, jid=None, node=None, category=None, itype=None, + lang=None, local=False, cached=True, ifrom=None): + """ + Check if a JID provides a given identity. + + Return values: + True -- The identity is provided + False -- The identity is not listed + None -- Nothing could be found due to a timeout + + Arguments: + jid -- Request info from this JID. + node -- The particular node to query. + category -- The category of the identity to check. + itype -- The type of the identity to check. + lang -- The language of the identity to check. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + ifrom -- Specifiy the sender's JID. + """ + data = {'category': category, + 'itype': itype, + 'lang': lang, + 'local': local, + 'cached': cached} + return self._run_node_handler('has_identity', jid, node, ifrom, data) + + def get_info(self, jid=None, node=None, local=False, + cached=None, **kwargs): """ Retrieve the disco#info results from a given JID/node combination. @@ -257,6 +326,13 @@ class xep_0030(base_plugin): no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. ifrom -- Specifiy the sender's JID. block -- If true, block and wait for the stanzas' reply. timeout -- The time in seconds to block while waiting for @@ -266,11 +342,31 @@ class xep_0030(base_plugin): received instead of blocking and waiting for the reply. """ - if local or jid is None: + if jid is not None and not isinstance(jid, JID): + jid = JID(jid) + if self.xmpp.is_component: + if jid.domain == self.xmpp.boundjid.domain: + local = True + else: + if str(jid) == str(self.xmpp.boundjid): + local = True + jid = jid.full + + if local or jid in (None, ''): log.debug("Looking up local disco#info data " + \ "for %s, node %s.", jid, node) - info = self._run_node_handler('get_info', jid, node, kwargs) - return self._fix_default_info(info) + info = self._run_node_handler('get_info', + jid, node, kwargs.get('ifrom', None), kwargs) + info = self._fix_default_info(info) + return self._wrap(kwargs.get('ifrom', None), jid, info) + + if cached: + log.debug("Looking up cached disco#info data " + \ + "for %s, node %s.", jid, node) + info = self._run_node_handler('get_cached_info', + jid, node, kwargs.get('ifrom', None), kwargs) + if info is not None: + return self._wrap(kwargs.get('ifrom', None), jid, info) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility @@ -282,6 +378,15 @@ class xep_0030(base_plugin): block=kwargs.get('block', True), callback=kwargs.get('callback', None)) + def set_info(self, jid=None, node=None, info=None): + """ + Set the disco#info data for a JID/node based on an existing + disco#info stanza. + """ + if isinstance(info, Iq): + info = info['disco_info'] + self._run_node_handler('set_info', jid, node, None, info) + def get_items(self, jid=None, node=None, local=False, **kwargs): """ Retrieve the disco#items results from a given JID/node combination. @@ -314,7 +419,9 @@ class xep_0030(base_plugin): Otherwise the parameter is ignored. """ if local or jid is None: - return self._run_node_handler('get_items', jid, node, kwargs) + items = self._run_node_handler('get_items', + jid, node, kwargs.get('ifrom', None), kwargs) + return self._wrap(kwargs.get('ifrom', None), jid, items) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility @@ -341,7 +448,7 @@ class xep_0030(base_plugin): node -- Optional node to modify. items -- A series of items in tuple format. """ - self._run_node_handler('set_items', jid, node, kwargs) + self._run_node_handler('set_items', jid, node, None, kwargs) def del_items(self, jid=None, node=None, **kwargs): """ @@ -351,7 +458,7 @@ class xep_0030(base_plugin): jid -- The JID to modify. node -- Optional node to modify. """ - self._run_node_handler('del_items', jid, node, kwargs) + self._run_node_handler('del_items', jid, node, None, kwargs) def add_item(self, jid='', name='', node=None, subnode='', ijid=None): """ @@ -372,7 +479,7 @@ class xep_0030(base_plugin): kwargs = {'ijid': jid, 'name': name, 'inode': subnode} - self._run_node_handler('add_item', ijid, node, kwargs) + self._run_node_handler('add_item', ijid, node, None, kwargs) def del_item(self, jid=None, node=None, **kwargs): """ @@ -384,7 +491,7 @@ class xep_0030(base_plugin): ijid -- The item's JID. inode -- The item's node. """ - self._run_node_handler('del_item', jid, node, kwargs) + self._run_node_handler('del_item', jid, node, None, kwargs) def add_identity(self, category='', itype='', name='', node=None, jid=None, lang=None): @@ -411,7 +518,7 @@ class xep_0030(base_plugin): 'itype': itype, 'name': name, 'lang': lang} - self._run_node_handler('add_identity', jid, node, kwargs) + self._run_node_handler('add_identity', jid, node, None, kwargs) def add_feature(self, feature, node=None, jid=None): """ @@ -423,7 +530,7 @@ class xep_0030(base_plugin): jid -- The JID to modify. """ kwargs = {'feature': feature} - self._run_node_handler('add_feature', jid, node, kwargs) + self._run_node_handler('add_feature', jid, node, None, kwargs) def del_identity(self, jid=None, node=None, **kwargs): """ @@ -437,7 +544,7 @@ class xep_0030(base_plugin): name -- Optional, human readable name for the identity. lang -- Optional, the identity's xml:lang value. """ - self._run_node_handler('del_identity', jid, node, kwargs) + self._run_node_handler('del_identity', jid, node, None, kwargs) def del_feature(self, jid=None, node=None, **kwargs): """ @@ -448,7 +555,7 @@ class xep_0030(base_plugin): node -- The node to modify. feature -- The feature's namespace. """ - self._run_node_handler('del_feature', jid, node, kwargs) + self._run_node_handler('del_feature', jid, node, None, kwargs) def set_identities(self, jid=None, node=None, **kwargs): """ @@ -463,7 +570,7 @@ class xep_0030(base_plugin): identities -- A set of identities in tuple form. lang -- Optional, xml:lang value. """ - self._run_node_handler('set_identities', jid, node, kwargs) + self._run_node_handler('set_identities', jid, node, None, kwargs) def del_identities(self, jid=None, node=None, **kwargs): """ @@ -478,7 +585,7 @@ class xep_0030(base_plugin): lang -- Optional. If given, only remove identities using this xml:lang value. """ - self._run_node_handler('del_identities', jid, node, kwargs) + self._run_node_handler('del_identities', jid, node, None, kwargs) def set_features(self, jid=None, node=None, **kwargs): """ @@ -490,7 +597,7 @@ class xep_0030(base_plugin): node -- The node to modify. features -- The new set of supported features. """ - self._run_node_handler('set_features', jid, node, kwargs) + self._run_node_handler('set_features', jid, node, None, kwargs) def del_features(self, jid=None, node=None, **kwargs): """ @@ -500,9 +607,9 @@ class xep_0030(base_plugin): jid -- The JID to modify. node -- The node to modify. """ - self._run_node_handler('del_features', jid, node, kwargs) + self._run_node_handler('del_features', jid, node, None, kwargs) - def _run_node_handler(self, htype, jid, node, data={}): + def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}): """ Execute the most specific node handler for the given JID/node combination. @@ -513,7 +620,10 @@ class xep_0030(base_plugin): node -- The node requested. data -- Optional, custom data to pass to the handler. """ - if jid is None: + if isinstance(jid, JID): + jid = jid.full + + if jid in (None, ''): if self.xmpp.is_component: jid = self.xmpp.boundjid.full else: @@ -521,14 +631,28 @@ class xep_0030(base_plugin): if node is None: node = '' - if self._handlers[htype]['node'].get((jid, node), False): - return self._handlers[htype]['node'][(jid, node)](jid, node, data) - elif self._handlers[htype]['jid'].get(jid, False): - return self._handlers[htype]['jid'][jid](jid, node, data) - elif self._handlers[htype]['global']: - return self._handlers[htype]['global'](jid, node, data) - else: - return None + try: + args = (jid, node, ifrom, data) + if self._handlers[htype]['node'].get((jid, node), False): + return self._handlers[htype]['node'][(jid, node)](*args) + elif self._handlers[htype]['jid'].get(jid, False): + return self._handlers[htype]['jid'][jid](*args) + elif self._handlers[htype]['global']: + return self._handlers[htype]['global'](*args) + else: + return None + except TypeError: + # To preserve backward compatibility, drop the ifrom parameter + # for existing handlers that don't understand it. + args = (jid, node, data) + if self._handlers[htype]['node'].get((jid, node), False): + return self._handlers[htype]['node'][(jid, node)](*args) + elif self._handlers[htype]['jid'].get(jid, False): + return self._handlers[htype]['jid'][jid](*args) + elif self._handlers[htype]['global']: + return self._handlers[htype]['global'](*args) + else: + return None def _handle_disco_info(self, iq): """ @@ -550,6 +674,7 @@ class xep_0030(base_plugin): info = self._run_node_handler('get_info', jid, iq['disco_info']['node'], + iq['from'], iq) if isinstance(info, Iq): info.send() @@ -560,8 +685,20 @@ class xep_0030(base_plugin): iq.set_payload(info.xml) iq.send() elif iq['type'] == 'result': - log.debug("Received disco info result from" + \ - "%s to %s.", iq['from'], iq['to']) + log.debug("Received disco info result from " + \ + "<%s> to <%s>.", iq['from'], iq['to']) + if self.use_cache: + log.debug("Caching disco info result from " \ + "<%s> to <%s>.", iq['from'], iq['to']) + if self.xmpp.is_component: + ito = iq['to'].full + else: + ito = None + self._run_node_handler('cache_info', + iq['from'].full, + iq['disco_info']['node'], + ito, + iq) self.xmpp.event('disco_info', iq) def _handle_disco_items(self, iq): @@ -583,6 +720,7 @@ class xep_0030(base_plugin): items = self._run_node_handler('get_items', jid, iq['disco_items']['node'], + iq['from'].full, iq) if isinstance(items, Iq): items.send() @@ -592,7 +730,7 @@ class xep_0030(base_plugin): iq.set_payload(items.xml) iq.send() elif iq['type'] == 'result': - log.debug("Received disco items result from" + \ + log.debug("Received disco items result from " + \ "%s to %s.", iq['from'], iq['to']) self.xmpp.event('disco_items', iq) @@ -607,24 +745,43 @@ class xep_0030(base_plugin): Arguments: info -- The disco#info quest (not the full Iq stanza) to modify. """ + result = info + if isinstance(info, Iq): + info = info['disco_info'] if not info['node']: if not info['identities']: if self.xmpp.is_component: - log.debug("No identity found for this entity." + \ + log.debug("No identity found for this entity. " + \ "Using default component identity.") info.add_identity('component', 'generic') else: - log.debug("No identity found for this entity." + \ + log.debug("No identity found for this entity. " + \ "Using default client identity.") info.add_identity('client', 'bot') if not info['features']: - log.debug("No features found for this entity." + \ + log.debug("No features found for this entity. " + \ "Using default disco#info feature.") info.add_feature(info.namespace) - return info + return result + def _wrap(self, ito, ifrom, payload, force=False): + """ + Ensure that results are wrapped in an Iq stanza + if self.wrap_results has been set to True. -# Retain some backwards compatibility -xep_0030.getInfo = xep_0030.get_info -xep_0030.getItems = xep_0030.get_items -xep_0030.make_static = xep_0030.restore_defaults + Arguments: + ito -- The JID to use as the 'to' value + ifrom -- The JID to use as the 'from' value + payload -- The disco data to wrap + force -- Force wrapping, regardless of self.wrap_results + """ + if (force or self.wrap_results) and not isinstance(payload, Iq): + iq = self.xmpp.Iq() + # Since we're simulating a result, we have to treat + # the 'from' and 'to' values opposite the normal way. + iq['to'] = self.xmpp.boundjid if ito is None else ito + iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom + iq['type'] = 'result' + iq.append(payload) + return iq + return payload diff --git a/sleekxmpp/plugins/xep_0030/stanza/info.py b/sleekxmpp/plugins/xep_0030/stanza/info.py index 6764acbb..25d1d07f 100644 --- a/sleekxmpp/plugins/xep_0030/stanza/info.py +++ b/sleekxmpp/plugins/xep_0030/stanza/info.py @@ -146,7 +146,7 @@ class DiscoInfo(ElementBase): return True return False - def get_identities(self, lang=None): + def get_identities(self, lang=None, dedupe=True): """ Return a set of all identities in tuple form as so: (category, type, lang, name) @@ -155,17 +155,25 @@ class DiscoInfo(ElementBase): that language. Arguments: - lang -- Optional, standard xml:lang value. + lang -- Optional, standard xml:lang value. + dedupe -- If True, de-duplicate identities, otherwise + return a list of all identities. """ - identities = set() + if dedupe: + identities = set() + else: + identities = [] for id_xml in self.findall('{%s}identity' % self.namespace): xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None) if lang is None or xml_lang == lang: - identities.add(( - id_xml.attrib['category'], - id_xml.attrib['type'], - id_xml.attrib.get('{%s}lang' % self.xml_ns, None), - id_xml.attrib.get('name', None))) + id = (id_xml.attrib['category'], + id_xml.attrib['type'], + id_xml.attrib.get('{%s}lang' % self.xml_ns, None), + id_xml.attrib.get('name', None)) + if dedupe: + identities.add(id) + else: + identities.append(id) return identities def set_identities(self, identities, lang=None): @@ -237,11 +245,17 @@ class DiscoInfo(ElementBase): return True return False - def get_features(self): + def get_features(self, dedupe=True): """Return the set of all supported features.""" - features = set() + if dedupe: + features = set() + else: + features = [] for feature_xml in self.findall('{%s}feature' % self.namespace): - features.add(feature_xml.attrib['var']) + if dedupe: + features.add(feature_xml.attrib['var']) + else: + features.append(feature_xml.attrib['var']) return features def set_features(self, features): diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py index a1fb819c..512f2336 100644 --- a/sleekxmpp/plugins/xep_0030/stanza/items.py +++ b/sleekxmpp/plugins/xep_0030/stanza/items.py @@ -6,7 +6,7 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream import ElementBase, ET +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin class DiscoItems(ElementBase): @@ -78,13 +78,11 @@ class DiscoItems(ElementBase): """ if (jid, node) not in self._items: self._items.add((jid, node)) - item_xml = ET.Element('{%s}item' % self.namespace) - item_xml.attrib['jid'] = jid - if name: - item_xml.attrib['name'] = name - if node: - item_xml.attrib['node'] = node - self.xml.append(item_xml) + item = DiscoItem(parent=self) + item['jid'] = jid + item['node'] = node + item['name'] = name + self.iterables.append(item) return True return False @@ -108,11 +106,9 @@ class DiscoItems(ElementBase): def get_items(self): """Return all items.""" items = set() - for item_xml in self.findall('{%s}item' % self.namespace): - item = (item_xml.attrib['jid'], - item_xml.attrib.get('node'), - item_xml.attrib.get('name')) - items.add(item) + for item in self['substanzas']: + if isinstance(item, DiscoItem): + items.add((item['jid'], item['node'], item['name'])) return items def set_items(self, items): @@ -132,5 +128,24 @@ class DiscoItems(ElementBase): def del_items(self): """Remove all items.""" self._items = set() - for item_xml in self.findall('{%s}item' % self.namespace): - self.xml.remove(item_xml) + for item in self['substanzas']: + if isinstance(item, DiscoItem): + self.xml.remove(item.xml) + + +class DiscoItem(ElementBase): + name = 'item' + namespace = 'http://jabber.org/protocol/disco#items' + plugin_attrib = name + interfaces = set(('jid', 'node', 'name')) + + def get_node(self): + """Return the item's node name or ``None``.""" + return self._get_attr('node', None) + + def get_name(self): + """Return the item's human readable name, or ``None``.""" + return self._get_attr('name', None) + + +register_stanza_plugin(DiscoItems, DiscoItem, iterable=True) diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py index 7e7f0353..7306461a 100644 --- a/sleekxmpp/plugins/xep_0030/static.py +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -7,14 +7,11 @@ """ import logging +import threading -import sleekxmpp from sleekxmpp import Iq -from sleekxmpp.exceptions import XMPPError -from sleekxmpp.plugins.base import base_plugin -from sleekxmpp.xmlstream.handler import Callback -from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout +from sleekxmpp.xmlstream import JID from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems @@ -38,7 +35,7 @@ class StaticDisco(object): xmpp -- The main SleekXMPP object. """ - def __init__(self, xmpp): + def __init__(self, xmpp, disco): """ Create a static disco interface. Sets of disco#info and disco#items are maintained for every given JID and node @@ -50,8 +47,10 @@ class StaticDisco(object): """ self.nodes = {} self.xmpp = xmpp + self.disco = disco + self.lock = threading.RLock() - def add_node(self, jid=None, node=None): + def add_node(self, jid=None, node=None, ifrom=None): """ Create a new set of stanzas for the provided JID and node combination. @@ -60,83 +59,218 @@ class StaticDisco(object): jid -- The JID that will own the new stanzas. node -- The node that will own the new stanzas. """ - if jid is None: - jid = self.xmpp.boundjid.full - if node is None: - node = '' - if (jid, node) not in self.nodes: - self.nodes[(jid, node)] = {'info': DiscoInfo(), - 'items': DiscoItems()} - self.nodes[(jid, node)]['info']['node'] = node - self.nodes[(jid, node)]['items']['node'] = node + with self.lock: + if jid is None: + jid = self.xmpp.boundjid.full + if node is None: + node = '' + if ifrom is None: + ifrom = '' + if isinstance(ifrom, JID): + ifrom = ifrom.full + if (jid, node, ifrom) not in self.nodes: + self.nodes[(jid, node, ifrom)] = {'info': DiscoInfo(), + 'items': DiscoItems()} + self.nodes[(jid, node, ifrom)]['info']['node'] = node + self.nodes[(jid, node, ifrom)]['items']['node'] = node + + def get_node(self, jid=None, node=None, ifrom=None): + with self.lock: + if jid is None: + jid = self.xmpp.boundjid.full + if node is None: + node = '' + if ifrom is None: + ifrom = '' + if isinstance(ifrom, JID): + ifrom = ifrom.full + if (jid, node, ifrom) not in self.nodes: + self.add_node(jid, node, ifrom) + return self.nodes[(jid, node, ifrom)] + + def node_exists(self, jid=None, node=None, ifrom=None): + with self.lock: + if jid is None: + jid = self.xmpp.boundjid.full + if node is None: + node = '' + if ifrom is None: + ifrom = '' + if isinstance(ifrom, JID): + ifrom = ifrom.full + if (jid, node, ifrom) not in self.nodes: + return False + return True # ================================================================= # Node Handlers # - # Each handler accepts three arguments: jid, node, and data. - # The jid and node parameters together determine the set of - # info and items stanzas that will be retrieved or added. - # The data parameter is a dictionary with additional paramters - # that will be passed to other calls. + # Each handler accepts four arguments: jid, node, ifrom, and data. + # The jid and node parameters together determine the set of info + # and items stanzas that will be retrieved or added. Additionally, + # the ifrom value allows for cached results when results vary based + # on the requester's JID. The data parameter is a dictionary with + # additional parameters that will be passed to other calls. + # + # This implementation does not allow different responses based on + # the requester's JID, except for cached results. To do that, + # register a custom node handler. - def get_info(self, jid, node, data): + def supports(self, jid, node, ifrom, data): + """ + Check if a JID supports a given feature. + + The data parameter may provide: + feature -- The feature to check for support. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + """ + feature = data.get('feature', None) + + data = {'local': data.get('local', False), + 'cached': data.get('cached', True)} + + if not feature: + return False + + try: + info = self.disco.get_info(jid=jid, node=node, + ifrom=ifrom, **data) + info = self.disco._wrap(ifrom, jid, info, True) + features = info['disco_info']['features'] + return feature in features + except IqError: + return False + except IqTimeout: + return None + + def has_identity(self, jid, node, ifrom, data): + """ + Check if a JID has a given identity. + + The data parameter may provide: + category -- The category of the identity to check. + itype -- The type of the identity to check. + lang -- The language of the identity to check. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + """ + identity = (data.get('category', None), + data.get('itype', None), + data.get('lang', None)) + + data = {'local': data.get('local', False), + 'cached': data.get('cached', True)} + + if node in (None, ''): + info = self.caps.get_caps(jid) + if info and identity in info['identities']: + return True + + try: + info = self.disco.get_info(jid=jid, node=node, + ifrom=ifrom, **data) + info = self.disco._wrap(ifrom, jid, info, True) + trunc = lambda i: (i[0], i[1], i[2]) + return identity in map(trunc, info['disco_info']['identities']) + except IqError: + return False + except IqTimeout: + return None + + def get_info(self, jid, node, ifrom, data): """ Return the stored info data for the requested JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.nodes: - if not node: - return DiscoInfo() + with self.lock: + if not self.node_exists(jid, node): + if not node: + return DiscoInfo() + else: + raise XMPPError(condition='item-not-found') else: - raise XMPPError(condition='item-not-found') - else: - return self.nodes[(jid, node)]['info'] + return self.get_node(jid, node)['info'] - def del_info(self, jid, node, data): + def set_info(self, jid, node, ifrom, data): + """ + Set the entire info stanza for a JID/node at once. + + The data parameter is a disco#info substanza. + """ + with self.lock: + self.add_node(jid, node) + self.get_node(jid, node)['info'] = data + + def del_info(self, jid, node, ifrom, data): """ Reset the info stanza for a given JID/node combination. The data parameter is not used. """ - if (jid, node) in self.nodes: - self.nodes[(jid, node)]['info'] = DiscoInfo() + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['info'] = DiscoInfo() - def get_items(self, jid, node, data): + def get_items(self, jid, node, ifrom, data): """ Return the stored items data for the requested JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.nodes: - if not node: - return DiscoInfo() + with self.lock: + if not self.node_exists(jid, node): + if not node: + return DiscoInfo() + else: + raise XMPPError(condition='item-not-found') else: - raise XMPPError(condition='item-not-found') - else: - return self.nodes[(jid, node)]['items'] + return self.get_node(jid, node)['items'] - def set_items(self, jid, node, data): + def set_items(self, jid, node, ifrom, data): """ Replace the stored items data for a JID/node combination. - The data parameter may provided: + The data parameter may provide: items -- A set of items in tuple format. """ - items = data.get('items', set()) - self.add_node(jid, node) - self.nodes[(jid, node)]['items']['items'] = items + with self.lock: + items = data.get('items', set()) + self.add_node(jid, node) + self.get_node(jid, node)['items']['items'] = items - def del_items(self, jid, node, data): + def del_items(self, jid, node, ifrom, data): """ Reset the items stanza for a given JID/node combination. The data parameter is not used. """ - if (jid, node) in self.nodes: - self.nodes[(jid, node)]['items'] = DiscoItems() + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['items'] = DiscoItems() - def add_identity(self, jid, node, data): + def add_identity(self, jid, node, ifrom, data): """ Add a new identity to te JID/node combination. @@ -146,14 +280,15 @@ class StaticDisco(object): name -- Optional human readable name for this identity. lang -- Optional standard xml:lang value. """ - self.add_node(jid, node) - self.nodes[(jid, node)]['info'].add_identity( - data.get('category', ''), - data.get('itype', ''), - data.get('name', None), - data.get('lang', None)) + with self.lock: + self.add_node(jid, node) + self.get_node(jid, node)['info'].add_identity( + data.get('category', ''), + data.get('itype', ''), + data.get('name', None), + data.get('lang', None)) - def set_identities(self, jid, node, data): + def set_identities(self, jid, node, ifrom, data): """ Add or replace all identities for a JID/node combination. @@ -161,11 +296,12 @@ class StaticDisco(object): identities -- A list of identities in tuple form: (category, type, name, lang) """ - identities = data.get('identities', set()) - self.add_node(jid, node) - self.nodes[(jid, node)]['info']['identities'] = identities + with self.lock: + identities = data.get('identities', set()) + self.add_node(jid, node) + self.get_node(jid, node)['info']['identities'] = identities - def del_identity(self, jid, node, data): + def del_identity(self, jid, node, ifrom, data): """ Remove an identity from a JID/node combination. @@ -175,67 +311,72 @@ class StaticDisco(object): name -- Optional human readable name for this identity. lang -- Optional, standard xml:lang value. """ - if (jid, node) not in self.nodes: - return - self.nodes[(jid, node)]['info'].del_identity( - data.get('category', ''), - data.get('itype', ''), - data.get('name', None), - data.get('lang', None)) + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['info'].del_identity( + data.get('category', ''), + data.get('itype', ''), + data.get('name', None), + data.get('lang', None)) - def del_identities(self, jid, node, data): + def del_identities(self, jid, node, ifrom, data): """ Remove all identities from a JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.nodes: - return - del self.nodes[(jid, node)]['info']['identities'] + with self.lock: + if self.node_exists(jid, node): + del self.get_node(jid, node)['info']['identities'] - def add_feature(self, jid, node, data): + def add_feature(self, jid, node, ifrom, data): """ Add a feature to a JID/node combination. The data parameter should include: feature -- The namespace of the supported feature. """ - self.add_node(jid, node) - self.nodes[(jid, node)]['info'].add_feature(data.get('feature', '')) + with self.lock: + self.add_node(jid, node) + self.get_node(jid, node)['info'].add_feature( + data.get('feature', '')) - def set_features(self, jid, node, data): + def set_features(self, jid, node, ifrom, data): """ Add or replace all features for a JID/node combination. The data parameter should include: features -- The new set of supported features. """ - features = data.get('features', set()) - self.add_node(jid, node) - self.nodes[(jid, node)]['info']['features'] = features + with self.lock: + features = data.get('features', set()) + self.add_node(jid, node) + self.get_node(jid, node)['info']['features'] = features - def del_feature(self, jid, node, data): + def del_feature(self, jid, node, ifrom, data): """ Remove a feature from a JID/node combination. The data parameter should include: feature -- The namespace of the removed feature. """ - if (jid, node) not in self.nodes: - return - self.nodes[(jid, node)]['info'].del_feature(data.get('feature', '')) + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['info'].del_feature( + data.get('feature', '')) - def del_features(self, jid, node, data): + def del_features(self, jid, node, ifrom, data): """ Remove all features from a JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.nodes: - return - del self.nodes[(jid, node)]['info']['features'] + with self.lock: + if not self.node_exists(jid, node): + return + del self.get_node(jid, node)['info']['features'] - def add_item(self, jid, node, data): + def add_item(self, jid, node, ifrom, data): """ Add an item to a JID/node combination. @@ -245,13 +386,14 @@ class StaticDisco(object): non-addressable items. name -- Optional human readable name for the item. """ - self.add_node(jid, node) - self.nodes[(jid, node)]['items'].add_item( - data.get('ijid', ''), - node=data.get('inode', ''), - name=data.get('name', '')) + with self.lock: + self.add_node(jid, node) + self.get_node(jid, node)['items'].add_item( + data.get('ijid', ''), + node=data.get('inode', ''), + name=data.get('name', '')) - def del_item(self, jid, node, data): + def del_item(self, jid, node, ifrom, data): """ Remove an item from a JID/node combination. @@ -259,7 +401,38 @@ class StaticDisco(object): ijid -- JID of the item to remove. inode -- Optional extra identifying information. """ - if (jid, node) in self.nodes: - self.nodes[(jid, node)]['items'].del_item( - data.get('ijid', ''), - node=data.get('inode', None)) + with self.lock: + if self.node_exists(jid, node): + self.get_node(jid, node)['items'].del_item( + data.get('ijid', ''), + node=data.get('inode', None)) + + def cache_info(self, jid, node, ifrom, data): + """ + Cache disco information for an external JID. + + The data parameter is the Iq result stanza + containing the disco info to cache, or + the disco#info substanza itself. + """ + with self.lock: + if isinstance(data, Iq): + data = data['disco_info'] + + self.add_node(jid, node, ifrom) + self.get_node(jid, node, ifrom)['info'] = data + + def get_cached_info(self, jid, node, ifrom, data): + """ + Retrieve cached disco info data. + + The data parameter is not used. + """ + with self.lock: + if isinstance(jid, JID): + jid = jid.full + + if not self.node_exists(jid, node, ifrom): + return None + else: + return self.get_node(jid, node, ifrom)['info'] diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py index c0c4d89d..feef5a13 100644 --- a/sleekxmpp/plugins/xep_0033.py +++ b/sleekxmpp/plugins/xep_0033.py @@ -7,155 +7,161 @@ """ import logging -from . import base -from .. xmlstream.handler.callback import Callback -from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID -from .. stanza.message import Message +from sleekxmpp import Message +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from sleekxmpp.plugins import BasePlugin, register_plugin class Addresses(ElementBase): - namespace = 'http://jabber.org/protocol/address' - name = 'addresses' - plugin_attrib = 'addresses' - interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + namespace = 'http://jabber.org/protocol/address' + name = 'addresses' + plugin_attrib = 'addresses' + interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) - def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False): - address = Address(parent=self) - address['type'] = atype - address['jid'] = jid - address['node'] = node - address['uri'] = uri - address['desc'] = desc - address['delivered'] = delivered - return address + def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False): + address = Address(parent=self) + address['type'] = atype + address['jid'] = jid + address['node'] = node + address['uri'] = uri + address['desc'] = desc + address['delivered'] = delivered + return address - def getAddresses(self, atype=None): - addresses = [] - for addrXML in self.xml.findall('{%s}address' % Address.namespace): - # ElementTree 1.2.6 does not support [@attr='value'] in findall - if atype is None or addrXML.attrib.get('type') == atype: - addresses.append(Address(xml=addrXML, parent=None)) - return addresses + def getAddresses(self, atype=None): + addresses = [] + for addrXML in self.xml.findall('{%s}address' % Address.namespace): + # ElementTree 1.2.6 does not support [@attr='value'] in findall + if atype is None or addrXML.attrib.get('type') == atype: + addresses.append(Address(xml=addrXML, parent=None)) + return addresses - def setAddresses(self, addresses, set_type=None): - self.delAddresses(set_type) - for addr in addresses: - addr = dict(addr) - # Remap 'type' to 'atype' to match the add method - if set_type is not None: - addr['type'] = set_type - curr_type = addr.get('type', None) - if curr_type is not None: - del addr['type'] - addr['atype'] = curr_type - self.addAddress(**addr) + def setAddresses(self, addresses, set_type=None): + self.delAddresses(set_type) + for addr in addresses: + addr = dict(addr) + # Remap 'type' to 'atype' to match the add method + if set_type is not None: + addr['type'] = set_type + curr_type = addr.get('type', None) + if curr_type is not None: + del addr['type'] + addr['atype'] = curr_type + self.addAddress(**addr) - def delAddresses(self, atype=None): - if atype is None: - return - for addrXML in self.xml.findall('{%s}address' % Address.namespace): - # ElementTree 1.2.6 does not support [@attr='value'] in findall - if addrXML.attrib.get('type') == atype: - self.xml.remove(addrXML) + def delAddresses(self, atype=None): + if atype is None: + return + for addrXML in self.xml.findall('{%s}address' % Address.namespace): + # ElementTree 1.2.6 does not support [@attr='value'] in findall + if addrXML.attrib.get('type') == atype: + self.xml.remove(addrXML) - # -------------------------------------------------------------- + # -------------------------------------------------------------- - def delBcc(self): - self.delAddresses('bcc') + def delBcc(self): + self.delAddresses('bcc') - def delCc(self): - self.delAddresses('cc') + def delCc(self): + self.delAddresses('cc') - def delNoreply(self): - self.delAddresses('noreply') + def delNoreply(self): + self.delAddresses('noreply') - def delReplyroom(self): - self.delAddresses('replyroom') + def delReplyroom(self): + self.delAddresses('replyroom') - def delReplyto(self): - self.delAddresses('replyto') + def delReplyto(self): + self.delAddresses('replyto') - def delTo(self): - self.delAddresses('to') + def delTo(self): + self.delAddresses('to') - # -------------------------------------------------------------- + # -------------------------------------------------------------- - def getBcc(self): - return self.getAddresses('bcc') + def getBcc(self): + return self.getAddresses('bcc') - def getCc(self): - return self.getAddresses('cc') + def getCc(self): + return self.getAddresses('cc') - def getNoreply(self): - return self.getAddresses('noreply') + def getNoreply(self): + return self.getAddresses('noreply') - def getReplyroom(self): - return self.getAddresses('replyroom') + def getReplyroom(self): + return self.getAddresses('replyroom') - def getReplyto(self): - return self.getAddresses('replyto') + def getReplyto(self): + return self.getAddresses('replyto') - def getTo(self): - return self.getAddresses('to') + def getTo(self): + return self.getAddresses('to') - # -------------------------------------------------------------- + # -------------------------------------------------------------- - def setBcc(self, addresses): - self.setAddresses(addresses, 'bcc') + def setBcc(self, addresses): + self.setAddresses(addresses, 'bcc') - def setCc(self, addresses): - self.setAddresses(addresses, 'cc') + def setCc(self, addresses): + self.setAddresses(addresses, 'cc') - def setNoreply(self, addresses): - self.setAddresses(addresses, 'noreply') + def setNoreply(self, addresses): + self.setAddresses(addresses, 'noreply') - def setReplyroom(self, addresses): - self.setAddresses(addresses, 'replyroom') + def setReplyroom(self, addresses): + self.setAddresses(addresses, 'replyroom') - def setReplyto(self, addresses): - self.setAddresses(addresses, 'replyto') + def setReplyto(self, addresses): + self.setAddresses(addresses, 'replyto') - def setTo(self, addresses): - self.setAddresses(addresses, 'to') + def setTo(self, addresses): + self.setAddresses(addresses, 'to') class Address(ElementBase): - namespace = 'http://jabber.org/protocol/address' - name = 'address' - plugin_attrib = 'address' - interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri')) - address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) - - def getDelivered(self): - return self.xml.attrib.get('delivered', False) - - def setDelivered(self, delivered): - if delivered: - self.xml.attrib['delivered'] = "true" - else: - del self['delivered'] - - def setUri(self, uri): - if uri: - del self['jid'] - del self['node'] - self.xml.attrib['uri'] = uri - elif 'uri' in self.xml.attrib: - del self.xml.attrib['uri'] - - -class xep_0033(base.base_plugin): - """ - XEP-0033: Extended Stanza Addressing - """ - - def plugin_init(self): - self.xep = '0033' - self.description = 'Extended Stanza Addressing' - - registerStanzaPlugin(Message, Addresses) - - def post_init(self): - base.base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace) + namespace = 'http://jabber.org/protocol/address' + name = 'address' + plugin_attrib = 'address' + interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri')) + address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) + + def getDelivered(self): + return self.xml.attrib.get('delivered', False) + + def setDelivered(self, delivered): + if delivered: + self.xml.attrib['delivered'] = "true" + else: + del self['delivered'] + + def setUri(self, uri): + if uri: + del self['jid'] + del self['node'] + self.xml.attrib['uri'] = uri + elif 'uri' in self.xml.attrib: + del self.xml.attrib['uri'] + + +class XEP_0033(BasePlugin): + + """ + XEP-0033: Extended Stanza Addressing + """ + + name = 'xep_0033' + description = 'XEP-0033: Extended Stanza Addressing' + dependencies = set(['xep_0033']) + + def plugin_init(self): + self.xep = '0033' + + register_stanza_plugin(Message, Addresses) + + self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace) + + +xep_0033 = XEP_0033 +register_plugin(XEP_0033) diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index ab3f750a..5035faae 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -6,14 +6,15 @@ See the file LICENSE for copying permission. """ from __future__ import with_statement -from . import base + import logging -from xml.etree import cElementTree as ET -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID -from .. stanza.presence import Presence -from .. xmlstream.handler.callback import Callback -from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.matcher.xmlmask import MatchXMLMask + +from sleekxmpp import Presence +from sleekxmpp.plugins import BasePlugin, register_plugin +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, JID, ET +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath +from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask from sleekxmpp.exceptions import IqError, IqTimeout @@ -107,18 +108,23 @@ class MUCPresence(ElementBase): log.warning("Cannot delete room through mucpresence plugin.") return self -class xep_0045(base.base_plugin): + +class XEP_0045(BasePlugin): + """ - Implements XEP-0045 Multi User Chat + Implements XEP-0045 Multi-User Chat """ + name = 'xep_0045' + description = 'XEP-0045: Multi-User Chat' + dependencies = set(['xep_0030', 'xep_0004']) + def plugin_init(self): self.rooms = {} self.ourNicks = {} self.xep = '0045' - self.description = 'Multi User Chat' # load MUC support in presence stanzas - registerStanzaPlugin(Presence, MUCPresence) + register_stanza_plugin(Presence, MUCPresence) self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence)) self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message)) self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject)) @@ -374,3 +380,7 @@ class xep_0045(base.base_plugin): if room not in self.rooms.keys(): return None return self.rooms[room].keys() + + +xep_0045 = XEP_0045 +register_plugin(XEP_0045) diff --git a/sleekxmpp/plugins/xep_0047/__init__.py b/sleekxmpp/plugins/xep_0047/__init__.py new file mode 100644 index 00000000..5cd7df2e --- /dev/null +++ b/sleekxmpp/plugins/xep_0047/__init__.py @@ -0,0 +1,21 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0047 import stanza +from sleekxmpp.plugins.xep_0047.stanza import Open, Close, Data +from sleekxmpp.plugins.xep_0047.stream import IBBytestream +from sleekxmpp.plugins.xep_0047.ibb import XEP_0047 + + +register_plugin(XEP_0047) + + +# Retain some backwards compatibility +xep_0047 = XEP_0047 diff --git a/sleekxmpp/plugins/xep_0047/ibb.py b/sleekxmpp/plugins/xep_0047/ibb.py new file mode 100644 index 00000000..c8a4b5e7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0047/ibb.py @@ -0,0 +1,148 @@ +import uuid +import logging +import threading + +from sleekxmpp import Message, Iq +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0047 import stanza, Open, Close, Data, IBBytestream + + +log = logging.getLogger(__name__) + + +class XEP_0047(BasePlugin): + + name = 'xep_0047' + description = 'XEP-0047: In-band Bytestreams' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + self.streams = {} + self.pending_streams = {3: 5} + self.pending_close_streams = {} + self._stream_lock = threading.Lock() + + self.max_block_size = self.config.get('max_block_size', 8192) + self.window_size = self.config.get('window_size', 1) + self.auto_accept = self.config.get('auto_accept', True) + self.accept_stream = self.config.get('accept_stream', None) + + register_stanza_plugin(Iq, Open) + register_stanza_plugin(Iq, Close) + register_stanza_plugin(Iq, Data) + + self.xmpp.register_handler(Callback( + 'IBB Open', + StanzaPath('iq@type=set/ibb_open'), + self._handle_open_request)) + + self.xmpp.register_handler(Callback( + 'IBB Close', + StanzaPath('iq@type=set/ibb_close'), + self._handle_close)) + + self.xmpp.register_handler(Callback( + 'IBB Data', + StanzaPath('iq@type=set/ibb_data'), + self._handle_data)) + + self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb') + + def _accept_stream(self, iq): + if self.accept_stream is not None: + return self.accept_stream(iq) + if self.auto_accept: + if iq['ibb_open']['block_size'] <= self.max_block_size: + return True + return False + + def open_stream(self, jid, block_size=4096, sid=None, window=1, + ifrom=None, block=True, timeout=None, callback=None): + if sid is None: + sid = str(uuid.uuid4()) + + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = jid + iq['from'] = ifrom + iq['ibb_open']['block_size'] = block_size + iq['ibb_open']['sid'] = sid + iq['ibb_open']['stanza'] = 'iq' + + stream = IBBytestream(self.xmpp, sid, block_size, + iq['to'], iq['from'], window) + + with self._stream_lock: + self.pending_streams[iq['id']] = stream + + self.pending_streams[iq['id']] = stream + + if block: + resp = iq.send(timeout=timeout) + self._handle_opened_stream(resp) + return stream + else: + cb = None + if callback is not None: + def chained(resp): + self._handle_opened_stream(resp) + callback(resp) + cb = chained + else: + cb = self._handle_opened_stream + return iq.send(block=block, timeout=timeout, callback=cb) + + def _handle_opened_stream(self, iq): + if iq['type'] == 'result': + with self._stream_lock: + stream = self.pending_streams.get(iq['id'], None) + if stream is not None: + stream.sender = iq['to'] + stream.receiver = iq['from'] + stream.stream_started.set() + self.streams[stream.sid] = stream + self.xmpp.event('ibb_stream_start', stream) + + with self._stream_lock: + if iq['id'] in self.pending_streams: + del self.pending_streams[iq['id']] + + def _handle_open_request(self, iq): + sid = iq['ibb_open']['sid'] + size = iq['ibb_open']['block_size'] + if not self._accept_stream(iq): + raise XMPPError('not-acceptable') + + if size > self.max_block_size: + raise XMPPError('resource-constraint') + + stream = IBBytestream(self.xmpp, sid, size, + iq['from'], iq['to'], + self.window_size) + stream.stream_started.set() + self.streams[sid] = stream + iq.reply() + iq.send() + + self.xmpp.event('ibb_stream_start', stream) + + def _handle_data(self, iq): + sid = iq['ibb_data']['sid'] + stream = self.streams.get(sid, None) + if stream is not None and iq['from'] != stream.sender: + stream._recv_data(iq) + else: + raise XMPPError('item-not-found') + + def _handle_close(self, iq): + sid = iq['ibb_close']['sid'] + stream = self.streams.get(sid, None) + if stream is not None and iq['from'] != stream.sender: + stream._closed(iq) + else: + raise XMPPError('item-not-found') diff --git a/sleekxmpp/plugins/xep_0047/stanza.py b/sleekxmpp/plugins/xep_0047/stanza.py new file mode 100644 index 00000000..afba07a8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0047/stanza.py @@ -0,0 +1,67 @@ +import re +import base64 + +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.thirdparty.suelta.util import bytes + + +VALID_B64 = re.compile(r'[A-Za-z0-9\+\/]*=*') + + +def to_b64(data): + return bytes(base64.b64encode(bytes(data))).decode('utf-8') + + +def from_b64(data): + return bytes(base64.b64decode(bytes(data))).decode('utf-8') + + +class Open(ElementBase): + name = 'open' + namespace = 'http://jabber.org/protocol/ibb' + plugin_attrib = 'ibb_open' + interfaces = set(('block_size', 'sid', 'stanza')) + + def get_block_size(self): + return int(self._get_attr('block-size')) + + def set_block_size(self, value): + self._set_attr('block-size', str(value)) + + def del_block_size(self): + self._del_attr('block-size') + + +class Data(ElementBase): + name = 'data' + namespace = 'http://jabber.org/protocol/ibb' + plugin_attrib = 'ibb_data' + interfaces = set(('seq', 'sid', 'data')) + sub_interfaces = set(['data']) + + def get_seq(self): + return int(self._get_attr('seq', '0')) + + def set_seq(self, value): + self._set_attr('seq', str(value)) + + def get_data(self): + b64_data = self.xml.text.strip() + if VALID_B64.match(b64_data).group() == b64_data: + return from_b64(b64_data) + else: + raise XMPPError('not-acceptable') + + def set_data(self, value): + self.xml.text = to_b64(value) + + def del_data(self): + self.xml.text = '' + + +class Close(ElementBase): + name = 'close' + namespace = 'http://jabber.org/protocol/ibb' + plugin_attrib = 'ibb_close' + interfaces = set(['sid']) diff --git a/sleekxmpp/plugins/xep_0047/stream.py b/sleekxmpp/plugins/xep_0047/stream.py new file mode 100644 index 00000000..49f56f36 --- /dev/null +++ b/sleekxmpp/plugins/xep_0047/stream.py @@ -0,0 +1,137 @@ +import socket +import threading +import logging +try: + import queue +except ImportError: + import Queue as queue + +from sleekxmpp.exceptions import XMPPError + + +log = logging.getLogger(__name__) + + +class IBBytestream(object): + + def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1): + self.xmpp = xmpp + self.sid = sid + self.block_size = block_size + self.window_size = window_size + + self.receiver = to + self.sender = ifrom + + self.send_seq = -1 + self.recv_seq = -1 + + self._send_seq_lock = threading.Lock() + self._recv_seq_lock = threading.Lock() + + self.stream_started = threading.Event() + self.stream_in_closed = threading.Event() + self.stream_out_closed = threading.Event() + + self.recv_queue = queue.Queue() + + self.send_window = threading.BoundedSemaphore(value=self.window_size) + self.window_ids = set() + self.window_empty = threading.Event() + self.window_empty.set() + + def send(self, data): + if not self.stream_started.is_set() or \ + self.stream_out_closed.is_set(): + raise socket.error + data = data[0:self.block_size] + self.send_window.acquire() + with self._send_seq_lock: + self.send_seq = (self.send_seq + 1) % 65535 + seq = self.send_seq + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = self.receiver + iq['from'] = self.sender + iq['ibb_data']['sid'] = self.sid + iq['ibb_data']['seq'] = seq + iq['ibb_data']['data'] = data + self.window_empty.clear() + self.window_ids.add(iq['id']) + iq.send(block=False, callback=self._recv_ack) + return len(data) + + def sendall(self, data): + sent_len = 0 + while sent_len < len(data): + sent_len += self.send(data[sent_len:]) + + def _recv_ack(self, iq): + self.window_ids.remove(iq['id']) + if not self.window_ids: + self.window_empty.set() + self.send_window.release() + if iq['type'] == 'error': + self.close() + + def _recv_data(self, iq): + with self._recv_seq_lock: + new_seq = iq['ibb_data']['seq'] + if new_seq != (self.recv_seq + 1) % 65535: + self.close() + raise XMPPError('unexpected-request') + self.recv_seq = new_seq + + data = iq['ibb_data']['data'] + if len(data) > self.block_size: + self.close() + raise XMPPError('not-acceptable') + + self.recv_queue.put(data) + self.xmpp.event('ibb_stream_data', {'stream': self, 'data': data}) + iq.reply() + iq.send() + + def recv(self, *args, **kwargs): + return self.read(block=True) + + def read(self, block=True, timeout=None, **kwargs): + if not self.stream_started.is_set() or \ + self.stream_in_closed.is_set(): + raise socket.error + if timeout is not None: + block = True + try: + return self.recv_queue.get(block, timeout) + except: + return None + + def close(self): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = self.receiver + iq['from'] = self.sender + iq['ibb_close']['sid'] = self.sid + self.stream_out_closed.set() + iq.send(block=False, + callback=lambda x: self.stream_in_closed.set()) + self.xmpp.event('ibb_stream_end', self) + + def _closed(self, iq): + self.stream_in_closed.set() + self.stream_out_closed.set() + while not self.window_empty.is_set(): + log.info('waiting for send window to empty') + self.window_empty.wait(timeout=1) + iq.reply() + iq.send() + self.xmpp.event('ibb_stream_end', self) + + def makefile(self, *args, **kwargs): + return self + + def connect(*args, **kwargs): + return None + + def shutdown(self, *args, **kwargs): + return None diff --git a/sleekxmpp/plugins/xep_0050/__init__.py b/sleekxmpp/plugins/xep_0050/__init__.py index 99f44f2a..640b182d 100644 --- a/sleekxmpp/plugins/xep_0050/__init__.py +++ b/sleekxmpp/plugins/xep_0050/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0050.stanza import Command -from sleekxmpp.plugins.xep_0050.adhoc import xep_0050 +from sleekxmpp.plugins.xep_0050.adhoc import XEP_0050 + + +register_plugin(XEP_0050) + + +# Retain some backwards compatibility +xep_0050 = XEP_0050 diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py index ec7b7041..8f2ea5c2 100644 --- a/sleekxmpp/plugins/xep_0050/adhoc.py +++ b/sleekxmpp/plugins/xep_0050/adhoc.py @@ -14,7 +14,7 @@ from sleekxmpp.exceptions import IqError from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream import register_stanza_plugin, JID -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0050 import stanza from sleekxmpp.plugins.xep_0050 import Command from sleekxmpp.plugins.xep_0004 import Form @@ -23,7 +23,7 @@ from sleekxmpp.plugins.xep_0004 import Form log = logging.getLogger(__name__) -class xep_0050(base_plugin): +class XEP_0050(BasePlugin): """ XEP-0050: Ad-Hoc Commands @@ -78,12 +78,13 @@ class xep_0050(base_plugin): terminate_command -- Command user API: delete a command's session """ + name = 'xep_0050' + description = 'XEP-0050: Ad-Hoc Commands' + dependencies = set(['xep_0030', 'xep_0004']) + stanza = stanza + def plugin_init(self): """Start the XEP-0050 plugin.""" - self.xep = '0050' - self.description = 'Ad-Hoc Commands' - self.stanza = stanza - self.threaded = self.config.get('threaded', True) self.commands = {} self.sessions = self.config.get('session_db', {}) @@ -109,10 +110,8 @@ class xep_0050(base_plugin): self._handle_command_complete, threaded=self.threaded) - def post_init(self): - """Handle cross-plugin interactions.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Command.namespace) + self.xmpp['xep_0030'].set_items(node=Command.namespace, items=tuple()) def set_backend(self, db): """ @@ -369,7 +368,6 @@ class xep_0050(base_plugin): del self.sessions[sessionid] - # ================================================================= # Client side (command user) API diff --git a/sleekxmpp/plugins/xep_0059/__init__.py b/sleekxmpp/plugins/xep_0059/__init__.py index 3a9b8edf..3464ce32 100644 --- a/sleekxmpp/plugins/xep_0059/__init__.py +++ b/sleekxmpp/plugins/xep_0059/__init__.py @@ -6,5 +6,13 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0059.stanza import Set -from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, xep_0059 +from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, XEP_0059 + + +register_plugin(XEP_0059) + +# Retain some backwards compatibility +xep_0059 = XEP_0059 diff --git a/sleekxmpp/plugins/xep_0059/rsm.py b/sleekxmpp/plugins/xep_0059/rsm.py index 35908473..9335ed22 100644 --- a/sleekxmpp/plugins/xep_0059/rsm.py +++ b/sleekxmpp/plugins/xep_0059/rsm.py @@ -10,9 +10,10 @@ import logging import sleekxmpp from sleekxmpp import Iq -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin, register_plugin from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins.xep_0059 import Set +from sleekxmpp.plugins.xep_0059 import stanza, Set +from sleekxmpp.exceptions import XMPPError log = logging.getLogger(__name__) @@ -70,38 +71,49 @@ class ResultIterator(): elif self.start: self.query[self.interface]['rsm']['after'] = self.start - r = self.query.send(block=True) + try: + r = self.query.send(block=True) - if not r or not r[self.interface]['rsm']['first'] and \ - not r[self.interface]['rsm']['last']: - raise StopIteration + if not r[self.interface]['rsm']['first'] and \ + not r[self.interface]['rsm']['last']: + raise StopIteration + + if r[self.interface]['rsm']['count'] and \ + r[self.interface]['rsm']['first_index']: + count = int(r[self.interface]['rsm']['count']) + first = int(r[self.interface]['rsm']['first_index']) + num_items = len(r[self.interface]['substanzas']) + if first + num_items == count: + raise StopIteration - if self.reverse: - self.start = r[self.interface]['rsm']['first'] - else: - self.start = r[self.interface]['rsm']['last'] + if self.reverse: + self.start = r[self.interface]['rsm']['first'] + else: + self.start = r[self.interface]['rsm']['last'] - return r + return r + except XMPPError: + raise StopIteration -class xep_0059(base_plugin): +class XEP_0059(BasePlugin): """ XEP-0050: Result Set Management """ + name = 'xep_0059' + description = 'XEP-0059: Result Set Management' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """ Start the XEP-0059 plugin. """ - self.xep = '0059' - self.description = 'Result Set Management' - self.stanza = sleekxmpp.plugins.xep_0059.stanza - - def post_init(self): - """Handle inter-plugin dependencies.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Set.namespace) + register_stanza_plugin(self.xmpp['xep_0030'].stanza.DiscoItems, + self.stanza.Set) def iterate(self, stanza, interface): """ diff --git a/sleekxmpp/plugins/xep_0060/__init__.py b/sleekxmpp/plugins/xep_0060/__init__.py index 026f7c2b..86e2f472 100644 --- a/sleekxmpp/plugins/xep_0060/__init__.py +++ b/sleekxmpp/plugins/xep_0060/__init__.py @@ -1,2 +1,19 @@ -from sleekxmpp.plugins.xep_0060.pubsub import xep_0060 +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0060.pubsub import XEP_0060 from sleekxmpp.plugins.xep_0060 import stanza + + +register_plugin(XEP_0060) + + +# Retain some backwards compatibility +xep_0060 = XEP_0060 diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py index 9e394ef2..31e59be9 100644 --- a/sleekxmpp/plugins/xep_0060/pubsub.py +++ b/sleekxmpp/plugins/xep_0060/pubsub.py @@ -9,23 +9,138 @@ import logging from sleekxmpp.xmlstream import JID -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.base import BasePlugin from sleekxmpp.plugins.xep_0060 import stanza log = logging.getLogger(__name__) -class xep_0060(base_plugin): +class XEP_0060(BasePlugin): """ XEP-0060 Publish Subscribe """ + name = 'xep_0060' + description = 'XEP-0060: Publish-Subscribe' + dependencies = set(['xep_0030', 'xep_0004']) + stanza = stanza + def plugin_init(self): - self.xep = '0060' - self.description = 'Publish-Subscribe' - self.stanza = stanza + self.node_event_map = {} + + self.xmpp.register_handler( + Callback('Pubsub Event: Items', + StanzaPath('message/pubsub_event/items'), + self._handle_event_items)) + self.xmpp.register_handler( + Callback('Pubsub Event: Purge', + StanzaPath('message/pubsub_event/purge'), + self._handle_event_purge)) + self.xmpp.register_handler( + Callback('Pubsub Event: Delete', + StanzaPath('message/pubsub_event/delete'), + self._handle_event_delete)) + self.xmpp.register_handler( + Callback('Pubsub Event: Configuration', + StanzaPath('message/pubsub_event/configuration'), + self._handle_event_configuration)) + self.xmpp.register_handler( + Callback('Pubsub Event: Subscription', + StanzaPath('message/pubsub_event/subscription'), + self._handle_event_subscription)) + + def _handle_event_items(self, msg): + """Raise events for publish and retraction notifications.""" + node = msg['pubsub_event']['items']['node'] + + multi = len(msg['pubsub_event']['items']) > 1 + values = {} + if multi: + values = msg.values + del values['pubsub_event'] + + for item in msg['pubsub_event']['items']: + event_name = self.node_event_map.get(node, None) + event_type = 'publish' + if item.name == 'retract': + event_type = 'retract' + + if multi: + condensed = self.xmpp.Message() + condensed.values = values + condensed['pubsub_event']['items']['node'] = node + condensed['pubsub_event']['items'].append(item) + self.xmpp.event('pubsub_%s' % event_type, msg) + if event_name: + self.xmpp.event('%s_%s' % (event_name, event_type), + condensed) + else: + self.xmpp.event('pubsub_%s' % event_type, msg) + if event_name: + self.xmpp.event('%s_%s' % (event_name, event_type), msg) + + def _handle_event_purge(self, msg): + """Raise events for node purge notifications.""" + node = msg['pubsub_event']['purge']['node'] + event_name = self.node_event_map.get(node, None) + + self.xmpp.event('pubsub_purge', msg) + if event_name: + self.xmpp.event('%s_purge' % event_name, msg) + + def _handle_event_delete(self, msg): + """Raise events for node deletion notifications.""" + node = msg['pubsub_event']['delete']['node'] + event_name = self.node_event_map.get(node, None) + + self.xmpp.event('pubsub_delete', msg) + if event_name: + self.xmpp.event('%s_delete' % event_name, msg) + + def _handle_event_configuration(self, msg): + """Raise events for node configuration notifications.""" + node = msg['pubsub_event']['configuration']['node'] + event_name = self.node_event_map.get(node, None) + + self.xmpp.event('pubsub_config', msg) + if event_name: + self.xmpp.event('%s_config' % event_name, msg) + + def _handle_event_subscription(self, msg): + """Raise events for node subscription notifications.""" + node = msg['pubsub_event']['subscription']['node'] + event_name = self.node_event_map.get(node, None) + + self.xmpp.event('pubsub_subscription', msg) + if event_name: + self.xmpp.event('%s_subscription' % event_name, msg) + + def map_node_event(self, node, event_name): + """ + Map node names to events. + + When a pubsub event is received for the given node, + raise the provided event. + + For example:: + + map_node_event('http://jabber.org/protocol/tune', + 'user_tune') + + will produce the events 'user_tune_publish' and 'user_tune_retract' + when the respective notifications are received from the node + 'http://jabber.org/protocol/tune', among other events. + + Arguments: + node -- The node name to map to an event. + event_name -- The name of the event to raise when a + notification from the given node is received. + """ + self.node_event_map[node] = event_name def create_node(self, jid, node, config=None, ntype=None, ifrom=None, block=True, callback=None, timeout=None): @@ -98,8 +213,9 @@ class xep_0060(base_plugin): ifrom -- Specify the sender's JID. block -- Specify if the send call will block until a response is received, or a timeout occurs. Defaults to True. - timeout -- The length of time (in seconds) to wait for a response - before exiting the send call if blocking is used. + timeout -- The length of time (in seconds) to wait for a + response before exiting the send call if blocking + is used. Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. @@ -146,8 +262,9 @@ class xep_0060(base_plugin): ifrom -- Specify the sender's JID. block -- Specify if the send call will block until a response is received, or a timeout occurs. Defaults to True. - timeout -- The length of time (in seconds) to wait for a response - before exiting the send call if blocking is used. + timeout -- The length of time (in seconds) to wait for a + response before exiting the send call if blocking + is used. Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. @@ -183,8 +300,9 @@ class xep_0060(base_plugin): iq['pubsub']['affiliations']['node'] = node return iq.send(block=block, callback=callback, timeout=timeout) - def get_subscription_options(self, jid, node=None, user_jid=None, ifrom=None, - block=True, callback=None, timeout=None): + def get_subscription_options(self, jid, node=None, user_jid=None, + ifrom=None, block=True, callback=None, + timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') if user_jid is None: iq['pubsub']['default']['node'] = node @@ -364,7 +482,7 @@ class xep_0060(base_plugin): """ Discover the nodes provided by a Pubsub service, using disco. """ - return self.xmpp.plugin['xep_0030'].get_items(*args, **kwargs) + return self.xmpp['xep_0030'].get_items(*args, **kwargs) def get_item(self, jid, node, item_id, ifrom=None, block=True, callback=None, timeout=None): @@ -372,7 +490,7 @@ class xep_0060(base_plugin): Retrieve the content of an individual item. """ iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='get') - item = self.stanza.Item() + item = stanza.Item() item['id'] = item_id iq['pubsub']['items']['node'] = node iq['pubsub']['items'].append(item) @@ -396,7 +514,7 @@ class xep_0060(base_plugin): if item_ids is not None: for item_id in item_ids: - item = self.stanza.Item() + item = stanza.Item() item['id'] = item_id iq['pubsub']['items'].append(item) @@ -410,12 +528,12 @@ class xep_0060(base_plugin): """ Retrieve the ItemIDs hosted by a given node, using disco. """ - return self.xmpp.plugin['xep_0030'].get_items(jid, node, - ifrom=ifrom, - block=block, - callback=callback, - timeout=timeout, - iterator=iterator) + return self.xmpp['xep_0030'].get_items(jid, node, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout, + iterator=iterator) def modify_affiliations(self, jid, node, affiliations=None, ifrom=None, block=True, callback=None, timeout=None): @@ -426,7 +544,7 @@ class xep_0060(base_plugin): affiliations = [] for jid, affiliation in affiliations: - aff = self.stanza.OwnerAffiliation() + aff = stanza.OwnerAffiliation() aff['jid'] = jid aff['affiliation'] = affiliation iq['pubsub_owner']['affiliations'].append(aff) @@ -442,7 +560,7 @@ class xep_0060(base_plugin): subscriptions = [] for jid, subscription in subscriptions: - sub = self.stanza.OwnerSubscription() + sub = stanza.OwnerSubscription() sub['jid'] = jid sub['subscription'] = subscription iq['pubsub_owner']['subscriptions'].append(sub) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py index c7263577..c0d4929e 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py @@ -6,23 +6,26 @@ See the file LICENSE for copying permission. """ +import datetime as dt + from sleekxmpp import Message from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from sleekxmpp.plugins.xep_0004 import Form +from sleekxmpp.plugins import xep_0082 class Event(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'event' plugin_attrib = 'pubsub_event' - interfaces = set(('node',)) + interfaces = set() class EventItem(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'item' plugin_attrib = name - interfaces = set(('id', 'payload')) + interfaces = set(('id', 'payload', 'node', 'publisher')) def set_payload(self, value): self.xml.append(value) @@ -76,7 +79,7 @@ class EventConfiguration(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'configuration' plugin_attrib = name - interfaces = set(('node', 'config')) + interfaces = set(('node',)) class EventPurge(ElementBase): @@ -86,12 +89,47 @@ class EventPurge(ElementBase): interfaces = set(('node',)) +class EventDelete(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'delete' + plugin_attrib = name + interfaces = set(('node', 'redirect')) + + def set_redirect(self, uri): + del self['redirect'] + redirect = ET.Element('{%s}redirect' % self.namespace) + redirect.attrib['uri'] = uri + self.xml.append(redirect) + + def get_redirect(self): + redirect = self.xml.find('{%s}redirect' % self.namespace) + if redirect is not None: + return redirect.attrib.get('uri', '') + return '' + + def del_redirect(self): + redirect = self.xml.find('{%s}redirect' % self.namespace) + if redirect is not None: + self.xml.remove(redirect) + + class EventSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'subscription' plugin_attrib = name interfaces = set(('node', 'expiry', 'jid', 'subid', 'subscription')) + def get_expiry(self): + expiry = self._get_attr('expiry') + if expiry.lower() == 'presence': + return expiry + return xep_0082.parse(expiry) + + def set_expiry(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('expiry', value) + def set_jid(self, value): self._set_attr('jid', str(value)) @@ -102,8 +140,9 @@ class EventSubscription(ElementBase): register_stanza_plugin(Message, Event) register_stanza_plugin(Event, EventCollection) register_stanza_plugin(Event, EventConfiguration) -register_stanza_plugin(Event, EventItems) register_stanza_plugin(Event, EventPurge) +register_stanza_plugin(Event, EventDelete) +register_stanza_plugin(Event, EventItems) register_stanza_plugin(Event, EventSubscription) register_stanza_plugin(EventCollection, EventAssociate) register_stanza_plugin(EventCollection, EventDisassociate) diff --git a/sleekxmpp/plugins/xep_0066/__init__.py b/sleekxmpp/plugins/xep_0066/__init__.py index ebfbd0c2..68a50180 100644 --- a/sleekxmpp/plugins/xep_0066/__init__.py +++ b/sleekxmpp/plugins/xep_0066/__init__.py @@ -6,6 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0066 import stanza from sleekxmpp.plugins.xep_0066.stanza import OOB, OOBTransfer -from sleekxmpp.plugins.xep_0066.oob import xep_0066 +from sleekxmpp.plugins.xep_0066.oob import XEP_0066 + + +register_plugin(XEP_0066) + + +# Retain some backwards compatibility +xep_0066 = XEP_0066 diff --git a/sleekxmpp/plugins/xep_0066/oob.py b/sleekxmpp/plugins/xep_0066/oob.py index d1f4b3ff..dc215e83 100644 --- a/sleekxmpp/plugins/xep_0066/oob.py +++ b/sleekxmpp/plugins/xep_0066/oob.py @@ -13,19 +13,19 @@ from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0066 import stanza log = logging.getLogger(__name__) -class xep_0066(base_plugin): +class XEP_0066(BasePlugin): """ - XEP-0066: Out-of-Band Data + XEP-0066: Out of Band Data - Out-of-Band Data is a basic method for transferring files between + Out of Band Data is a basic method for transferring files between XMPP agents. The URL of the resource in question is sent to the receiving entity, which then downloads the resource before responding to the OOB request. OOB is also used as a generic means to transmit URLs in other @@ -42,11 +42,13 @@ class xep_0066(base_plugin): or other addressable resource. """ + name = 'xep_0066' + description = 'XEP-0066: Out of Band Data' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """Start the XEP-0066 plugin.""" - self.xep = '0066' - self.description = 'Out-of-Band Transfer' - self.stanza = stanza self.url_handlers = {'global': self._default_handler, 'jid': {}} @@ -60,9 +62,6 @@ class xep_0066(base_plugin): StanzaPath('iq@type=set/oob_transfer'), self._handle_transfer)) - def post_init(self): - """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace) self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace) @@ -121,7 +120,7 @@ class xep_0066(base_plugin): iq -- The Iq stanza containing the OOB transfer request. """ if iq['to'] in self.url_handlers['jid']: - return self.url_handlers['jid'][jid](iq) + return self.url_handlers['jid'][iq['to']](iq) else: if self.url_handlers['global']: self.url_handlers['global'](iq) diff --git a/sleekxmpp/plugins/xep_0077/__init__.py b/sleekxmpp/plugins/xep_0077/__init__.py new file mode 100644 index 00000000..779ae0ac --- /dev/null +++ b/sleekxmpp/plugins/xep_0077/__init__.py @@ -0,0 +1,19 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0077.stanza import Register, RegisterFeature +from sleekxmpp.plugins.xep_0077.register import XEP_0077 + + +register_plugin(XEP_0077) + + +# Retain some backwards compatibility +xep_0077 = XEP_0077 diff --git a/sleekxmpp/plugins/xep_0077/register.py b/sleekxmpp/plugins/xep_0077/register.py new file mode 100644 index 00000000..53cc9ef5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0077/register.py @@ -0,0 +1,90 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import StreamFeatures, Iq +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0077 import stanza, Register, RegisterFeature + + +log = logging.getLogger(__name__) + + +class XEP_0077(BasePlugin): + + """ + XEP-0077: In-Band Registration + """ + + name = 'xep_0077' + description = 'XEP-0077: In-Band Registration' + dependencies = set(['xep_0004', 'xep_0066']) + stanza = stanza + + def plugin_init(self): + self.create_account = self.config.get('create_account', True) + + register_stanza_plugin(StreamFeatures, RegisterFeature) + register_stanza_plugin(Iq, Register) + + if self.xmpp.is_component: + pass + else: + self.xmpp.register_feature('register', + self._handle_register_feature, + restart=False, + order=self.config.get('order', 50)) + + register_stanza_plugin(Register, self.xmpp['xep_0004'].stanza.Form) + register_stanza_plugin(Register, self.xmpp['xep_0066'].stanza.OOB) + + def _handle_register_feature(self, features): + if 'mechanisms' in self.xmpp.features: + # We have already logged in with an account + return False + + if self.create_account: + form = self.get_registration() + self.xmpp.event('register', form, direct=True) + return True + return False + + def get_registration(self, jid=None, ifrom=None, block=True, + timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['to'] = jid + iq['from'] = ifrom + iq.enable('register') + return iq.send(block=block, timeout=timeout, + callback=callback, now=True) + + def cancel_registration(self, jid=None, ifrom=None, block=True, + timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = jid + iq['from'] = ifrom + iq['register']['remove'] = True + return iq.send(block=block, timeout=timeout, callback=callback) + + def change_password(self, password, jid=None, ifrom=None, block=True, + timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = jid + iq['from'] = ifrom + if self.xmpp.is_component: + ifrom = JID(ifrom) + iq['register']['username'] = ifrom.user + else: + iq['register']['username'] = self.xmpp.boundjid.user + iq['register']['password'] = password + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0077/stanza.py b/sleekxmpp/plugins/xep_0077/stanza.py new file mode 100644 index 00000000..e06c1910 --- /dev/null +++ b/sleekxmpp/plugins/xep_0077/stanza.py @@ -0,0 +1,73 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from __future__ import unicode_literals + +from sleekxmpp.xmlstream import ElementBase, ET + + +class Register(ElementBase): + + namespace = 'jabber:iq:register' + name = 'query' + plugin_attrib = 'register' + interfaces = set(('username', 'password', 'email', 'nick', 'name', + 'first', 'last', 'address', 'city', 'state', 'zip', + 'phone', 'url', 'date', 'misc', 'text', 'key', + 'registered', 'remove', 'instructions', 'fields')) + sub_interfaces = interfaces + form_fields = set(('username', 'password', 'email', 'nick', 'name', + 'first', 'last', 'address', 'city', 'state', 'zip', + 'phone', 'url', 'date', 'misc', 'text', 'key')) + + def get_registered(self): + present = self.xml.find('{%s}registered' % self.namespace) + return present is not None + + def get_remove(self): + present = self.xml.find('{%s}remove' % self.namespace) + return present is not None + + def set_registered(self, value): + if value: + self.add_field('registered') + else: + del self['registered'] + + def set_remove(self, value): + if value: + self.add_field('remove') + else: + del self['remove'] + + def add_field(self, value): + self._set_sub_text(value, '', keep=True) + + def get_fields(self): + fields = set() + for field in self.form_fields: + if self.xml.find('{%s}%s' % (self.namespace, field)) is not None: + fields.add(field) + return fields + + def set_fields(self, fields): + del self['fields'] + for field in fields: + self._set_sub_text(field, '', keep=True) + + def del_fields(self): + for field in self.form_fields: + self._del_sub(field) + + +class RegisterFeature(ElementBase): + + name = 'register' + namespace = 'http://jabber.org/features/iq-register' + plugin_attrib = name + interfaces = set() diff --git a/sleekxmpp/plugins/xep_0078/__init__.py b/sleekxmpp/plugins/xep_0078/__init__.py index 5a2bda77..2ea72ffb 100644 --- a/sleekxmpp/plugins/xep_0078/__init__.py +++ b/sleekxmpp/plugins/xep_0078/__init__.py @@ -6,7 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0078 import stanza from sleekxmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature -from sleekxmpp.plugins.xep_0078.legacyauth import xep_0078 +from sleekxmpp.plugins.xep_0078.legacyauth import XEP_0078 + + +register_plugin(XEP_0078) + +# Retain some backwards compatibility +xep_0078 = XEP_0078 diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py index dec775a3..95587843 100644 --- a/sleekxmpp/plugins/xep_0078/legacyauth.py +++ b/sleekxmpp/plugins/xep_0078/legacyauth.py @@ -9,17 +9,19 @@ import logging import hashlib import random +import sys +from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0078 import stanza log = logging.getLogger(__name__) -class xep_0078(base_plugin): +class XEP_0078(BasePlugin): """ XEP-0078 NON-SASL Authentication @@ -28,11 +30,12 @@ class xep_0078(base_plugin): unless you are forced to use an old XMPP server implementation. """ - def plugin_init(self): - self.xep = "0078" - self.description = "Non-SASL Authentication" - self.stanza = stanza + name = 'xep_0078' + description = 'XEP-0078: Non-SASL Authentication' + dependencies = set() + stanza = stanza + def plugin_init(self): self.xmpp.register_feature('auth', self._handle_auth, restart=False, @@ -41,7 +44,6 @@ class xep_0078(base_plugin): register_stanza_plugin(Iq, stanza.IqAuth) register_stanza_plugin(StreamFeatures, stanza.AuthFeature) - def _handle_auth(self, features): # If we can or have already authenticated with SASL, do nothing. if 'mechanisms' in features['features']: diff --git a/sleekxmpp/plugins/xep_0080/__init__.py b/sleekxmpp/plugins/xep_0080/__init__.py new file mode 100644 index 00000000..cad23d22 --- /dev/null +++ b/sleekxmpp/plugins/xep_0080/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0080.stanza import Geoloc +from sleekxmpp.plugins.xep_0080.geoloc import XEP_0080 + + +register_plugin(XEP_0080) diff --git a/sleekxmpp/plugins/xep_0080/geoloc.py b/sleekxmpp/plugins/xep_0080/geoloc.py new file mode 100644 index 00000000..20dde4dd --- /dev/null +++ b/sleekxmpp/plugins/xep_0080/geoloc.py @@ -0,0 +1,122 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +import sleekxmpp +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0080 import stanza, Geoloc + + +log = logging.getLogger(__name__) + + +class XEP_0080(BasePlugin): + + """ + XEP-0080: User Location + """ + + name = 'xep_0080' + description = 'XEP-0080: User Location' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + """Start the XEP-0080 plugin.""" + self.xmpp['xep_0163'].register_pep('user_location', Geoloc) + + def publish_location(self, **kwargs): + """ + Publish the user's current location. + + Arguments: + accuracy -- Horizontal GPS error in meters. + alt -- Altitude in meters above or below sea level. + area -- A named area such as a campus or neighborhood. + bearing -- GPS bearing (direction in which the entity is + heading to reach its next waypoint), measured in + decimal degrees relative to true north. + building -- A specific building on a street or in an area. + country -- The nation where the user is located. + countrycode -- The ISO 3166 two-letter country code. + datum -- GPS datum. + description -- A natural-language name for or description of + the location. + error -- Horizontal GPS error in arc minutes. Obsoleted by + the accuracy parameter. + floor -- A particular floor in a building. + lat -- Latitude in decimal degrees North. + locality -- A locality within the administrative region, such + as a town or city. + lon -- Longitude in decimal degrees East. + postalcode -- A code used for postal delivery. + region -- An administrative region of the nation, such + as a state or province. + room -- A particular room in a building. + speed -- The speed at which the entity is moving, + in meters per second. + street -- A thoroughfare within the locality, or a crossing + of two thoroughfares. + text -- A catch-all element that captures any other + information about the location. + timestamp -- UTC timestamp specifying the moment when the + reading was taken. + uri -- A URI or URL pointing to information about + the location. + + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + options = kwargs.get('options', None) + ifrom = kwargs.get('ifrom', None) + block = kwargs.get('block', None) + callback = kwargs.get('callback', None) + timeout = kwargs.get('timeout', None) + for param in ('ifrom', 'block', 'callback', 'timeout', 'options'): + if param in kwargs: + del kwargs[param] + + geoloc = Geoloc() + geoloc.values = kwargs + + return self.xmpp['xep_0163'].publish(geoloc, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user location information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + geoloc = Geoloc() + return self.xmpp['xep_0163'].publish(geoloc, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0080/stanza.py b/sleekxmpp/plugins/xep_0080/stanza.py new file mode 100644 index 00000000..a83a8b1b --- /dev/null +++ b/sleekxmpp/plugins/xep_0080/stanza.py @@ -0,0 +1,266 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class Geoloc(ElementBase): + + """ + XMPP's <geoloc> stanza allows entities to know the current + geographical or physical location of an entity. (XEP-0080: User Location) + + Example <geoloc> stanzas: + <geoloc xmlns='http://jabber.org/protocol/geoloc'/> + + <geoloc xmlns='http://jabber.org/protocol/geoloc' xml:lang='en'> + <accuracy>20</accuracy> + <country>Italy</country> + <lat>45.44</lat> + <locality>Venice</locality> + <lon>12.33</lon> + </geoloc> + + Stanza Interface: + accuracy -- Horizontal GPS error in meters. + alt -- Altitude in meters above or below sea level. + area -- A named area such as a campus or neighborhood. + bearing -- GPS bearing (direction in which the entity is + heading to reach its next waypoint), measured in + decimal degrees relative to true north. + building -- A specific building on a street or in an area. + country -- The nation where the user is located. + countrycode -- The ISO 3166 two-letter country code. + datum -- GPS datum. + description -- A natural-language name for or description of + the location. + error -- Horizontal GPS error in arc minutes. Obsoleted by + the accuracy parameter. + floor -- A particular floor in a building. + lat -- Latitude in decimal degrees North. + locality -- A locality within the administrative region, such + as a town or city. + lon -- Longitude in decimal degrees East. + postalcode -- A code used for postal delivery. + region -- An administrative region of the nation, such + as a state or province. + room -- A particular room in a building. + speed -- The speed at which the entity is moving, + in meters per second. + street -- A thoroughfare within the locality, or a crossing + of two thoroughfares. + text -- A catch-all element that captures any other + information about the location. + timestamp -- UTC timestamp specifying the moment when the + reading was taken. + uri -- A URI or URL pointing to information about + the location. + """ + + namespace = 'http://jabber.org/protocol/geoloc' + name = 'geoloc' + interfaces = set(('accuracy', 'alt', 'area', 'bearing', 'building', + 'country', 'countrycode', 'datum', 'dscription', + 'error', 'floor', 'lat', 'locality', 'lon', + 'postalcode', 'region', 'room', 'speed', 'street', + 'text', 'timestamp', 'uri')) + sub_interfaces = interfaces + plugin_attrib = name + + def exception(self, e): + """ + Override exception passback for presence. + """ + pass + + def set_accuracy(self, accuracy): + """ + Set the value of the <accuracy> element. + + Arguments: + accuracy -- Horizontal GPS error in meters + """ + self._set_sub_text('accuracy', text=str(accuracy)) + return self + + def get_accuracy(self): + """ + Return the value of the <accuracy> element as an integer. + """ + p = self._get_sub_text('accuracy') + if not p: + return None + else: + try: + return int(p) + except ValueError: + return None + + def set_alt(self, alt): + """ + Set the value of the <alt> element. + + Arguments: + alt -- Altitude in meters above or below sea level + """ + self._set_sub_text('alt', text=str(alt)) + return self + + def get_alt(self): + """ + Return the value of the <alt> element as an integer. + """ + p = self._get_sub_text('alt') + if not p: + return None + else: + try: + return int(p) + except ValueError: + return None + + def set_bearing(self, bearing): + """ + Set the value of the <bearing> element. + + Arguments: + bearing -- GPS bearing (direction in which the entity is heading + to reach its next waypoint), measured in decimal + degrees relative to true north + """ + self._set_sub_text('bearing', text=str(bearing)) + return self + + def get_bearing(self): + """ + Return the value of the <bearing> element as a float. + """ + p = self._get_sub_text('bearing') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_error(self, error): + """ + Set the value of the <error> element. + + Arguments: + error -- Horizontal GPS error in arc minutes; this + element is deprecated in favor of <accuracy/> + """ + self._set_sub_text('error', text=str(error)) + return self + + def get_error(self): + """ + Return the value of the <error> element as a float. + """ + p = self._get_sub_text('error') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_lat(self, lat): + """ + Set the value of the <lat> element. + + Arguments: + lat -- Latitude in decimal degrees North + """ + self._set_sub_text('lat', text=str(lat)) + return self + + def get_lat(self): + """ + Return the value of the <lat> element as a float. + """ + p = self._get_sub_text('lat') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_lon(self, lon): + """ + Set the value of the <lon> element. + + Arguments: + lon -- Longitude in decimal degrees East + """ + self._set_sub_text('lon', text=str(lon)) + return self + + def get_lon(self): + """ + Return the value of the <lon> element as a float. + """ + p = self._get_sub_text('lon') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_speed(self, speed): + """ + Set the value of the <speed> element. + + Arguments: + speed -- The speed at which the entity is moving, + in meters per second + """ + self._set_sub_text('speed', text=str(speed)) + return self + + def get_speed(self): + """ + Return the value of the <speed> element as a float. + """ + p = self._get_sub_text('speed') + if not p: + return None + else: + try: + return float(p) + except ValueError: + return None + + def set_timestamp(self, timestamp): + """ + Set the value of the <timestamp> element. + + Arguments: + timestamp -- UTC timestamp specifying the moment when + the reading was taken + """ + self._set_sub_text('timestamp', text=str(xep_0082.datetime(timestamp))) + return self + + def get_timestamp(self): + """ + Return the value of the <timestamp> element as a DateTime. + """ + p = self._get_sub_text('timestamp') + if not p: + return None + else: + return xep_0082.datetime(p) diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py index 25c80fd0..96eb331a 100644 --- a/sleekxmpp/plugins/xep_0082.py +++ b/sleekxmpp/plugins/xep_0082.py @@ -9,7 +9,7 @@ import logging import datetime as dt -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin, register_plugin from sleekxmpp.thirdparty import tzutc, tzoffset, parse_iso @@ -184,7 +184,8 @@ def datetime(year=None, month=None, day=None, hour=None, return value return format_datetime(value) -class xep_0082(base_plugin): + +class XEP_0082(BasePlugin): """ XEP-0082: XMPP Date and Time Profiles @@ -205,11 +206,12 @@ class xep_0082(base_plugin): parse -- Convert a time string into a Python datetime object. """ + name = 'xep_0082' + description = 'XEP-0082: XMPP Date and Time Profiles' + dependencies = set() + def plugin_init(self): """Start the XEP-0082 plugin.""" - self.xep = '0082' - self.description = 'XMPP Date and Time Profiles' - self.date = date self.datetime = datetime self.time = time @@ -217,3 +219,6 @@ class xep_0082(base_plugin): self.format_datetime = format_datetime self.format_time = format_time self.parse = parse + + +register_plugin(XEP_0082) diff --git a/sleekxmpp/plugins/xep_0085/__init__.py b/sleekxmpp/plugins/xep_0085/__init__.py index ff882f05..445d5059 100644 --- a/sleekxmpp/plugins/xep_0085/__init__.py +++ b/sleekxmpp/plugins/xep_0085/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permissio """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0085.stanza import ChatState -from sleekxmpp.plugins.xep_0085.chat_states import xep_0085 +from sleekxmpp.plugins.xep_0085.chat_states import XEP_0085 + + +register_plugin(XEP_0085) + + +# Retain some backwards compatibility +xep_0085 = XEP_0085 diff --git a/sleekxmpp/plugins/xep_0085/chat_states.py b/sleekxmpp/plugins/xep_0085/chat_states.py index e95434d2..d10b317b 100644 --- a/sleekxmpp/plugins/xep_0085/chat_states.py +++ b/sleekxmpp/plugins/xep_0085/chat_states.py @@ -13,34 +13,36 @@ from sleekxmpp.stanza import Message from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0085 import stanza, ChatState log = logging.getLogger(__name__) -class xep_0085(base_plugin): +class XEP_0085(BasePlugin): """ XEP-0085 Chat State Notifications """ - def plugin_init(self): - self.xep = '0085' - self.description = 'Chat State Notifications' - self.stanza = stanza + name = 'xep_0085' + description = 'XEP-0085: Chat State Notifications' + dependencies = set(['xep_0030']) + stanza = stanza - for state in ChatState.states: - self.xmpp.register_handler( - Callback('Chat State: %s' % state, - StanzaPath('message@chat_state=%s' % state), - self._handle_chat_state)) + def plugin_init(self): + self.xmpp.register_handler( + Callback('Chat State', + StanzaPath('message/chat_state'), + self._handle_chat_state)) - register_stanza_plugin(Message, ChatState) + register_stanza_plugin(Message, stanza.Active) + register_stanza_plugin(Message, stanza.Composing) + register_stanza_plugin(Message, stanza.Gone) + register_stanza_plugin(Message, stanza.Inactive) + register_stanza_plugin(Message, stanza.Paused) - def post_init(self): - base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace) def _handle_chat_state(self, msg): diff --git a/sleekxmpp/plugins/xep_0085/stanza.py b/sleekxmpp/plugins/xep_0085/stanza.py index 8c46758c..c2cafb19 100644 --- a/sleekxmpp/plugins/xep_0085/stanza.py +++ b/sleekxmpp/plugins/xep_0085/stanza.py @@ -38,6 +38,7 @@ class ChatState(ElementBase): namespace = 'http://jabber.org/protocol/chatstates' plugin_attrib = 'chat_state' interfaces = set(('chat_state',)) + sub_interfaces = interfaces is_extension = True states = set(('active', 'composing', 'gone', 'inactive', 'paused')) @@ -71,3 +72,23 @@ class ChatState(ElementBase): if state_xml is not None: self.xml = ET.Element('') parent.xml.remove(state_xml) + + +class Active(ChatState): + name = 'active' + + +class Composing(ChatState): + name = 'composing' + + +class Gone(ChatState): + name = 'gone' + + +class Inactive(ChatState): + name = 'inactive' + + +class Paused(ChatState): + name = 'paused' diff --git a/sleekxmpp/plugins/xep_0086/__init__.py b/sleekxmpp/plugins/xep_0086/__init__.py index b021e2b5..94600e85 100644 --- a/sleekxmpp/plugins/xep_0086/__init__.py +++ b/sleekxmpp/plugins/xep_0086/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0086.stanza import LegacyError -from sleekxmpp.plugins.xep_0086.legacy_error import xep_0086 +from sleekxmpp.plugins.xep_0086.legacy_error import XEP_0086 + + +register_plugin(XEP_0086) + + +# Retain some backwards compatibility +xep_0086 = XEP_0086 diff --git a/sleekxmpp/plugins/xep_0086/legacy_error.py b/sleekxmpp/plugins/xep_0086/legacy_error.py index 25b98c5a..bed22ee2 100644 --- a/sleekxmpp/plugins/xep_0086/legacy_error.py +++ b/sleekxmpp/plugins/xep_0086/legacy_error.py @@ -8,11 +8,11 @@ from sleekxmpp.stanza import Error
from sleekxmpp.xmlstream import register_stanza_plugin
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins.xep_0086 import stanza, LegacyError
-class xep_0086(base_plugin):
+class XEP_0086(BasePlugin):
"""
XEP-0086: Error Condition Mappings
@@ -33,10 +33,11 @@ class xep_0086(base_plugin): iq['error']['legacy']['condition'] = ...
"""
- def plugin_init(self):
- self.xep = '0086'
- self.description = 'Error Condition Mappings'
- self.stanza = stanza
+ name = 'xep_0086'
+ description = 'XEP-0086: Error Condition Mappings'
+ dependencies = set()
+ stanza = stanza
+ def plugin_init(self):
register_stanza_plugin(Error, LegacyError,
overrides=self.config.get('override', True))
diff --git a/sleekxmpp/plugins/xep_0092/__init__.py b/sleekxmpp/plugins/xep_0092/__init__.py index 7c5bdb76..293eaae6 100644 --- a/sleekxmpp/plugins/xep_0092/__init__.py +++ b/sleekxmpp/plugins/xep_0092/__init__.py @@ -6,6 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0092 import stanza from sleekxmpp.plugins.xep_0092.stanza import Version -from sleekxmpp.plugins.xep_0092.version import xep_0092 +from sleekxmpp.plugins.xep_0092.version import XEP_0092 + + +register_plugin(XEP_0092) + + +# Retain some backwards compatibility +xep_0092 = XEP_0092 diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index ba72a9c3..c6223c10 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -13,27 +13,28 @@ from sleekxmpp import Iq from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.base import base_plugin -from sleekxmpp.plugins.xep_0092 import Version +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0092 import Version, stanza log = logging.getLogger(__name__) -class xep_0092(base_plugin): +class XEP_0092(BasePlugin): """ XEP-0092: Software Version """ + name = 'xep_0092' + description = 'XEP-0092: Software Version' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """ Start the XEP-0092 plugin. """ - self.xep = "0092" - self.description = "Software Version" - self.stanza = sleekxmpp.plugins.xep_0092.stanza - self.name = self.config.get('name', 'SleekXMPP') self.version = self.config.get('version', sleekxmpp.__version__) self.os = self.config.get('os', '') @@ -47,11 +48,6 @@ class xep_0092(base_plugin): register_stanza_plugin(Iq, Version) - def post_init(self): - """ - Handle cross-plugin dependencies. - """ - base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version') def _handle_version(self, iq): diff --git a/sleekxmpp/plugins/xep_0107/__init__.py b/sleekxmpp/plugins/xep_0107/__init__.py new file mode 100644 index 00000000..04302df8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0107/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0107 import stanza +from sleekxmpp.plugins.xep_0107.stanza import UserMood +from sleekxmpp.plugins.xep_0107.user_mood import XEP_0107 + + +register_plugin(XEP_0107) diff --git a/sleekxmpp/plugins/xep_0107/stanza.py b/sleekxmpp/plugins/xep_0107/stanza.py new file mode 100644 index 00000000..2c5814ea --- /dev/null +++ b/sleekxmpp/plugins/xep_0107/stanza.py @@ -0,0 +1,55 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class UserMood(ElementBase): + + name = 'mood' + namespace = 'http://jabber.org/protocol/mood' + plugin_attrib = 'mood' + interfaces = set(['value', 'text']) + sub_interfaces = set(['text']) + moods = set(['afraid', 'amazed', 'amorous', 'angry', 'annoyed', 'anxious', + 'aroused', 'ashamed', 'bored', 'brave', 'calm', 'cautious', + 'cold', 'confident', 'confused', 'contemplative', 'contented', + 'cranky', 'crazy', 'creative', 'curious', 'dejected', + 'depressed', 'disappointed', 'disgusted', 'dismayed', + 'distracted', 'embarrassed', 'envious', 'excited', + 'flirtatious', 'frustrated', 'grateful', 'grieving', 'grumpy', + 'guilty', 'happy', 'hopeful', 'hot', 'humbled', 'humiliated', + 'hungry', 'hurt', 'impressed', 'in_awe', 'in_love', + 'indignant', 'interested', 'intoxicated', 'invincible', + 'jealous', 'lonely', 'lost', 'lucky', 'mean', 'moody', + 'nervous', 'neutral', 'offended', 'outraged', 'playful', + 'proud', 'relaxed', 'relieved', 'remorseful', 'restless', + 'sad', 'sarcastic', 'satisfied', 'serious', 'shocked', + 'shy', 'sick', 'sleepy', 'spontaneous', 'stressed', 'strong', + 'surprised', 'thankful', 'thirsty', 'tired', 'undefined', + 'weak', 'worried']) + + def set_value(self, value): + self.del_value() + if value in self.moods: + self._set_sub_text(value, '', keep=True) + else: + raise ValueError('Unknown mood value') + + def get_value(self): + for child in self.xml: + if child.tag.startswith('{%s}' % self.namespace): + elem_name = child.tag.split('}')[-1] + if elem_name in self.moods: + return elem_name + return '' + + def del_value(self): + curr_value = self.get_value() + if curr_value: + self._set_sub_text(curr_value, '', keep=False) diff --git a/sleekxmpp/plugins/xep_0107/user_mood.py b/sleekxmpp/plugins/xep_0107/user_mood.py new file mode 100644 index 00000000..11aaace4 --- /dev/null +++ b/sleekxmpp/plugins/xep_0107/user_mood.py @@ -0,0 +1,87 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Message +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0107 import stanza, UserMood + + +log = logging.getLogger(__name__) + + +class XEP_0107(BasePlugin): + + """ + XEP-0107: User Mood + """ + + name = 'xep_0107' + description = 'XEP-0107: User Mood' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, UserMood) + self.xmpp['xep_0163'].register_pep('user_mood', UserMood) + + def publish_mood(self, value=None, text=None, options=None, + ifrom=None, block=True, callback=None, timeout=None): + """ + Publish the user's current mood. + + Arguments: + value -- The name of the mood to publish. + text -- Optional natural-language description or reason + for the mood. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + mood = UserMood() + mood['value'] = value + mood['text'] = text + return self.xmpp['xep_0163'].publish(mood, + node=UserMood.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user mood information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + mood = UserMood() + return self.xmpp['xep_0163'].publish(mood, + node=UserMood.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0108/__init__.py b/sleekxmpp/plugins/xep_0108/__init__.py new file mode 100644 index 00000000..34d45113 --- /dev/null +++ b/sleekxmpp/plugins/xep_0108/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0108 import stanza +from sleekxmpp.plugins.xep_0108.stanza import UserActivity +from sleekxmpp.plugins.xep_0108.user_activity import XEP_0108 + + +register_plugin(XEP_0108) diff --git a/sleekxmpp/plugins/xep_0108/stanza.py b/sleekxmpp/plugins/xep_0108/stanza.py new file mode 100644 index 00000000..4dc18f43 --- /dev/null +++ b/sleekxmpp/plugins/xep_0108/stanza.py @@ -0,0 +1,83 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class UserActivity(ElementBase): + + name = 'activity' + namespace = 'http://jabber.org/protocol/activity' + plugin_attrib = 'activity' + interfaces = set(['value', 'text']) + sub_interfaces = set(['text']) + general = set(['doing_chores', 'drinking', 'eating', 'exercising', + 'grooming', 'having_appointment', 'inactive', 'relaxing', + 'talking', 'traveling', 'undefined', 'working']) + specific = set(['at_the_spa', 'brushing_teeth', 'buying_groceries', + 'cleaning', 'coding', 'commuting', 'cooking', 'cycling', + 'dancing', 'day_off', 'doing_maintenance', + 'doing_the_dishes', 'doing_the_laundry', 'driving', + 'fishing', 'gaming', 'gardening', 'getting_a_haircut', + 'going_out', 'hanging_out', 'having_a_beer', + 'having_a_snack', 'having_breakfast', 'having_coffee', + 'having_dinner', 'having_lunch', 'having_tea', 'hiding', + 'hiking', 'in_a_car', 'in_a_meeting', 'in_real_life', + 'jogging', 'on_a_bus', 'on_a_plane', 'on_a_train', + 'on_a_trip', 'on_the_phone', 'on_vacation', + 'on_video_phone', 'other', 'partying', 'playing_sports', + 'praying', 'reading', 'rehearsing', 'running', + 'running_an_errand', 'scheduled_holiday', 'shaving', + 'shopping', 'skiing', 'sleeping', 'smoking', + 'socializing', 'studying', 'sunbathing', 'swimming', + 'taking_a_bath', 'taking_a_shower', 'thinking', + 'walking', 'walking_the_dog', 'watching_a_movie', + 'watching_tv', 'working_out', 'writing']) + + def set_value(self, value): + self.del_value() + general = value + specific = None + if isinstance(value, tuple) or isinstance(value, list): + general = value[0] + specific = value[1] + + if general in self.general: + gen_xml = ET.Element('{%s}%s' % (self.namespace, general)) + if specific: + spec_xml = ET.Element('{%s}%s' % (self.namespace, specific)) + if specific in self.specific: + gen_xml.append(spec_xml) + else: + raise ValueError('Unknown specific activity') + self.xml.append(gen_xml) + else: + raise ValueError('Unknown general activity') + + def get_value(self): + general = None + specific = None + gen_xml = None + for child in self.xml: + if child.tag.startswith('{%s}' % self.namespace): + elem_name = child.tag.split('}')[-1] + if elem_name in self.general: + general = elem_name + gen_xml = child + if gen_xml is not None: + for child in gen_xml: + if child.tag.startswith('{%s}' % self.namespace): + elem_name = child.tag.split('}')[-1] + if elem_name in self.specific: + specific = elem_name + return (general, specific) + + def del_value(self): + curr_value = self.get_value() + if curr_value[0]: + self._set_sub_text(curr_value[0], '', keep=False) diff --git a/sleekxmpp/plugins/xep_0108/user_activity.py b/sleekxmpp/plugins/xep_0108/user_activity.py new file mode 100644 index 00000000..43270486 --- /dev/null +++ b/sleekxmpp/plugins/xep_0108/user_activity.py @@ -0,0 +1,84 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0108 import stanza, UserActivity + + +log = logging.getLogger(__name__) + + +class XEP_0108(BasePlugin): + + """ + XEP-0108: User Activity + """ + + name = 'xep_0108' + description = 'XEP-0108: User Activity' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + self.xmpp['xep_0163'].register_pep('user_activity', UserActivity) + + def publish_activity(self, general, specific=None, text=None, options=None, + ifrom=None, block=True, callback=None, timeout=None): + """ + Publish the user's current activity. + + Arguments: + general -- The required general category of the activity. + specific -- Optional specific activity being done as part + of the general category. + text -- Optional natural-language description or reason + for the activity. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + activity = UserActivity() + activity['value'] = (general, specific) + activity['text'] = text + return self.xmpp['xep_0163'].publish(activity, + node=UserActivity.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user activity information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + activity = UserActivity() + return self.xmpp['xep_0163'].publish(activity, + node=UserActivity.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0115/__init__.py b/sleekxmpp/plugins/xep_0115/__init__.py new file mode 100644 index 00000000..31a2c03a --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/__init__.py @@ -0,0 +1,20 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0115.stanza import Capabilities +from sleekxmpp.plugins.xep_0115.static import StaticCaps +from sleekxmpp.plugins.xep_0115.caps import XEP_0115 + + +register_plugin(XEP_0115) + + +# Retain some backwards compatibility +xep_0115 = XEP_0115 diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py new file mode 100644 index 00000000..3aa0f70f --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -0,0 +1,305 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import hashlib +import base64 + +import sleekxmpp +from sleekxmpp.stanza import StreamFeatures, Presence, Iq +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.exceptions import XMPPError, IqError, IqTimeout +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0115 import stanza, StaticCaps + + +log = logging.getLogger(__name__) + + +class XEP_0115(BasePlugin): + + """ + XEP-0115: Entity Capabalities + """ + + name = 'xep_0115' + description = 'XEP-0115: Entity Capabilities' + dependencies = set(['xep_0030', 'xep_0128', 'xep_0004']) + stanza = stanza + + def plugin_init(self): + self.hashes = {'sha-1': hashlib.sha1, + 'md5': hashlib.md5} + + self.hash = self.config.get('hash', 'sha-1') + self.caps_node = self.config.get('caps_node', None) + self.broadcast = self.config.get('broadcast', True) + + if self.caps_node is None: + ver = sleekxmpp.__version__ + self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver + + register_stanza_plugin(Presence, stanza.Capabilities) + register_stanza_plugin(StreamFeatures, stanza.Capabilities) + + self._disco_ops = ['cache_caps', + 'get_caps', + 'assign_verstring', + 'get_verstring', + 'supports', + 'has_identity'] + + self.xmpp.register_handler( + Callback('Entity Capabilites', + StanzaPath('presence/caps'), + self._handle_caps)) + + self.xmpp.add_filter('out', self._filter_add_caps) + + self.xmpp.add_event_handler('entity_caps', self._process_caps, + threaded=True) + + if not self.xmpp.is_component: + self.xmpp.register_feature('caps', + self._handle_caps_feature, + restart=False, + order=10010) + + self.xmpp['xep_0030'].add_feature(stanza.Capabilities.namespace) + + disco = self.xmpp['xep_0030'] + self.static = StaticCaps(self.xmpp, disco.static) + + for op in self._disco_ops: + disco._add_disco_op(op, getattr(self.static, op)) + + self._run_node_handler = disco._run_node_handler + + disco.cache_caps = self.cache_caps + disco.update_caps = self.update_caps + disco.assign_verstring = self.assign_verstring + disco.get_verstring = self.get_verstring + + def _filter_add_caps(self, stanza): + if isinstance(stanza, Presence) and self.broadcast: + ver = self.get_verstring(stanza['from']) + if ver: + stanza['caps']['node'] = self.caps_node + stanza['caps']['hash'] = self.hash + stanza['caps']['ver'] = ver + return stanza + + def _handle_caps(self, presence): + if not self.xmpp.is_component: + if presence['from'] == self.xmpp.boundjid: + return + self.xmpp.event('entity_caps', presence) + + def _handle_caps_feature(self, features): + # We already have a method to process presence with + # caps, so wrap things up and use that. + p = Presence() + p['from'] = self.xmpp.boundjid.domain + p.append(features['caps']) + self.xmpp.features.add('caps') + + self.xmpp.event('entity_caps', p) + + def _process_caps(self, pres): + if not pres['caps']['hash']: + log.debug("Received unsupported legacy caps.") + self.xmpp.event('entity_caps_legacy', pres) + return + + existing_verstring = self.get_verstring(pres['from'].full) + if str(existing_verstring) == str(pres['caps']['ver']): + return + + if pres['caps']['hash'] not in self.hashes: + try: + log.debug("Unknown caps hash: %s", pres['caps']['hash']) + self.xmpp['xep_003'].get_info(jid=pres['from'].full) + return + except XMPPError: + return + + log.debug("New caps verification string: %s", pres['caps']['ver']) + try: + caps = self.xmpp['xep_0030'].get_info( + jid=pres['from'].full, + node='%s#%s' % (pres['caps']['node'], + pres['caps']['ver'])) + + if self._validate_caps(caps['disco_info'], + pres['caps']['hash'], + pres['caps']['ver']): + self.assign_verstring(pres['from'], pres['caps']['ver']) + except XMPPError: + log.debug("Could not retrieve disco#info results for caps") + + def _validate_caps(self, caps, hash, check_verstring): + # Check Identities + full_ids = caps.get_identities(dedupe=False) + deduped_ids = caps.get_identities() + if len(full_ids) != len(deduped_ids): + log.debug("Duplicate disco identities found, invalid for caps") + return False + + # Check Features + + full_features = caps.get_features(dedupe=False) + deduped_features = caps.get_features() + if len(full_features) != len(deduped_features): + log.debug("Duplicate disco features found, invalid for caps") + return False + + # Check Forms + form_types = [] + deduped_form_types = set() + for stanza in caps['substanzas']: + if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): + if 'FORM_TYPE' in stanza['fields']: + f_type = tuple(stanza['fields']['FORM_TYPE']['value']) + form_types.append(f_type) + deduped_form_types.add(f_type) + if len(form_types) != len(deduped_form_types): + log.debug("Duplicated FORM_TYPE values, invalid for caps") + return False + + if len(f_type) > 1: + deduped_type = set(f_type) + if len(f_type) != len(deduped_type): + log.debug("Extra FORM_TYPE data, invalid for caps") + return False + + if stanza['fields']['FORM_TYPE']['type'] != 'hidden': + log.debug("Field FORM_TYPE type not 'hidden', ignoring form for caps") + caps.xml.remove(stanza.xml) + else: + log.debug("No FORM_TYPE found, ignoring form for caps") + caps.xml.remove(stanza.xml) + + verstring = self.generate_verstring(caps, hash) + if verstring != check_verstring: + log.debug("Verification strings do not match: %s, %s" % ( + verstring, check_verstring)) + return False + + self.cache_caps(verstring, caps) + return True + + def generate_verstring(self, info, hash): + hash = self.hashes.get(hash, None) + if hash is None: + return None + + S = '' + + # Convert None to '' in the identities + def clean_identity(id): + return map(lambda i: i or '', id) + identities = map(clean_identity, info['identities']) + + identities = sorted(('/'.join(i) for i in identities)) + features = sorted(info['features']) + + S += '<'.join(identities) + '<' + S += '<'.join(features) + '<' + + form_types = {} + + for stanza in info['substanzas']: + if isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): + if 'FORM_TYPE' in stanza['fields']: + f_type = stanza['values']['FORM_TYPE'] + if len(f_type): + f_type = f_type[0] + if f_type not in form_types: + form_types[f_type] = [] + form_types[f_type].append(stanza) + + sorted_forms = sorted(form_types.keys()) + for f_type in sorted_forms: + for form in form_types[f_type]: + S += '%s<' % f_type + fields = sorted(form['fields'].keys()) + fields.remove('FORM_TYPE') + for field in fields: + S += '%s<' % field + vals = form['fields'][field].get_value(convert=False) + if vals is None: + S += '<' + else: + if not isinstance(vals, list): + vals = [vals] + S += '<'.join(sorted(vals)) + '<' + + binary = hash(S.encode('utf8')).digest() + return base64.b64encode(binary).decode('utf-8') + + def update_caps(self, jid=None, node=None): + try: + info = self.xmpp['xep_0030'].get_info(jid, node, local=True) + if isinstance(info, Iq): + info = info['disco_info'] + ver = self.generate_verstring(info, self.hash) + self.xmpp['xep_0030'].set_info( + jid=jid, + node='%s#%s' % (self.caps_node, ver), + info=info) + self.cache_caps(ver, info) + self.assign_verstring(jid, ver) + + if self.xmpp.session_started_event.is_set() and self.broadcast: + # Check if we've sent directed presence. If we haven't, we + # can just send a normal presence stanza. If we have, then + # we will send presence to each contact individually so + # that we don't clobber existing statuses. + directed = False + for contact in self.xmpp.roster[jid]: + if self.xmpp.roster[jid][contact].last_status is not None: + directed = True + if not directed: + self.xmpp.roster[jid].send_last_presence() + else: + for contact in self.xmpp.roster[jid]: + self.xmpp.roster[jid][contact].send_last_presence() + except XMPPError: + return + + def get_verstring(self, jid=None): + if jid in ('', None): + jid = self.xmpp.boundjid.full + if isinstance(jid, JID): + jid = jid.full + return self._run_node_handler('get_verstring', jid) + + def assign_verstring(self, jid=None, verstring=None): + if jid in (None, ''): + jid = self.xmpp.boundjid.full + if isinstance(jid, JID): + jid = jid.full + return self._run_node_handler('assign_verstring', jid, + data={'verstring': verstring}) + + def cache_caps(self, verstring=None, info=None): + data = {'verstring': verstring, 'info': info} + return self._run_node_handler('cache_caps', None, None, data=data) + + def get_caps(self, jid=None, verstring=None): + if verstring is None: + if jid is not None: + verstring = self.get_verstring(jid) + else: + return None + if isinstance(jid, JID): + jid = jid.full + data = {'verstring': verstring} + return self._run_node_handler('get_caps', jid, None, None, data) diff --git a/sleekxmpp/plugins/xep_0115/stanza.py b/sleekxmpp/plugins/xep_0115/stanza.py new file mode 100644 index 00000000..3e80b5cf --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/stanza.py @@ -0,0 +1,19 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from __future__ import unicode_literals + +from sleekxmpp.xmlstream import ElementBase + + +class Capabilities(ElementBase): + + namespace = 'http://jabber.org/protocol/caps' + name = 'c' + plugin_attrib = 'caps' + interfaces = set(('hash', 'node', 'ver', 'ext')) diff --git a/sleekxmpp/plugins/xep_0115/static.py b/sleekxmpp/plugins/xep_0115/static.py new file mode 100644 index 00000000..a0a8fb23 --- /dev/null +++ b/sleekxmpp/plugins/xep_0115/static.py @@ -0,0 +1,146 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.xmlstream import JID +from sleekxmpp.exceptions import IqError, IqTimeout + + +log = logging.getLogger(__name__) + + +class StaticCaps(object): + + """ + Extend the default StaticDisco implementation to provide + support for extended identity information. + """ + + def __init__(self, xmpp, static): + """ + Augment the default XEP-0030 static handler object. + + Arguments: + static -- The default static XEP-0030 handler object. + """ + self.xmpp = xmpp + self.disco = self.xmpp['xep_0030'] + self.caps = self.xmpp['xep_0115'] + self.static = static + self.ver_cache = {} + self.jid_vers = {} + + def supports(self, jid, node, ifrom, data): + """ + Check if a JID supports a given feature. + + The data parameter may provide: + feature -- The feature to check for support. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + """ + feature = data.get('feature', None) + + data = {'local': data.get('local', False), + 'cached': data.get('cached', True)} + + if not feature: + return False + + if node in (None, ''): + info = self.caps.get_caps(jid) + if info and feature in info['features']: + return True + + try: + info = self.disco.get_info(jid=jid, node=node, + ifrom=ifrom, **data) + info = self.disco._wrap(ifrom, jid, info, True) + return feature in info['disco_info']['features'] + except IqError: + return False + except IqTimeout: + return None + + def has_identity(self, jid, node, ifrom, data): + """ + Check if a JID has a given identity. + + The data parameter may provide: + category -- The category of the identity to check. + itype -- The type of the identity to check. + lang -- The language of the identity to check. + local -- If true, then the query is for a JID/node + combination handled by this Sleek instance and + no stanzas need to be sent. + Otherwise, a disco stanza must be sent to the + remove JID to retrieve the info. + cached -- If true, then look for the disco info data from + the local cache system. If no results are found, + send the query as usual. The self.use_cache + setting must be set to true for this option to + be useful. If set to false, then the cache will + be skipped, even if a result has already been + cached. Defaults to false. + """ + identity = (data.get('category', None), + data.get('itype', None), + data.get('lang', None)) + + data = {'local': data.get('local', False), + 'cached': data.get('cached', True)} + + trunc = lambda i: (i[0], i[1], i[2]) + + if node in (None, ''): + info = self.caps.get_caps(jid) + if info and identity in map(trunc, info['identities']): + return True + + try: + info = self.disco.get_info(jid=jid, node=node, + ifrom=ifrom, **data) + info = self.disco._wrap(ifrom, jid, info, True) + return identity in map(trunc, info['disco_info']['identities']) + except IqError: + return False + except IqTimeout: + return None + + def cache_caps(self, jid, node, ifrom, data): + with self.static.lock: + verstring = data.get('verstring', None) + info = data.get('info', None) + if not verstring or not info: + return + self.ver_cache[verstring] = info + + def assign_verstring(self, jid, node, ifrom, data): + with self.static.lock: + if isinstance(jid, JID): + jid = jid.full + self.jid_vers[jid] = data.get('verstring', None) + + def get_verstring(self, jid, node, ifrom, data): + with self.static.lock: + return self.jid_vers.get(jid, None) + + def get_caps(self, jid, node, ifrom, data): + with self.static.lock: + return self.ver_cache.get(data.get('verstring', None), None) diff --git a/sleekxmpp/plugins/xep_0118/__init__.py b/sleekxmpp/plugins/xep_0118/__init__.py new file mode 100644 index 00000000..565f7844 --- /dev/null +++ b/sleekxmpp/plugins/xep_0118/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0118 import stanza +from sleekxmpp.plugins.xep_0118.stanza import UserTune +from sleekxmpp.plugins.xep_0118.user_tune import XEP_0118 + + +register_plugin(XEP_0118) diff --git a/sleekxmpp/plugins/xep_0118/stanza.py b/sleekxmpp/plugins/xep_0118/stanza.py new file mode 100644 index 00000000..80e0358a --- /dev/null +++ b/sleekxmpp/plugins/xep_0118/stanza.py @@ -0,0 +1,25 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class UserTune(ElementBase): + + name = 'tune' + namespace = 'http://jabber.org/protocol/tune' + plugin_attrib = 'tune' + interfaces = set(['artist', 'length', 'rating', 'source', + 'title', 'track', 'uri']) + sub_interfaces = interfaces + + def set_length(self, value): + self._set_sub_text('length', str(value)) + + def set_rating(self, value): + self._set_sub_text('rating', str(value)) diff --git a/sleekxmpp/plugins/xep_0118/user_tune.py b/sleekxmpp/plugins/xep_0118/user_tune.py new file mode 100644 index 00000000..c848eaa8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0118/user_tune.py @@ -0,0 +1,92 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0118 import stanza, UserTune + + +log = logging.getLogger(__name__) + + +class XEP_0118(BasePlugin): + + """ + XEP-0118: User Tune + """ + + name = 'xep_0118' + description = 'XEP-0118: User Tune' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + self.xmpp['xep_0163'].register_pep('user_tune', UserTune) + + def publish_tune(self, artist=None, length=None, rating=None, source=None, + title=None, track=None, uri=None, options=None, + ifrom=None, block=True, callback=None, timeout=None): + """ + Publish the user's current tune. + + Arguments: + artist -- The artist or performer of the song. + length -- The length of the song in seconds. + rating -- The user's rating of the song (from 1 to 10) + source -- The album name, website, or other source of the song. + title -- The title of the song. + track -- The song's track number, or other unique identifier. + uri -- A URL to more information about the song. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + tune = UserTune() + tune['artist'] = artist + tune['length'] = length + tune['rating'] = rating + tune['source'] = source + tune['title'] = title + tune['track'] = track + tune['uri'] = uri + return self.xmpp['xep_0163'].publish(tune, + node=UserTune.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user tune information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + tune = UserTune() + return self.xmpp['xep_0163'].publish(tune, + node=UserTune.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0128/__init__.py b/sleekxmpp/plugins/xep_0128/__init__.py index 3c6379a3..27c2cc33 100644 --- a/sleekxmpp/plugins/xep_0128/__init__.py +++ b/sleekxmpp/plugins/xep_0128/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0128.static import StaticExtendedDisco -from sleekxmpp.plugins.xep_0128.extended_disco import xep_0128 +from sleekxmpp.plugins.xep_0128.extended_disco import XEP_0128 + + +register_plugin(XEP_0128) + + +# Retain some backwards compatibility +xep_0128 = XEP_0128 diff --git a/sleekxmpp/plugins/xep_0128/extended_disco.py b/sleekxmpp/plugins/xep_0128/extended_disco.py index 63b3cfee..d49741de 100644 --- a/sleekxmpp/plugins/xep_0128/extended_disco.py +++ b/sleekxmpp/plugins/xep_0128/extended_disco.py @@ -11,13 +11,13 @@ import logging import sleekxmpp from sleekxmpp import Iq from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0004 import Form from sleekxmpp.plugins.xep_0030 import DiscoInfo from sleekxmpp.plugins.xep_0128 import StaticExtendedDisco -class xep_0128(base_plugin): +class XEP_0128(BasePlugin): """ XEP-0128: Service Discovery Extensions @@ -39,11 +39,12 @@ class xep_0128(base_plugin): del_extended_info -- Remove all extensions from a disco#info result. """ + name = 'xep_0128' + description = 'XEP-0128: Service Discovery Extensions' + dependencies = set(['xep_0030', 'xep_0004']) + def plugin_init(self): """Start the XEP-0128 plugin.""" - self.xep = '0128' - self.description = 'Service Discovery Extensions' - self._disco_ops = ['set_extended_info', 'add_extended_info', 'del_extended_info'] @@ -52,7 +53,6 @@ class xep_0128(base_plugin): def post_init(self): """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) self.disco = self.xmpp['xep_0030'] self.static = StaticExtendedDisco(self.disco.static) @@ -76,7 +76,7 @@ class xep_0128(base_plugin): as extended information, replacing any existing extensions. """ - self.disco._run_node_handler('set_extended_info', jid, node, kwargs) + self.disco._run_node_handler('set_extended_info', jid, node, None, kwargs) def add_extended_info(self, jid=None, node=None, **kwargs): """ @@ -88,7 +88,7 @@ class xep_0128(base_plugin): data -- Either a form, or a list of forms to add as extended information. """ - self.disco._run_node_handler('add_extended_info', jid, node, kwargs) + self.disco._run_node_handler('add_extended_info', jid, node, None, kwargs) def del_extended_info(self, jid=None, node=None, **kwargs): """ @@ -98,4 +98,4 @@ class xep_0128(base_plugin): jid -- The JID to modify. node -- The node to modify. """ - self.disco._run_node_handler('del_extended_info', jid, node, kwargs) + self.disco._run_node_handler('del_extended_info', jid, node, None, kwargs) diff --git a/sleekxmpp/plugins/xep_0128/static.py b/sleekxmpp/plugins/xep_0128/static.py index 493d9370..427011c0 100644 --- a/sleekxmpp/plugins/xep_0128/static.py +++ b/sleekxmpp/plugins/xep_0128/static.py @@ -31,42 +31,43 @@ class StaticExtendedDisco(object): """ self.static = static - def set_extended_info(self, jid, node, data): + def set_extended_info(self, jid, node, ifrom, data): """ Replace the extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ - self.del_extended_info(jid, node, data) - self.add_extended_info(jid, node, data) + with self.static.lock: + self.del_extended_info(jid, node, ifrom, data) + self.add_extended_info(jid, node, ifrom, data) - def add_extended_info(self, jid, node, data): + def add_extended_info(self, jid, node, ifrom, data): """ Add additional extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ - self.static.add_node(jid, node) + with self.static.lock: + self.static.add_node(jid, node) - forms = data.get('data', []) - if not isinstance(forms, list): - forms = [forms] + forms = data.get('data', []) + if not isinstance(forms, list): + forms = [forms] - for form in forms: - self.static.nodes[(jid, node)]['info'].append(form) + info = self.static.get_node(jid, node)['info'] + for form in forms: + info.append(form) - def del_extended_info(self, jid, node, data): + def del_extended_info(self, jid, node, ifrom, data): """ Replace the extended identity data for a JID/node combination. The data parameter is not used. """ - if (jid, node) not in self.static.nodes: - return - - info = self.static.nodes[(jid, node)]['info'] - - for form in info['substanza']: - info.xml.remove(form.xml) + with self.static.lock: + if self.static.node_exists(jid, node): + info = self.static.get_node(jid, node)['info'] + for form in info['substanza']: + info.xml.remove(form.xml) diff --git a/sleekxmpp/plugins/xep_0163.py b/sleekxmpp/plugins/xep_0163.py new file mode 100644 index 00000000..5a6df1c8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0163.py @@ -0,0 +1,120 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.base import BasePlugin, register_plugin + + +log = logging.getLogger(__name__) + + +class XEP_0163(BasePlugin): + + """ + XEP-0163: Personal Eventing Protocol (PEP) + """ + + name = 'xep_0163' + description = 'XEP-0163: Personal Eventing Protocol (PEP)' + dependencies = set(['xep_0030', 'xep_0060', 'xep_0115']) + + def register_pep(self, name, stanza): + """ + Setup and configure events and stanza registration for + the given PEP stanza: + + - Add disco feature for the PEP content. + - Register disco interest in the PEP content. + - Map events from the PEP content's namespace to the given name. + + :param str name: The event name prefix to use for PEP events. + :param stanza: The stanza class for the PEP content. + """ + pubsub_stanza = self.xmpp['xep_0060'].stanza + register_stanza_plugin(pubsub_stanza.EventItem, stanza) + + self.add_interest(stanza.namespace) + self.xmpp['xep_0030'].add_feature(stanza.namespace) + self.xmpp['xep_0060'].map_node_event(stanza.namespace, name) + + def add_interest(self, namespace, jid=None): + """ + Mark an interest in a PEP subscription by including a disco + feature with the '+notify' extension. + + Arguments: + namespace -- The base namespace to register as an interest, such + as 'http://jabber.org/protocol/tune'. This may also + be a list of such namespaces. + jid -- Optionally specify the JID. + """ + if not isinstance(namespace, set) and not isinstance(namespace, list): + namespace = [namespace] + + for ns in namespace: + self.xmpp['xep_0030'].add_feature('%s+notify' % ns, + jid=jid) + self.xmpp['xep_0115'].update_caps(jid) + + def remove_interest(self, namespace, jid=None): + """ + Mark an interest in a PEP subscription by including a disco + feature with the '+notify' extension. + + Arguments: + namespace -- The base namespace to remove as an interest, such + as 'http://jabber.org/protocol/tune'. This may also + be a list of such namespaces. + jid -- Optionally specify the JID. + """ + if not isinstance(namespace, set) and not isinstance(namespace, list): + namespace = [namespace] + + for ns in namespace: + self.xmpp['xep_0030'].del_feature(jid=jid, + feature='%s+notify' % namespace) + self.xmpp['xep_0115'].update_caps(jid) + + def publish(self, stanza, node=None, id=None, options=None, ifrom=None, + block=True, callback=None, timeout=None): + """ + Publish a PEP update. + + This is just a (very) thin wrapper around the XEP-0060 publish() + method to set the defaults expected by PEP. + + Arguments: + stanza -- The PEP update stanza to publish. + node -- The node to publish the item to. If not specified, + the stanza's namespace will be used. + id -- Optionally specify the ID of the item. + options -- A form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + if node is None: + node = stanza.namespace + + return self.xmpp['xep_0060'].publish(ifrom, node, + payload=stanza.xml, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + +register_plugin(XEP_0163) diff --git a/sleekxmpp/plugins/xep_0172/__init__.py b/sleekxmpp/plugins/xep_0172/__init__.py new file mode 100644 index 00000000..aa7b9f72 --- /dev/null +++ b/sleekxmpp/plugins/xep_0172/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0172 import stanza +from sleekxmpp.plugins.xep_0172.stanza import UserNick +from sleekxmpp.plugins.xep_0172.user_nick import XEP_0172 + + +register_plugin(XEP_0172) diff --git a/sleekxmpp/plugins/xep_0172/stanza.py b/sleekxmpp/plugins/xep_0172/stanza.py new file mode 100644 index 00000000..110c237b --- /dev/null +++ b/sleekxmpp/plugins/xep_0172/stanza.py @@ -0,0 +1,67 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class UserNick(ElementBase): + + """ + XEP-0172: User Nickname allows the addition of a <nick> element + in several stanza types, including <message> and <presence> stanzas. + + The nickname contained in a <nick> should be the global, friendly or + informal name chosen by the owner of a bare JID. The <nick> element + may be included when establishing communications with new entities, + such as normal XMPP users or MUC services. + + The nickname contained in a <nick> element will not necessarily be + the same as the nickname used in a MUC. + + Example stanzas: + <message to="user@example.com"> + <nick xmlns="http://jabber.org/nick/nick">The User</nick> + <body>...</body> + </message> + + <presence to="otheruser@example.com" type="subscribe"> + <nick xmlns="http://jabber.org/nick/nick">The User</nick> + </presence> + + Stanza Interface: + nick -- A global, friendly or informal name chosen by a user. + + Methods: + setup -- Overrides ElementBase.setup. + get_nick -- Return the nickname in the <nick> element. + set_nick -- Add a <nick> element with the given nickname. + del_nick -- Remove the <nick> element. + """ + + namespace = 'http://jabber.org/protocol/nick' + name = 'nick' + plugin_attrib = name + interfaces = set(('nick',)) + + def set_nick(self, nick): + """ + Add a <nick> element with the given nickname. + + Arguments: + nick -- A human readable, informal name. + """ + self.xml.text = nick + + def get_nick(self): + """Return the nickname in the <nick> element.""" + return self.xml.text + + def del_nick(self): + """Remove the <nick> element.""" + if self.parent is not None: + self.parent().xml.remove(self.xml) diff --git a/sleekxmpp/plugins/xep_0172/user_nick.py b/sleekxmpp/plugins/xep_0172/user_nick.py new file mode 100644 index 00000000..c20c3583 --- /dev/null +++ b/sleekxmpp/plugins/xep_0172/user_nick.py @@ -0,0 +1,86 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza.message import Message +from sleekxmpp.stanza.presence import Presence +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0172 import stanza, UserNick + + +log = logging.getLogger(__name__) + + +class XEP_0172(BasePlugin): + + """ + XEP-0172: User Nickname + """ + + name = 'xep_0172' + description = 'XEP-0172: User Nickname' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, UserNick) + register_stanza_plugin(Presence, UserNick) + self.xmpp['xep_0163'].register_pep('user_nick', UserNick) + + def publish_nick(self, nick=None, options=None, ifrom=None, block=True, + callback=None, timeout=None): + """ + Publish the user's current nick. + + Arguments: + nick -- The user nickname to publish. + options -- Optional form of publish options. + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + nickname = UserNick() + nickname['nick'] = nick + return self.xmpp['xep_0163'].publish(nickname, + node=UserNick.namespace, + options=options, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) + + def stop(self, ifrom=None, block=True, callback=None, timeout=None): + """ + Clear existing user nick information to stop notifications. + + Arguments: + ifrom -- Specify the sender's JID. + block -- Specify if the send call will block until a response + is received, or a timeout occurs. Defaults to True. + timeout -- The length of time (in seconds) to wait for a response + before exiting the send call if blocking is used. + Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT + callback -- Optional reference to a stream handler function. Will + be executed when a reply stanza is received. + """ + nick = UserNick() + return self.xmpp['xep_0163'].publish(nick, + node=UserNick.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0184/__init__.py b/sleekxmpp/plugins/xep_0184/__init__.py new file mode 100644 index 00000000..4b129b6b --- /dev/null +++ b/sleekxmpp/plugins/xep_0184/__init__.py @@ -0,0 +1,19 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0184.stanza import Request, Received +from sleekxmpp.plugins.xep_0184.receipt import XEP_0184 + + +register_plugin(XEP_0184) + + +# Retain some backwards compatibility +xep_0184 = XEP_0184 diff --git a/sleekxmpp/plugins/xep_0184/receipt.py b/sleekxmpp/plugins/xep_0184/receipt.py new file mode 100644 index 00000000..c0086b03 --- /dev/null +++ b/sleekxmpp/plugins/xep_0184/receipt.py @@ -0,0 +1,120 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import Message +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0184 import stanza, Request, Received + + +class XEP_0184(BasePlugin): + + """ + XEP-0184: Message Delivery Receipts + """ + + name = 'xep_0184' + description = 'XEP-0184: Message Delivery Receipts' + dependencies = set(['xep_0030']) + stanza = stanza + + ack_types = ('normal', 'chat', 'headline') + + def plugin_init(self): + self.auto_ack = self.config.get('auto_ack', True) + self.auto_request = self.config.get('auto_request', False) + + register_stanza_plugin(Message, Request) + register_stanza_plugin(Message, Received) + + self.xmpp.add_filter('out', self._filter_add_receipt_request) + + self.xmpp.register_handler( + Callback('Message Receipt', + StanzaPath('message/receipt'), + self._handle_receipt_received)) + + self.xmpp.register_handler( + Callback('Message Receipt Request', + StanzaPath('message/request_receipt'), + self._handle_receipt_request)) + + self.xmpp['xep_0030'].add_feature('urn:xmpp:receipts') + + def ack(self, msg): + """ + Acknowledge a message by sending a receipt. + + Arguments: + msg -- The message to acknowledge. + """ + ack = self.xmpp.Message() + ack['to'] = msg['from'] + ack['from'] = msg['to'] + ack['receipt'] = msg['id'] + ack['id'] = self.xmpp.new_id() + ack.send() + + def _handle_receipt_received(self, msg): + self.xmpp.event('receipt_received', msg) + + def _handle_receipt_request(self, msg): + """ + Auto-ack message receipt requests if ``self.auto_ack`` is ``True``. + + Arguments: + msg -- The incoming message requesting a receipt. + """ + if self.auto_ack: + if msg['type'] in self.ack_types: + if not msg['receipt']: + self.ack(msg) + + def _filter_add_receipt_request(self, stanza): + """ + Auto add receipt requests to outgoing messages, if: + + - ``self.auto_request`` is set to ``True`` + - The message is not for groupchat + - The message does not contain a receipt acknowledgment + - The recipient is a bare JID or, if a full JID, one + that has the ``urn:xmpp:receipts`` feature enabled + + The disco cache is checked if a full JID is specified in + the outgoing message, which may mean a round-trip disco#info + delay for the first message sent to the JID if entity caps + are not used. + """ + + if not self.auto_request: + return stanza + + if not isinstance(stanza, Message): + return stanza + + if stanza['request_receipt']: + return stanza + + if not stanza['type'] in self.ack_types: + return stanza + + if stanza['receipt']: + return stanza + + if stanza['to'].resource: + if not self.xmpp['xep_0030'].supports(stanza['to'], + feature='urn:xmpp:receipts', + cached=True): + return stanza + + stanza['request_receipt'] = True + return stanza diff --git a/sleekxmpp/plugins/xep_0184/stanza.py b/sleekxmpp/plugins/xep_0184/stanza.py new file mode 100644 index 00000000..a7607035 --- /dev/null +++ b/sleekxmpp/plugins/xep_0184/stanza.py @@ -0,0 +1,72 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Erik Reuterborg Larsson, Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.stanzabase import ElementBase, ET + + +class Request(ElementBase): + namespace = 'urn:xmpp:receipts' + name = 'request' + plugin_attrib = 'request_receipt' + interfaces = set(('request_receipt',)) + sub_interfaces = interfaces + is_extension = True + + def setup(self, xml=None): + self.xml = ET.Element('') + return True + + def set_request_receipt(self, val): + self.del_request_receipt() + if val: + parent = self.parent() + parent._set_sub_text("{%s}request" % self.namespace, keep=True) + if not parent['id']: + if parent.stream: + parent['id'] = parent.stream.new_id() + + def get_request_receipt(self): + parent = self.parent() + if parent.find("{%s}request" % self.namespace) is not None: + return True + else: + return False + + def del_request_receipt(self): + self.parent()._del_sub("{%s}request" % self.namespace) + + +class Received(ElementBase): + namespace = 'urn:xmpp:receipts' + name = 'received' + plugin_attrib = 'receipt' + interfaces = set(['receipt']) + sub_interfaces = interfaces + is_extension = True + + def setup(self, xml=None): + self.xml = ET.Element('') + return True + + def set_receipt(self, value): + self.del_receipt() + if value: + parent = self.parent() + xml = ET.Element("{%s}received" % self.namespace) + xml.attrib['id'] = value + parent.append(xml) + + def get_receipt(self): + parent = self.parent() + xml = parent.find("{%s}received" % self.namespace) + if xml is not None: + return xml.attrib.get('id', '') + return '' + + def del_receipt(self): + self.parent()._del_sub('{%s}received' % self.namespace) diff --git a/sleekxmpp/plugins/xep_0198/__init__.py b/sleekxmpp/plugins/xep_0198/__init__.py new file mode 100644 index 00000000..db930347 --- /dev/null +++ b/sleekxmpp/plugins/xep_0198/__init__.py @@ -0,0 +1,20 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0198.stanza import Enable, Enabled +from sleekxmpp.plugins.xep_0198.stanza import Resume, Resumed +from sleekxmpp.plugins.xep_0198.stanza import Failed +from sleekxmpp.plugins.xep_0198.stanza import StreamManagement +from sleekxmpp.plugins.xep_0198.stanza import Ack, RequestAck + +from sleekxmpp.plugins.xep_0198.stream_management import XEP_0198 + + +register_plugin(XEP_0198) diff --git a/sleekxmpp/plugins/xep_0198/stanza.py b/sleekxmpp/plugins/xep_0198/stanza.py new file mode 100644 index 00000000..5cf93436 --- /dev/null +++ b/sleekxmpp/plugins/xep_0198/stanza.py @@ -0,0 +1,151 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import Error +from sleekxmpp.xmlstream import ElementBase, StanzaBase + + +class Enable(StanzaBase): + name = 'enable' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['max', 'resume']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_resume(self): + return self._get_attr('resume', 'false').lower() in ('true', '1') + + def set_resume(self, val): + self._del_attr('resume') + self._set_attr('resume', 'true' if val else 'false') + + +class Enabled(StanzaBase): + name = 'enabled' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['id', 'location', 'max', 'resume']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_resume(self): + return self._get_attr('resume', 'false').lower() in ('true', '1') + + def set_resume(self, val): + self._del_attr('resume') + self._set_attr('resume', 'true' if val else 'false') + + +class Resume(StanzaBase): + name = 'resume' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['h', 'previd']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_h(self): + h = self._get_attr('h', None) + if h: + return int(h) + return None + + def set_h(self, val): + self._set_attr('h', str(val)) + + +class Resumed(StanzaBase): + name = 'resumed' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['h', 'previd']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_h(self): + h = self._get_attr('h', None) + if h: + return int(h) + return None + + def set_h(self, val): + self._set_attr('h', str(val)) + + + +class Failed(StanzaBase, Error): + name = 'failed' + namespace = 'urn:xmpp:sm:3' + interfaces = set() + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + +class StreamManagement(ElementBase): + name = 'sm' + namespace = 'urn:xmpp:sm:3' + plugin_attrib = name + interfaces = set(['required', 'optional']) + + def get_required(self): + return self.find('{%s}required' % self.namespace) is not None + + def set_required(self, val): + self.del_required() + if val: + self._set_sub_text('required', '', keep=True) + + def del_required(self): + self._del_sub('required') + + def get_optional(self): + return self.find('{%s}optional' % self.namespace) is not None + + def set_optional(self, val): + self.del_optional() + if val: + self._set_sub_text('optional', '', keep=True) + + def del_optional(self): + self._del_sub('optional') + + +class RequestAck(StanzaBase): + name = 'r' + namespace = 'urn:xmpp:sm:3' + interfaces = set() + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + +class Ack(StanzaBase): + name = 'a' + namespace = 'urn:xmpp:sm:3' + interfaces = set(['h']) + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + def get_h(self): + h = self._get_attr('h', None) + if h: + return int(h) + return None + + def set_h(self, val): + self._set_attr('h', str(val)) diff --git a/sleekxmpp/plugins/xep_0198/stream_management.py b/sleekxmpp/plugins/xep_0198/stream_management.py new file mode 100644 index 00000000..6ed1ea26 --- /dev/null +++ b/sleekxmpp/plugins/xep_0198/stream_management.py @@ -0,0 +1,266 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import threading +import collections + +from sleekxmpp.stanza import Message, Presence, Iq, StreamFeatures +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback, Waiter +from sleekxmpp.xmlstream.matcher import MatchXPath, MatchMany +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0198 import stanza + + +log = logging.getLogger(__name__) + + +MAX_SEQ = 2**32 + + +class XEP_0198(BasePlugin): + + """ + XEP-0198: Stream Management + """ + + name = 'xep_0198' + description = 'XEP-0198: Stream Management' + dependencies = set() + stanza = stanza + + def plugin_init(self): + """Start the XEP-0198 plugin.""" + + # Only enable stream management for non-components, + # since components do not yet perform feature negotiation. + if self.xmpp.is_component: + return + + #: The stream management ID for the stream. Knowing this value is + #: required in order to do stream resumption. + self.sm_id = self.config.get('sm_id', None) + + #: A counter of handled incoming stanzas, mod 2^32. + self.handled = self.config.get('handled', 0) + + #: A counter of unacked outgoing stanzas, mod 2^32. + self.seq = self.config.get('seq', 0) + + #: The last ack number received from the server. + self.last_ack = self.config.get('last_ack', 0) + + #: The number of stanzas to wait between sending ack requests to + #: the server. Setting this to ``1`` will send an ack request after + #: every sent stanza. Defaults to ``5``. + self.window = self.config.get('window', 5) + + #: Control whether or not the ability to resume the stream will be + #: requested when enabling stream management. Defaults to ``True``. + self.allow_resume = self.config.get('allow_resume', True) + + self.enabled = threading.Event() + self.unacked_queue = collections.deque() + + self.seq_lock = threading.Lock() + self.handled_lock = threading.Lock() + self.ack_lock = threading.Lock() + + register_stanza_plugin(StreamFeatures, stanza.StreamManagement) + self.xmpp.register_stanza(stanza.Enable) + self.xmpp.register_stanza(stanza.Enabled) + self.xmpp.register_stanza(stanza.Resume) + self.xmpp.register_stanza(stanza.Resumed) + self.xmpp.register_stanza(stanza.Ack) + self.xmpp.register_stanza(stanza.RequestAck) + + # Register the feature twice because it may be ordered two + # different ways: enabling after binding and resumption + # before binding. + self.xmpp.register_feature('sm', + self._handle_sm_feature, + restart=True, + order=self.config.get('order', 10100)) + self.xmpp.register_feature('sm', + self._handle_sm_feature, + restart=True, + order=self.config.get('resume_order', 9000)) + + self.xmpp.register_handler( + Callback('Stream Management Enabled', + MatchXPath(stanza.Enabled.tag_name()), + self._handle_enabled, + instream=True)) + + self.xmpp.register_handler( + Callback('Stream Management Resumed', + MatchXPath(stanza.Resumed.tag_name()), + self._handle_resumed, + instream=True)) + + self.xmpp.register_handler( + Callback('Stream Management Failed', + MatchXPath(stanza.Failed.tag_name()), + self._handle_failed, + instream=True)) + + self.xmpp.register_handler( + Callback('Stream Management Ack', + MatchXPath(stanza.Ack.tag_name()), + self._handle_ack, + instream=True)) + + self.xmpp.register_handler( + Callback('Stream Management Request Ack', + MatchXPath(stanza.RequestAck.tag_name()), + self._handle_request_ack, + instream=True)) + + self.xmpp.add_filter('in', self._handle_incoming) + self.xmpp.add_filter('out_sync', self._handle_outgoing) + + self.xmpp.add_event_handler('need_ack', self.request_ack) + + def send_ack(self): + """Send the current ack count to the server.""" + ack = stanza.Ack(self.xmpp) + with self.handled_lock: + ack['h'] = self.handled + ack.send() + + def request_ack(self, e=None): + """Request an ack from the server.""" + req = stanza.RequestAck(self.xmpp) + req.send() + + def _handle_sm_feature(self, features): + """ + Enable or resume stream management. + + If no SM-ID is stored, and resource binding has taken place, + stream management will be enabled. + + If an SM-ID is known, and the server allows resumption, the + previous stream will be resumed. + """ + if 'stream_management' in self.xmpp.features: + # We've already negotiated stream management, + # so no need to do it again. + return False + if not self.sm_id: + if 'bind' in self.xmpp.features: + self.enabled.set() + enable = stanza.Enable(self.xmpp) + enable['resume'] = self.allow_resume + enable.send() + self.handled = 0 + elif self.sm_id and self.allow_resume: + self.enabled.set() + resume = stanza.Resume(self.xmpp) + resume['h'] = self.handled + resume['previd'] = self.sm_id + resume.send(now=True) + + # Wait for a response before allowing stream feature processing + # to continue. The actual result processing will be done in the + # _handle_resumed() or _handle_failed() methods. + waiter = Waiter('resumed_or_failed', + MatchMany([ + MatchXPath(stanza.Resumed.tag_name()), + MatchXPath(stanza.Failed.tag_name())])) + self.xmpp.register_handler(waiter) + result = waiter.wait() + if result is not None and result.name == 'resumed': + return True + return False + + def _handle_enabled(self, stanza): + """Save the SM-ID, if provided. + + Raises an :term:`sm_enabled` event. + """ + self.xmpp.features.add('stream_management') + if stanza['id']: + self.sm_id = stanza['id'] + self.xmpp.event('sm_enabled', stanza) + + def _handle_resumed(self, stanza): + """Finish resuming a stream by resending unacked stanzas. + + Raises a :term:`session_resumed` event. + """ + self.xmpp.features.add('stream_management') + self._handle_ack(stanza) + for id, stanza in self.unacked_queue: + self.xmpp.send(stanza, now=True, use_filters=False) + self.xmpp.session_started_event.set() + self.xmpp.event('session_resumed', stanza) + + def _handle_failed(self, stanza): + """ + Disable and reset any features used since stream management was + requested (tracked stanzas may have been sent during the interval + between the enable request and the enabled response). + + Raises an :term:`sm_failed` event. + """ + self.enabled.clear() + self.unacked_queue.clear() + self.xmpp.event('sm_failed', stanza) + + def _handle_ack(self, ack): + """Process a server ack by freeing acked stanzas from the queue. + + Raises a :term:`stanza_acked` event for each acked stanza. + """ + if ack['h'] == self.last_ack: + return + + with self.ack_lock: + num_acked = (ack['h'] - self.last_ack) % MAX_SEQ + log.debug("Ack: %s, Last Ack: %s, Num Acked: %s, Unacked: %s", + ack['h'], + self.last_ack, + num_acked, + len(self.unacked_queue)) + for x in range(num_acked): + seq, stanza = self.unacked_queue.popleft() + self.xmpp.event('stanza_acked', stanza) + self.last_ack = ack['h'] + + def _handle_request_ack(self, req): + """Handle an ack request by sending an ack.""" + self.send_ack() + + def _handle_incoming(self, stanza): + """Increment the handled counter for each inbound stanza.""" + if not self.enabled.is_set(): + return stanza + + if isinstance(stanza, (Message, Presence, Iq)): + with self.handled_lock: + # Sequence numbers are mod 2^32 + self.handled = (self.handled + 1) % MAX_SEQ + return stanza + + def _handle_outgoing(self, stanza): + """Store outgoing stanzas in a queue to be acked.""" + if not self.enabled.is_set(): + return stanza + + if isinstance(stanza, (Message, Presence, Iq)): + seq = None + with self.seq_lock: + # Sequence numbers are mod 2^32 + self.seq = (self.seq + 1) % MAX_SEQ + seq = self.seq + self.unacked_queue.append((seq, stanza)) + if len(self.unacked_queue) > self.window: + self.xmpp.event('need_ack') + return stanza diff --git a/sleekxmpp/plugins/xep_0199/__init__.py b/sleekxmpp/plugins/xep_0199/__init__.py index 3444fe94..5231a5b5 100644 --- a/sleekxmpp/plugins/xep_0199/__init__.py +++ b/sleekxmpp/plugins/xep_0199/__init__.py @@ -6,5 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0199.stanza import Ping -from sleekxmpp.plugins.xep_0199.ping import xep_0199 +from sleekxmpp.plugins.xep_0199.ping import XEP_0199 + + +register_plugin(XEP_0199) + + +# Backwards compatibility for names +xep_0199 = XEP_0199 +xep_0199.sendPing = xep_0199.send_ping diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py index a0f60532..851e5ae5 100644 --- a/sleekxmpp/plugins/xep_0199/ping.py +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -15,14 +15,14 @@ from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream.handler import Callback -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0199 import stanza, Ping log = logging.getLogger(__name__) -class xep_0199(base_plugin): +class XEP_0199(BasePlugin): """ XEP-0199: XMPP Ping @@ -47,14 +47,15 @@ class xep_0199(base_plugin): round trip time. """ + name = 'xep_0199' + description = 'XEP-0199: XMPP Ping' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """ Start the XEP-0199 plugin. """ - self.description = 'XMPP Ping' - self.xep = '0199' - self.stanza = stanza - self.keepalive = self.config.get('keepalive', False) self.frequency = float(self.config.get('frequency', 300)) self.timeout = self.config.get('timeout', 30) @@ -73,9 +74,6 @@ class xep_0199(base_plugin): self.xmpp.add_event_handler('session_end', self._handle_session_end) - def post_init(self): - """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Ping.namespace) def _handle_keepalive(self, event): @@ -169,7 +167,3 @@ class xep_0199(base_plugin): log.debug("Pong: %s %f", jid, delay) return delay - - -# Backwards compatibility for names -xep_0199.sendPing = xep_0199.send_ping diff --git a/sleekxmpp/plugins/xep_0202/__init__.py b/sleekxmpp/plugins/xep_0202/__init__.py index a34b2376..cdab3665 100644 --- a/sleekxmpp/plugins/xep_0202/__init__.py +++ b/sleekxmpp/plugins/xep_0202/__init__.py @@ -6,7 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin from sleekxmpp.plugins.xep_0202 import stanza from sleekxmpp.plugins.xep_0202.stanza import EntityTime -from sleekxmpp.plugins.xep_0202.time import xep_0202 +from sleekxmpp.plugins.xep_0202.time import XEP_0202 + + +register_plugin(XEP_0202) + + +# Retain some backwards compatibility +xep_0202 = XEP_0202 diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py index 2c6faa4b..ca388c5b 100644 --- a/sleekxmpp/plugins/xep_0202/time.py +++ b/sleekxmpp/plugins/xep_0202/time.py @@ -12,7 +12,7 @@ from sleekxmpp.stanza.iq import Iq from sleekxmpp.xmlstream import register_stanza_plugin
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.xmlstream.matcher import StanzaPath
-from sleekxmpp.plugins.base import base_plugin
+from sleekxmpp.plugins import BasePlugin
from sleekxmpp.plugins import xep_0082
from sleekxmpp.plugins.xep_0202 import stanza
@@ -20,18 +20,19 @@ from sleekxmpp.plugins.xep_0202 import stanza log = logging.getLogger(__name__)
-class xep_0202(base_plugin):
+class XEP_0202(BasePlugin):
"""
XEP-0202: Entity Time
"""
+ name = 'xep_0202'
+ description = 'XEP-0202: Entity Time'
+ dependencies = set(['xep_0030', 'xep_0082'])
+ stanza = stanza
+
def plugin_init(self):
"""Start the XEP-0203 plugin."""
- self.xep = '0202'
- self.description = 'Entity Time'
- self.stanza = stanza
-
self.tz_offset = self.config.get('tz_offset', 0)
# As a default, respond to time requests with the
@@ -48,12 +49,8 @@ class xep_0202(base_plugin): self._handle_time_request))
register_stanza_plugin(Iq, stanza.EntityTime)
- def post_init(self):
- """Handle cross-plugin interactions."""
- base_plugin.post_init(self)
self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
-
def _handle_time_request(self, iq):
"""
Respond to a request for the local time.
diff --git a/sleekxmpp/plugins/xep_0203/__init__.py b/sleekxmpp/plugins/xep_0203/__init__.py index 445ccf37..d4d99a6c 100644 --- a/sleekxmpp/plugins/xep_0203/__init__.py +++ b/sleekxmpp/plugins/xep_0203/__init__.py @@ -6,7 +6,16 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0203 import stanza from sleekxmpp.plugins.xep_0203.stanza import Delay -from sleekxmpp.plugins.xep_0203.delay import xep_0203 +from sleekxmpp.plugins.xep_0203.delay import XEP_0203 + + + +register_plugin(XEP_0203) + +# Retain some backwards compatibility +xep_0203 = XEP_0203 diff --git a/sleekxmpp/plugins/xep_0203/delay.py b/sleekxmpp/plugins/xep_0203/delay.py index 8ff14d18..31f31ce3 100644 --- a/sleekxmpp/plugins/xep_0203/delay.py +++ b/sleekxmpp/plugins/xep_0203/delay.py @@ -9,11 +9,11 @@ from sleekxmpp.stanza import Message, Presence from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0203 import stanza -class xep_0203(base_plugin): +class XEP_0203(BasePlugin): """ XEP-0203: Delayed Delivery @@ -26,11 +26,12 @@ class xep_0203(base_plugin): Also see <http://www.xmpp.org/extensions/xep-0203.html>. """ + name = 'xep_0203' + description = 'XEP-0203: Delayed Delivery' + dependencies = set() + stanza = stanza + def plugin_init(self): """Start the XEP-0203 plugin.""" - self.xep = '0203' - self.description = 'Delayed Delivery' - self.stanza = stanza - register_stanza_plugin(Message, stanza.Delay) register_stanza_plugin(Presence, stanza.Delay) diff --git a/sleekxmpp/plugins/xep_0224/__init__.py b/sleekxmpp/plugins/xep_0224/__init__.py index 62f5bf82..1a9d2342 100644 --- a/sleekxmpp/plugins/xep_0224/__init__.py +++ b/sleekxmpp/plugins/xep_0224/__init__.py @@ -6,6 +6,15 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0224 import stanza from sleekxmpp.plugins.xep_0224.stanza import Attention -from sleekxmpp.plugins.xep_0224.attention import xep_0224 +from sleekxmpp.plugins.xep_0224.attention import XEP_0224 + + +register_plugin(XEP_0224) + + +# Retain some backwards compatibility +xep_0224 = XEP_0224 diff --git a/sleekxmpp/plugins/xep_0224/attention.py b/sleekxmpp/plugins/xep_0224/attention.py index 4a3ff368..6eea5d9d 100644 --- a/sleekxmpp/plugins/xep_0224/attention.py +++ b/sleekxmpp/plugins/xep_0224/attention.py @@ -12,25 +12,26 @@ from sleekxmpp.stanza import Message from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.plugins.xep_0224 import stanza log = logging.getLogger(__name__) -class xep_0224(base_plugin): +class XEP_0224(BasePlugin): """ XEP-0224: Attention """ + name = 'xep_0224' + description = 'XEP-0224: Attention' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): """Start the XEP-0224 plugin.""" - self.xep = '0224' - self.description = 'Attention' - self.stanza = stanza - register_stanza_plugin(Message, stanza.Attention) self.xmpp.register_handler( @@ -38,9 +39,6 @@ class xep_0224(base_plugin): StanzaPath('message/attention'), self._handle_attention)) - def post_init(self): - """Handle cross-plugin dependencies.""" - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace) def request_attention(self, to, mfrom=None, mbody=''): diff --git a/sleekxmpp/plugins/xep_0249/__init__.py b/sleekxmpp/plugins/xep_0249/__init__.py index e88d87ac..b85f55ce 100644 --- a/sleekxmpp/plugins/xep_0249/__init__.py +++ b/sleekxmpp/plugins/xep_0249/__init__.py @@ -6,5 +6,14 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.plugins.base import register_plugin + from sleekxmpp.plugins.xep_0249.stanza import Invite -from sleekxmpp.plugins.xep_0249.invite import xep_0249 +from sleekxmpp.plugins.xep_0249.invite import XEP_0249 + + +register_plugin(XEP_0249) + + +# Retain some backwards compatibility +xep_0249 = XEP_0249 diff --git a/sleekxmpp/plugins/xep_0249/invite.py b/sleekxmpp/plugins/xep_0249/invite.py index 95fcb37c..737684f5 100644 --- a/sleekxmpp/plugins/xep_0249/invite.py +++ b/sleekxmpp/plugins/xep_0249/invite.py @@ -10,27 +10,28 @@ import logging import sleekxmpp from sleekxmpp import Message -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins import BasePlugin from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.plugins.xep_0249 import Invite +from sleekxmpp.plugins.xep_0249 import Invite, stanza log = logging.getLogger(__name__) -class xep_0249(base_plugin): +class XEP_0249(BasePlugin): """ XEP-0249: Direct MUC Invitations """ - def plugin_init(self): - self.xep = "0249" - self.description = "Direct MUC Invitations" - self.stanza = sleekxmpp.plugins.xep_0249.stanza + name = 'xep_0249' + description = 'XEP-0249: Direct MUC Invitations' + dependencies = set(['xep_0030']) + stanza = stanza + def plugin_init(self): self.xmpp.register_handler( Callback('Direct MUC Invitations', StanzaPath('message/groupchat_invite'), @@ -38,8 +39,6 @@ class xep_0249(base_plugin): register_stanza_plugin(Message, Invite) - def post_init(self): - base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Invite.namespace) def _handle_invite(self, msg): |