diff options
Diffstat (limited to 'sleekxmpp')
138 files changed, 6639 insertions, 2310 deletions
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index f0dc2ce2..85ee32b6 100644 --- a/sleekxmpp/__init__.py +++ b/sleekxmpp/__init__.py @@ -6,14 +6,25 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.basexmpp import BaseXMPP -from sleekxmpp.clientxmpp import ClientXMPP -from sleekxmpp.componentxmpp import ComponentXMPP +import logging +if hasattr(logging, 'NullHandler'): + NullHandler = logging.NullHandler +else: + class NullHandler(logging.Handler): + def handle(self, record): + pass +logging.getLogger(__name__).addHandler(NullHandler()) +del NullHandler + + from sleekxmpp.stanza import Message, Presence, Iq from sleekxmpp.jid import JID, InvalidJID +from sleekxmpp.xmlstream.stanzabase import ET, ElementBase, register_stanza_plugin from sleekxmpp.xmlstream.handler import * from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream.matcher import * -from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET +from sleekxmpp.basexmpp import BaseXMPP +from sleekxmpp.clientxmpp import ClientXMPP +from sleekxmpp.componentxmpp import ComponentXMPP from sleekxmpp.version import __version__, __version_info__ diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index c3ff5ba3..8cd61b63 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -18,15 +18,13 @@ import sys import logging import threading -import sleekxmpp -from sleekxmpp import plugins, features, roster +from sleekxmpp import plugins, roster, stanza from sleekxmpp.api import APIRegistry from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.stanza import Message, Presence, Iq, StreamError from sleekxmpp.stanza.roster import Roster from sleekxmpp.stanza.nick import Nick -from sleekxmpp.stanza.htmlim import HTMLIM from sleekxmpp.xmlstream import XMLStream, JID from sleekxmpp.xmlstream import ET, register_stanza_plugin @@ -34,8 +32,7 @@ from sleekxmpp.xmlstream.matcher import MatchXPath from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.stanzabase import XML_NS -from sleekxmpp.features import * -from sleekxmpp.plugins import PluginManager, register_plugin, load_plugin +from sleekxmpp.plugins import PluginManager, load_plugin log = logging.getLogger(__name__) @@ -148,7 +145,7 @@ class BaseXMPP(XMLStream): #: A reference to :mod:`sleekxmpp.stanza` to make accessing #: stanza classes easier. - self.stanza = sleekxmpp.stanza + self.stanza = stanza self.register_handler( Callback('IM', @@ -201,7 +198,6 @@ class BaseXMPP(XMLStream): # Initialize a few default stanza plugins. register_stanza_plugin(Iq, Roster) register_stanza_plugin(Message, Nick) - register_stanza_plugin(Message, HTMLIM) def start_stream_handler(self, xml): """Save the stream ID once the streams have been established. @@ -248,7 +244,7 @@ class BaseXMPP(XMLStream): self.plugin[name].post_inited = True return XMLStream.process(self, *args, **kwargs) - def register_plugin(self, plugin, pconfig={}, module=None): + def register_plugin(self, plugin, pconfig=None, module=None): """Register and configure a plugin for use in this stream. :param plugin: The name of the plugin class. Plugin names must diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 3fa158af..8db6ef17 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -52,7 +52,6 @@ class ClientXMPP(BaseXMPP): :param jid: The JID of the XMPP user account. :param password: The password for the XMPP user account. - :param ssl: **Deprecated.** :param plugin_config: A dictionary of plugin configurations. :param plugin_whitelist: A list of approved plugins that will be loaded when calling @@ -60,8 +59,13 @@ class ClientXMPP(BaseXMPP): :param escape_quotes: **Deprecated.** """ - def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[], - escape_quotes=True, sasl_mech=None, lang='en'): + def __init__(self, jid, password, plugin_config=None, plugin_whitelist=None, escape_quotes=True, sasl_mech=None, + lang='en'): + if not plugin_whitelist: + plugin_whitelist = [] + if not plugin_config: + plugin_config = {} + BaseXMPP.__init__(self, jid, 'jabber:client') self.escape_quotes = escape_quotes @@ -96,6 +100,7 @@ class ClientXMPP(BaseXMPP): self.add_event_handler('connected', self._reset_connection_state) self.add_event_handler('session_bind', self._handle_session_bind) + self.add_event_handler('roster_update', self._handle_roster) self.register_stanza(StreamFeatures) @@ -106,7 +111,7 @@ class ClientXMPP(BaseXMPP): self.register_handler( Callback('Roster Update', StanzaPath('iq@type=set/roster'), - self._handle_roster)) + lambda iq: self.event('roster_update', iq))) # Setup default stream features self.register_plugin('feature_starttls') @@ -114,8 +119,10 @@ class ClientXMPP(BaseXMPP): self.register_plugin('feature_session') self.register_plugin('feature_rosterver') self.register_plugin('feature_preapproval') - self.register_plugin('feature_mechanisms', - pconfig={'use_mech': sasl_mech} if sasl_mech else None) + self.register_plugin('feature_mechanisms') + + if sasl_mech: + self['feature_mechanisms'].use_mech = sasl_mech @property def password(self): @@ -133,7 +140,7 @@ class ClientXMPP(BaseXMPP): be attempted. If that fails, the server user in the JID will be used. - :param address -- A tuple containing the server's host and port. + :param address: A tuple containing the server's host and port. :param reattempt: If ``True``, repeat attempting to connect if an error occurs. Defaults to ``True``. :param use_tls: Indicates if TLS should be used for the @@ -152,8 +159,6 @@ class ClientXMPP(BaseXMPP): address = (self.boundjid.host, 5222) self.dns_service = 'xmpp-client' - self._expected_server_name = self.boundjid.host - return XMLStream.connect(self, address[0], address[1], use_tls=use_tls, use_ssl=use_ssl, reattempt=reattempt) @@ -242,14 +247,22 @@ class ClientXMPP(BaseXMPP): if 'rosterver' in self.features: iq['roster']['ver'] = self.client_roster.version - if not block and callback is None: - callback = lambda resp: self._handle_roster(resp) + + if not block or callback is not None: + block = False + if callback is None: + callback = lambda resp: self.event('roster_update', resp) + else: + orig_cb = callback + def wrapped(resp): + self.event('roster_update', resp) + orig_cb(resp) + callback = wrapped response = iq.send(block, timeout, callback) - self.event('roster_received', response) if block: - self._handle_roster(response) + self.event('roster_update', response) return response def _reset_connection_state(self, event=None): @@ -300,7 +313,6 @@ class ClientXMPP(BaseXMPP): roster[jid].save(remove=(item['subscription'] == 'remove')) - self.event("roster_update", iq) if iq['type'] == 'set': resp = self.Iq(stype='result', sto=iq['from'], diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index 44f82e96..4b229a6f 100644 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -49,8 +49,13 @@ class ComponentXMPP(BaseXMPP): Defaults to ``False``. """ - def __init__(self, jid, secret, host=None, port=None, - plugin_config={}, plugin_whitelist=[], use_jc_ns=False): + def __init__(self, jid, secret, host=None, port=None, plugin_config=None, plugin_whitelist=None, use_jc_ns=False): + + if not plugin_whitelist: + plugin_whitelist = [] + if not plugin_config: + plugin_config = {} + if use_jc_ns: default_ns = 'jabber:client' else: @@ -123,12 +128,6 @@ class ComponentXMPP(BaseXMPP): """ if xml.tag.startswith('{jabber:client}'): xml.tag = xml.tag.replace('jabber:client', self.default_ns) - - # The incoming_filter call is only made on top level stanza - # elements. So we manually continue filtering on sub-elements. - for sub in xml: - self.incoming_filter(sub) - return xml def start_stream_handler(self, xml): @@ -158,8 +157,8 @@ class ComponentXMPP(BaseXMPP): """ self.session_bind_event.set() self.session_started_event.set() - self.event("session_bind", self.boundjid, direct=True) - self.event("session_start") + self.event('session_bind', self.boundjid, direct=True) + self.event('session_start') def _handle_probe(self, pres): self.roster[pres['to']][pres['from']].handle_probe(pres) diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py index 8036532d..8a2aa75c 100644 --- a/sleekxmpp/exceptions.py +++ b/sleekxmpp/exceptions.py @@ -42,7 +42,7 @@ class XMPPError(Exception): Defaults to ``True``. """ - def __init__(self, condition='undefined-condition', text=None, + def __init__(self, condition='undefined-condition', text='', etype='cancel', extension=None, extension_ns=None, extension_args=None, clear=True): if extension_args is None: diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py index 0f97952d..ee4c1e9b 100644 --- a/sleekxmpp/features/feature_bind/bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -12,7 +12,7 @@ from sleekxmpp.jid import JID from sleekxmpp.stanza import Iq, StreamFeatures from sleekxmpp.features.feature_bind import stanza from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.plugins import BasePlugin, register_plugin +from sleekxmpp.plugins import BasePlugin log = logging.getLogger(__name__) @@ -41,12 +41,12 @@ class FeatureBind(BasePlugin): Arguments: features -- The stream features stanza. """ - log.debug("Requesting resource: %s", self.xmpp.boundjid.resource) + log.debug("Requesting resource: %s", self.xmpp.requested_jid.resource) iq = self.xmpp.Iq() iq['type'] = 'set' iq.enable('bind') - if self.xmpp.boundjid.resource: - iq['bind']['resource'] = self.xmpp.boundjid.resource + if self.xmpp.requested_jid.resource: + iq['bind']['resource'] = self.xmpp.requested_jid.resource response = iq.send(now=True) self.xmpp.boundjid = JID(response['bind']['jid'], cache_lock=True) @@ -56,10 +56,10 @@ class FeatureBind(BasePlugin): self.xmpp.features.add('bind') - log.info("Node set to: %s", self.xmpp.boundjid.full) + log.info("JID set to: %s", self.xmpp.boundjid.full) if 'session' not in features['features']: log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.session_started_event.set() - self.xmpp.event("session_start") + self.xmpp.event('session_start') diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index b480d5be..1d8f8798 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -6,7 +6,6 @@ See the file LICENSE for copying permission. """ -import sys import ssl import logging @@ -44,15 +43,16 @@ class FeatureMechanisms(BasePlugin): } def plugin_init(self): - if not self.use_mech and not self.xmpp.requested_jid.user: - self.use_mech = 'ANONYMOUS' - if self.sasl_callback is None: self.sasl_callback = self._default_credentials if self.security_callback is None: self.security_callback = self._default_security + creds = self.sasl_callback(set(['username']), set()) + if not self.use_mech and not creds['username']: + self.use_mech = 'ANONYMOUS' + self.mech = None self.mech_list = set() self.attempted_mechs = set() @@ -92,27 +92,26 @@ class FeatureMechanisms(BasePlugin): values = required_values.union(optional_values) for value in values: if value == 'username': - result[value] = self.xmpp.requested_jid.user - elif value == 'password': - result[value] = creds['password'] - elif value == 'authzid': - result[value] = creds.get('authzid', '') + result[value] = creds.get('username', self.xmpp.requested_jid.user) elif value == 'email': jid = self.xmpp.requested_jid.bare result[value] = creds.get('email', jid) elif value == 'channel_binding': - if sys.version_info >= (3, 3): + if hasattr(self.xmpp.socket, 'get_channel_binding'): result[value] = self.xmpp.socket.get_channel_binding() else: + log.debug("Channel binding not supported.") + log.debug("Use Python 3.3+ for channel binding and " + \ + "SCRAM-SHA-1-PLUS support") result[value] = None elif value == 'host': - result[value] = self.xmpp.requested_jid.domain + result[value] = creds.get('host', self.xmpp.requested_jid.domain) elif value == 'realm': - result[value] = self.xmpp.requested_jid.domain + result[value] = creds.get('realm', self.xmpp.requested_jid.domain) elif value == 'service-name': - result[value] = self.xmpp._service_name + result[value] = creds.get('service-name', self.xmpp._service_name) elif value == 'service': - result[value] = 'xmpp' + result[value] = creds.get('service', 'xmpp') elif value in creds: result[value] = creds[value] return result @@ -174,8 +173,12 @@ class FeatureMechanisms(BasePlugin): except sasl.SASLNoAppropriateMechanism: log.error("No appropriate login method.") self.xmpp.event("no_auth", direct=True) + self.xmpp.event("failed_auth", direct=True) self.attempted_mechs = set() return self.xmpp.disconnect() + except StringPrepError: + log.exception("A credential value did not pass SASLprep.") + self.xmpp.disconnect() resp = stanza.Auth(self.xmpp) resp['mechanism'] = self.mech.name @@ -184,17 +187,14 @@ class FeatureMechanisms(BasePlugin): except sasl.SASLCancelled: self.attempted_mechs.add(self.mech.name) self._send_auth() - except sasl.SASLFailed: - self.attempted_mechs.add(self.mech.name) - self._send_auth() except sasl.SASLMutualAuthFailed: log.error("Mutual authentication failed! " + \ "A security breach is possible.") self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() - except StringPrepError: - log.exception("A credential value did not pass SASLprep.") - self.xmpp.disconnect() + except sasl.SASLFailed: + self.attempted_mechs.add(self.mech.name) + self._send_auth() else: resp.send(now=True) @@ -207,14 +207,16 @@ class FeatureMechanisms(BasePlugin): resp['value'] = self.mech.process(stanza['value']) except sasl.SASLCancelled: self.stanza.Abort(self.xmpp).send() - except sasl.SASLFailed: - self.stanza.Abort(self.xmpp).send() except sasl.SASLMutualAuthFailed: log.error("Mutual authentication failed! " + \ "A security breach is possible.") self.attempted_mechs.add(self.mech.name) self.xmpp.disconnect() + except sasl.SASLFailed: + self.stanza.Abort(self.xmpp).send() else: + if resp.get_value() == '': + resp.del_value() resp.send(now=True) def _handle_success(self, stanza): diff --git a/sleekxmpp/features/feature_mechanisms/stanza/auth.py b/sleekxmpp/features/feature_mechanisms/stanza/auth.py index 7b665345..6b6f85a3 100644 --- a/sleekxmpp/features/feature_mechanisms/stanza/auth.py +++ b/sleekxmpp/features/feature_mechanisms/stanza/auth.py @@ -40,7 +40,7 @@ class Auth(StanzaBase): if not self['mechanism'] in self.plain_mechs: if values: self.xml.text = bytes(base64.b64encode(values)).decode('utf-8') - else: + elif values == b'': self.xml.text = '=' else: self.xml.text = bytes(values).decode('utf-8') diff --git a/sleekxmpp/features/feature_preapproval/preapproval.py b/sleekxmpp/features/feature_preapproval/preapproval.py index 3823c472..c7106ed3 100644 --- a/sleekxmpp/features/feature_preapproval/preapproval.py +++ b/sleekxmpp/features/feature_preapproval/preapproval.py @@ -8,7 +8,7 @@ import logging -from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.stanza import StreamFeatures from sleekxmpp.features.feature_preapproval import stanza from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.base import BasePlugin diff --git a/sleekxmpp/features/feature_rosterver/rosterver.py b/sleekxmpp/features/feature_rosterver/rosterver.py index 9e0bb8e8..2991f587 100644 --- a/sleekxmpp/features/feature_rosterver/rosterver.py +++ b/sleekxmpp/features/feature_rosterver/rosterver.py @@ -8,7 +8,7 @@ import logging -from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.stanza import StreamFeatures from sleekxmpp.features.feature_rosterver import stanza from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.base import BasePlugin diff --git a/sleekxmpp/features/feature_session/session.py b/sleekxmpp/features/feature_session/session.py index c799a763..ceadd5f3 100644 --- a/sleekxmpp/features/feature_session/session.py +++ b/sleekxmpp/features/feature_session/session.py @@ -51,4 +51,4 @@ class FeatureSession(BasePlugin): log.debug("Established Session") self.xmpp.sessionstarted = True self.xmpp.session_started_event.set() - self.xmpp.event("session_start") + self.xmpp.event('session_start') diff --git a/sleekxmpp/jid.py b/sleekxmpp/jid.py index 8f1a81d4..ac5ba30d 100644 --- a/sleekxmpp/jid.py +++ b/sleekxmpp/jid.py @@ -19,6 +19,8 @@ import stringprep import threading import encodings.idna +from copy import deepcopy + from sleekxmpp.util import stringprep_profiles from sleekxmpp.thirdparty import OrderedDict @@ -75,7 +77,7 @@ def _cache(key, parts, locked): with JID_CACHE_LOCK: while len(JID_CACHE) > JID_CACHE_MAX_SIZE: found = None - for key, item in JID_CACHE.iteritems(): + for key, item in JID_CACHE.items(): if not item[1]: # if not locked found = key break @@ -202,7 +204,7 @@ def _validate_domain(domain): socket.inet_pton(socket.AF_INET6, domain.strip('[]')) domain = '[%s]' % domain.strip('[]') ip_addr = True - except socket.error: + except (socket.error, ValueError): pass if not ip_addr: @@ -228,7 +230,7 @@ def _validate_domain(domain): for char in label: if char in ILLEGAL_CHARS: - raise InvalidJID('Domain contains illegar characters') + raise InvalidJID('Domain contains illegal characters') if '-' in (label[0], label[-1]): raise InvalidJID('Domain started or ended with -') @@ -506,50 +508,100 @@ class JID(object): """ self._jid = JID(data)._jid - # pylint: disable=R0911 - def __getattr__(self, name): - """Retrieve the given JID component. + @property + def resource(self): + return self._jid[2] or '' - :param name: one of: user, server, domain, resource, - full, or bare. - """ - if name == 'resource': - return self._jid[2] or '' - elif name in ('user', 'username', 'local', 'node'): - return self._jid[0] or '' - elif name in ('server', 'domain', 'host'): - return self._jid[1] or '' - elif name in ('full', 'jid'): - return _format_jid(*self._jid) - elif name == 'bare': - return _format_jid(self._jid[0], self._jid[1]) - elif name == '_jid': - return getattr(super(JID, self), '_jid') - else: - return None + @property + def user(self): + return self._jid[0] or '' - # pylint: disable=W0212 - def __setattr__(self, name, value): - """Update the given JID component. + @property + def local(self): + return self._jid[0] or '' + + @property + def node(self): + return self._jid[0] or '' + + @property + def username(self): + return self._jid[0] or '' + + @property + def bare(self): + return _format_jid(self._jid[0], self._jid[1]) + + @property + def server(self): + return self._jid[1] or '' + + @property + def domain(self): + return self._jid[1] or '' + + @property + def host(self): + return self._jid[1] or '' + + @property + def full(self): + return _format_jid(*self._jid) + + @property + def jid(self): + return _format_jid(*self._jid) + + @property + def bare(self): + return _format_jid(self._jid[0], self._jid[1]) + + + @resource.setter + def resource(self, value): + self._jid = JID(self, resource=value)._jid + + @user.setter + def user(self, value): + self._jid = JID(self, local=value)._jid + + @username.setter + def username(self, value): + self._jid = JID(self, local=value)._jid + + @local.setter + def local(self, value): + self._jid = JID(self, local=value)._jid + + @node.setter + def node(self, value): + self._jid = JID(self, local=value)._jid + + @server.setter + def server(self, value): + self._jid = JID(self, domain=value)._jid + + @domain.setter + def domain(self, value): + self._jid = JID(self, domain=value)._jid + + @host.setter + def host(self, value): + self._jid = JID(self, domain=value)._jid + + @full.setter + def full(self, value): + self._jid = JID(value)._jid + + @jid.setter + def jid(self, value): + self._jid = JID(value)._jid + + @bare.setter + def bare(self, value): + parsed = JID(value)._jid + self._jid = (parsed[0], parsed[1], self._jid[2]) - :param name: one of: ``user``, ``username``, ``local``, - ``node``, ``server``, ``domain``, ``host``, - ``resource``, ``full``, ``jid``, or ``bare``. - :param value: The new string value of the JID component. - """ - if name == '_jid': - super(JID, self).__setattr__('_jid', value) - elif name == 'resource': - self._jid = JID(self, resource=value)._jid - elif name in ('user', 'username', 'local', 'node'): - self._jid = JID(self, local=value)._jid - elif name in ('server', 'domain', 'host'): - self._jid = JID(self, domain=value)._jid - elif name in ('full', 'jid'): - self._jid = JID(value)._jid - elif name == 'bare': - parsed = JID(value)._jid - self._jid = (parsed[0], parsed[1], self._jid[2]) def __str__(self): """Use the full JID as the string value.""" @@ -580,3 +632,7 @@ class JID(object): def __copy__(self): """Generate a duplicate JID.""" return JID(self) + + def __deepcopy__(self, memo): + """Generate a duplicate JID.""" + return JID(deepcopy(str(self), memo)) diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index c6617a87..951f31eb 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -11,20 +11,19 @@ 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_0013', # Flexible Offline Message Retrieval 'xep_0016', # Privacy Lists + 'xep_0020', # Feature Negotiation 'xep_0027', # Current Jabber OpenPGP Usage 'xep_0030', # Service Discovery 'xep_0033', # Extended Stanza Addresses 'xep_0045', # Multi-User Chat (Client) 'xep_0047', # In-Band Bytestreams + 'xep_0048', # Bookmarks 'xep_0049', # Private XML Storage 'xep_0050', # Ad-hoc Commands 'xep_0054', # vcard-temp @@ -32,8 +31,10 @@ __all__ = [ 'xep_0060', # Pubsub (Client) 'xep_0065', # SOCKS5 Bytestreams 'xep_0066', # Out of Band Data + 'xep_0071', # XHTML-IM 'xep_0077', # In-Band Registration # 'xep_0078', # Non-SASL auth. Don't automatically load + 'xep_0079', # Advanced Message Processing 'xep_0080', # User Location 'xep_0082', # XMPP Date and Time Profiles 'xep_0084', # User Avatar @@ -49,12 +50,14 @@ __all__ = [ 'xep_0128', # Extended Service Discovery 'xep_0131', # Standard Headers and Internet Metadata 'xep_0133', # Service Administration + 'xep_0152', # Reachability Addresses 'xep_0153', # vCard-Based Avatars 'xep_0163', # Personal Eventing Protocol 'xep_0172', # User Nickname 'xep_0184', # Message Receipts 'xep_0186', # Invisible Command 'xep_0191', # Blocking Command + 'xep_0196', # User Gaming 'xep_0198', # Stream Management 'xep_0199', # Ping 'xep_0202', # Entity Time @@ -77,4 +80,7 @@ __all__ = [ 'xep_0302', # XMPP Compliance Suites 2012 'xep_0308', # Last Message Correction 'xep_0313', # Message Archive Management + 'xep_0319', # Last User Interaction in Presence + 'xep_0323', # IoT Systems Sensor Data + 'xep_0325', # IoT Systems Control ] diff --git a/sleekxmpp/plugins/google/__init__.py b/sleekxmpp/plugins/google/__init__.py new file mode 100644 index 00000000..bd7ca123 --- /dev/null +++ b/sleekxmpp/plugins/google/__init__.py @@ -0,0 +1,47 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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, BasePlugin + +from sleekxmpp.plugins.google.gmail import Gmail +from sleekxmpp.plugins.google.auth import GoogleAuth +from sleekxmpp.plugins.google.settings import GoogleSettings +from sleekxmpp.plugins.google.nosave import GoogleNoSave + + +class Google(BasePlugin): + + """ + Google: Custom GTalk Features + + Also see: <https://developers.google.com/talk/jep_extensions/extensions> + """ + + name = 'google' + description = 'Google: Custom GTalk Features' + dependencies = set([ + 'gmail', + 'google_settings', + 'google_nosave', + 'google_auth' + ]) + + def __getitem__(self, attr): + if attr in ('settings', 'nosave', 'auth'): + return self.xmpp['google_%s' % attr] + elif attr == 'gmail': + return self.xmpp['gmail'] + else: + raise KeyError(attr) + + +register_plugin(Gmail) +register_plugin(GoogleAuth) +register_plugin(GoogleSettings) +register_plugin(GoogleNoSave) +register_plugin(Google) diff --git a/sleekxmpp/plugins/google/auth/__init__.py b/sleekxmpp/plugins/google/auth/__init__.py new file mode 100644 index 00000000..5a8feb0d --- /dev/null +++ b/sleekxmpp/plugins/google/auth/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.google.auth import stanza +from sleekxmpp.plugins.google.auth.auth import GoogleAuth diff --git a/sleekxmpp/plugins/google/auth/auth.py b/sleekxmpp/plugins/google/auth/auth.py new file mode 100644 index 00000000..042bd404 --- /dev/null +++ b/sleekxmpp/plugins/google/auth/auth.py @@ -0,0 +1,52 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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 import BasePlugin +from sleekxmpp.plugins.google.auth import stanza + + +log = logging.getLogger(__name__) + + +class GoogleAuth(BasePlugin): + + """ + Google: Auth Extensions (JID Domain Discovery, OAuth2) + + Also see: + <https://developers.google.com/talk/jep_extensions/jid_domain_change> + <https://developers.google.com/talk/jep_extensions/oauth> + """ + + name = 'google_auth' + description = 'Google: Auth Extensions (JID Domain Discovery, OAuth2)' + dependencies = set(['feature_mechanisms']) + stanza = stanza + + def plugin_init(self): + self.xmpp.namespace_map['http://www.google.com/talk/protocol/auth'] = 'ga' + + register_stanza_plugin(self.xmpp['feature_mechanisms'].stanza.Auth, + stanza.GoogleAuth) + + self.xmpp.add_filter('out', self._auth) + + def plugin_end(self): + self.xmpp.del_filter('out', self._auth) + + def _auth(self, stanza): + if isinstance(stanza, self.xmpp['feature_mechanisms'].stanza.Auth): + stanza.stream = self.xmpp + stanza['google']['client_uses_full_bind_result'] = True + if stanza['mechanism'] == 'X-OAUTH2': + stanza['google']['service'] = 'oauth2' + print(stanza) + return stanza diff --git a/sleekxmpp/plugins/google/auth/stanza.py b/sleekxmpp/plugins/google/auth/stanza.py new file mode 100644 index 00000000..2d13f85a --- /dev/null +++ b/sleekxmpp/plugins/google/auth/stanza.py @@ -0,0 +1,49 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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 GoogleAuth(ElementBase): + name = 'auth' + namespace = 'http://www.google.com/talk/protocol/auth' + plugin_attrib = 'google' + interfaces = set(['client_uses_full_bind_result', 'service']) + + discovery_attr= '{%s}client-uses-full-bind-result' % namespace + service_attr= '{%s}service' % namespace + + def setup(self, xml): + """Don't create XML for the plugin.""" + self.xml = ET.Element('') + print('setting up google extension') + + def get_client_uses_full_bind_result(self): + return self.parent()._get_attr(self.discovery_attr) == 'true' + + def set_client_uses_full_bind_result(self, value): + print('>>>', value) + if value in (True, 'true'): + self.parent()._set_attr(self.discovery_attr, 'true') + else: + self.parent()._del_attr(self.discovery_attr) + + def del_client_uses_full_bind_result(self): + self.parent()._del_attr(self.discovery_attr) + + def get_service(self): + return self.parent()._get_attr(self.service_attr, '') + + def set_service(self, value): + if value: + self.parent()._set_attr(self.service_attr, value) + else: + self.parent()._del_attr(self.service_attr) + + def del_service(self): + self.parent()._del_attr(self.service_attr) diff --git a/sleekxmpp/plugins/google/gmail/__init__.py b/sleekxmpp/plugins/google/gmail/__init__.py new file mode 100644 index 00000000..a92e363b --- /dev/null +++ b/sleekxmpp/plugins/google/gmail/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.google.gmail import stanza +from sleekxmpp.plugins.google.gmail.notifications import Gmail diff --git a/sleekxmpp/plugins/google/gmail/notifications.py b/sleekxmpp/plugins/google/gmail/notifications.py new file mode 100644 index 00000000..e65b2ca7 --- /dev/null +++ b/sleekxmpp/plugins/google/gmail/notifications.py @@ -0,0 +1,96 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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 Iq +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.google.gmail import stanza + + +log = logging.getLogger(__name__) + + +class Gmail(BasePlugin): + + """ + Google: Gmail Notifications + + Also see <https://developers.google.com/talk/jep_extensions/gmail>. + """ + + name = 'gmail' + description = 'Google: Gmail Notifications' + dependencies = set() + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, stanza.GmailQuery) + register_stanza_plugin(Iq, stanza.MailBox) + register_stanza_plugin(Iq, stanza.NewMail) + + self.xmpp.register_handler( + Callback('Gmail New Mail', + MatchXPath('{%s}iq/{%s}%s' % ( + self.xmpp.default_ns, + stanza.NewMail.namespace, + stanza.NewMail.name)), + self._handle_new_mail)) + + self._last_result_time = None + self._last_result_tid = None + + def plugin_end(self): + self.xmpp.remove_handler('Gmail New Mail') + + def _handle_new_mail(self, iq): + log.info('Gmail: New email!') + iq.reply().send() + self.xmpp.event('gmail_notification') + + def check(self, block=True, timeout=None, callback=None): + last_time = self._last_result_time + last_tid = self._last_result_tid + + if not block: + callback = lambda iq: self._update_last_results(iq, callback) + + resp = self.search(newer_time=last_time, + newer_tid=last_tid, + block=block, + timeout=timeout, + callback=callback) + + if block: + self._update_last_results(resp) + return resp + + def _update_last_results(self, iq, callback=None): + self._last_result_time = iq['gmail_messages']['result_time'] + threads = iq['gmail_messages']['threads'] + if threads: + self._last_result_tid = threads[0]['tid'] + if callback: + callback(iq) + + def search(self, query=None, newer_time=None, newer_tid=None, block=True, + timeout=None, callback=None): + if not query: + log.info('Gmail: Checking for new email') + else: + log.info('Gmail: Searching for emails matching: "%s"', query) + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['to'] = self.xmpp.boundjid.bare + iq['gmail']['search'] = query + iq['gmail']['newer_than_time'] = newer_time + iq['gmail']['newer_than_tid'] = newer_tid + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/google/gmail/stanza.py b/sleekxmpp/plugins/google/gmail/stanza.py new file mode 100644 index 00000000..e7e308e1 --- /dev/null +++ b/sleekxmpp/plugins/google/gmail/stanza.py @@ -0,0 +1,101 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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, register_stanza_plugin + + +class GmailQuery(ElementBase): + namespace = 'google:mail:notify' + name = 'query' + plugin_attrib = 'gmail' + interfaces = set(['newer_than_time', 'newer_than_tid', 'search']) + + def get_search(self): + return self._get_attr('q', '') + + def set_search(self, search): + self._set_attr('q', search) + + def del_search(self): + self._del_attr('q') + + def get_newer_than_time(self): + return self._get_attr('newer-than-time', '') + + def set_newer_than_time(self, value): + self._set_attr('newer-than-time', value) + + def del_newer_than_time(self): + self._del_attr('newer-than-time') + + def get_newer_than_tid(self): + return self._get_attr('newer-than-tid', '') + + def set_newer_than_tid(self, value): + self._set_attr('newer-than-tid', value) + + def del_newer_than_tid(self): + self._del_attr('newer-than-tid') + + +class MailBox(ElementBase): + namespace = 'google:mail:notify' + name = 'mailbox' + plugin_attrib = 'gmail_messages' + interfaces = set(['result_time', 'url', 'matched', 'estimate']) + + def get_matched(self): + return self._get_attr('total-matched', '') + + def get_estimate(self): + return self._get_attr('total-estimate', '') == '1' + + def get_result_time(self): + return self._get_attr('result-time', '') + + +class MailThread(ElementBase): + namespace = 'google:mail:notify' + name = 'mail-thread-info' + plugin_attrib = 'thread' + plugin_multi_attrib = 'threads' + interfaces = set(['tid', 'participation', 'messages', 'date', + 'senders', 'url', 'labels', 'subject', 'snippet']) + sub_interfaces = set(['labels', 'subject', 'snippet']) + + def get_senders(self): + result = [] + senders = self.xml.findall('{%s}senders/{%s}sender' % ( + self.namespace, self.namespace)) + + for sender in senders: + result.append(MailSender(xml=sender)) + + return result + + +class MailSender(ElementBase): + namespace = 'google:mail:notify' + name = 'sender' + plugin_attrib = name + interfaces = set(['address', 'name', 'originator', 'unread']) + + def get_originator(self): + return self.xml.attrib.get('originator', '0') == '1' + + def get_unread(self): + return self.xml.attrib.get('unread', '0') == '1' + + +class NewMail(ElementBase): + namespace = 'google:mail:notify' + name = 'new-mail' + plugin_attrib = 'gmail_notification' + + +register_stanza_plugin(MailBox, MailThread, iterable=True) diff --git a/sleekxmpp/plugins/google/nosave/__init__.py b/sleekxmpp/plugins/google/nosave/__init__.py new file mode 100644 index 00000000..57847af5 --- /dev/null +++ b/sleekxmpp/plugins/google/nosave/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.google.nosave import stanza +from sleekxmpp.plugins.google.nosave.nosave import GoogleNoSave diff --git a/sleekxmpp/plugins/google/nosave/nosave.py b/sleekxmpp/plugins/google/nosave/nosave.py new file mode 100644 index 00000000..d6bef615 --- /dev/null +++ b/sleekxmpp/plugins/google/nosave/nosave.py @@ -0,0 +1,83 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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 Iq, Message +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.google.nosave import stanza + + +log = logging.getLogger(__name__) + + +class GoogleNoSave(BasePlugin): + + """ + Google: Off the Record Chats + + NOTE: This is NOT an encryption method. + + Also see <https://developers.google.com/talk/jep_extensions/otr>. + """ + + name = 'google_nosave' + description = 'Google: Off the Record Chats' + dependencies = set(['google_settings']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, stanza.NoSave) + register_stanza_plugin(Iq, stanza.NoSaveQuery) + + self.xmpp.register_handler( + Callback('Google Nosave', + StanzaPath('iq@type=set/google_nosave'), + self._handle_nosave_change)) + + def plugin_end(self): + self.xmpp.remove_handler('Google Nosave') + + def enable(self, jid=None, block=True, timeout=None, callback=None): + if jid is None: + self.xmpp['google_settings'].update({'archiving_enabled': False}, + block=block, timeout=timeout, callback=callback) + else: + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['google_nosave']['item']['jid'] = jid + iq['google_nosave']['item']['value'] = True + return iq.send(block=block, timeout=timeout, callback=callback) + + def disable(self, jid=None, block=True, timeout=None, callback=None): + if jid is None: + self.xmpp['google_settings'].update({'archiving_enabled': True}, + block=block, timeout=timeout, callback=callback) + else: + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['google_nosave']['item']['jid'] = jid + iq['google_nosave']['item']['value'] = False + return iq.send(block=block, timeout=timeout, callback=callback) + + def get(self, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq.enable('google_nosave') + return iq.send(block=block, timeout=timeout, callback=callback) + + def _handle_nosave_change(self, iq): + reply = self.xmpp.Iq() + reply['type'] = 'result' + reply['id'] = iq['id'] + reply['to'] = iq['from'] + reply.send() + self.xmpp.event('google_nosave_change', iq) diff --git a/sleekxmpp/plugins/google/nosave/stanza.py b/sleekxmpp/plugins/google/nosave/stanza.py new file mode 100644 index 00000000..791d4b0c --- /dev/null +++ b/sleekxmpp/plugins/google/nosave/stanza.py @@ -0,0 +1,59 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.jid import JID +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin + + +class NoSave(ElementBase): + name = 'x' + namespace = 'google:nosave' + plugin_attrib = 'google_nosave' + interfaces = set(['value']) + + def get_value(self): + return self._get_attr('value', '') == 'enabled' + + def set_value(self, value): + self._set_attr('value', 'enabled' if value else 'disabled') + + +class NoSaveQuery(ElementBase): + name = 'query' + namespace = 'google:nosave' + plugin_attrib = 'google_nosave' + interfaces = set() + + +class Item(ElementBase): + name = 'item' + namespace = 'google:nosave' + plugin_attrib = 'item' + plugin_multi_attrib = 'items' + interfaces = set(['jid', 'source', 'value']) + + def get_value(self): + return self._get_attr('value', '') == 'enabled' + + def set_value(self, value): + self._set_attr('value', 'enabled' if value else 'disabled') + + def get_jid(self): + return JID(self._get_attr('jid', '')) + + def set_jid(self, value): + self._set_attr('jid', str(value)) + + def get_source(self): + return JID(self._get_attr('source', '')) + + def set_source(self, value): + self._set_attr('source', str(value)) + + +register_stanza_plugin(NoSaveQuery, Item) diff --git a/sleekxmpp/plugins/google/settings/__init__.py b/sleekxmpp/plugins/google/settings/__init__.py new file mode 100644 index 00000000..c3a0471d --- /dev/null +++ b/sleekxmpp/plugins/google/settings/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.google.settings import stanza +from sleekxmpp.plugins.google.settings.settings import GoogleSettings diff --git a/sleekxmpp/plugins/google/settings/settings.py b/sleekxmpp/plugins/google/settings/settings.py new file mode 100644 index 00000000..591956fc --- /dev/null +++ b/sleekxmpp/plugins/google/settings/settings.py @@ -0,0 +1,63 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import Iq +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.google.settings import stanza + + +class GoogleSettings(BasePlugin): + + """ + Google: Gmail Notifications + + Also see <https://developers.google.com/talk/jep_extensions/usersettings>. + """ + + name = 'google_settings' + description = 'Google: User Settings' + dependencies = set() + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, stanza.UserSettings) + + self.xmpp.register_handler( + Callback('Google Settings', + StanzaPath('iq@type=set/google_settings'), + self._handle_settings_change)) + + def plugin_end(self): + self.xmpp.remove_handler('Google Settings') + + def get(self, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq.enable('google_settings') + return iq.send(block=block, timeout=timeout, callback=callback) + + def update(self, settings, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq.enable('google_settings') + + for setting, value in settings.items(): + iq['google_settings'][setting] = value + + return iq.send(block=block, timeout=timeout, callback=callback) + + def _handle_settings_change(self, iq): + reply = self.xmpp.Iq() + reply['type'] = 'result' + reply['id'] = iq['id'] + reply['to'] = iq['from'] + reply.send() + self.xmpp.event('google_settings_change', iq) diff --git a/sleekxmpp/plugins/google/settings/stanza.py b/sleekxmpp/plugins/google/settings/stanza.py new file mode 100644 index 00000000..d8161770 --- /dev/null +++ b/sleekxmpp/plugins/google/settings/stanza.py @@ -0,0 +1,110 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin + + +class UserSettings(ElementBase): + name = 'usersetting' + namespace = 'google:setting' + plugin_attrib = 'google_settings' + interfaces = set(['auto_accept_suggestions', + 'mail_notifications', + 'archiving_enabled', + 'gmail', + 'email_verified', + 'domain_privacy_notice', + 'display_name']) + + def _get_setting(self, setting): + xml = self.xml.find('{%s}%s' % (self.namespace, setting)) + if xml is not None: + return xml.attrib.get('value', '') == 'true' + return False + + def _set_setting(self, setting, value): + self._del_setting(setting) + if value in (True, False): + xml = ET.Element('{%s}%s' % (self.namespace, setting)) + xml.attrib['value'] = 'true' if value else 'false' + self.xml.append(xml) + + def _del_setting(self, setting): + xml = self.xml.find('{%s}%s' % (self.namespace, setting)) + if xml is not None: + self.xml.remove(xml) + + def get_display_name(self): + xml = self.xml.find('{%s}%s' % (self.namespace, 'displayname')) + if xml is not None: + return xml.attrib.get('value', '') + return '' + + def set_display_name(self, value): + self._del_setting(setting) + if value: + xml = ET.Element('{%s}%s' % (self.namespace, 'displayname')) + xml.attrib['value'] = value + self.xml.append(xml) + + def del_display_name(self): + self._del_setting('displayname') + + def get_auto_accept_suggestions(self): + return self._get_setting('autoacceptsuggestions') + + def get_mail_notifications(self): + return self._get_setting('mailnotifications') + + def get_archiving_enabled(self): + return self._get_setting('archivingenabled') + + def get_gmail(self): + return self._get_setting('gmail') + + def get_email_verified(self): + return self._get_setting('emailverified') + + def get_domain_privacy_notice(self): + return self._get_setting('domainprivacynotice') + + def set_auto_accept_suggestions(self, value): + self._set_setting('autoacceptsuggestions', value) + + def set_mail_notifications(self, value): + self._set_setting('mailnotifications', value) + + def set_archiving_enabled(self, value): + self._set_setting('archivingenabled', value) + + def set_gmail(self, value): + self._set_setting('gmail', value) + + def set_email_verified(self, value): + self._set_setting('emailverified', value) + + def set_domain_privacy_notice(self, value): + self._set_setting('domainprivacynotice', value) + + def del_auto_accept_suggestions(self): + self._del_setting('autoacceptsuggestions') + + def del_mail_notifications(self): + self._del_setting('mailnotifications') + + def del_archiving_enabled(self): + self._del_setting('archivingenabled') + + def del_gmail(self): + self._del_setting('gmail') + + def del_email_verified(self): + self._del_setting('emailverified') + + def del_domain_privacy_notice(self): + self._del_setting('domainprivacynotice') diff --git a/sleekxmpp/plugins/jobs.py b/sleekxmpp/plugins/jobs.py deleted file mode 100644 index cb9deba8..00000000 --- a/sleekxmpp/plugins/jobs.py +++ /dev/null @@ -1,49 +0,0 @@ -from . import base -import logging -from xml.etree import cElementTree as ET - - -log = logging.getLogger(__name__) - - -class jobs(base.base_plugin): - def plugin_init(self): - self.xep = 'pubsubjob' - self.description = "Job distribution over Pubsub" - - def post_init(self): - pass - #TODO add event - - def createJobNode(self, host, jid, node, config=None): - pass - - def createJob(self, host, node, jobid=None, payload=None): - return self.xmpp.plugin['xep_0060'].setItem(host, node, ((jobid, payload),)) - - def claimJob(self, host, node, jobid, ifrom=None): - return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}claimed')) - - def unclaimJob(self, host, node, jobid): - return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}unclaimed')) - - def finishJob(self, host, node, jobid, payload=None): - finished = ET.Element('{http://andyet.net/protocol/pubsubjob}finished') - if payload is not None: - finished.append(payload) - return self._setState(host, node, jobid, finished) - - def _setState(self, host, node, jobid, state, ifrom=None): - iq = self.xmpp.Iq() - iq['to'] = host - if ifrom: iq['from'] = ifrom - iq['type'] = 'set' - iq['psstate']['node'] = node - iq['psstate']['item'] = jobid - iq['psstate']['payload'] = state - result = iq.send() - if result is None or type(result) == bool or result['type'] != 'result': - log.error("Unable to change %s:%s to %s", node, jobid, state) - return False - return True - diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py deleted file mode 100644 index 7f086866..00000000 --- a/sleekxmpp/plugins/old_0004.py +++ /dev/null @@ -1,421 +0,0 @@ -""" - 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 . import base -import logging -from xml.etree import cElementTree as ET -import copy -import logging -#TODO support item groups and results - - -log = logging.getLogger(__name__) - - -class old_0004(base.base_plugin): - - def plugin_init(self): - self.xep = '0004' - self.description = '*Deprecated Data Forms' - self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform, name='Old Message Form') - - def post_init(self): - base.base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') - log.warning("This implementation of XEP-0004 is deprecated.") - - def handler_message_xform(self, xml): - object = self.handle_form(xml) - self.xmpp.event("message_form", object) - - def handler_presence_xform(self, xml): - object = self.handle_form(xml) - self.xmpp.event("presence_form", object) - - def handle_form(self, xml): - xmlform = xml.find('{jabber:x:data}x') - object = self.buildForm(xmlform) - self.xmpp.event("message_xform", object) - return object - - def buildForm(self, xml): - form = Form(ftype=xml.attrib['type']) - form.fromXML(xml) - return form - - def makeForm(self, ftype='form', title='', instructions=''): - return Form(self.xmpp, ftype, title, instructions) - -class FieldContainer(object): - def __init__(self, stanza = 'form'): - self.fields = [] - self.field = {} - self.stanza = stanza - - def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None): - self.field[var] = FormField(var, ftype, label, desc, required, value) - self.fields.append(self.field[var]) - return self.field[var] - - def buildField(self, xml): - self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single')) - self.fields.append(self.field[xml.get('var', '__unnamed__')]) - self.field[xml.get('var', '__unnamed__')].buildField(xml) - - def buildContainer(self, xml): - self.stanza = xml.tag - for field in xml.findall('{jabber:x:data}field'): - self.buildField(field) - - def getXML(self, ftype): - container = ET.Element(self.stanza) - for field in self.fields: - container.append(field.getXML(ftype)) - return container - -class Form(FieldContainer): - types = ('form', 'submit', 'cancel', 'result') - def __init__(self, xmpp=None, ftype='form', title='', instructions=''): - if not ftype in self.types: - raise ValueError("Invalid Form Type") - FieldContainer.__init__(self) - self.xmpp = xmpp - self.type = ftype - self.title = title - self.instructions = instructions - self.reported = [] - self.items = [] - - def merge(self, form2): - form1 = Form(ftype=self.type) - form1.fromXML(self.getXML(self.type)) - for field in form2.fields: - if not field.var in form1.field: - form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value) - else: - form1.field[field.var].value = field.value - for option, label in field.options: - if (option, label) not in form1.field[field.var].options: - form1.fields[field.var].addOption(option, label) - return form1 - - def copy(self): - newform = Form(ftype=self.type) - newform.fromXML(self.getXML(self.type)) - return newform - - def update(self, form): - values = form.getValues() - for var in values: - if var in self.fields: - self.fields[var].setValue(self.fields[var]) - - def getValues(self): - result = {} - for field in self.fields: - value = field.value - if len(value) == 1: - value = value[0] - result[field.var] = value - return result - - def setValues(self, values={}): - for field in values: - if field in self.field: - if isinstance(values[field], list) or isinstance(values[field], tuple): - for value in values[field]: - self.field[field].setValue(value) - else: - self.field[field].setValue(values[field]) - - def fromXML(self, xml): - self.buildForm(xml) - - def addItem(self): - newitem = FieldContainer('item') - self.items.append(newitem) - return newitem - - def buildItem(self, xml): - newitem = self.addItem() - newitem.buildContainer(xml) - - def addReported(self): - reported = FieldContainer('reported') - self.reported.append(reported) - return reported - - def buildReported(self, xml): - reported = self.addReported() - reported.buildContainer(xml) - - def setTitle(self, title): - self.title = title - - def setInstructions(self, instructions): - self.instructions = instructions - - def setType(self, ftype): - self.type = ftype - - def getXMLMessage(self, to): - msg = self.xmpp.makeMessage(to) - msg.append(self.getXML()) - return msg - - def buildForm(self, xml): - self.type = xml.get('type', 'form') - if xml.find('{jabber:x:data}title') is not None: - self.setTitle(xml.find('{jabber:x:data}title').text) - if xml.find('{jabber:x:data}instructions') is not None: - self.setInstructions(xml.find('{jabber:x:data}instructions').text) - for field in xml.findall('{jabber:x:data}field'): - self.buildField(field) - for reported in xml.findall('{jabber:x:data}reported'): - self.buildReported(reported) - for item in xml.findall('{jabber:x:data}item'): - self.buildItem(item) - - #def getXML(self, tostring = False): - def getXML(self, ftype=None): - if ftype: - self.type = ftype - form = ET.Element('{jabber:x:data}x') - form.attrib['type'] = self.type - if self.title and self.type in ('form', 'result'): - title = ET.Element('{jabber:x:data}title') - title.text = self.title - form.append(title) - if self.instructions and self.type == 'form': - instructions = ET.Element('{jabber:x:data}instructions') - instructions.text = self.instructions - form.append(instructions) - for field in self.fields: - form.append(field.getXML(self.type)) - for reported in self.reported: - form.append(reported.getXML('{jabber:x:data}reported')) - for item in self.items: - form.append(item.getXML(self.type)) - #if tostring: - # form = self.xmpp.tostring(form) - return form - - def getXHTML(self): - form = ET.Element('{http://www.w3.org/1999/xhtml}form') - if self.title: - title = ET.Element('h2') - title.text = self.title - form.append(title) - if self.instructions: - instructions = ET.Element('p') - instructions.text = self.instructions - form.append(instructions) - for field in self.fields: - form.append(field.getXHTML()) - for field in self.reported: - form.append(field.getXHTML()) - for field in self.items: - form.append(field.getXHTML()) - return form - - - def makeSubmit(self): - self.setType('submit') - -class FormField(object): - types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single') - listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single') - lbtypes = ('fixed', 'text-multi') - def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None): - if not ftype in self.types: - raise ValueError("Invalid Field Type") - self.type = ftype - self.var = var - self.label = label - self.desc = desc - self.options = [] - self.required = False - self.value = [] - if self.type in self.listtypes: - self.islist = True - else: - self.islist = False - if self.type in self.lbtypes: - self.islinebreak = True - else: - self.islinebreak = False - if value: - self.setValue(value) - - def addOption(self, value, label): - if self.islist: - self.options.append((value, label)) - else: - raise ValueError("Cannot add options to non-list type field.") - - def setTrue(self): - if self.type == 'boolean': - self.value = [True] - - def setFalse(self): - if self.type == 'boolean': - self.value = [False] - - def require(self): - self.required = True - - def setDescription(self, desc): - self.desc = desc - - def setValue(self, value): - if self.type == 'boolean': - if value in ('1', 1, True, 'true', 'True', 'yes'): - value = True - else: - value = False - if self.islinebreak and value is not None: - self.value += value.split('\n') - else: - if len(self.value) and (not self.islist or self.type == 'list-single'): - self.value = [value] - else: - self.value.append(value) - - def delValue(self, value): - if type(self.value) == type([]): - try: - idx = self.value.index(value) - if idx != -1: - self.value.pop(idx) - except ValueError: - pass - else: - self.value = '' - - def setAnswer(self, value): - self.setValue(value) - - def buildField(self, xml): - self.type = xml.get('type', 'text-single') - self.label = xml.get('label', '') - for option in xml.findall('{jabber:x:data}option'): - self.addOption(option.find('{jabber:x:data}value').text, option.get('label', '')) - for value in xml.findall('{jabber:x:data}value'): - self.setValue(value.text) - if xml.find('{jabber:x:data}required') is not None: - self.require() - if xml.find('{jabber:x:data}desc') is not None: - self.setDescription(xml.find('{jabber:x:data}desc').text) - - def getXML(self, ftype): - field = ET.Element('{jabber:x:data}field') - if ftype != 'result': - field.attrib['type'] = self.type - if self.type != 'fixed': - if self.var: - field.attrib['var'] = self.var - if self.label: - field.attrib['label'] = self.label - if ftype == 'form': - for option in self.options: - optionxml = ET.Element('{jabber:x:data}option') - optionxml.attrib['label'] = option[1] - optionval = ET.Element('{jabber:x:data}value') - optionval.text = option[0] - optionxml.append(optionval) - field.append(optionxml) - if self.required: - required = ET.Element('{jabber:x:data}required') - field.append(required) - if self.desc: - desc = ET.Element('{jabber:x:data}desc') - desc.text = self.desc - field.append(desc) - for value in self.value: - valuexml = ET.Element('{jabber:x:data}value') - if value is True or value is False: - if value: - valuexml.text = '1' - else: - valuexml.text = '0' - else: - valuexml.text = value - field.append(valuexml) - return field - - def getXHTML(self): - field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type}) - if self.label: - label = ET.Element('p') - label.text = "%s: " % self.label - else: - label = ET.Element('p') - label.text = "%s: " % self.var - field.append(label) - if self.type == 'boolean': - formf = ET.Element('input', {'type': 'checkbox', 'name': self.var}) - if len(self.value) and self.value[0] in (True, 'true', '1'): - formf.attrib['checked'] = 'checked' - elif self.type == 'fixed': - formf = ET.Element('p') - try: - formf.text = ', '.join(self.value) - except: - pass - field.append(formf) - formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) - try: - formf.text = ', '.join(self.value) - except: - pass - elif self.type == 'hidden': - formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) - try: - formf.text = ', '.join(self.value) - except: - pass - elif self.type in ('jid-multi', 'list-multi'): - formf = ET.Element('select', {'name': self.var}) - for option in self.options: - optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'}) - optf.text = option[1] - if option[1] in self.value: - optf.attrib['selected'] = 'selected' - formf.append(option) - elif self.type in ('jid-single', 'text-single'): - formf = ET.Element('input', {'type': 'text', 'name': self.var}) - try: - formf.attrib['value'] = ', '.join(self.value) - except: - pass - elif self.type == 'list-single': - formf = ET.Element('select', {'name': self.var}) - for option in self.options: - optf = ET.Element('option', {'value': option[0]}) - optf.text = option[1] - if not optf.text: - optf.text = option[0] - if option[1] in self.value: - optf.attrib['selected'] = 'selected' - formf.append(optf) - elif self.type == 'text-multi': - formf = ET.Element('textarea', {'name': self.var}) - try: - formf.text = ', '.join(self.value) - except: - pass - if not formf.text: - formf.text = ' ' - elif self.type == 'text-private': - formf = ET.Element('input', {'type': 'password', 'name': self.var}) - try: - formf.attrib['value'] = ', '.join(self.value) - except: - pass - label.append(formf) - return field - diff --git a/sleekxmpp/plugins/old_0009.py b/sleekxmpp/plugins/old_0009.py deleted file mode 100644 index 625b03fb..00000000 --- a/sleekxmpp/plugins/old_0009.py +++ /dev/null @@ -1,277 +0,0 @@ -"""
-XEP-0009 XMPP Remote Procedure Calls
-"""
-from __future__ import with_statement
-from . import base
-import logging
-from xml.etree import cElementTree as ET
-import copy
-import time
-import base64
-
-def py2xml(*args):
- params = ET.Element("params")
- for x in args:
- param = ET.Element("param")
- param.append(_py2xml(x))
- params.append(param) #<params><param>...
- return params
-
-def _py2xml(*args):
- for x in args:
- val = ET.Element("value")
- if type(x) is int:
- i4 = ET.Element("i4")
- i4.text = str(x)
- val.append(i4)
- if type(x) is bool:
- boolean = ET.Element("boolean")
- boolean.text = str(int(x))
- val.append(boolean)
- elif type(x) is str:
- string = ET.Element("string")
- string.text = x
- val.append(string)
- elif type(x) is float:
- double = ET.Element("double")
- double.text = str(x)
- val.append(double)
- elif type(x) is rpcbase64:
- b64 = ET.Element("Base64")
- b64.text = x.encoded()
- val.append(b64)
- elif type(x) is rpctime:
- iso = ET.Element("dateTime.iso8601")
- iso.text = str(x)
- val.append(iso)
- elif type(x) is list:
- array = ET.Element("array")
- data = ET.Element("data")
- for y in x:
- data.append(_py2xml(y))
- array.append(data)
- val.append(array)
- elif type(x) is dict:
- struct = ET.Element("struct")
- for y in x.keys():
- member = ET.Element("member")
- name = ET.Element("name")
- name.text = y
- member.append(name)
- member.append(_py2xml(x[y]))
- struct.append(member)
- val.append(struct)
- return val
-
-def xml2py(params):
- vals = []
- for param in params.findall('param'):
- vals.append(_xml2py(param.find('value')))
- return vals
-
-def _xml2py(value):
- if value.find('i4') is not None:
- return int(value.find('i4').text)
- if value.find('int') is not None:
- return int(value.find('int').text)
- if value.find('boolean') is not None:
- return bool(value.find('boolean').text)
- if value.find('string') is not None:
- return value.find('string').text
- if value.find('double') is not None:
- return float(value.find('double').text)
- if value.find('Base64') is not None:
- return rpcbase64(value.find('Base64').text)
- if value.find('dateTime.iso8601') is not None:
- return rpctime(value.find('dateTime.iso8601'))
- if value.find('struct') is not None:
- struct = {}
- for member in value.find('struct').findall('member'):
- struct[member.find('name').text] = _xml2py(member.find('value'))
- return struct
- if value.find('array') is not None:
- array = []
- for val in value.find('array').find('data').findall('value'):
- array.append(_xml2py(val))
- return array
- raise ValueError()
-
-class rpcbase64(object):
- def __init__(self, data):
- #base 64 encoded string
- self.data = data
-
- def decode(self):
- return base64.decodestring(data)
-
- def __str__(self):
- return self.decode()
-
- def encoded(self):
- return self.data
-
-class rpctime(object):
- def __init__(self,data=None):
- #assume string data is in iso format YYYYMMDDTHH:MM:SS
- if type(data) is str:
- self.timestamp = time.strptime(data,"%Y%m%dT%H:%M:%S")
- elif type(data) is time.struct_time:
- self.timestamp = data
- elif data is None:
- self.timestamp = time.gmtime()
- else:
- raise ValueError()
-
- def iso8601(self):
- #return a iso8601 string
- return time.strftime("%Y%m%dT%H:%M:%S",self.timestamp)
-
- def __str__(self):
- return self.iso8601()
-
-class JabberRPCEntry(object):
- def __init__(self,call):
- self.call = call
- self.result = None
- self.error = None
- self.allow = {} #{'<jid>':['<resource1>',...],...}
- self.deny = {}
-
- def check_acl(self, jid, resource):
- #Check for deny
- if jid in self.deny.keys():
- if self.deny[jid] == None or resource in self.deny[jid]:
- return False
- #Check for allow
- if allow == None:
- return True
- if jid in self.allow.keys():
- if self.allow[jid] == None or resource in self.allow[jid]:
- return True
- return False
-
- def acl_allow(self, jid, resource):
- if jid == None:
- self.allow = None
- elif resource == None:
- self.allow[jid] = None
- elif jid in self.allow.keys():
- self.allow[jid].append(resource)
- else:
- self.allow[jid] = [resource]
-
- def acl_deny(self, jid, resource):
- if jid == None:
- self.deny = None
- elif resource == None:
- self.deny[jid] = None
- elif jid in self.deny.keys():
- self.deny[jid].append(resource)
- else:
- self.deny[jid] = [resource]
-
- def call_method(self, args):
- ret = self.call(*args)
-
-class xep_0009(base.base_plugin):
-
- def plugin_init(self):
- self.xep = '0009'
- self.description = 'Jabber-RPC'
- self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>",
- self._callMethod, name='Jabber RPC Call')
- self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>",
- self._callResult, name='Jabber RPC Result')
- self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>",
- self._callError, name='Jabber RPC Error')
- self.entries = {}
- 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('automatition','rpc')
-
- def register_call(self, method, name=None):
- #@returns an string that can be used in acl commands.
- with self.lock:
- if name is None:
- self.entries[method.__name__] = JabberRPCEntry(method)
- return method.__name__
- else:
- self.entries[name] = JabberRPCEntry(method)
- return name
-
- def acl_allow(self, entry, jid=None, resource=None):
- #allow the method entry to be called by the given jid and resource.
- #if jid is None it will allow any jid/resource.
- #if resource is None it will allow any resource belonging to the jid.
- with self.lock:
- if self.entries[entry]:
- self.entries[entry].acl_allow(jid,resource)
- else:
- raise ValueError()
-
- def acl_deny(self, entry, jid=None, resource=None):
- #Note: by default all requests are denied unless allowed with acl_allow.
- #If you deny an entry it will not be allowed regardless of acl_allow
- with self.lock:
- if self.entries[entry]:
- self.entries[entry].acl_deny(jid,resource)
- else:
- raise ValueError()
-
- def unregister_call(self, entry):
- #removes the registered call
- with self.lock:
- if self.entries[entry]:
- del self.entries[entry]
- else:
- raise ValueError()
-
- def makeMethodCallQuery(self,pmethod,params):
- query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
- methodCall = ET.Element('methodCall')
- methodName = ET.Element('methodName')
- methodName.text = pmethod
- methodCall.append(methodName)
- methodCall.append(params)
- query.append(methodCall)
- return query
-
- def makeIqMethodCall(self,pto,pmethod,params):
- iq = self.xmpp.makeIqSet()
- iq.set('to',pto)
- iq.append(self.makeMethodCallQuery(pmethod,params))
- return iq
-
- def makeIqMethodResponse(self,pto,pid,params):
- iq = self.xmpp.makeIqResult(pid)
- iq.set('to',pto)
- query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc")
- methodResponse = ET.Element('methodResponse')
- methodResponse.append(params)
- query.append(methodResponse)
- return iq
-
- def makeIqMethodError(self,pto,id,pmethod,params,condition):
- iq = self.xmpp.makeIqError(id)
- iq.set('to',pto)
- iq.append(self.makeMethodCallQuery(pmethod,params))
- iq.append(self.xmpp['xep_0086'].makeError(condition))
- return iq
-
-
-
- def call_remote(self, pto, pmethod, *args):
- #calls a remote method. Returns the id of the Iq.
- pass
-
- def _callMethod(self,xml):
- pass
-
- def _callResult(self,xml):
- pass
-
- def _callError(self,xml):
- pass
diff --git a/sleekxmpp/plugins/old_0050.py b/sleekxmpp/plugins/old_0050.py deleted file mode 100644 index 6e969a51..00000000 --- a/sleekxmpp/plugins/old_0050.py +++ /dev/null @@ -1,133 +0,0 @@ -""" - 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 __future__ import with_statement -from . import base -import logging -from xml.etree import cElementTree as ET -import time - -class old_0050(base.base_plugin): - """ - XEP-0050 Ad-Hoc Commands - """ - - def plugin_init(self): - self.xep = '0050' - self.description = 'Ad-Hoc Commands' - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None') - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute') - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True) - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel') - self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete') - self.commands = {} - self.sessions = {} - self.sd = self.xmpp.plugin['xep_0030'] - - def post_init(self): - base.base_plugin.post_init(self) - self.sd.add_feature('http://jabber.org/protocol/commands') - - def addCommand(self, node, name, form, pointer=None, multi=False): - self.sd.add_item(None, name, 'http://jabber.org/protocol/commands', node) - self.sd.add_identity('automation', 'command-node', name, node) - self.sd.add_feature('http://jabber.org/protocol/commands', node) - self.sd.add_feature('jabber:x:data', node) - self.commands[node] = (name, form, pointer, multi) - - def getNewSession(self): - return str(time.time()) + '-' + self.xmpp.getNewId() - - def handler_command(self, xml): - in_command = xml.find('{http://jabber.org/protocol/commands}command') - sessionid = in_command.get('sessionid', None) - node = in_command.get('node') - sessionid = self.getNewSession() - name, form, pointer, multi = self.commands[node] - self.sessions[sessionid] = {} - self.sessions[sessionid]['jid'] = xml.get('from') - self.sessions[sessionid]['to'] = xml.get('to') - self.sessions[sessionid]['past'] = [(form, None)] - self.sessions[sessionid]['next'] = pointer - npointer = pointer - if multi: - actions = ['next'] - status = 'executing' - else: - if pointer is None: - status = 'completed' - actions = [] - else: - status = 'executing' - actions = ['complete'] - self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions)) - - def handler_command_complete(self, xml): - in_command = xml.find('{http://jabber.org/protocol/commands}command') - sessionid = in_command.get('sessionid', None) - pointer = self.sessions[sessionid]['next'] - results = self.xmpp.plugin['old_0004'].makeForm('result') - results.fromXML(in_command.find('{jabber:x:data}x')) - pointer(results,sessionid) - self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=None, id=xml.attrib['id'], sessionid=sessionid, status='completed', actions=[])) - del self.sessions[in_command.get('sessionid')] - - - def handler_command_next(self, xml): - in_command = xml.find('{http://jabber.org/protocol/commands}command') - sessionid = in_command.get('sessionid', None) - pointer = self.sessions[sessionid]['next'] - results = self.xmpp.plugin['old_0004'].makeForm('result') - results.fromXML(in_command.find('{jabber:x:data}x')) - form, npointer, next = pointer(results,sessionid) - self.sessions[sessionid]['next'] = npointer - self.sessions[sessionid]['past'].append((form, pointer)) - actions = [] - actions.append('prev') - if npointer is None: - status = 'completed' - else: - status = 'executing' - if next: - actions.append('next') - else: - actions.append('complete') - self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions)) - - def handler_command_cancel(self, xml): - command = xml.find('{http://jabber.org/protocol/commands}command') - try: - del self.sessions[command.get('sessionid')] - except: - pass - self.xmpp.send(self.makeCommand(xml.attrib['from'], command.attrib['node'], id=xml.attrib['id'], sessionid=command.attrib['sessionid'], status='canceled')) - - def makeCommand(self, to, node, id=None, form=None, sessionid=None, status='executing', actions=[]): - if not id: - id = self.xmpp.getNewId() - iq = self.xmpp.makeIqResult(id) - iq.attrib['from'] = self.xmpp.boundjid.full - iq.attrib['to'] = to - command = ET.Element('{http://jabber.org/protocol/commands}command') - command.attrib['node'] = node - command.attrib['status'] = status - xmlactions = ET.Element('actions') - for action in actions: - xmlactions.append(ET.Element(action)) - if xmlactions: - command.append(xmlactions) - if not sessionid: - sessionid = self.getNewSession() - else: - iq.attrib['from'] = self.sessions[sessionid]['to'] - command.attrib['sessionid'] = sessionid - if form is not None: - if hasattr(form,'getXML'): - form = form.getXML() - command.append(form) - iq.append(command) - return iq diff --git a/sleekxmpp/plugins/old_0060.py b/sleekxmpp/plugins/old_0060.py deleted file mode 100644 index 93124fca..00000000 --- a/sleekxmpp/plugins/old_0060.py +++ /dev/null @@ -1,313 +0,0 @@ -from __future__ import with_statement -from . import base -import logging -#from xml.etree import cElementTree as ET -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET -from . import stanza_pubsub -from . xep_0004 import Form - - -log = logging.getLogger(__name__) - - -class xep_0060(base.base_plugin): - """ - XEP-0060 Publish Subscribe - """ - - def plugin_init(self): - self.xep = '0060' - self.description = 'Publish-Subscribe' - - def create_node(self, jid, node, config=None, collection=False, ntype=None): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - create = ET.Element('create') - create.set('node', node) - pubsub.append(create) - configure = ET.Element('configure') - if collection: - ntype = 'collection' - #if config is None: - # submitform = self.xmpp.plugin['xep_0004'].makeForm('submit') - #else: - if config is not None: - submitform = config - if 'FORM_TYPE' in submitform.field: - submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config') - else: - submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config') - if ntype: - if 'pubsub#node_type' in submitform.field: - submitform.field['pubsub#node_type'].setValue(ntype) - else: - submitform.addField('pubsub#node_type', value=ntype) - else: - if 'pubsub#node_type' in submitform.field: - submitform.field['pubsub#node_type'].setValue('leaf') - else: - submitform.addField('pubsub#node_type', value='leaf') - submitform['type'] = 'submit' - configure.append(submitform.xml) - pubsub.append(configure) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is False or result is None or result['type'] == 'error': return False - return True - - def subscribe(self, jid, node, bare=True, subscribee=None): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - subscribe = ET.Element('subscribe') - subscribe.attrib['node'] = node - if subscribee is None: - if bare: - subscribe.attrib['jid'] = self.xmpp.boundjid.bare - else: - subscribe.attrib['jid'] = self.xmpp.boundjid.full - else: - subscribe.attrib['jid'] = subscribee - pubsub.append(subscribe) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is False or result is None or result['type'] == 'error': return False - return True - - def unsubscribe(self, jid, node, bare=True, subscribee=None): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - unsubscribe = ET.Element('unsubscribe') - unsubscribe.attrib['node'] = node - if subscribee is None: - if bare: - unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare - else: - unsubscribe.attrib['jid'] = self.xmpp.boundjid.full - else: - unsubscribe.attrib['jid'] = subscribee - pubsub.append(unsubscribe) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is False or result is None or result['type'] == 'error': return False - return True - - def getNodeConfig(self, jid, node=None): # if no node, then grab default - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - if node is not None: - configure = ET.Element('configure') - configure.attrib['node'] = node - else: - configure = ET.Element('default') - pubsub.append(configure) - #TODO: Add configure support. - iq = self.xmpp.makeIqGet() - iq.append(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - #self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse) - result = iq.send() - if result is None or result == False or result['type'] == 'error': - log.warning("got error instead of config") - return False - if node is not None: - form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x') - else: - form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x') - if not form or form is None: - log.error("No form found.") - return False - return Form(xml=form) - - def getNodeSubscriptions(self, jid, node): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - subscriptions = ET.Element('subscriptions') - subscriptions.attrib['node'] = node - pubsub.append(subscriptions) - iq = self.xmpp.makeIqGet() - iq.append(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result == False or result['type'] == 'error': - log.warning("got error instead of config") - return False - else: - results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription') - if results is None: - return False - subs = {} - for sub in results: - subs[sub.get('jid')] = sub.get('subscription') - return subs - - def getNodeAffiliations(self, jid, node): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - affiliations = ET.Element('affiliations') - affiliations.attrib['node'] = node - pubsub.append(affiliations) - iq = self.xmpp.makeIqGet() - iq.append(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result == False or result['type'] == 'error': - log.warning("got error instead of config") - return False - else: - results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation') - if results is None: - return False - subs = {} - for sub in results: - subs[sub.get('jid')] = sub.get('affiliation') - return subs - - def deleteNode(self, jid, node): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - iq = self.xmpp.makeIqSet() - delete = ET.Element('delete') - delete.attrib['node'] = node - pubsub.append(delete) - iq.append(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - result = iq.send() - if result is not None and result is not False and result['type'] != 'error': - return True - else: - return False - - - def setNodeConfig(self, jid, node, config): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - configure = ET.Element('configure') - configure.attrib['node'] = node - config = config.getXML('submit') - configure.append(config) - pubsub.append(configure) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result['type'] == 'error': - return False - return True - - def setItem(self, jid, node, items=[]): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - publish = ET.Element('publish') - publish.attrib['node'] = node - for pub_item in items: - id, payload = pub_item - item = ET.Element('item') - if id is not None: - item.attrib['id'] = id - item.append(payload) - publish.append(item) - pubsub.append(publish) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result is False or result['type'] == 'error': return False - return True - - def addItem(self, jid, node, items=[]): - return self.setItem(jid, node, items) - - def deleteItem(self, jid, node, item): - pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') - retract = ET.Element('retract') - retract.attrib['node'] = node - itemn = ET.Element('item') - itemn.attrib['id'] = item - retract.append(itemn) - pubsub.append(retract) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result is False or result['type'] == 'error': return False - return True - - def getNodes(self, jid): - response = self.xmpp.plugin['xep_0030'].getItems(jid) - items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') - nodes = {} - if items is not None and items is not False: - for item in items: - nodes[item.get('node')] = item.get('name') - return nodes - - def getItems(self, jid, node): - response = self.xmpp.plugin['xep_0030'].getItems(jid, node) - items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') - nodeitems = [] - if items is not None and items is not False: - for item in items: - nodeitems.append(item.get('node')) - return nodeitems - - def addNodeToCollection(self, jid, child, parent=''): - config = self.getNodeConfig(jid, child) - if not config or config is None: - self.lasterror = "Config Error" - return False - try: - config.field['pubsub#collection'].setValue(parent) - except KeyError: - log.warning("pubsub#collection doesn't exist in config, trying to add it") - config.addField('pubsub#collection', value=parent) - if not self.setNodeConfig(jid, child, config): - return False - return True - - def modifyAffiliation(self, ps_jid, node, user_jid, affiliation): - if affiliation not in ('owner', 'publisher', 'member', 'none', 'outcast'): - raise TypeError - pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') - affs = ET.Element('affiliations') - affs.attrib['node'] = node - aff = ET.Element('affiliation') - aff.attrib['jid'] = user_jid - aff.attrib['affiliation'] = affiliation - affs.append(aff) - pubsub.append(affs) - iq = self.xmpp.makeIqSet(pubsub) - iq.attrib['to'] = ps_jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq['id'] - result = iq.send() - if result is None or result is False or result['type'] == 'error': - return False - return True - - def addNodeToCollection(self, jid, child, parent=''): - config = self.getNodeConfig(jid, child) - if not config or config is None: - self.lasterror = "Config Error" - return False - try: - config.field['pubsub#collection'].setValue(parent) - except KeyError: - log.warning("pubsub#collection doesn't exist in config, trying to add it") - config.addField('pubsub#collection', value=parent) - if not self.setNodeConfig(jid, child, config): - return False - return True - - def removeNodeFromCollection(self, jid, child): - self.addNodeToCollection(jid, child, '') - diff --git a/sleekxmpp/plugins/xep_0004/stanza/field.py b/sleekxmpp/plugins/xep_0004/stanza/field.py index 1e175966..51f85995 100644 --- a/sleekxmpp/plugins/xep_0004/stanza/field.py +++ b/sleekxmpp/plugins/xep_0004/stanza/field.py @@ -41,10 +41,11 @@ class FormField(ElementBase): self._type = value def add_option(self, label='', value=''): - if self._type in self.option_types: - opt = FieldOption(parent=self) + if self._type is None or self._type in self.option_types: + opt = FieldOption() opt['label'] = label opt['value'] = value + self.append(opt) else: raise ValueError("Cannot add options to " + \ "a %s field." % self['type']) diff --git a/sleekxmpp/plugins/xep_0004/stanza/form.py b/sleekxmpp/plugins/xep_0004/stanza/form.py index 721ecc35..1d733760 100644 --- a/sleekxmpp/plugins/xep_0004/stanza/form.py +++ b/sleekxmpp/plugins/xep_0004/stanza/form.py @@ -65,7 +65,7 @@ class Form(ElementBase): if kwtype is None: kwtype = ftype - field = FormField(parent=self) + field = FormField() field['var'] = var field['type'] = kwtype field['value'] = value @@ -77,6 +77,7 @@ class Form(ElementBase): field['options'] = options else: del field['type'] + self.append(field) return field def getXML(self, type='submit'): @@ -144,14 +145,12 @@ class Form(ElementBase): def get_fields(self, use_dict=False): fields = OrderedDict() - fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) - for fieldXML in fieldsXML: - field = FormField(xml=fieldXML) - fields[field['var']] = field + for stanza in self['substanzas']: + if isinstance(stanza, FormField): + fields[stanza['var']] = stanza return fields def get_instructions(self): - instructions = '' instsXML = self.xml.findall('{%s}instructions' % self.namespace) return "\n".join([instXML.text for instXML in instsXML]) @@ -195,7 +194,14 @@ class Form(ElementBase): fields = fields.items() for var, field in fields: field['var'] = var - self.add_field(**field) + self.add_field( + var = field.get('var'), + label = field.get('label'), + desc = field.get('desc'), + required = field.get('required'), + value = field.get('value'), + options = field.get('options'), + type = field.get('type')) def set_instructions(self, instructions): del self['instructions'] @@ -221,6 +227,8 @@ class Form(ElementBase): def set_values(self, values): fields = self['fields'] for field in values: + if field not in fields: + fields[field] = self.add_field(var=field) fields[field]['value'] = values[field] def merge(self, other): diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py index 8c08e8f3..b02f587e 100644 --- a/sleekxmpp/plugins/xep_0009/remote.py +++ b/sleekxmpp/plugins/xep_0009/remote.py @@ -6,7 +6,7 @@ See the file LICENSE for copying permission. """ -from binding import py2xml, xml2py, xml2fault, fault2xml +from sleekxmpp.plugins.xep_0009.binding import py2xml, xml2py, xml2fault, fault2xml from threading import RLock import abc import inspect @@ -18,6 +18,45 @@ import traceback log = logging.getLogger(__name__) +# Define a function _isstr() to check if an object is a string in a way +# compatible with Python 2 and Python 3 (basestring does not exists in Python 3). +try: + basestring # This evaluation will throw an exception if basestring does not exists (Python 3). + def _isstr(obj): + return isinstance(obj, basestring) +except NameError: + def _isstr(obj): + return isinstance(obj, str) + + +# Class decorator to declare a metaclass to a class in a way compatible with Python 2 and 3. +# This decorator is copied from 'six' (https://bitbucket.org/gutworth/six): +# +# Copyright (c) 2010-2015 Benjamin Peterson +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +def _add_metaclass(metaclass): + def wrapper(cls): + orig_vars = cls.__dict__.copy() + slots = orig_vars.get('__slots__') + if slots is not None: + if isinstance(slots, str): + slots = [slots] + for slots_var in slots: + orig_vars.pop(slots_var) + orig_vars.pop('__dict__', None) + orig_vars.pop('__weakref__', None) + return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper + def _intercept(method, name, public): def _resolver(instance, *args, **kwargs): log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args) @@ -68,7 +107,7 @@ def remote(function_argument, public = True): if hasattr(function_argument, '__call__'): return _intercept(function_argument, None, public) else: - if not isinstance(function_argument, basestring): + if not _isstr(function_argument): if not isinstance(function_argument, bool): raise Exception('Expected an RPC method name or visibility modifier!') else: @@ -222,12 +261,11 @@ class TimeoutException(Exception): pass +@_add_metaclass(abc.ABCMeta) class Callback(object): ''' A base class for callback handlers. ''' - __metaclass__ = abc.ABCMeta - @abc.abstractproperty def set_value(self, value): @@ -291,7 +329,7 @@ class Future(Callback): self._event.set() - +@_add_metaclass(abc.ABCMeta) class Endpoint(object): ''' The Endpoint class is an abstract base class for all objects @@ -303,8 +341,6 @@ class Endpoint(object): which specifies which object an RPC call refers to. It is the first part in a RPC method name '<fqn>.<method>'. ''' - __metaclass__ = abc.ABCMeta - def __init__(self, session, target_jid): ''' @@ -491,7 +527,7 @@ class RemoteSession(object): def _find_key(self, dict, value): """return the key of dictionary dic given the value""" - search = [k for k, v in dict.iteritems() if v == value] + search = [k for k, v in dict.items() if v == value] if len(search) == 0: return None else: @@ -547,7 +583,7 @@ class RemoteSession(object): result = handler_cls(*args, **kwargs) Endpoint.__init__(result, self, self._client.boundjid.full) method_dict = result.get_methods() - for method_name, method in method_dict.iteritems(): + for method_name, method in method_dict.items(): #!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name) self._register_call(result.FQN(), method, method_name) self._register_acl(result.FQN(), acl) @@ -569,11 +605,11 @@ class RemoteSession(object): self._register_callback(pid, callback) iq.send() - def close(self): + def close(self, wait=False): ''' Closes this session. ''' - self._client.disconnect(False) + self._client.disconnect(wait=wait) self._session_close_callback() def _on_jabber_rpc_method_call(self, iq): @@ -697,7 +733,8 @@ class Remote(object): if(client.boundjid.bare in cls._sessions): raise RemoteException("There already is a session associated with these credentials!") else: - cls._sessions[client.boundjid.bare] = client; + cls._sessions[client.boundjid.bare] = client + def _session_close_callback(): with Remote._lock: del cls._sessions[client.boundjid.bare] diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py index 4e1c538b..6179355e 100644 --- a/sleekxmpp/plugins/xep_0009/rpc.py +++ b/sleekxmpp/plugins/xep_0009/rpc.py @@ -32,15 +32,15 @@ class XEP_0009(BasePlugin): register_stanza_plugin(RPCQuery, MethodCall)
register_stanza_plugin(RPCQuery, MethodResponse)
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodCall' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
self._handle_method_call)
)
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)),
self._handle_method_response)
)
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('RPC Call', MatchXPath('{%s}iq/{%s}error' % (self.xmpp.default_ns, self.xmpp.default_ns)),
self._handle_error)
)
@@ -61,7 +61,7 @@ class XEP_0009(BasePlugin): iq.enable('rpc_query')
iq['rpc_query']['method_call']['method_name'] = pmethod
iq['rpc_query']['method_call']['params'] = params
- return iq;
+ return iq
def make_iq_method_response(self, pid, pto, params):
iq = self.xmpp.makeIqResult(pid)
@@ -93,7 +93,7 @@ class XEP_0009(BasePlugin): def _item_not_found(self, iq):
payload = iq.get_payload()
- iq.reply().error().set_payload(payload);
+ iq.reply().error().set_payload(payload)
iq['error']['code'] = '404'
iq['error']['type'] = 'cancel'
iq['error']['condition'] = 'item-not-found'
diff --git a/sleekxmpp/plugins/xep_0020/__init__.py b/sleekxmpp/plugins/xep_0020/__init__.py new file mode 100644 index 00000000..c6aafe97 --- /dev/null +++ b/sleekxmpp/plugins/xep_0020/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0020 import stanza +from sleekxmpp.plugins.xep_0020.stanza import FeatureNegotiation +from sleekxmpp.plugins.xep_0020.feature_negotiation import XEP_0020 + + +register_plugin(XEP_0020) diff --git a/sleekxmpp/plugins/xep_0020/feature_negotiation.py b/sleekxmpp/plugins/xep_0020/feature_negotiation.py new file mode 100644 index 00000000..7cb82cd5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0020/feature_negotiation.py @@ -0,0 +1,36 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Iq, Message +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0020 import stanza, FeatureNegotiation +from sleekxmpp.plugins.xep_0004 import Form + + +log = logging.getLogger(__name__) + + +class XEP_0020(BasePlugin): + + name = 'xep_0020' + description = 'XEP-0020: Feature Negotiation' + dependencies = set(['xep_0004', 'xep_0030']) + stanza = stanza + + def plugin_init(self): + self.xmpp['xep_0030'].add_feature(FeatureNegotiation.namespace) + + register_stanza_plugin(FeatureNegotiation, Form) + + register_stanza_plugin(Iq, FeatureNegotiation) + register_stanza_plugin(Message, FeatureNegotiation) diff --git a/sleekxmpp/plugins/xep_0020/stanza.py b/sleekxmpp/plugins/xep_0020/stanza.py new file mode 100644 index 00000000..13e4056e --- /dev/null +++ b/sleekxmpp/plugins/xep_0020/stanza.py @@ -0,0 +1,17 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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 + + +class FeatureNegotiation(ElementBase): + + name = 'feature' + namespace = 'http://jabber.org/protocol/feature-neg' + plugin_attrib = 'feature_neg' + interfaces = set() diff --git a/sleekxmpp/plugins/xep_0027/gpg.py b/sleekxmpp/plugins/xep_0027/gpg.py index 2aa6e5a0..52c1c461 100644 --- a/sleekxmpp/plugins/xep_0027/gpg.py +++ b/sleekxmpp/plugins/xep_0027/gpg.py @@ -24,7 +24,7 @@ def _extract_data(data, kind): if not begin_headers and 'BEGIN PGP %s' % kind in line: begin_headers = True continue - if begin_headers and line.stripped() == '': + if begin_headers and line.strip() == '': begin_data = True continue if 'END PGP %s' % kind in line: diff --git a/sleekxmpp/plugins/xep_0027/stanza.py b/sleekxmpp/plugins/xep_0027/stanza.py index 3170ca6e..08f2032b 100644 --- a/sleekxmpp/plugins/xep_0027/stanza.py +++ b/sleekxmpp/plugins/xep_0027/stanza.py @@ -39,7 +39,7 @@ class Encrypted(ElementBase): def set_encrypted(self, value): parent = self.parent() xmpp = parent.stream - data = xmpp['xep_0027'].encrypt(value, parent['to'].bare) + data = xmpp['xep_0027'].encrypt(value, parent['to']) if data: self.xml.text = data else: diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 8a397923..721f73f6 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -324,7 +324,7 @@ class XEP_0030(BasePlugin): callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. - timeout_callback -- Optional callback to execute when no result + timeout_callback -- Optional callback to execute when no result has been received in timeout seconds. """ if local is None: @@ -408,7 +408,7 @@ class XEP_0030(BasePlugin): iterator -- If True, return a result set iterator using the XEP-0059 plugin, if the plugin is loaded. Otherwise the parameter is ignored. - timeout_callback -- Optional callback to execute when no result + timeout_callback -- Optional callback to execute when no result has been received in timeout seconds. """ if local or local is None and jid is None: @@ -604,7 +604,7 @@ class XEP_0030(BasePlugin): """ self.api['del_features'](jid, node, None, kwargs) - def _run_node_handler(self, htype, jid, node=None, ifrom=None, data={}): + def _run_node_handler(self, htype, jid, node=None, ifrom=None, data=None): """ Execute the most specific node handler for the given JID/node combination. @@ -615,6 +615,9 @@ class XEP_0030(BasePlugin): node -- The node requested. data -- Optional, custom data to pass to the handler. """ + if not data: + data = {} + return self.api[htype](jid, node, ifrom, data) def _handle_disco_info(self, iq): diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py index 512f2336..10458614 100644 --- a/sleekxmpp/plugins/xep_0030/stanza/items.py +++ b/sleekxmpp/plugins/xep_0030/stanza/items.py @@ -128,9 +128,10 @@ class DiscoItems(ElementBase): def del_items(self): """Remove all items.""" self._items = set() - for item in self['substanzas']: - if isinstance(item, DiscoItem): - self.xml.remove(item.xml) + items = [i for i in self.iterables if isinstance(i, DiscoItem)] + for item in items: + self.xml.remove(item.xml) + self.iterables.remove(item) class DiscoItem(ElementBase): diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index cba07702..ca5ed1ef 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -125,11 +125,12 @@ class XEP_0045(BasePlugin): self.xep = '0045' # load MUC support in presence stanzas 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)) - self.xmpp.registerHandler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change)) - self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % ( + self.xmpp.register_handler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence)) + self.xmpp.register_handler(Callback('MUCError', MatchXMLMask("<message xmlns='%s' type='error'><error/></message>" % self.xmpp.default_ns), self.handle_groupchat_error_message)) + self.xmpp.register_handler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message)) + self.xmpp.register_handler(Callback('MUCSubject', MatchXMLMask("<message xmlns='%s' type='groupchat'><subject/></message>" % self.xmpp.default_ns), self.handle_groupchat_subject)) + self.xmpp.register_handler(Callback('MUCConfig', MatchXMLMask("<message xmlns='%s' type='groupchat'><x xmlns='http://jabber.org/protocol/muc#user'><status/></x></message>" % self.xmpp.default_ns), self.handle_config_change)) + self.xmpp.register_handler(Callback('MUCInvite', MatchXPath("{%s}message/{%s}x/{%s}invite" % ( self.xmpp.default_ns, 'http://jabber.org/protocol/muc#user', 'http://jabber.org/protocol/muc#user')), self.handle_groupchat_invite)) @@ -179,6 +180,14 @@ class XEP_0045(BasePlugin): self.xmpp.event('groupchat_message', msg) self.xmpp.event("muc::%s::message" % msg['from'].bare, msg) + def handle_groupchat_error_message(self, msg): + """ Handle a message error event in a muc. + """ + self.xmpp.event('groupchat_message_error', msg) + self.xmpp.event("muc::%s::message_error" % msg['from'].bare, msg) + + + def handle_groupchat_subject(self, msg): """ Handle a message coming from a muc indicating a change of subject (or announcing it when joining the room) @@ -198,30 +207,9 @@ class XEP_0045(BasePlugin): if entry is not None and entry['jid'].full == jid: return nick - def getRoomForm(self, room, ifrom=None): - iq = self.xmpp.makeIqGet() - iq['to'] = room - if ifrom is not None: - iq['from'] = ifrom - query = ET.Element('{http://jabber.org/protocol/muc#owner}query') - iq.append(query) - # For now, swallow errors to preserve existing API - try: - result = iq.send() - except IqError: - return False - except IqTimeout: - return False - xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') - if xform is None: return False - form = self.xmpp.plugin['old_0004'].buildForm(xform) - return form - def configureRoom(self, room, form=None, ifrom=None): if form is None: - form = self.getRoomForm(room, ifrom=ifrom) - #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit') - #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig') + form = self.getRoomConfig(room, ifrom=ifrom) iq = self.xmpp.makeIqSet() iq['to'] = room if ifrom is not None: @@ -310,6 +298,24 @@ class XEP_0045(BasePlugin): return False return True + def setRole(self, room, nick, role): + """ Change role property of a nick in a room. + Typically, roles are temporary (they last only as long as you are in the + room), whereas affiliations are permanent (they last across groupchat + sessions). + """ + if role not in ('moderator', 'participant', 'visitor', 'none'): + raise TypeError + query = ET.Element('{http://jabber.org/protocol/muc#admin}query') + item = ET.Element('item', {'role':role, 'nick':nick}) + query.append(item) + iq = self.xmpp.makeIqSet(query) + iq['to'] = room + result = iq.send() + if result is False or result['type'] != 'result': + raise ValueError + return True + def invite(self, room, jid, reason='', mfrom=''): """ Invite a jid to a room.""" msg = self.xmpp.makeMessage(room) diff --git a/sleekxmpp/plugins/xep_0047/ibb.py b/sleekxmpp/plugins/xep_0047/ibb.py index fb48a9b9..62dddac2 100644 --- a/sleekxmpp/plugins/xep_0047/ibb.py +++ b/sleekxmpp/plugins/xep_0047/ibb.py @@ -21,21 +21,25 @@ class XEP_0047(BasePlugin): dependencies = set(['xep_0030']) stanza = stanza default_config = { + 'block_size': 4096, 'max_block_size': 8192, 'window_size': 1, - 'auto_accept': True, - 'accept_stream': None + 'auto_accept': False, } def plugin_init(self): - self.streams = {} - self.pending_streams = {} - self.pending_close_streams = {} + self._streams = {} + self._pending_streams = {} + self._pending_lock = threading.Lock() self._stream_lock = threading.Lock() + self._preauthed_sids_lock = threading.Lock() + self._preauthed_sids = {} + register_stanza_plugin(Iq, Open) register_stanza_plugin(Iq, Close) register_stanza_plugin(Iq, Data) + register_stanza_plugin(Message, Data) self.xmpp.register_handler(Callback( 'IBB Open', @@ -52,27 +56,71 @@ class XEP_0047(BasePlugin): StanzaPath('iq@type=set/ibb_data'), self._handle_data)) + self.xmpp.register_handler(Callback( + 'IBB Message Data', + StanzaPath('message/ibb_data'), + self._handle_data)) + + self.api.register(self._authorized, 'authorized', default=True) + self.api.register(self._authorized_sid, 'authorized_sid', default=True) + self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) + self.api.register(self._get_stream, 'get_stream', default=True) + self.api.register(self._set_stream, 'set_stream', default=True) + self.api.register(self._del_stream, 'del_stream', default=True) + def plugin_end(self): self.xmpp.remove_handler('IBB Open') self.xmpp.remove_handler('IBB Close') self.xmpp.remove_handler('IBB Data') + self.xmpp.remove_handler('IBB Message Data') self.xmpp['xep_0030'].del_feature(feature='http://jabber.org/protocol/ibb') def session_bind(self, jid): self.xmpp['xep_0030'].add_feature('http://jabber.org/protocol/ibb') + def _get_stream(self, jid, sid, peer_jid, data): + return self._streams.get((jid, sid, peer_jid), None) + + def _set_stream(self, jid, sid, peer_jid, stream): + self._streams[(jid, sid, peer_jid)] = stream + + def _del_stream(self, jid, sid, peer_jid, data): + with self._stream_lock: + if (jid, sid, peer_jid) in self._streams: + del self._streams[(jid, sid, peer_jid)] + def _accept_stream(self, iq): - if self.accept_stream is not None: - return self.accept_stream(iq) + receiver = iq['to'] + sender = iq['from'] + sid = iq['ibb_open']['sid'] + + if self.api['authorized_sid'](receiver, sid, sender, iq): + return True + return self.api['authorized'](receiver, sid, sender, iq) + + def _authorized(self, jid, sid, ifrom, 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, + def _authorized_sid(self, jid, sid, ifrom, iq): + with self._preauthed_sids_lock: + if (jid, sid, ifrom) in self._preauthed_sids: + del self._preauthed_sids[(jid, sid, ifrom)] + return True + return False + + def _preauthorize_sid(self, jid, sid, ifrom, data): + with self._preauthed_sids_lock: + self._preauthed_sids[(jid, sid, ifrom)] = True + + def open_stream(self, jid, block_size=None, sid=None, window=1, use_messages=False, ifrom=None, block=True, timeout=None, callback=None): if sid is None: sid = str(uuid.uuid4()) + if block_size is None: + block_size = self.block_size iq = self.xmpp.Iq() iq['type'] = 'set' @@ -83,12 +131,13 @@ class XEP_0047(BasePlugin): iq['ibb_open']['stanza'] = 'iq' stream = IBBytestream(self.xmpp, sid, block_size, - iq['to'], iq['from'], window) + iq['from'], iq['to'], window, + use_messages) with self._stream_lock: - self.pending_streams[iq['id']] = stream + self._pending_streams[iq['id']] = stream - self.pending_streams[iq['id']] = stream + self._pending_streams[iq['id']] = stream if block: resp = iq.send(timeout=timeout) @@ -108,49 +157,59 @@ class XEP_0047(BasePlugin): 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) + stream = self._pending_streams.get(iq['id'], None) + if stream is not None: + log.debug('IBB stream (%s) accepted by %s', stream.sid, iq['from']) + stream.self_jid = iq['to'] + stream.peer_jid = iq['from'] + stream.stream_started.set() + self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) + self.xmpp.event('ibb_stream_start', stream) + self.xmpp.event('stream:%s:%s' % (stream.sid, stream.peer_jid), stream) with self._stream_lock: - if iq['id'] in self.pending_streams: - del self.pending_streams[iq['id']] + 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'] + size = iq['ibb_open']['block_size'] or self.block_size + + log.debug('Received IBB stream request from %s', iq['from']) + + if not sid: + raise XMPPError(etype='modify', condition='bad-request') + if not self._accept_stream(iq): - raise XMPPError('not-acceptable') + raise XMPPError(etype='modify', condition='not-acceptable') if size > self.max_block_size: raise XMPPError('resource-constraint') stream = IBBytestream(self.xmpp, sid, size, - iq['from'], iq['to'], + iq['to'], iq['from'], self.window_size) stream.stream_started.set() - self.streams[sid] = stream + self.api['set_stream'](stream.self_jid, stream.sid, stream.peer_jid, stream) iq.reply() iq.send() self.xmpp.event('ibb_stream_start', stream) + self.xmpp.event('stream:%s:%s' % (sid, stream.peer_jid), 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) + def _handle_data(self, stanza): + sid = stanza['ibb_data']['sid'] + stream = self.api['get_stream'](stanza['to'], sid, stanza['from']) + if stream is not None and stanza['from'] == stream.peer_jid: + stream._recv_data(stanza) 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 = self.api['get_stream'](iq['to'], sid, iq['from']) + if stream is not None and iq['from'] == stream.peer_jid: stream._closed(iq) + self.api['del_stream'](stream.self_jid, stream.sid, stream.peer_jid) else: raise XMPPError('item-not-found') diff --git a/sleekxmpp/plugins/xep_0047/stream.py b/sleekxmpp/plugins/xep_0047/stream.py index b49a077b..9651edf8 100644 --- a/sleekxmpp/plugins/xep_0047/stream.py +++ b/sleekxmpp/plugins/xep_0047/stream.py @@ -2,6 +2,7 @@ import socket import threading import logging +from sleekxmpp.stanza import Iq from sleekxmpp.util import Queue from sleekxmpp.exceptions import XMPPError @@ -11,14 +12,17 @@ log = logging.getLogger(__name__) class IBBytestream(object): - def __init__(self, xmpp, sid, block_size, to, ifrom, window_size=1): + def __init__(self, xmpp, sid, block_size, jid, peer, window_size=1, use_messages=False): self.xmpp = xmpp self.sid = sid self.block_size = block_size self.window_size = window_size + self.use_messages = use_messages - self.receiver = to - self.sender = ifrom + if jid is None: + jid = xmpp.boundjid + self.self_jid = jid + self.peer_jid = peer self.send_seq = -1 self.recv_seq = -1 @@ -46,16 +50,27 @@ class IBBytestream(object): 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) + if self.use_messages: + msg = self.xmpp.Message() + msg['to'] = self.peer_jid + msg['from'] = self.self_jid + msg['id'] = self.xmpp.new_id() + msg['ibb_data']['sid'] = self.sid + msg['ibb_data']['seq'] = seq + msg['ibb_data']['data'] = data + msg.send() + self.send_window.release() + else: + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = self.peer_jid + iq['from'] = self.self_jid + 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): @@ -71,23 +86,25 @@ class IBBytestream(object): if iq['type'] == 'error': self.close() - def _recv_data(self, iq): + def _recv_data(self, stanza): with self._recv_seq_lock: - new_seq = iq['ibb_data']['seq'] + new_seq = stanza['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'] + data = stanza['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() + + if isinstance(stanza, Iq): + stanza.reply() + stanza.send() def recv(self, *args, **kwargs): return self.read(block=True) @@ -106,8 +123,8 @@ class IBBytestream(object): def close(self): iq = self.xmpp.Iq() iq['type'] = 'set' - iq['to'] = self.receiver - iq['from'] = self.sender + iq['to'] = self.peer_jid + iq['from'] = self.self_jid iq['ibb_close']['sid'] = self.sid self.stream_out_closed.set() iq.send(block=False, @@ -117,9 +134,6 @@ class IBBytestream(object): 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) diff --git a/sleekxmpp/plugins/xep_0048/__init__.py b/sleekxmpp/plugins/xep_0048/__init__.py new file mode 100644 index 00000000..2c98d061 --- /dev/null +++ b/sleekxmpp/plugins/xep_0048/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0048.stanza import Bookmarks, Conference, URL +from sleekxmpp.plugins.xep_0048.bookmarks import XEP_0048 + + +register_plugin(XEP_0048) diff --git a/sleekxmpp/plugins/xep_0048/bookmarks.py b/sleekxmpp/plugins/xep_0048/bookmarks.py new file mode 100644 index 00000000..0bb5ae38 --- /dev/null +++ b/sleekxmpp/plugins/xep_0048/bookmarks.py @@ -0,0 +1,76 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Iq +from sleekxmpp.plugins import BasePlugin +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.xep_0048 import stanza, Bookmarks, Conference, URL + + +log = logging.getLogger(__name__) + + +class XEP_0048(BasePlugin): + + name = 'xep_0048' + description = 'XEP-0048: Bookmarks' + dependencies = set(['xep_0045', 'xep_0049', 'xep_0060', 'xep_0163', 'xep_0223']) + stanza = stanza + default_config = { + 'auto_join': False, + 'storage_method': 'xep_0049' + } + + def plugin_init(self): + register_stanza_plugin(self.xmpp['xep_0060'].stanza.Item, Bookmarks) + + self.xmpp['xep_0049'].register(Bookmarks) + self.xmpp['xep_0163'].register_pep('bookmarks', Bookmarks) + + self.xmpp.add_event_handler('session_start', self._autojoin) + + def plugin_end(self): + self.xmpp.del_event_handler('session_start', self._autojoin) + + def _autojoin(self, __): + if not self.auto_join: + return + + try: + result = self.get_bookmarks(method=self.storage_method) + except XMPPError: + return + + if self.storage_method == 'xep_0223': + bookmarks = result['pubsub']['items']['item']['bookmarks'] + else: + bookmarks = result['private']['bookmarks'] + + for conf in bookmarks['conferences']: + if conf['autojoin']: + log.debug('Auto joining %s as %s', conf['jid'], conf['nick']) + self.xmpp['xep_0045'].joinMUC(conf['jid'], conf['nick'], + password=conf['password']) + + def set_bookmarks(self, bookmarks, method=None, **iqargs): + if not method: + method = self.storage_method + return self.xmpp[method].store(bookmarks, **iqargs) + + def get_bookmarks(self, method=None, **iqargs): + if not method: + method = self.storage_method + + loc = 'storage:bookmarks' if method == 'xep_0223' else 'bookmarks' + + return self.xmpp[method].retrieve(loc, **iqargs) diff --git a/sleekxmpp/plugins/xep_0048/stanza.py b/sleekxmpp/plugins/xep_0048/stanza.py new file mode 100644 index 00000000..21829392 --- /dev/null +++ b/sleekxmpp/plugins/xep_0048/stanza.py @@ -0,0 +1,65 @@ +""" + 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 ET, ElementBase, register_stanza_plugin + + +class Bookmarks(ElementBase): + name = 'storage' + namespace = 'storage:bookmarks' + plugin_attrib = 'bookmarks' + interfaces = set() + + def add_conference(self, jid, nick, name=None, autojoin=None, password=None): + conf = Conference() + conf['jid'] = jid + conf['nick'] = nick + if name is None: + name = jid + conf['name'] = name + conf['autojoin'] = autojoin + conf['password'] = password + self.append(conf) + + def add_url(self, url, name=None): + saved_url = URL() + saved_url['url'] = url + if name is None: + name = url + saved_url['name'] = name + self.append(saved_url) + + +class Conference(ElementBase): + name = 'conference' + namespace = 'storage:bookmarks' + plugin_attrib = 'conference' + plugin_multi_attrib = 'conferences' + interfaces = set(['nick', 'password', 'autojoin', 'jid', 'name']) + sub_interfaces = set(['nick', 'password']) + + def get_autojoin(self): + value = self._get_attr('autojoin') + return value in ('1', 'true') + + def set_autojoin(self, value): + del self['autojoin'] + if value in ('1', 'true', True): + self._set_attr('autojoin', 'true') + + +class URL(ElementBase): + name = 'url' + namespace = 'storage:bookmarks' + plugin_attrib = 'url' + plugin_multi_attrib = 'urls' + interfaces = set(['url', 'name']) + + +register_stanza_plugin(Bookmarks, Conference, iterable=True) +register_stanza_plugin(Bookmarks, URL, iterable=True) diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py index 90256228..e5594c3f 100644 --- a/sleekxmpp/plugins/xep_0050/adhoc.py +++ b/sleekxmpp/plugins/xep_0050/adhoc.py @@ -267,20 +267,50 @@ class XEP_0050(BasePlugin): iq -- The command continuation request. """ sessionid = iq['command']['sessionid'] - session = self.sessions[sessionid] + session = self.sessions.get(sessionid) - handler = session['next'] - interfaces = session['interfaces'] - results = [] - for stanza in iq['command']['substanzas']: - if stanza.plugin_attrib in interfaces: - results.append(stanza) - if len(results) == 1: - results = results[0] + if session: + handler = session['next'] + interfaces = session['interfaces'] + results = [] + for stanza in iq['command']['substanzas']: + if stanza.plugin_attrib in interfaces: + results.append(stanza) + if len(results) == 1: + results = results[0] - session = handler(results, session) + session = handler(results, session) - self._process_command_response(iq, session) + self._process_command_response(iq, session) + else: + raise XMPPError('item-not-found') + + def _handle_command_prev(self, iq): + """ + Process a request for the prev step in the workflow + for a command with multiple steps. + + Arguments: + iq -- The command continuation request. + """ + sessionid = iq['command']['sessionid'] + session = self.sessions.get(sessionid) + + if session: + handler = session['prev'] + interfaces = session['interfaces'] + results = [] + for stanza in iq['command']['substanzas']: + if stanza.plugin_attrib in interfaces: + results.append(stanza) + if len(results) == 1: + results = results[0] + + session = handler(results, session) + + self._process_command_response(iq, session) + else: + raise XMPPError('item-not-found') def _process_command_response(self, iq, session): """ @@ -348,23 +378,23 @@ class XEP_0050(BasePlugin): """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] - session = self.sessions[sessionid] - handler = session['cancel'] - if handler: - handler(iq, session) + session = self.sessions.get(sessionid) - try: + if session: + handler = session['cancel'] + if handler: + handler(iq, session) del self.sessions[sessionid] - except: - pass + iq.reply() + iq['command']['node'] = node + iq['command']['sessionid'] = sessionid + iq['command']['status'] = 'canceled' + iq['command']['notes'] = session['notes'] + iq.send() + else: + raise XMPPError('item-not-found') - iq.reply() - iq['command']['node'] = node - iq['command']['sessionid'] = sessionid - iq['command']['status'] = 'canceled' - iq['command']['notes'] = session['notes'] - iq.send() def _handle_command_complete(self, iq): """ @@ -378,28 +408,32 @@ class XEP_0050(BasePlugin): """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] - session = self.sessions[sessionid] - handler = session['next'] - interfaces = session['interfaces'] - results = [] - for stanza in iq['command']['substanzas']: - if stanza.plugin_attrib in interfaces: - results.append(stanza) - if len(results) == 1: - results = results[0] + session = self.sessions.get(sessionid) - if handler: - handler(results, session) + if session: + handler = session['next'] + interfaces = session['interfaces'] + results = [] + for stanza in iq['command']['substanzas']: + if stanza.plugin_attrib in interfaces: + results.append(stanza) + if len(results) == 1: + results = results[0] - iq.reply() - iq['command']['node'] = node - iq['command']['sessionid'] = sessionid - iq['command']['actions'] = [] - iq['command']['status'] = 'completed' - iq['command']['notes'] = session['notes'] - iq.send() + if handler: + handler(results, session) + + del self.sessions[sessionid] - del self.sessions[sessionid] + iq.reply() + iq['command']['node'] = node + iq['command']['sessionid'] = sessionid + iq['command']['actions'] = [] + iq['command']['status'] = 'completed' + iq['command']['notes'] = session['notes'] + iq.send() + else: + raise XMPPError('item-not-found') # ================================================================= # Client side (command user) API @@ -537,7 +571,7 @@ class XEP_0050(BasePlugin): else: iq.send(block=False, callback=self._handle_command_result) - def continue_command(self, session): + def continue_command(self, session, direction='next'): """ Execute the next action of the command. @@ -551,7 +585,7 @@ class XEP_0050(BasePlugin): self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), - action='next', + action=direction, payload=session.get('payload', None), sessionid=session['id'], flow=True, diff --git a/sleekxmpp/plugins/xep_0054/vcard_temp.py b/sleekxmpp/plugins/xep_0054/vcard_temp.py index 24da1c05..97da8c7c 100644 --- a/sleekxmpp/plugins/xep_0054/vcard_temp.py +++ b/sleekxmpp/plugins/xep_0054/vcard_temp.py @@ -8,7 +8,7 @@ import logging -from sleekxmpp import Iq +from sleekxmpp import JID, Iq from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback @@ -59,10 +59,20 @@ class XEP_0054(BasePlugin): def make_vcard(self): return VCardTemp() - def get_vcard(self, jid=None, ifrom=None, local=False, cached=False, + def get_vcard(self, jid=None, ifrom=None, local=None, cached=False, block=True, callback=None, timeout=None): - if self.xmpp.is_component and jid.domain == self.xmpp.boundjid.domain: - local = True + if local 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 + elif jid in (None, ''): + local = True if local: vcard = self.api['get_vcard'](jid, None, ifrom) diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py index 952cad85..bec5f565 100644 --- a/sleekxmpp/plugins/xep_0060/pubsub.py +++ b/sleekxmpp/plugins/xep_0060/pubsub.py @@ -423,7 +423,7 @@ class XEP_0060(BasePlugin): callback=None, timeout=None): iq = self.xmpp.Iq(sto=jid, sfrom=ifrom, stype='set') iq['pubsub_owner']['configure']['node'] = node - iq['pubsub_owner']['configure']['form'].values = config.values + iq['pubsub_owner']['configure'].append(config) return iq.send(block=block, callback=callback, timeout=timeout) def publish(self, jid, node, id=None, payload=None, options=None, diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py index b2fe3010..c1907a13 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py @@ -74,7 +74,12 @@ class Item(ElementBase): def set_payload(self, value): del self['payload'] - self.append(value) + if isinstance(value, ElementBase): + if value.tag_name() in self.plugin_tag_map: + self.init_plugin(value.plugin_attrib, existing_xml=value.xml) + self.xml.append(value.xml) + else: + self.xml.append(value) def get_payload(self): childs = list(self.xml) @@ -243,39 +248,6 @@ class PublishOptions(ElementBase): self.parent().xml.remove(self.xml) -class PubsubState(ElementBase): - """This is an experimental pubsub extension.""" - namespace = 'http://jabber.org/protocol/psstate' - name = 'state' - plugin_attrib = 'psstate' - interfaces = set(('node', 'item', 'payload')) - - def set_payload(self, value): - self.xml.append(value) - - def get_payload(self): - childs = list(self.xml) - if len(childs) > 0: - return childs[0] - - def del_payload(self): - for child in self.xml: - self.xml.remove(child) - - -class PubsubStateEvent(ElementBase): - """This is an experimental pubsub extension.""" - namespace = 'http://jabber.org/protocol/psstate#event' - name = 'event' - plugin_attrib = 'psstate_event' - intefaces = set(tuple()) - - -register_stanza_plugin(Iq, PubsubState) -register_stanza_plugin(Message, PubsubStateEvent) -register_stanza_plugin(PubsubStateEvent, PubsubState) - - register_stanza_plugin(Iq, Pubsub) register_stanza_plugin(Pubsub, Affiliations) register_stanza_plugin(Pubsub, Configure) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py index 4a35db9d..d975a46d 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py @@ -34,7 +34,8 @@ class DefaultConfig(ElementBase): return self['form'] def set_config(self, value): - self['form'].values = value.values + del self['from'] + self.append(value) return self @@ -93,7 +94,9 @@ class OwnerRedirect(ElementBase): class OwnerSubscriptions(Subscriptions): + name = 'subscriptions' namespace = 'http://jabber.org/protocol/pubsub#owner' + plugin_attrib = name interfaces = set(('node',)) def append(self, subscription): diff --git a/sleekxmpp/plugins/xep_0065/__init__.py b/sleekxmpp/plugins/xep_0065/__init__.py index c577d859..feca2ef1 100644 --- a/sleekxmpp/plugins/xep_0065/__init__.py +++ b/sleekxmpp/plugins/xep_0065/__init__.py @@ -1,4 +1,6 @@ from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0065.stanza import Socks5 from sleekxmpp.plugins.xep_0065.proxy import XEP_0065 diff --git a/sleekxmpp/plugins/xep_0065/proxy.py b/sleekxmpp/plugins/xep_0065/proxy.py index b027e4e0..d890b57a 100644 --- a/sleekxmpp/plugins/xep_0065/proxy.py +++ b/sleekxmpp/plugins/xep_0065/proxy.py @@ -1,359 +1,292 @@ -import sys import logging -import struct +import threading +import socket -from threading import Thread, Event from hashlib import sha1 -from select import select from uuid import uuid4 -from sleekxmpp.plugins.xep_0065 import stanza +from sleekxmpp.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5 -from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.stanza import Iq +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.thirdparty.socks import socksocket, PROXY_TYPE_SOCKS5 +from sleekxmpp.plugins.base import base_plugin + +from sleekxmpp.plugins.xep_0065 import stanza, Socks5 + -# Registers the sleekxmpp logger log = logging.getLogger(__name__) class XEP_0065(base_plugin): - """ - XEP-0065 Socks5 Bytestreams - """ - description = "Socks5 Bytestreams" - dependencies = set(['xep_0030', ]) - xep = '0065' name = 'xep_0065' - - # A dict contains for each SID, the proxy thread currently - # running. - proxy_threads = {} + description = "Socks5 Bytestreams" + dependencies = set(['xep_0030']) + default_config = { + 'auto_accept': False + } def plugin_init(self): - """ Initializes the xep_0065 plugin and all event callbacks. - """ + register_stanza_plugin(Iq, Socks5) - # Shortcuts to access to the xep_0030 plugin. - self.disco = self.xmpp['xep_0030'] + self._proxies = {} + self._sessions = {} + self._sessions_lock = threading.Lock() - # Handler for the streamhost stanza. - self.xmpp.registerHandler( + self._preauthed_sids_lock = threading.Lock() + self._preauthed_sids = {} + + self.xmpp.register_handler( Callback('Socks5 Bytestreams', StanzaPath('iq@type=set/socks/streamhost'), self._handle_streamhost)) - # Handler for the streamhost-used stanza. - self.xmpp.registerHandler( - Callback('Socks5 Bytestreams', - StanzaPath('iq@type=result/socks/streamhost-used'), - self._handle_streamhost_used)) + self.api.register(self._authorized, 'authorized', default=True) + self.api.register(self._authorized_sid, 'authorized_sid', default=True) + self.api.register(self._preauthorize_sid, 'preauthorize_sid', default=True) - def get_socket(self, sid): - """ Returns the socket associated to the SID. - """ + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(Socks5.namespace) + + def plugin_end(self): + self.xmpp.remove_handler('Socks5 Bytestreams') + self.xmpp.remove_handler('Socks5 Streamhost Used') + self.xmpp['xep_0030'].del_feature(feature=Socks5.namespace) - proxy = self.proxy_threads.get(sid) - if proxy: - return proxy.s + def get_socket(self, sid): + """Returns the socket associated to the SID.""" + return self._sessions.get(sid, None) - def handshake(self, to, streamer=None): + def handshake(self, to, ifrom=None, sid=None, timeout=None): """ Starts the handshake to establish the socks5 bytestreams connection. """ - - # Discovers the proxy. - self.streamer = streamer or self.discover_proxy() - - # Requester requests network address from the proxy. - streamhost = self.get_network_address(self.streamer) - self.proxy_host = streamhost['socks']['streamhost']['host'] - self.proxy_port = streamhost['socks']['streamhost']['port'] - - # Generates the SID for this new handshake. - sid = uuid4().hex - - # Requester initiates S5B negotation with Target by sending + if not self._proxies: + self._proxies = self.discover_proxies() + + if sid is None: + sid = uuid4().hex + + used = self.request_stream(to, sid=sid, ifrom=ifrom, timeout=timeout) + proxy = used['socks']['streamhost_used']['jid'] + + if proxy not in self._proxies: + log.warning('Received unknown SOCKS5 proxy: %s', proxy) + return + + with self._sessions_lock: + self._sessions[sid] = self._connect_proxy( + sid, + self.xmpp.boundjid, + to, + self._proxies[proxy][0], + self._proxies[proxy][1], + peer=to) + + # Request that the proxy activate the session with the target. + self.activate(proxy, sid, to, timeout=timeout) + socket = self.get_socket(sid) + self.xmpp.event('stream:%s:%s' % (sid, to), socket) + return socket + + def request_stream(self, to, sid=None, ifrom=None, block=True, timeout=None, callback=None): + if sid is None: + sid = uuid4().hex + + # Requester initiates S5B negotiation with Target by sending # IQ-set that includes the JabberID and network address of # StreamHost as well as the StreamID (SID) of the proposed # bytestream. - iq = self.xmpp.Iq(sto=to, stype='set') + iq = self.xmpp.Iq() + iq['to'] = to + iq['from'] = ifrom + iq['type'] = 'set' iq['socks']['sid'] = sid - iq['socks']['streamhost']['jid'] = self.streamer - iq['socks']['streamhost']['host'] = self.proxy_host - iq['socks']['streamhost']['port'] = self.proxy_port - - # Sends the new IQ. - return iq.send() + for proxy, (host, port) in self._proxies.items(): + iq['socks'].add_streamhost(proxy, host, port) + return iq.send(block=block, timeout=timeout, callback=callback) + + def discover_proxies(self, jid=None, ifrom=None, timeout=None): + """Auto-discover the JIDs of SOCKS5 proxies on an XMPP server.""" + if jid is None: + if self.xmpp.is_component: + jid = self.xmpp.server + else: + jid = self.xmpp.boundjid.server - def discover_proxy(self): - """ Auto-discovers (using XEP 0030) the available bytestream - proxy on the XMPP server. + discovered = set() - Returns the JID of the proxy. - """ - - # Gets all disco items. - disco_items = self.disco.get_items(self.xmpp.server) + disco_items = self.xmpp['xep_0030'].get_items(jid, timeout=timeout) for item in disco_items['disco_items']['items']: - # For each items, gets the disco info. - disco_info = self.disco.get_info(item[0]) - - # Gets and verifies if the identity is a bytestream proxy. - identities = disco_info['disco_info']['identities'] - for identity in identities: - if identity[0] == 'proxy' and identity[1] == 'bytestreams': - # Returns when the first occurence is found. - return '%s' % disco_info['from'] - - def get_network_address(self, streamer): - """ Gets the streamhost information of the proxy. + try: + disco_info = self.xmpp['xep_0030'].get_info(item[0], timeout=timeout) + except XMPPError: + continue + else: + # Verify that the identity is a bytestream proxy. + identities = disco_info['disco_info']['identities'] + for identity in identities: + if identity[0] == 'proxy' and identity[1] == 'bytestreams': + discovered.add(disco_info['from']) - streamer : The jid of the proxy. - """ + for jid in discovered: + try: + addr = self.get_network_address(jid, ifrom=ifrom, timeout=timeout) + self._proxies[jid] = (addr['socks']['streamhost']['host'], + addr['socks']['streamhost']['port']) + except XMPPError: + continue - iq = self.xmpp.Iq(sto=streamer, stype='get') - iq['socks'] # Adds the query eleme to the iq. + return self._proxies - return iq.send() + def get_network_address(self, proxy, ifrom=None, block=True, timeout=None, callback=None): + """Get the network information of a proxy.""" + iq = self.xmpp.Iq(sto=proxy, stype='get', sfrom=ifrom) + iq.enable('socks') + return iq.send(block=block, timeout=timeout, callback=callback) def _handle_streamhost(self, iq): - """ Handles all streamhost stanzas. - """ - - # Registers the streamhost info. - self.streamer = iq['socks']['streamhost']['jid'] - self.proxy_host = iq['socks']['streamhost']['host'] - self.proxy_port = iq['socks']['streamhost']['port'] - - # Sets the SID, the requester and the target. - sid = iq['socks']['sid'] - requester = '%s' % iq['from'] - target = '%s' % self.xmpp.boundjid - - # Next the Target attempts to open a standard TCP socket on - # the network address of the Proxy. - self.proxy_thread = Proxy(sid, requester, target, self.proxy_host, - self.proxy_port, self.on_recv) - self.proxy_thread.start() - - # Registers the new thread in the proxy_thread dict. - self.proxy_threads[sid] = self.proxy_thread - - # Wait until the proxy is connected - self.proxy_thread.connected.wait() - - # Replies to the incoming iq with a streamhost-used stanza. - res_iq = iq.reply() - res_iq['socks']['sid'] = sid - res_iq['socks']['streamhost-used']['jid'] = self.streamer - - # Sends the IQ - return res_iq.send() - - def _handle_streamhost_used(self, iq): - """ Handles all streamhost-used stanzas. - """ - - # Sets the SID, the requester and the target. + """Handle incoming SOCKS5 session request.""" sid = iq['socks']['sid'] - requester = '%s' % self.xmpp.boundjid - target = '%s' % iq['from'] - - # The Requester will establish a connection to the SOCKS5 - # proxy in the same way the Target did. - self.proxy_thread = Proxy(sid, requester, target, self.proxy_host, - self.proxy_port, self.on_recv) - self.proxy_thread.start() - - # Registers the new thread in the proxy_thread dict. - self.proxy_threads[sid] = self.proxy_thread + if not sid: + raise XMPPError(etype='modify', condition='bad-request') - # Wait until the proxy is connected - self.proxy_thread.connected.wait() + if not self._accept_stream(iq): + raise XMPPError(etype='modify', condition='not-acceptable') - # Requester sends IQ-set to StreamHost requesting that - # StreamHost activate the bytestream associated with the - # StreamID. - self.activate(iq['socks']['sid'], target) + streamhosts = iq['socks']['streamhosts'] + conn = None + used_streamhost = None - def activate(self, sid, to): - """ IQ-set to StreamHost requesting that StreamHost activate - the bytestream associated with the StreamID. - """ - - # Creates the activate IQ. - act_iq = self.xmpp.Iq(sto=self.streamer, stype='set') - act_iq['socks']['sid'] = sid - act_iq['socks']['activate'] = to - - # Send the IQ. - act_iq.send() + sender = iq['from'] + for streamhost in streamhosts: + try: + conn = self._connect_proxy(sid, + sender, + self.xmpp.boundjid, + streamhost['host'], + streamhost['port'], + peer=sender) + used_streamhost = streamhost['jid'] + break + except socket.error: + continue + else: + raise XMPPError(etype='cancel', condition='item-not-found') + + iq.reply() + with self._sessions_lock: + self._sessions[sid] = conn + iq['socks']['sid'] = sid + iq['socks']['streamhost_used']['jid'] = used_streamhost + iq.send() + self.xmpp.event('socks5_stream', conn) + self.xmpp.event('stream:%s:%s' % (sid, conn.peer_jid), conn) + + def activate(self, proxy, sid, target, ifrom=None, block=True, timeout=None, callback=None): + """Activate the socks5 session that has been negotiated.""" + iq = self.xmpp.Iq(sto=proxy, stype='set', sfrom=ifrom) + iq['socks']['sid'] = sid + iq['socks']['activate'] = target + iq.send(block=block, timeout=timeout, callback=callback) def deactivate(self, sid): - """ Closes the Proxy thread associated to this SID. - """ - - proxy = self.proxy_threads.get(sid) - if proxy: - proxy.s.close() - del self.proxy_threads[sid] + """Closes the proxy socket associated with this SID.""" + sock = self._sessions.get(sid) + if sock: + try: + # sock.close() will also delete sid from self._sessions (see _connect_proxy) + sock.close() + except socket.error: + pass + # Though this should not be neccessary remove the closed session anyway + with self._sessions_lock: + if sid in self._sessions: + log.warn(('SOCKS5 session with sid = "%s" was not ' + + 'removed from _sessions by sock.close()') % sid) + del self._sessions[sid] def close(self): - """ Closes all Proxy threads. - """ - - for sid, proxy in self.proxy_threads.items(): - proxy.s.close() - del self.proxy_threads[sid] - - def send(self, sid, data): - """ Sends the data over the Proxy socket associated to the - SID. - """ - - proxy = self.proxy_threads.get(sid) - if proxy: - proxy.s.sendall(data) + """Closes all proxy sockets.""" + for sid, sock in self._sessions.items(): + sock.close() + with self._sessions_lock: + self._sessions = {} - def on_recv(self, sid, data): - """ Calls when data is recv from the Proxy socket associated - to the SID. - - Triggers a socks_closed event if the socket is closed. The sid - is passed to this event. - - Triggers a socks_recv event if there's available data. A dict - that contains the sid and the data is passed to this event. - """ - - proxy = self.proxy_threads.get(sid) - if proxy: - if not data: - self.xmpp.event('socks_closed', sid) - else: - self.xmpp.event('socks_recv', {'sid': sid, 'data': data}) - - -class Proxy(Thread): - """ Establishes in a thread a connection between the client and - the server-side Socks5 proxy. - """ - - def __init__(self, sid, requester, target, proxy, proxy_port, - on_recv): - """ Initializes the proxy thread. + def _connect_proxy(self, sid, requester, target, proxy, proxy_port, peer=None): + """ Establishes a connection between the client and the server-side + Socks5 proxy. sid : The StreamID. <str> requester : The JID of the requester. <str> target : The JID of the target. <str> proxy_host : The hostname or the IP of the proxy. <str> proxy_port : The port of the proxy. <str> or <int> - on_recv : A callback called when data are received from the - socket. <Callable> + peer : The JID for the other side of the stream, regardless + of target or requester status. """ - - # Initializes the thread. - Thread.__init__(self) - # Because the xep_0065 plugin uses the proxy_port as string, # the Proxy class accepts the proxy_port argument as a string # or an integer. Here, we force to use the port as an integer. proxy_port = int(proxy_port) - # Creates a connected event to warn when to proxy is - # connected. - self.connected = Event() - - # Registers the arguments. - self.sid = sid - self.requester = requester - self.target = target - self.proxy = proxy - self.proxy_port = proxy_port - self.on_recv = on_recv - - def run(self): - """ Starts the thread. - """ - - # Creates the socks5 proxy socket - self.s = socksocket() - self.s.setproxy(PROXY_TYPE_SOCKS5, self.proxy, port=self.proxy_port) + sock = socksocket() + sock.setproxy(PROXY_TYPE_SOCKS5, proxy, port=proxy_port) # The hostname MUST be SHA1(SID + Requester JID + Target JID) # where the output is hexadecimal-encoded (not binary). digest = sha1() - digest.update(self.sid) # SID - digest.update(self.requester) # Requester JID - digest.update(self.target) # Target JID + digest.update(sid.encode('utf-8')) + digest.update(str(requester).encode('utf-8')) + digest.update(str(target).encode('utf-8')) - # Computes the digest in hex. - dest = '%s' % digest.hexdigest() + dest = digest.hexdigest() # The port MUST be 0. - self.s.connect((dest, 0)) + sock.connect((dest, 0)) log.info('Socket connected.') - self.connected.set() - # Blocks until the socket need to be closed. - self.listen() + _close = sock.close + def close(*args, **kwargs): + with self._sessions_lock: + if sid in self._sessions: + del self._sessions[sid] + _close() + log.info('Socket closed.') + sock.close = close - # Closes the socket. - self.s.close() - log.info('Socket closed.') + sock.peer_jid = peer + sock.self_jid = target if requester == peer else requester - def listen(self): - """ Listen for data on the socket. When receiving data, call - the callback on_recv callable. - """ + self.xmpp.event('socks_connected', sid) + return sock - socket_open = True - while socket_open: - ins = [] - try: - # Wait any read available data on socket. Timeout - # after 5 secs. - ins, out, err = select([self.s, ], [], [], 5) - except Exception as e: - # There's an error with the socket (maybe the socket - # has been closed and the file descriptor is bad). - log.debug('Socket error: %s' % e) - break + def _accept_stream(self, iq): + receiver = iq['to'] + sender = iq['from'] + sid = iq['socks']['sid'] - for s in ins: - data = self.recv_size(self.s) - if not data: - socket_open = False - - self.on_recv(self.sid, data) - - def recv_size(self, the_socket): - total_len = 0 - total_data = [] - size = sys.maxint - size_data = sock_data = '' - recv_size = 8192 - - while total_len < size: - sock_data = the_socket.recv(recv_size) - if not sock_data: - return ''.join(total_data) - - if not total_data: - if len(sock_data) > 4: - size_data += sock_data - size = struct.unpack('>i', size_data[:4])[0] - recv_size = size - if recv_size > 524288: - recv_size = 524288 - total_data.append(size_data[4:]) - else: - size_data += sock_data - else: - total_data.append(sock_data) - total_len = sum([len(i) for i in total_data]) - return ''.join(total_data) + if self.api['authorized_sid'](receiver, sid, sender, iq): + return True + return self.api['authorized'](receiver, sid, sender, iq) + + def _authorized(self, jid, sid, ifrom, iq): + return self.auto_accept + + def _authorized_sid(self, jid, sid, ifrom, iq): + with self._preauthed_sids_lock: + log.debug('>>> authed sids: %s', self._preauthed_sids) + log.debug('>>> lookup: %s %s %s', jid, sid, ifrom) + if (jid, sid, ifrom) in self._preauthed_sids: + del self._preauthed_sids[(jid, sid, ifrom)] + return True + return False + + def _preauthorize_sid(self, jid, sid, ifrom, data): + log.debug('>>>> %s %s %s %s', jid, sid, ifrom, data) + with self._preauthed_sids_lock: + self._preauthed_sids[(jid, sid, ifrom)] = True diff --git a/sleekxmpp/plugins/xep_0065/stanza.py b/sleekxmpp/plugins/xep_0065/stanza.py index ae57aba8..e48bf1b5 100644 --- a/sleekxmpp/plugins/xep_0065/stanza.py +++ b/sleekxmpp/plugins/xep_0065/stanza.py @@ -1,41 +1,47 @@ -from sleekxmpp import Iq +from sleekxmpp.jid import JID from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin -# The protocol namespace defined in the Socks5Bytestream (0065) spec. -namespace = 'http://jabber.org/protocol/bytestreams' +class Socks5(ElementBase): + name = 'query' + namespace = 'http://jabber.org/protocol/bytestreams' + plugin_attrib = 'socks' + interfaces = set(['sid', 'activate']) + sub_interfaces = set(['activate']) + def add_streamhost(self, jid, host, port): + sh = StreamHost(parent=self) + sh['jid'] = jid + sh['host'] = host + sh['port'] = port -class StreamHost(ElementBase): - """ The streamhost xml element. - """ - namespace = namespace +class StreamHost(ElementBase): name = 'streamhost' + namespace = 'http://jabber.org/protocol/bytestreams' plugin_attrib = 'streamhost' - interfaces = set(('host', 'jid', 'port')) + plugin_multi_attrib = 'streamhosts' + interfaces = set(['host', 'jid', 'port']) + def set_jid(self, value): + return self._set_attr('jid', str(value)) -class StreamHostUsed(ElementBase): - """ The streamhost-used xml element. - """ + def get_jid(self): + return JID(self._get_attr('jid')) - namespace = namespace + +class StreamHostUsed(ElementBase): name = 'streamhost-used' - plugin_attrib = 'streamhost-used' - interfaces = set(('jid',)) + namespace = 'http://jabber.org/protocol/bytestreams' + plugin_attrib = 'streamhost_used' + interfaces = set(['jid']) + def set_jid(self, value): + return self._set_attr('jid', str(value)) -class Socks5(ElementBase): - """ The query xml element. - """ + def get_jid(self): + return JID(self._get_attr('jid')) - namespace = namespace - name = 'query' - plugin_attrib = 'socks' - interfaces = set(('sid', 'activate')) - sub_interfaces = set(('activate',)) -register_stanza_plugin(Iq, Socks5) -register_stanza_plugin(Socks5, StreamHost) +register_stanza_plugin(Socks5, StreamHost, iterable=True) register_stanza_plugin(Socks5, StreamHostUsed) diff --git a/sleekxmpp/plugins/xep_0071/__init__.py b/sleekxmpp/plugins/xep_0071/__init__.py new file mode 100644 index 00000000..c21e9265 --- /dev/null +++ b/sleekxmpp/plugins/xep_0071/__init__.py @@ -0,0 +1,15 @@ +""" + 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 permissio +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0071.stanza import XHTML_IM +from sleekxmpp.plugins.xep_0071.xhtml_im import XEP_0071 + + +register_plugin(XEP_0071) diff --git a/sleekxmpp/plugins/xep_0071/stanza.py b/sleekxmpp/plugins/xep_0071/stanza.py new file mode 100644 index 00000000..d5ff1a1b --- /dev/null +++ b/sleekxmpp/plugins/xep_0071/stanza.py @@ -0,0 +1,81 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza import Message +from sleekxmpp.util import unicode +from sleekxmpp.thirdparty import OrderedDict +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin, tostring + + +XHTML_NS = 'http://www.w3.org/1999/xhtml' + + +class XHTML_IM(ElementBase): + + namespace = 'http://jabber.org/protocol/xhtml-im' + name = 'html' + interfaces = set(['body']) + lang_interfaces = set(['body']) + plugin_attrib = name + + def set_body(self, content, lang=None): + if lang is None: + lang = self.get_lang() + self.del_body(lang) + if lang == '*': + for sublang, subcontent in content.items(): + self.set_body(subcontent, sublang) + else: + if isinstance(content, type(ET.Element('test'))): + content = unicode(ET.tostring(content)) + else: + content = unicode(content) + header = '<body xmlns="%s"' % XHTML_NS + if lang: + header = '%s xml:lang="%s"' % (header, lang) + content = '%s>%s</body>' % (header, content) + xhtml = ET.fromstring(content) + self.xml.append(xhtml) + + def get_body(self, lang=None): + """Return the contents of the HTML body.""" + if lang is None: + lang = self.get_lang() + + bodies = self.xml.findall('{%s}body' % XHTML_NS) + + if lang == '*': + result = OrderedDict() + for body in bodies: + body_lang = body.attrib.get('{%s}lang' % self.xml_ns, '') + body_result = [] + body_result.append(body.text if body.text else '') + for child in body: + body_result.append(tostring(child, xmlns=XHTML_NS)) + body_result.append(body.tail if body.tail else '') + result[body_lang] = ''.join(body_result) + return result + else: + for body in bodies: + if body.attrib.get('{%s}lang' % self.xml_ns, self.get_lang()) == lang: + result = [] + result.append(body.text if body.text else '') + for child in body: + result.append(tostring(child, xmlns=XHTML_NS)) + result.append(body.tail if body.tail else '') + return ''.join(result) + return '' + + def del_body(self, lang=None): + if lang is None: + lang = self.get_lang() + bodies = self.xml.findall('{%s}body' % XHTML_NS) + for body in bodies: + if body.attrib.get('{%s}lang' % self.xml_ns, self.get_lang()) == lang: + self.xml.remove(body) + return diff --git a/sleekxmpp/plugins/xep_0071/xhtml_im.py b/sleekxmpp/plugins/xep_0071/xhtml_im.py new file mode 100644 index 00000000..096a00aa --- /dev/null +++ b/sleekxmpp/plugins/xep_0071/xhtml_im.py @@ -0,0 +1,30 @@ +""" + 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 Message +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0071 import stanza, XHTML_IM + + +class XEP_0071(BasePlugin): + + name = 'xep_0071' + description = 'XEP-0071: XHTML-IM' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, XHTML_IM) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(feature=XHTML_IM.namespace) + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=XHTML_IM.namespace) diff --git a/sleekxmpp/plugins/xep_0077/register.py b/sleekxmpp/plugins/xep_0077/register.py index d4da21a5..ee07548b 100644 --- a/sleekxmpp/plugins/xep_0077/register.py +++ b/sleekxmpp/plugins/xep_0077/register.py @@ -7,6 +7,7 @@ """ import logging +import ssl from sleekxmpp.stanza import StreamFeatures, Iq from sleekxmpp.xmlstream import register_stanza_plugin, JID @@ -29,6 +30,7 @@ class XEP_0077(BasePlugin): stanza = stanza default_config = { 'create_account': True, + 'force_registration': False, 'order': 50 } @@ -45,10 +47,29 @@ class XEP_0077(BasePlugin): register_stanza_plugin(Register, self.xmpp['xep_0004'].stanza.Form) register_stanza_plugin(Register, self.xmpp['xep_0066'].stanza.OOB) + self.xmpp.add_event_handler('connected', self._force_registration) + def plugin_end(self): if not self.xmpp.is_component: self.xmpp.unregister_feature('register', self.order) + def _force_registration(self, event): + if self.force_registration: + self.xmpp.add_filter('in', self._force_stream_feature) + + def _force_stream_feature(self, stanza): + if isinstance(stanza, StreamFeatures): + if self.xmpp.use_tls or self.xmpp.use_ssl: + if 'starttls' not in self.xmpp.features: + return stanza + elif not isinstance(self.xmpp.socket, ssl.SSLSocket): + return stanza + if 'mechanisms' not in self.xmpp.features: + log.debug('Forced adding in-band registration stream feature') + stanza.enable('register') + self.xmpp.del_filter('in', self._force_stream_feature) + return stanza + def _handle_register_feature(self, features): if 'mechanisms' in self.xmpp.features: # We have already logged in with an account diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py index 7e2d7bdf..da6bfa2c 100644 --- a/sleekxmpp/plugins/xep_0078/legacyauth.py +++ b/sleekxmpp/plugins/xep_0078/legacyauth.py @@ -6,6 +6,7 @@ See the file LICENSE for copying permission. """ +import uuid import logging import hashlib import random @@ -98,7 +99,7 @@ class XEP_0078(BasePlugin): # A resource is required, so create a random one if necessary resource = self.xmpp.requested_jid.resource if not resource: - resource = uuid.uuid4() + resource = str(uuid.uuid4()) iq['auth']['resource'] = resource diff --git a/sleekxmpp/plugins/xep_0079/__init__.py b/sleekxmpp/plugins/xep_0079/__init__.py new file mode 100644 index 00000000..09e66715 --- /dev/null +++ b/sleekxmpp/plugins/xep_0079/__init__.py @@ -0,0 +1,18 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0079.stanza import ( + AMP, Rule, InvalidRules, UnsupportedConditions, + UnsupportedActions, FailedRules, FailedRule, + AMPFeature) +from sleekxmpp.plugins.xep_0079.amp import XEP_0079 + + +register_plugin(XEP_0079) diff --git a/sleekxmpp/plugins/xep_0079/amp.py b/sleekxmpp/plugins/xep_0079/amp.py new file mode 100644 index 00000000..918fb841 --- /dev/null +++ b/sleekxmpp/plugins/xep_0079/amp.py @@ -0,0 +1,79 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import logging + +from sleekxmpp.stanza import Message, Error, StreamFeatures +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.matcher import StanzaPath, MatchMany +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0079 import stanza + + +log = logging.getLogger(__name__) + + +class XEP_0079(BasePlugin): + + """ + XEP-0079 Advanced Message Processing + """ + + name = 'xep_0079' + description = 'XEP-0079: Advanced Message Processing' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, stanza.AMP) + register_stanza_plugin(Error, stanza.InvalidRules) + register_stanza_plugin(Error, stanza.UnsupportedConditions) + register_stanza_plugin(Error, stanza.UnsupportedActions) + register_stanza_plugin(Error, stanza.FailedRules) + + self.xmpp.register_handler( + Callback('AMP Response', + MatchMany([ + StanzaPath('message/error/failed_rules'), + StanzaPath('message/amp') + ]), + self._handle_amp_response)) + + if not self.xmpp.is_component: + self.xmpp.register_feature('amp', + self._handle_amp_feature, + restart=False, + order=9000) + register_stanza_plugin(StreamFeatures, stanza.AMPFeature) + + def plugin_end(self): + self.xmpp.remove_handler('AMP Response') + + def _handle_amp_response(self, msg): + log.debug('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>') + if msg['type'] == 'error': + self.xmpp.event('amp_error', msg) + elif msg['amp']['status'] in ('alert', 'notify'): + self.xmpp.event('amp_%s' % msg['amp']['status'], msg) + + def _handle_amp_feature(self, features): + log.debug('Advanced Message Processing is available.') + self.xmpp.features.add('amp') + + def discover_support(self, jid=None, **iqargs): + if jid is None: + if self.xmpp.is_component: + jid = self.xmpp.server_host + else: + jid = self.xmpp.boundjid.host + + return self.xmpp['xep_0030'].get_info( + jid=jid, + node='http://jabber.org/protocol/amp', + **iqargs) diff --git a/sleekxmpp/plugins/xep_0079/stanza.py b/sleekxmpp/plugins/xep_0079/stanza.py new file mode 100644 index 00000000..cb6932d6 --- /dev/null +++ b/sleekxmpp/plugins/xep_0079/stanza.py @@ -0,0 +1,96 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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, register_stanza_plugin + + +class AMP(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'amp' + plugin_attrib = 'amp' + interfaces = set(['from', 'to', 'status', 'per_hop']) + + def get_from(self): + return JID(self._get_attr('from')) + + def set_from(self, value): + return self._set_attr('from', str(value)) + + def get_to(self): + return JID(self._get_attr('from')) + + def set_to(self, value): + return self._set_attr('to', str(value)) + + def get_per_hop(self): + return self._get_attr('per-hop') == 'true' + + def set_per_hop(self, value): + if value: + return self._set_attr('per-hop', 'true') + else: + return self._del_attr('per-hop') + + def del_per_hop(self): + return self._del_attr('per-hop') + + def add_rule(self, action, condition, value): + rule = Rule(parent=self) + rule['action'] = action + rule['condition'] = condition + rule['value'] = value + + +class Rule(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'rule' + plugin_attrib = name + plugin_multi_attrib = 'rules' + interfaces = set(['action', 'condition', 'value']) + + +class InvalidRules(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'invalid-rules' + plugin_attrib = 'invalid_rules' + + +class UnsupportedConditions(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'unsupported-conditions' + plugin_attrib = 'unsupported_conditions' + + +class UnsupportedActions(ElementBase): + namespace = 'http://jabber.org/protocol/amp' + name = 'unsupported-actions' + plugin_attrib = 'unsupported_actions' + + +class FailedRule(Rule): + namespace = 'http://jabber.org/protocol/amp#errors' + + +class FailedRules(ElementBase): + namespace = 'http://jabber.org/protocol/amp#errors' + name = 'failed-rules' + plugin_attrib = 'failed_rules' + + +class AMPFeature(ElementBase): + namespace = 'http://jabber.org/features/amp' + name = 'amp' + + +register_stanza_plugin(AMP, Rule, iterable=True) +register_stanza_plugin(InvalidRules, Rule, iterable=True) +register_stanza_plugin(UnsupportedConditions, Rule, iterable=True) +register_stanza_plugin(UnsupportedActions, Rule, iterable=True) +register_stanza_plugin(FailedRules, FailedRule, iterable=True) diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py index 02571fa7..26eb68fa 100644 --- a/sleekxmpp/plugins/xep_0082.py +++ b/sleekxmpp/plugins/xep_0082.py @@ -6,7 +6,6 @@ See the file LICENSE for copying permission. """ -import logging import datetime as dt from sleekxmpp.plugins import BasePlugin, register_plugin diff --git a/sleekxmpp/plugins/xep_0084/avatar.py b/sleekxmpp/plugins/xep_0084/avatar.py index 2454afc7..677a888d 100644 --- a/sleekxmpp/plugins/xep_0084/avatar.py +++ b/sleekxmpp/plugins/xep_0084/avatar.py @@ -82,6 +82,7 @@ class XEP_0084(BasePlugin): metadata.add_pointer(pointer) return self.xmpp['xep_0163'].publish(metadata, + id=info['id'], ifrom=ifrom, block=block, callback=callback, diff --git a/sleekxmpp/plugins/xep_0084/stanza.py b/sleekxmpp/plugins/xep_0084/stanza.py index 22f11b72..fd21e6f1 100644 --- a/sleekxmpp/plugins/xep_0084/stanza.py +++ b/sleekxmpp/plugins/xep_0084/stanza.py @@ -8,7 +8,7 @@ from base64 import b64encode, b64decode -from sleekxmpp.util import bytes +from sleekxmpp.util import bytes as sbytes from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin @@ -20,12 +20,15 @@ class Data(ElementBase): def get_value(self): if self.xml.text: - return b64decode(bytes(self.xml.text)) + return b64decode(sbytes(self.xml.text)) return '' def set_value(self, value): if value: - self.xml.text = b64encode(bytes(value)) + self.xml.text = b64encode(sbytes(value)) + # Python3 base64 encoded is bytes and needs to be decoded to string + if isinstance(self.xml.text, bytes): + self.xml.text = self.xml.text.decode() else: self.xml.text = '' diff --git a/sleekxmpp/plugins/xep_0091/stanza.py b/sleekxmpp/plugins/xep_0091/stanza.py index 0b70ff63..17e55764 100644 --- a/sleekxmpp/plugins/xep_0091/stanza.py +++ b/sleekxmpp/plugins/xep_0091/stanza.py @@ -21,14 +21,15 @@ class LegacyDelay(ElementBase): interfaces = set(('from', 'stamp', 'text')) def get_from(self): - return JID(self._get_attr('from')) + from_ = self._get_attr('from') + return JID(from_) if from_ else None def set_from(self, value): self._set_attr('from', str(value)) def get_stamp(self): timestamp = self._get_attr('stamp') - return xep_0082.parse('%sZ' % timestamp) + return xep_0082.parse('%sZ' % timestamp) if timestamp else None def set_stamp(self, value): if isinstance(value, dt.datetime): diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index 35813e1d..b16ad516 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -70,7 +70,7 @@ class XEP_0092(BasePlugin): iq['software_version']['os'] = self.os iq.send() - def get_version(self, jid, ifrom=None): + def get_version(self, jid, ifrom=None, block=True, timeout=None, callback=None): """ Retrieve the software version of a remote agent. @@ -82,14 +82,4 @@ class XEP_0092(BasePlugin): iq['from'] = ifrom iq['type'] = 'get' iq['query'] = Version.namespace - - result = iq.send() - - if result and result['type'] != 'error': - values = result['software_version'].values - del values['lang'] - return values - return False - - -XEP_0092.getVersion = XEP_0092.get_version + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0095/__init__.py b/sleekxmpp/plugins/xep_0095/__init__.py new file mode 100644 index 00000000..4465ef5c --- /dev/null +++ b/sleekxmpp/plugins/xep_0095/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0095 import stanza +from sleekxmpp.plugins.xep_0095.stanza import SI +from sleekxmpp.plugins.xep_0095.stream_initiation import XEP_0095 + + +register_plugin(XEP_0095) diff --git a/sleekxmpp/plugins/xep_0095/stanza.py b/sleekxmpp/plugins/xep_0095/stanza.py new file mode 100644 index 00000000..34999a11 --- /dev/null +++ b/sleekxmpp/plugins/xep_0095/stanza.py @@ -0,0 +1,25 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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 + + +class SI(ElementBase): + name = 'si' + namespace = 'http://jabber.org/protocol/si' + plugin_attrib = 'si' + interfaces = set(['id', 'mime_type', 'profile']) + + def get_mime_type(self): + return self._get_attr('mime-type', 'application/octet-stream') + + def set_mime_type(self, value): + self._set_attr('mime-type', value) + + def del_mime_type(self): + self._del_attr('mime-type') diff --git a/sleekxmpp/plugins/xep_0095/stream_initiation.py b/sleekxmpp/plugins/xep_0095/stream_initiation.py new file mode 100644 index 00000000..927248a5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0095/stream_initiation.py @@ -0,0 +1,214 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import threading + +from uuid import uuid4 + +from sleekxmpp import Iq, Message +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0095 import stanza, SI + + +log = logging.getLogger(__name__) + + +SOCKS5 = 'http://jabber.org/protocol/bytestreams' +IBB = 'http://jabber.org/protocol/ibb' + + +class XEP_0095(BasePlugin): + + name = 'xep_0095' + description = 'XEP-0095: Stream Initiation' + dependencies = set(['xep_0020', 'xep_0030', 'xep_0047', 'xep_0065']) + stanza = stanza + + def plugin_init(self): + self._profiles = {} + self._methods = {} + self._methods_order = [] + self._pending_lock = threading.Lock() + self._pending= {} + + self.register_method(SOCKS5, 'xep_0065', 100) + self.register_method(IBB, 'xep_0047', 50) + + register_stanza_plugin(Iq, SI) + register_stanza_plugin(SI, self.xmpp['xep_0020'].stanza.FeatureNegotiation) + + self.xmpp.register_handler( + Callback('SI Request', + StanzaPath('iq@type=set/si'), + self._handle_request)) + + self.api.register(self._add_pending, 'add_pending', default=True) + self.api.register(self._get_pending, 'get_pending', default=True) + self.api.register(self._del_pending, 'del_pending', default=True) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(SI.namespace) + + def plugin_end(self): + self.xmpp.remove_handler('SI Request') + self.xmpp['xep_0030'].del_feature(feature=SI.namespace) + + def register_profile(self, profile_name, plugin): + self._profiles[profile_name] = plugin + + def unregister_profile(self, profile_name): + try: + del self._profiles[profile_name] + except KeyError: + pass + + def register_method(self, method, plugin_name, order=50): + self._methods[method] = (plugin_name, order) + self._methods_order.append((order, method, plugin_name)) + self._methods_order.sort() + + def unregister_method(self, method): + if method in self._methods: + plugin_name, order = self._methods[method] + del self._methods[method] + self._methods_order.remove((order, method, plugin_name)) + self._methods_order.sort() + + def _handle_request(self, iq): + profile = iq['si']['profile'] + sid = iq['si']['id'] + + if not sid: + raise XMPPError(etype='modify', condition='bad-request') + if profile not in self._profiles: + raise XMPPError( + etype='modify', + condition='bad-request', + extension='bad-profile', + extension_ns=SI.namespace) + + neg = iq['si']['feature_neg']['form']['fields'] + options = neg['stream-method']['options'] or [] + methods = [] + for opt in options: + methods.append(opt['value']) + for method in methods: + if method in self._methods: + supported = True + break + else: + raise XMPPError('bad-request', + extension='no-valid-streams', + extension_ns=SI.namespace) + + selected_method = None + log.debug('Available: %s', methods) + for order, method, plugin in self._methods_order: + log.debug('Testing: %s', method) + if method in methods: + selected_method = method + break + + receiver = iq['to'] + sender = iq['from'] + + self.api['add_pending'](receiver, sid, sender, { + 'response_id': iq['id'], + 'method': selected_method, + 'profile': profile + }) + self.xmpp.event('si_request', iq) + + def offer(self, jid, sid=None, mime_type=None, profile=None, + methods=None, payload=None, ifrom=None, + **iqargs): + if sid is None: + sid = uuid4().hex + if methods is None: + methods = list(self._methods.keys()) + if not isinstance(methods, (list, tuple, set)): + methods = [methods] + + si = self.xmpp.Iq() + si['to'] = jid + si['from'] = ifrom + si['type'] = 'set' + si['si']['id'] = sid + si['si']['mime_type'] = mime_type + si['si']['profile'] = profile + if not isinstance(payload, (list, tuple, set)): + payload = [payload] + for item in payload: + si['si'].append(item) + si['si']['feature_neg']['form'].add_field( + var='stream-method', + ftype='list-single', + options=methods) + return si.send(**iqargs) + + def accept(self, jid, sid, payload=None, ifrom=None, stream_handler=None): + stream = self.api['get_pending'](ifrom, sid, jid) + iq = self.xmpp.Iq() + iq['id'] = stream['response_id'] + iq['to'] = jid + iq['from'] = ifrom + iq['type'] = 'result' + if payload: + iq['si'].append(payload) + iq['si']['feature_neg']['form']['type'] = 'submit' + iq['si']['feature_neg']['form'].add_field( + var='stream-method', + ftype='list-single', + value=stream['method']) + + if ifrom is None: + ifrom = self.xmpp.boundjid + + method_plugin = self._methods[stream['method']][0] + self.xmpp[method_plugin].api['preauthorize_sid'](ifrom, sid, jid) + + self.api['del_pending'](ifrom, sid, jid) + + if stream_handler: + self.xmpp.add_event_handler('stream:%s:%s' % (sid, jid), + stream_handler, + threaded=True, + disposable=True) + return iq.send() + + def decline(self, jid, sid, ifrom=None): + stream = self.api['get_pending'](ifrom, sid, jid) + if not stream: + return + iq = self.xmpp.Iq() + iq['id'] = stream['response_id'] + iq['to'] = jid + iq['from'] = ifrom + iq['type'] = 'error' + iq['error']['condition'] = 'forbidden' + iq['error']['text'] = 'Offer declined' + self.api['del_pending'](ifrom, sid, jid) + return iq.send() + + def _add_pending(self, jid, node, ifrom, data): + with self._pending_lock: + self._pending[(jid, node, ifrom)] = data + + def _get_pending(self, jid, node, ifrom, data): + with self._pending_lock: + return self._pending.get((jid, node, ifrom), None) + + def _del_pending(self, jid, node, ifrom, data): + with self._pending_lock: + if (jid, node, ifrom) in self._pending: + del self._pending[(jid, node, ifrom)] diff --git a/sleekxmpp/plugins/xep_0096/__init__.py b/sleekxmpp/plugins/xep_0096/__init__.py new file mode 100644 index 00000000..5f836169 --- /dev/null +++ b/sleekxmpp/plugins/xep_0096/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0096 import stanza +from sleekxmpp.plugins.xep_0096.stanza import File +from sleekxmpp.plugins.xep_0096.file_transfer import XEP_0096 + + +register_plugin(XEP_0096) diff --git a/sleekxmpp/plugins/xep_0096/file_transfer.py b/sleekxmpp/plugins/xep_0096/file_transfer.py new file mode 100644 index 00000000..6873c7f5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0096/file_transfer.py @@ -0,0 +1,58 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp import Iq, Message +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, JID +from sleekxmpp.plugins.xep_0096 import stanza, File + + +log = logging.getLogger(__name__) + + +class XEP_0096(BasePlugin): + + name = 'xep_0096' + description = 'XEP-0096: SI File Transfer' + dependencies = set(['xep_0095']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(self.xmpp['xep_0095'].stanza.SI, File) + + self.xmpp['xep_0095'].register_profile(File.namespace, self) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature(File.namespace) + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=File.namespace) + self.xmpp['xep_0095'].unregister_profile(File.namespace, self) + + def request_file_transfer(self, jid, sid=None, name=None, size=None, + desc=None, hash=None, date=None, + allow_ranged=False, mime_type=None, + **iqargs): + data = File() + data['name'] = name + data['size'] = size + data['date'] = date + data['desc'] = desc + if allow_ranged: + data.enable('range') + + return self.xmpp['xep_0095'].offer(jid, + sid=sid, + mime_type=mime_type, + profile=File.namespace, + payload=data, + **iqargs) diff --git a/sleekxmpp/plugins/xep_0096/stanza.py b/sleekxmpp/plugins/xep_0096/stanza.py new file mode 100644 index 00000000..65eb5bc5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0096/stanza.py @@ -0,0 +1,48 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin +from sleekxmpp.plugins import xep_0082 + + +class File(ElementBase): + name = 'file' + namespace = 'http://jabber.org/protocol/si/profile/file-transfer' + plugin_attrib = 'file' + interfaces = set(['name', 'size', 'date', 'hash', 'desc']) + sub_interfaces = set(['desc']) + + def set_size(self, value): + self._set_attr('size', str(value)) + + def get_date(self): + timestamp = self._get_attr('date') + return xep_0082.parse(timestamp) + + def set_date(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('date', value) + + +class Range(ElementBase): + name = 'range' + namespace = 'http://jabber.org/protocol/si/profile/file-transfer' + plugin_attrib = 'range' + interfaces = set(['length', 'offset']) + + def set_length(self, value): + self._set_attr('length', str(value)) + + def set_offset(self, value): + self._set_attr('offset', str(value)) + + +register_stanza_plugin(File, Range) diff --git a/sleekxmpp/plugins/xep_0115/caps.py b/sleekxmpp/plugins/xep_0115/caps.py index b7a346c0..41b5c52e 100644 --- a/sleekxmpp/plugins/xep_0115/caps.py +++ b/sleekxmpp/plugins/xep_0115/caps.py @@ -9,8 +9,9 @@ import logging import hashlib import base64 +import threading -import sleekxmpp +from sleekxmpp import __version__ from sleekxmpp.stanza import StreamFeatures, Presence, Iq from sleekxmpp.xmlstream import register_stanza_plugin, JID from sleekxmpp.xmlstream.handler import Callback @@ -45,8 +46,7 @@ class XEP_0115(BasePlugin): 'md5': hashlib.md5} if self.caps_node is None: - ver = sleekxmpp.__version__ - self.caps_node = 'http://sleekxmpp.com/ver/%s' % ver + self.caps_node = 'http://sleekxmpp.com/ver/%s' % __version__ register_stanza_plugin(Presence, stanza.Capabilities) register_stanza_plugin(StreamFeatures, stanza.Capabilities) @@ -90,6 +90,9 @@ class XEP_0115(BasePlugin): disco.assign_verstring = self.assign_verstring disco.get_verstring = self.get_verstring + self._processing_lock = threading.Lock() + self._processing = set() + def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=stanza.Capabilities.namespace) self.xmpp.del_filter('out', self._filter_add_caps) @@ -135,17 +138,22 @@ class XEP_0115(BasePlugin): def _process_caps(self, pres): if not pres['caps']['hash']: - log.debug("Received unsupported legacy caps.") + log.debug("Received unsupported legacy caps: %s, %s, %s", + pres['caps']['node'], + pres['caps']['ver'], + pres['caps']['ext']) self.xmpp.event('entity_caps_legacy', pres) return + ver = pres['caps']['ver'] + existing_verstring = self.get_verstring(pres['from'].full) - if str(existing_verstring) == str(pres['caps']['ver']): + if str(existing_verstring) == str(ver): return - existing_caps = self.get_caps(verstring=pres['caps']['ver']) + existing_caps = self.get_caps(verstring=ver) if existing_caps is not None: - self.assign_verstring(pres['from'], pres['caps']['ver']) + self.assign_verstring(pres['from'], ver) return if pres['caps']['hash'] not in self.hashes: @@ -156,9 +164,16 @@ class XEP_0115(BasePlugin): except XMPPError: return - log.debug("New caps verification string: %s", pres['caps']['ver']) + # Only lookup the same caps once at a time. + with self._processing_lock: + if ver in self._processing: + log.debug('Already processing verstring %s' % ver) + return + self._processing.add(ver) + + log.debug("New caps verification string: %s", ver) try: - node = '%s#%s' % (pres['caps']['node'], pres['caps']['ver']) + node = '%s#%s' % (pres['caps']['node'], ver) caps = self.xmpp['xep_0030'].get_info(pres['from'], node) if isinstance(caps, Iq): @@ -168,7 +183,10 @@ class XEP_0115(BasePlugin): pres['caps']['ver']): self.assign_verstring(pres['from'], pres['caps']['ver']) except XMPPError: - log.debug("Could not retrieve disco#info results for caps") + log.debug("Could not retrieve disco#info results for caps for %s", node) + + with self._processing_lock: + self._processing.remove(ver) def _validate_caps(self, caps, hash, check_verstring): # Check Identities @@ -179,7 +197,6 @@ class XEP_0115(BasePlugin): return False # Check Features - full_features = caps.get_features(dedupe=False) deduped_features = caps.get_features() if len(full_features) != len(deduped_features): @@ -190,29 +207,32 @@ class XEP_0115(BasePlugin): 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") + if not isinstance(stanza, self.xmpp['xep_0004'].stanza.Form): + log.debug("Non form extension found, ignoring for caps") + caps.xml.remove(stanza.xml) + continue + 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 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") + 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: @@ -272,7 +292,7 @@ class XEP_0115(BasePlugin): binary = hash(S.encode('utf8')).digest() return base64.b64encode(binary).decode('utf-8') - def update_caps(self, jid=None, node=None): + def update_caps(self, jid=None, node=None, preserve=False): try: info = self.xmpp['xep_0030'].get_info(jid, node, local=True) if isinstance(info, Iq): @@ -286,19 +306,11 @@ class XEP_0115(BasePlugin): 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: + if self.xmpp.is_component or preserve: for contact in self.xmpp.roster[jid]: self.xmpp.roster[jid][contact].send_last_presence() + else: + self.xmpp.roster[jid].send_last_presence() except XMPPError: return diff --git a/sleekxmpp/plugins/xep_0152/__init__.py b/sleekxmpp/plugins/xep_0152/__init__.py new file mode 100644 index 00000000..7de031b7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0152/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0152 import stanza +from sleekxmpp.plugins.xep_0152.stanza import Reachability +from sleekxmpp.plugins.xep_0152.reachability import XEP_0152 + + +register_plugin(XEP_0152) diff --git a/sleekxmpp/plugins/xep_0152/reachability.py b/sleekxmpp/plugins/xep_0152/reachability.py new file mode 100644 index 00000000..4cf81739 --- /dev/null +++ b/sleekxmpp/plugins/xep_0152/reachability.py @@ -0,0 +1,93 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0152 import stanza, Reachability + + +log = logging.getLogger(__name__) + + +class XEP_0152(BasePlugin): + + """ + XEP-0152: Reachability Addresses + """ + + name = 'xep_0152' + description = 'XEP-0152: Reachability Addresses' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=Reachability.namespace) + self.xmpp['xep_0163'].remove_interest(Reachability.namespace) + + def session_bind(self, jid): + self.xmpp['xep_0163'].register_pep('reachability', Reachability) + + def publish_reachability(self, addresses, options=None, + ifrom=None, block=True, callback=None, timeout=None): + """ + Publish alternative addresses where the user can be reached. + + Arguments: + addresses -- A list of dictionaries containing the URI and + optional description for each address. + 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. + """ + if not isinstance(addresses, (list, tuple)): + addresses = [addresses] + reach = Reachability() + for address in addresses: + if not hasattr(address, 'items'): + address = {'uri': address} + + addr = stanza.Address() + for key, val in address.items(): + addr[key] = val + reach.append(addr) + return self.xmpp['xep_0163'].publish(reach, + node=Reachability.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. + """ + reach = Reachability() + return self.xmpp['xep_0163'].publish(reach, + node=Reachability.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0152/stanza.py b/sleekxmpp/plugins/xep_0152/stanza.py new file mode 100644 index 00000000..bd173ce1 --- /dev/null +++ b/sleekxmpp/plugins/xep_0152/stanza.py @@ -0,0 +1,29 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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, register_stanza_plugin + + +class Reachability(ElementBase): + name = 'reach' + namespace = 'urn:xmpp:reach:0' + plugin_attrib = 'reach' + interfaces = set() + + +class Address(ElementBase): + name = 'addr' + namespace = 'urn:xmpp:reach:0' + plugin_attrib = 'address' + plugin_multi_attrib = 'addresses' + interfaces = set(['uri', 'desc']) + lang_interfaces = set(['desc']) + sub_interfaces = set(['desc']) + + +register_stanza_plugin(Reachability, Address, iterable=True) diff --git a/sleekxmpp/plugins/xep_0153/vcard_avatar.py b/sleekxmpp/plugins/xep_0153/vcard_avatar.py index c74713e5..ec1ae782 100644 --- a/sleekxmpp/plugins/xep_0153/vcard_avatar.py +++ b/sleekxmpp/plugins/xep_0153/vcard_avatar.py @@ -10,12 +10,9 @@ import hashlib import logging import threading -from sleekxmpp import JID from sleekxmpp.stanza import Presence from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream import register_stanza_plugin -from sleekxmpp.xmlstream.matcher import StanzaPath -from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.plugins.base import BasePlugin from sleekxmpp.plugins.xep_0153 import stanza, VCardTempUpdate @@ -78,8 +75,17 @@ class XEP_0153(BasePlugin): self.xmpp.roster[jid].send_last_presence() def _start(self, event): - vcard = self.xmpp['xep_0054'].get_vcard() - self._allow_advertising.set() + try: + vcard = self.xmpp['xep_0054'].get_vcard(self.xmpp.boundjid.bare) + data = vcard['vcard_temp']['PHOTO']['BINVAL'] + if not data: + new_hash = '' + else: + new_hash = hashlib.sha1(data).hexdigest() + self.api['set_hash'](self.xmpp.boundjid, args=new_hash) + self._allow_advertising.set() + except XMPPError: + log.debug('Could not retrieve vCard for %s' % self.xmpp.boundjid.bare) def _end(self, event): self._allow_advertising.clear() @@ -118,6 +124,13 @@ class XEP_0153(BasePlugin): log.debug('Could not retrieve vCard for %s' % jid) def _recv_presence(self, pres): + try: + if pres['muc']['affiliation']: + # Don't process vCard avatars for MUC occupants + # since they all share the same bare JID. + return + except: pass + if not pres.match('presence/vcard_temp_update'): self.api['set_hash'](pres['from'], args=None) return @@ -125,7 +138,7 @@ class XEP_0153(BasePlugin): data = pres['vcard_temp_update']['photo'] if data is None: return - elif data == '' or data != self.api['get_hash'](pres['to']): + elif data == '' or data != self.api['get_hash'](pres['from']): ifrom = pres['to'] if self.xmpp.is_component else None self.api['reset_hash'](pres['from'], ifrom=ifrom) self.xmpp.event('vcard_avatar_update', pres) diff --git a/sleekxmpp/plugins/xep_0163.py b/sleekxmpp/plugins/xep_0163.py index 5aa3aef9..2d1a63b7 100644 --- a/sleekxmpp/plugins/xep_0163.py +++ b/sleekxmpp/plugins/xep_0163.py @@ -107,6 +107,8 @@ class XEP_0163(BasePlugin): """ if node is None: node = stanza.namespace + if id is None: + id = 'current' return self.xmpp['xep_0060'].publish(ifrom, node, id=id, diff --git a/sleekxmpp/plugins/xep_0184/receipt.py b/sleekxmpp/plugins/xep_0184/receipt.py index 1fda2066..3e97d8db 100644 --- a/sleekxmpp/plugins/xep_0184/receipt.py +++ b/sleekxmpp/plugins/xep_0184/receipt.py @@ -118,6 +118,9 @@ class XEP_0184(BasePlugin): if stanza['receipt']: return stanza + if not stanza['body']: + return stanza + if stanza['to'].resource: if not self.xmpp['xep_0030'].supports(stanza['to'], feature='urn:xmpp:receipts', diff --git a/sleekxmpp/plugins/xep_0196/__init__.py b/sleekxmpp/plugins/xep_0196/__init__.py new file mode 100644 index 00000000..7aeaf6c9 --- /dev/null +++ b/sleekxmpp/plugins/xep_0196/__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_0196 import stanza +from sleekxmpp.plugins.xep_0196.stanza import UserGaming +from sleekxmpp.plugins.xep_0196.user_gaming import XEP_0196 + + +register_plugin(XEP_0196) diff --git a/sleekxmpp/plugins/xep_0196/stanza.py b/sleekxmpp/plugins/xep_0196/stanza.py new file mode 100644 index 00000000..571c89d7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0196/stanza.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.xmlstream import ElementBase, ET + + +class UserGaming(ElementBase): + + name = 'gaming' + namespace = 'urn:xmpp:gaming:0' + plugin_attrib = 'gaming' + interfaces = set(['character_name', 'character_profile', 'name', + 'level', 'server_address', 'server_name', 'uri']) + sub_interfaces = interfaces + diff --git a/sleekxmpp/plugins/xep_0196/user_gaming.py b/sleekxmpp/plugins/xep_0196/user_gaming.py new file mode 100644 index 00000000..e78f1acc --- /dev/null +++ b/sleekxmpp/plugins/xep_0196/user_gaming.py @@ -0,0 +1,97 @@ +""" + 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_0196 import stanza, UserGaming + + +log = logging.getLogger(__name__) + + +class XEP_0196(BasePlugin): + + """ + XEP-0196: User Gaming + """ + + name = 'xep_0196' + description = 'XEP-0196: User Gaming' + dependencies = set(['xep_0163']) + stanza = stanza + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature=UserGaming.namespace) + self.xmpp['xep_0163'].remove_interest(UserGaming.namespace) + + def session_bind(self, jid): + self.xmpp['xep_0163'].register_pep('user_gaming', UserGaming) + + def publish_gaming(self, name=None, level=None, server_name=None, uri=None, + character_name=None, character_profile=None, server_address=None, + options=None, ifrom=None, block=True, callback=None, timeout=None): + """ + Publish the user's current gaming status. + + Arguments: + name -- The name of the game. + level -- The user's level in the game. + uri -- A URI for the game or relevant gaming service + server_name -- The name of the server where the user is playing. + server_address -- The hostname or IP address of the server where the + user is playing. + character_name -- The name of the user's character in the game. + character_profile -- A URI for a profile of the user's character. + 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. + """ + gaming = UserGaming() + gaming['name'] = name + gaming['level'] = level + gaming['uri'] = uri + gaming['character_name'] = character_name + gaming['character_profile'] = character_profile + gaming['server_name'] = server_name + gaming['server_address'] = server_address + return self.xmpp['xep_0163'].publish(gaming, + node=UserGaming.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 gaming 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. + """ + gaming = UserGaming() + return self.xmpp['xep_0163'].publish(gaming, + node=UserGaming.namespace, + ifrom=ifrom, + block=block, + callback=callback, + timeout=timeout) diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py index 0bdeabf3..836ff4ae 100644 --- a/sleekxmpp/plugins/xep_0199/ping.py +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -9,8 +9,8 @@ import time import logging -import sleekxmpp -from sleekxmpp import Iq +from sleekxmpp.jid import JID +from sleekxmpp.stanza import Iq from sleekxmpp.exceptions import IqError, IqTimeout from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.matcher import StanzaPath @@ -38,7 +38,7 @@ class XEP_0199(BasePlugin): keepalive -- If True, periodically send ping requests to the server. If a ping is not answered, the connection will be reset. - frequency -- Time in seconds between keepalive pings. + interval -- Time in seconds between keepalive pings. Defaults to 300 seconds. timeout -- Time in seconds to wait for a ping response. Defaults to 30 seconds. @@ -53,7 +53,7 @@ class XEP_0199(BasePlugin): stanza = stanza default_config = { 'keepalive': False, - 'frequency': 300, + 'interval': 300, 'timeout': 30 } @@ -61,6 +61,7 @@ class XEP_0199(BasePlugin): """ Start the XEP-0199 plugin. """ + register_stanza_plugin(Iq, Ping) self.xmpp.register_handler( @@ -70,88 +71,70 @@ class XEP_0199(BasePlugin): if self.keepalive: self.xmpp.add_event_handler('session_start', - self._handle_keepalive, + self.enable_keepalive, threaded=True) self.xmpp.add_event_handler('session_end', - self._handle_session_end) + self.disable_keepalive) def plugin_end(self): self.xmpp['xep_0030'].del_feature(feature=Ping.namespace) self.xmpp.remove_handler('Ping') if self.keepalive: self.xmpp.del_event_handler('session_start', - self._handle_keepalive) + self.enable_keepalive) self.xmpp.del_event_handler('session_end', - self._handle_session_end) + self.disable_keepalive) def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Ping.namespace) - def _handle_keepalive(self, event): - """ - Begin periodic pinging of the server. If a ping is not - answered, the connection will be restarted. - - The pinging interval can be adjused using self.frequency - before beginning processing. + def enable_keepalive(self, interval=None, timeout=None): + if interval: + self.interval = interval + if timeout: + self.timeout = timeout - Arguments: - event -- The session_start event. - """ - def scheduled_ping(): - """Send ping request to the server.""" - log.debug("Pinging...") - try: - self.send_ping(self.xmpp.boundjid.host, self.timeout) - except IqError: - log.debug("Ping response was an error." + \ - "Requesting Reconnect.") - self.xmpp.reconnect() - except IqTimeout: - log.debug("Did not recieve ping back in time." + \ - "Requesting Reconnect.") - self.xmpp.reconnect() - - self.xmpp.schedule('Ping Keep Alive', - self.frequency, - scheduled_ping, + self.keepalive = True + self.xmpp.schedule('Ping keepalive', + self.interval, + self._keepalive, repeat=True) - def _handle_session_end(self, event): - self.xmpp.scheduler.remove('Ping Keep Alive') + def disable_keepalive(self, event=None): + self.xmpp.scheduler.remove('Ping keepalive') - def _handle_ping(self, iq): - """ - Automatically reply to ping requests. + def _keepalive(self, event=None): + log.debug("Keepalive ping...") + try: + rtt = self.ping(self.xmpp.boundjid.host, timeout=self.timeout) + except IqTimeout: + log.debug("Did not recieve ping back in time." + \ + "Requesting Reconnect.") + self.xmpp.reconnect() + else: + log.debug('Keepalive RTT: %s' % rtt) - Arguments: - iq -- The ping request. - """ + def _handle_ping(self, iq): + """Automatically reply to ping requests.""" log.debug("Pinged by %s", iq['from']) iq.reply().send() - def send_ping(self, jid, timeout=None, errorfalse=False, - ifrom=None, block=True, callback=None): - """ - Send a ping request and calculate the response time. + def send_ping(self, jid, ifrom=None, block=True, timeout=None, callback=None): + """Send a ping request. Arguments: jid -- The JID that will receive the ping. - timeout -- Time in seconds to wait for a response. - Defaults to self.timeout. - errorfalse -- Indicates if False should be returned - if an error stanza is received. Defaults - to False. ifrom -- Specifiy the sender JID. block -- Indicate if execution should block until a pong response is received. Defaults to True. + timeout -- Time in seconds to wait for a response. + Defaults to self.timeout. callback -- Optional handler to execute when a pong is received. Useful in conjunction with the option block=False. """ - log.debug("Pinging %s", jid) - if timeout is None: + if not timeout: timeout = self.timeout iq = self.xmpp.Iq() @@ -160,21 +143,44 @@ class XEP_0199(BasePlugin): iq['from'] = ifrom iq.enable('ping') - start_time = time.clock() + return iq.send(block=block, timeout=timeout, callback=callback) - try: - resp = iq.send(block=block, - timeout=timeout, - callback=callback) - except IqError as err: - resp = err.iq + def ping(self, jid=None, ifrom=None, timeout=None): + """Send a ping request and calculate RTT. - end_time = time.clock() - - delay = end_time - start_time + Arguments: + jid -- The JID that will receive the ping. + ifrom -- Specifiy the sender JID. + timeout -- Time in seconds to wait for a response. + Defaults to self.timeout. + """ + own_host = False + if not jid: + if self.xmpp.is_component: + jid = self.xmpp.server + else: + jid = self.xmpp.boundjid.host + jid = JID(jid) + if jid == self.xmpp.boundjid.host or \ + self.xmpp.is_component and jid == self.xmpp.server: + own_host = True + + if not timeout: + timeout = self.timeout - if not block: - return None + start = time.time() - log.debug("Pong: %s %f", jid, delay) - return delay + log.debug('Pinging %s' % jid) + try: + self.send_ping(jid, ifrom=ifrom, timeout=timeout) + except IqError as e: + if own_host: + rtt = time.time() - start + log.debug('Pinged %s, RTT: %s', jid, rtt) + return rtt + else: + raise e + else: + rtt = time.time() - start + log.debug('Pinged %s, RTT: %s', jid, rtt) + return rtt diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py index fe20449d..d5b3af37 100644 --- a/sleekxmpp/plugins/xep_0202/time.py +++ b/sleekxmpp/plugins/xep_0202/time.py @@ -48,7 +48,7 @@ class XEP_0202(BasePlugin): self.local_time = default_local_time
- self.xmpp.registerHandler(
+ self.xmpp.register_handler(
Callback('Entity Time',
StanzaPath('iq/entity_time'),
self._handle_time_request))
diff --git a/sleekxmpp/plugins/xep_0203/stanza.py b/sleekxmpp/plugins/xep_0203/stanza.py index 9a11cae9..e147e975 100644 --- a/sleekxmpp/plugins/xep_0203/stanza.py +++ b/sleekxmpp/plugins/xep_0203/stanza.py @@ -8,6 +8,7 @@ import datetime as dt +from sleekxmpp.jid import JID from sleekxmpp.xmlstream import ElementBase from sleekxmpp.plugins import xep_0082 @@ -20,14 +21,15 @@ class Delay(ElementBase): interfaces = set(('from', 'stamp', 'text')) def get_from(self): - return JID(self._get_attr('from')) + from_ = self._get_attr('from') + return JID(from_) if from_ else None def set_from(self, value): self._set_attr('from', str(value)) def get_stamp(self): timestamp = self._get_attr('stamp') - return xep_0082.parse(timestamp) + return xep_0082.parse(timestamp) if timestamp else None def set_stamp(self, value): if isinstance(value, dt.datetime): diff --git a/sleekxmpp/plugins/xep_0222.py b/sleekxmpp/plugins/xep_0222.py index 1073c1a1..2cc7f703 100644 --- a/sleekxmpp/plugins/xep_0222.py +++ b/sleekxmpp/plugins/xep_0222.py @@ -76,10 +76,11 @@ class XEP_0222(BasePlugin): ftype='hidden', value='http://jabber.org/protocol/pubsub#publish-options') + fields = options['fields'] for field, value in self.profile.items(): - if field not in options.fields: + if field not in fields: options.add_field(var=field) - options.fields[field]['value'] = value + options['fields'][field]['value'] = value return self.xmpp['xep_0163'].publish(stanza, node, options=options, diff --git a/sleekxmpp/plugins/xep_0223.py b/sleekxmpp/plugins/xep_0223.py index ab99f277..abbecfc7 100644 --- a/sleekxmpp/plugins/xep_0223.py +++ b/sleekxmpp/plugins/xep_0223.py @@ -76,10 +76,11 @@ class XEP_0223(BasePlugin): ftype='hidden', value='http://jabber.org/protocol/pubsub#publish-options') + fields = options['fields'] for field, value in self.profile.items(): - if field not in options.fields: + if field not in fields: options.add_field(var=field) - options.fields[field]['value'] = value + options['fields'][field]['value'] = value return self.xmpp['xep_0163'].publish(stanza, node, options=options, diff --git a/sleekxmpp/plugins/xep_0231/bob.py b/sleekxmpp/plugins/xep_0231/bob.py index d86a5ddf..5e1f590b 100644 --- a/sleekxmpp/plugins/xep_0231/bob.py +++ b/sleekxmpp/plugins/xep_0231/bob.py @@ -10,7 +10,7 @@ import logging import hashlib -from sleekxmpp.stanza import Iq +from sleekxmpp.stanza import Iq, Message, Presence from sleekxmpp.exceptions import XMPPError from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath @@ -36,6 +36,8 @@ class XEP_0231(BasePlugin): self._cids = {} register_stanza_plugin(Iq, BitsOfBinary) + register_stanza_plugin(Message, BitsOfBinary) + register_stanza_plugin(Presence, BitsOfBinary) self.xmpp.register_handler( Callback('Bits of Binary - Iq', diff --git a/sleekxmpp/plugins/xep_0231/stanza.py b/sleekxmpp/plugins/xep_0231/stanza.py index a51f5a03..8bf0d6ee 100644 --- a/sleekxmpp/plugins/xep_0231/stanza.py +++ b/sleekxmpp/plugins/xep_0231/stanza.py @@ -7,9 +7,10 @@ See the file LICENSE for copying permission. """ +import base64 -from base64 import b64encode, b64decode +from sleekxmpp.util import bytes from sleekxmpp.xmlstream import ElementBase @@ -26,10 +27,10 @@ class BitsOfBinary(ElementBase): self._set_attr('max-age', value) def get_data(self): - return b64decode(self.xml.text) + return base64.b64decode(bytes(self.xml.text)) def set_data(self, value): - self.xml.text = b64encode(value) + self.xml.text = bytes(base64.b64encode(value)).decode('utf-8') def del_data(self): self.xml.text = '' diff --git a/sleekxmpp/plugins/xep_0257/stanza.py b/sleekxmpp/plugins/xep_0257/stanza.py index 17e20136..c3c41db2 100644 --- a/sleekxmpp/plugins/xep_0257/stanza.py +++ b/sleekxmpp/plugins/xep_0257/stanza.py @@ -10,7 +10,7 @@ from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin class Certs(ElementBase): - name = 'query' + name = 'items' namespace = 'urn:xmpp:saslcert:1' plugin_attrib = 'sasl_certs' interfaces = set() diff --git a/sleekxmpp/plugins/xep_0313/mam.py b/sleekxmpp/plugins/xep_0313/mam.py index 15aee828..4b82ca03 100644 --- a/sleekxmpp/plugins/xep_0313/mam.py +++ b/sleekxmpp/plugins/xep_0313/mam.py @@ -36,6 +36,8 @@ class XEP_0313(BasePlugin): register_stanza_plugin(Iq, stanza.MAM) register_stanza_plugin(Iq, stanza.Preferences) register_stanza_plugin(Message, stanza.Result) + register_stanza_plugin(Message, stanza.Archived, iterable=True) + register_stanza_plugin(stanza.Result, self.xmpp['xep_0297'].stanza.Forwarded) register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set) def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None, diff --git a/sleekxmpp/plugins/xep_0313/stanza.py b/sleekxmpp/plugins/xep_0313/stanza.py index a33c2e35..81576cd4 100644 --- a/sleekxmpp/plugins/xep_0313/stanza.py +++ b/sleekxmpp/plugins/xep_0313/stanza.py @@ -25,13 +25,13 @@ class MAM(ElementBase): self._results = [] def get_start(self): - timestamp = self._get_attr('start') + timestamp = self._get_sub_text('start') return xep_0082.parse(timestamp) def set_start(self, value): if isinstance(value, dt.datetime): value = xep_0082.format_datetime(value) - self._set_attr('start', value) + self._set_sub_text('start', value) def get_end(self): timestamp = self._get_sub_text('end') @@ -122,10 +122,18 @@ class Result(ElementBase): name = 'result' namespace = 'urn:xmpp:mam:tmp' plugin_attrib = 'mam_result' - interfaces = set(['forwarded', 'queryid', 'id']) + interfaces = set(['queryid', 'id']) - def get_forwarded(self): - return self.parent()['forwarded'] - def del_forwarded(self): - del self.parent()['forwarded'] +class Archived(ElementBase): + name = 'archived' + namespace = 'urn:xmpp:mam:tmp' + plugin_attrib = 'mam_archived' + plugin_multi_attrib = 'mam_archives' + interfaces = set(['by', 'id']) + + def get_by(self): + return JID(self._get_attr('by')) + + def set_by(self): + return self._set_attr('by', str(value)) diff --git a/sleekxmpp/plugins/xep_0319/__init__.py b/sleekxmpp/plugins/xep_0319/__init__.py new file mode 100644 index 00000000..4756e63e --- /dev/null +++ b/sleekxmpp/plugins/xep_0319/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 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_0319 import stanza +from sleekxmpp.plugins.xep_0319.stanza import Idle +from sleekxmpp.plugins.xep_0319.idle import XEP_0319 + + +register_plugin(XEP_0319) diff --git a/sleekxmpp/plugins/xep_0319/idle.py b/sleekxmpp/plugins/xep_0319/idle.py new file mode 100644 index 00000000..90456f9f --- /dev/null +++ b/sleekxmpp/plugins/xep_0319/idle.py @@ -0,0 +1,75 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from datetime import datetime, timedelta + +from sleekxmpp.stanza import Presence +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_0319 import stanza + + +class XEP_0319(BasePlugin): + name = 'xep_0319' + description = 'XEP-0319: Last User Interaction in Presence' + dependencies = set(['xep_0012']) + stanza = stanza + + def plugin_init(self): + self._idle_stamps = {} + register_stanza_plugin(Presence, stanza.Idle) + self.api.register(self._set_idle, + 'set_idle', + default=True) + self.api.register(self._get_idle, + 'get_idle', + default=True) + self.xmpp.register_handler( + Callback('Idle Presence', + StanzaPath('presence/idle'), + self._idle_presence)) + self.xmpp.add_filter('out', self._stamp_idle_presence) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature('urn:xmpp:idle:1') + + def plugin_end(self): + self.xmpp['xep_0030'].del_feature(feature='urn:xmpp:idle:1') + self.xmpp.del_filter('out', self._stamp_idle_presence) + self.xmpp.remove_handler('Idle Presence') + + def idle(self, jid=None, since=None): + seconds = None + if since is None: + since = datetime.now() + else: + seconds = datetime.now() - since + self.api['set_idle'](jid, None, None, since) + self.xmpp['xep_0012'].set_last_activity(jid=jid, seconds=seconds) + + def active(self, jid=None): + self.api['set_idle'](jid, None, None, None) + self.xmpp['xep_0012'].del_last_activity(jid) + + def _set_idle(self, jid, node, ifrom, data): + self._idle_stamps[jid] = data + + def _get_idle(self, jid, node, ifrom, data): + return self._idle_stamps.get(jid, None) + + def _idle_presence(self, pres): + self.xmpp.event('presence_idle', pres) + + def _stamp_idle_presence(self, stanza): + if isinstance(stanza, Presence): + since = self.api['get_idle'](stanza['from'] or self.xmpp.boundjid) + if since: + stanza['idle']['since'] = since + return stanza diff --git a/sleekxmpp/plugins/xep_0319/stanza.py b/sleekxmpp/plugins/xep_0319/stanza.py new file mode 100644 index 00000000..abfb4f41 --- /dev/null +++ b/sleekxmpp/plugins/xep_0319/stanza.py @@ -0,0 +1,28 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class Idle(ElementBase): + name = 'idle' + namespace = 'urn:xmpp:idle:1' + plugin_attrib = 'idle' + interfaces = set(['since']) + + def get_since(self): + timestamp = self._get_attr('since') + return xep_0082.parse(timestamp) + + def set_since(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('since', value) diff --git a/sleekxmpp/plugins/xep_0323/__init__.py b/sleekxmpp/plugins/xep_0323/__init__.py new file mode 100644 index 00000000..10779ada --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/__init__.py @@ -0,0 +1,18 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0323.sensordata import XEP_0323 +from sleekxmpp.plugins.xep_0323 import stanza + +register_plugin(XEP_0323) + +xep_0323=XEP_0323 diff --git a/sleekxmpp/plugins/xep_0323/device.py b/sleekxmpp/plugins/xep_0323/device.py new file mode 100644 index 00000000..80e6fd95 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/device.py @@ -0,0 +1,258 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime +import logging + +class Device(object): + """ + Example implementation of a device readout object. + Is registered in the XEP_0323.register_node call + The device object may be any custom implementation to support + specific devices, but it must implement the functions: + has_field + request_fields + """ + + def __init__(self, nodeId, fields=None): + if not fields: + fields = {} + + self.nodeId = nodeId + self.fields = fields # see fields described below + # {'type':'numeric', + # 'name':'myname', + # 'value': 42, + # 'unit':'Z'}]; + self.timestamp_data = {} + self.momentary_data = {} + self.momentary_timestamp = "" + logging.debug("Device object started nodeId %s",nodeId) + + def has_field(self, field): + """ + Returns true if the supplied field name exists in this device. + + Arguments: + field -- The field name + """ + if field in self.fields.keys(): + return True + return False + + def refresh(self, fields): + """ + override method to do the refresh work + refresh values from hardware or other + """ + pass + + + def request_fields(self, fields, flags, session, callback): + """ + Starts a data readout. Verifies the requested fields, + refreshes the data (if needed) and calls the callback + with requested data. + + + Arguments: + fields -- List of field names to readout + flags -- [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + session -- Session id, only used in the callback as identifier + callback -- Callback function to call when data is available. + + The callback function must support the following arguments: + + session -- Session id, as supplied in the request_fields call + nodeId -- Identifier for this device + result -- The current result status of the readout. Valid values are: + "error" - Readout failed. + "fields" - Contains readout data. + "done" - Indicates that the readout is complete. May contain + readout data. + timestamp_block -- [optional] Only applies when result != "error" + The readout data. Structured as a dictionary: + { + timestamp: timestamp for this datablock, + fields: list of field dictionary (one per readout field). + readout field dictionary format: + { + type: The field type (numeric, boolean, dateTime, timeSpan, string, enum) + name: The field name + value: The field value + unit: The unit of the field. Only applies to type numeric. + dataType: The datatype of the field. Only applies to type enum. + flags: [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + } + } + error_msg -- [optional] Only applies when result == "error". + Error details when a request failed. + + """ + logging.debug("request_fields called looking for fields %s",fields) + if len(fields) > 0: + # Check availiability + for f in fields: + if f not in self.fields.keys(): + self._send_reject(session, callback) + return False + else: + # Request all fields + fields = self.fields.keys() + + + # Refresh data from device + # ... + logging.debug("about to refresh device fields %s",fields) + self.refresh(fields) + + if "momentary" in flags and flags['momentary'] == "true" or \ + "all" in flags and flags['all'] == "true": + ts_block = {} + timestamp = "" + + if len(self.momentary_timestamp) > 0: + timestamp = self.momentary_timestamp + else: + timestamp = self._get_timestamp() + + field_block = [] + for f in self.momentary_data: + if f in fields: + field_block.append({"name": f, + "type": self.fields[f]["type"], + "unit": self.fields[f]["unit"], + "dataType": self.fields[f]["dataType"], + "value": self.momentary_data[f]["value"], + "flags": self.momentary_data[f]["flags"]}) + ts_block["timestamp"] = timestamp + ts_block["fields"] = field_block + + callback(session, result="done", nodeId=self.nodeId, timestamp_block=ts_block) + return + + from_flag = self._datetime_flag_parser(flags, 'from') + to_flag = self._datetime_flag_parser(flags, 'to') + + for ts in sorted(self.timestamp_data.keys()): + tsdt = datetime.datetime.strptime(ts, "%Y-%m-%dT%H:%M:%S") + if not from_flag is None: + if tsdt < from_flag: + #print (str(tsdt) + " < " + str(from_flag)) + continue + if not to_flag is None: + if tsdt > to_flag: + #print (str(tsdt) + " > " + str(to_flag)) + continue + + ts_block = {} + field_block = [] + + for f in self.timestamp_data[ts]: + if f in fields: + field_block.append({"name": f, + "type": self.fields[f]["type"], + "unit": self.fields[f]["unit"], + "dataType": self.fields[f]["dataType"], + "value": self.timestamp_data[ts][f]["value"], + "flags": self.timestamp_data[ts][f]["flags"]}) + + ts_block["timestamp"] = ts + ts_block["fields"] = field_block + callback(session, result="fields", nodeId=self.nodeId, timestamp_block=ts_block) + callback(session, result="done", nodeId=self.nodeId, timestamp_block=None) + + def _datetime_flag_parser(self, flags, flagname): + if not flagname in flags: + return None + + dt = None + try: + dt = datetime.datetime.strptime(flags[flagname], "%Y-%m-%dT%H:%M:%S") + except ValueError: + # Badly formatted datetime, ignore it + pass + return dt + + + def _get_timestamp(self): + """ + Generates a properly formatted timestamp of current time + """ + return datetime.datetime.now().replace(microsecond=0).isoformat() + + def _send_reject(self, session, callback): + """ + Sends a reject to the caller + + Arguments: + session -- Session id, see definition in request_fields function + callback -- Callback function, see definition in request_fields function + """ + callback(session, result="error", nodeId=self.nodeId, timestamp_block=None, error_msg="Reject") + + def _add_field(self, name, typename, unit=None, dataType=None): + """ + Adds a field to the device + + Arguments: + name -- Name of the field + typename -- Type of the field (numeric, boolean, dateTime, timeSpan, string, enum) + unit -- [optional] only applies to "numeric". Unit for the field. + dataType -- [optional] only applies to "enum". Datatype for the field. + """ + self.fields[name] = {"type": typename, "unit": unit, "dataType": dataType} + + def _add_field_timestamp_data(self, name, timestamp, value, flags=None): + """ + Adds timestamped data to a field + + Arguments: + name -- Name of the field + timestamp -- Timestamp for the data (string) + value -- Field value at the timestamp + flags -- [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + """ + if not name in self.fields.keys(): + return False + if not timestamp in self.timestamp_data: + self.timestamp_data[timestamp] = {} + + self.timestamp_data[timestamp][name] = {"value": value, "flags": flags} + return True + + def _add_field_momentary_data(self, name, value, flags=None): + """ + Sets momentary data to a field + + Arguments: + name -- Name of the field + value -- Field value at the timestamp + flags -- [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + """ + if name not in self.fields: + return False + if flags is None: + flags = {} + + flags["momentary"] = "true" + self.momentary_data[name] = {"value": value, "flags": flags} + return True + + def _set_momentary_timestamp(self, timestamp): + """ + This function is only for unit testing to produce predictable results. + """ + self.momentary_timestamp = timestamp + diff --git a/sleekxmpp/plugins/xep_0323/sensordata.py b/sleekxmpp/plugins/xep_0323/sensordata.py new file mode 100644 index 00000000..30c28504 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/sensordata.py @@ -0,0 +1,723 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import time +import datetime +from threading import Thread, Lock, Timer + +from sleekxmpp.plugins.xep_0323.timerreset import TimerReset + +from sleekxmpp.xmlstream import JID +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0323 import stanza +from sleekxmpp.plugins.xep_0323.stanza import Sensordata + + +log = logging.getLogger(__name__) + + +class XEP_0323(BasePlugin): + + """ + XEP-0323: IoT Sensor Data + + + This XEP provides the underlying architecture, basic operations and data + structures for sensor data communication over XMPP networks. It includes + a hardware abstraction model, removing any technical detail implemented + in underlying technologies. + + Also see <http://xmpp.org/extensions/xep-0323.html> + + Configuration Values: + threaded -- Indicates if communication with sensors should be threaded. + Defaults to True. + + Events: + Sensor side + ----------- + Sensordata Event:Req -- Received a request for data + Sensordata Event:Cancel -- Received a cancellation for a request + + Client side + ----------- + Sensordata Event:Accepted -- Received a accept from sensor for a request + Sensordata Event:Rejected -- Received a reject from sensor for a request + Sensordata Event:Cancelled -- Received a cancel confirm from sensor + Sensordata Event:Fields -- Received fields from sensor for a request + This may be triggered multiple times since + the sensor can split up its response in + multiple messages. + Sensordata Event:Failure -- Received a failure indication from sensor + for a request. Typically a comm timeout. + + Attributes: + threaded -- Indicates if command events should be threaded. + Defaults to True. + sessions -- A dictionary or equivalent backend mapping + session IDs to dictionaries containing data + relevant to a request's session. This dictionary is used + both by the client and sensor side. On client side, seqnr + is used as key, while on sensor side, a session_id is used + as key. This ensures that the two will not collide, so + one instance can be both client and sensor. + Sensor side + ----------- + nodes -- A dictionary mapping sensor nodes that are serviced through + this XMPP instance to their device handlers ("drivers"). + Client side + ----------- + last_seqnr -- The last used sequence number (integer). One sequence of + communication (e.g. -->request, <--accept, <--fields) + between client and sensor is identified by a unique + sequence number (unique between the client/sensor pair) + + Methods: + plugin_init -- Overrides base_plugin.plugin_init + post_init -- Overrides base_plugin.post_init + plugin_end -- Overrides base_plugin.plugin_end + + Sensor side + ----------- + register_node -- Register a sensor as available from this XMPP + instance. + + Client side + ----------- + request_data -- Initiates a request for data from one or more + sensors. Non-blocking, a callback function will + be called when data is available. + + """ + + name = 'xep_0323' + description = 'XEP-0323 Internet of Things - Sensor Data' + dependencies = set(['xep_0030']) + stanza = stanza + + + default_config = { + 'threaded': True +# 'session_db': None + } + + def plugin_init(self): + """ Start the XEP-0323 plugin """ + + self.xmpp.register_handler( + Callback('Sensordata Event:Req', + StanzaPath('iq@type=get/req'), + self._handle_event_req)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Accepted', + StanzaPath('iq@type=result/accepted'), + self._handle_event_accepted)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Rejected', + StanzaPath('iq@type=error/rejected'), + self._handle_event_rejected)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Cancel', + StanzaPath('iq@type=get/cancel'), + self._handle_event_cancel)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Cancelled', + StanzaPath('iq@type=result/cancelled'), + self._handle_event_cancelled)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Fields', + StanzaPath('message/fields'), + self._handle_event_fields)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Failure', + StanzaPath('message/failure'), + self._handle_event_failure)) + + self.xmpp.register_handler( + Callback('Sensordata Event:Started', + StanzaPath('message/started'), + self._handle_event_started)) + + # Server side dicts + self.nodes = {} + self.sessions = {} + + self.last_seqnr = 0 + self.seqnr_lock = Lock() + + ## For testning only + self.test_authenticated_from = "" + + def post_init(self): + """ Init complete. Register our features in Serivce discovery. """ + BasePlugin.post_init(self) + self.xmpp['xep_0030'].add_feature(Sensordata.namespace) + self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple()) + + def _new_session(self): + """ Return a new session ID. """ + return str(time.time()) + '-' + self.xmpp.new_id() + + def session_bind(self, jid): + logging.debug("setting the Disco discovery for %s" % Sensordata.namespace) + self.xmpp['xep_0030'].add_feature(Sensordata.namespace) + self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple()) + + + def plugin_end(self): + """ Stop the XEP-0323 plugin """ + self.sessions.clear() + self.xmpp.remove_handler('Sensordata Event:Req') + self.xmpp.remove_handler('Sensordata Event:Accepted') + self.xmpp.remove_handler('Sensordata Event:Rejected') + self.xmpp.remove_handler('Sensordata Event:Cancel') + self.xmpp.remove_handler('Sensordata Event:Cancelled') + self.xmpp.remove_handler('Sensordata Event:Fields') + self.xmpp['xep_0030'].del_feature(feature=Sensordata.namespace) + + + # ================================================================= + # Sensor side (data provider) API + + def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None): + """ + Register a sensor/device as available for serving of data through this XMPP + instance. + + The device object may by any custom implementation to support + specific devices, but it must implement the functions: + has_field + request_fields + according to the interfaces shown in the example device.py file. + + Arguments: + nodeId -- The identifier for the device + device -- The device object + commTimeout -- Time in seconds to wait between each callback from device during + a data readout. Float. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + self.nodes[nodeId] = {"device": device, + "commTimeout": commTimeout, + "sourceId": sourceId, + "cacheType": cacheType} + + def _set_authenticated(self, auth=''): + """ Internal testing function """ + self.test_authenticated_from = auth + + + def _handle_event_req(self, iq): + """ + Event handler for reception of an Iq with req - this is a request. + + Verifies that + - all the requested nodes are available + - at least one of the requested fields is available from at least + one of the nodes + + If the request passes verification, an accept response is sent, and + the readout process is started in a separate thread. + If the verification fails, a reject message is sent. + """ + + seqnr = iq['req']['seqnr'] + error_msg = '' + req_ok = True + + # Authentication + if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from: + # Invalid authentication + req_ok = False + error_msg = "Access denied" + + # Nodes + process_nodes = [] + if len(iq['req']['nodes']) > 0: + for n in iq['req']['nodes']: + if not n['nodeId'] in self.nodes: + req_ok = False + error_msg = "Invalid nodeId " + n['nodeId'] + process_nodes = [n['nodeId'] for n in iq['req']['nodes']] + else: + process_nodes = self.nodes.keys() + + # Fields - if we just find one we are happy, otherwise we reject + process_fields = [] + if len(iq['req']['fields']) > 0: + found = False + for f in iq['req']['fields']: + for node in self.nodes: + if self.nodes[node]["device"].has_field(f['name']): + found = True + break + if not found: + req_ok = False + error_msg = "Invalid field " + f['name'] + process_fields = [f['name'] for n in iq['req']['fields']] + + req_flags = iq['req']._get_flags() + + request_delay_sec = None + if 'when' in req_flags: + # Timed request - requires datetime string in iso format + # ex. 2013-04-05T15:00:03 + dt = None + try: + dt = datetime.datetime.strptime(req_flags['when'], "%Y-%m-%dT%H:%M:%S") + except ValueError: + req_ok = False + error_msg = "Invalid datetime in 'when' flag, please use ISO format (i.e. 2013-04-05T15:00:03)." + + if not dt is None: + # Datetime properly formatted + dtnow = datetime.datetime.now() + dtdiff = dt - dtnow + request_delay_sec = dtdiff.seconds + dtdiff.days * 24 * 3600 + if request_delay_sec <= 0: + req_ok = False + error_msg = "Invalid datetime in 'when' flag, cannot set a time in the past. Current time: " + dtnow.isoformat() + + if req_ok: + session = self._new_session() + self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr} + self.sessions[session]["commTimers"] = {} + self.sessions[session]["nodeDone"] = {} + + #print("added session: " + str(self.sessions)) + + iq.reply() + iq['accepted']['seqnr'] = seqnr + if not request_delay_sec is None: + iq['accepted']['queued'] = "true" + iq.send(block=False) + + self.sessions[session]["node_list"] = process_nodes + + if not request_delay_sec is None: + # Delay request to requested time + timer = Timer(request_delay_sec, self._event_delayed_req, args=(session, process_fields, req_flags)) + self.sessions[session]["commTimers"]["delaytimer"] = timer + timer.start() + return + + if self.threaded: + #print("starting thread") + tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags)) + tr_req.start() + #print("started thread") + else: + self._threaded_node_request(session, process_fields, req_flags) + + else: + iq.reply() + iq['type'] = 'error' + iq['rejected']['seqnr'] = seqnr + iq['rejected']['error'] = error_msg + iq.send(block=False) + + def _threaded_node_request(self, session, process_fields, flags): + """ + Helper function to handle the device readouts in a separate thread. + + Arguments: + session -- The request session id + process_fields -- The fields to request from the devices + flags -- [optional] flags to pass to the devices, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + """ + for node in self.sessions[session]["node_list"]: + self.sessions[session]["nodeDone"][node] = False + + for node in self.sessions[session]["node_list"]: + timer = TimerReset(self.nodes[node]['commTimeout'], self._event_comm_timeout, args=(session, node)) + self.sessions[session]["commTimers"][node] = timer + #print("Starting timer " + str(timer) + ", timeout: " + str(self.nodes[node]['commTimeout'])) + timer.start() + self.nodes[node]['device'].request_fields(process_fields, flags=flags, session=session, callback=self._device_field_request_callback) + + def _event_comm_timeout(self, session, nodeId): + """ + Triggered if any of the readout operations timeout. + Sends a failure message back to the client, stops communicating + with the failing device. + + Arguments: + session -- The request session id + nodeId -- The id of the device which timed out + """ + msg = self.xmpp.Message() + msg['from'] = self.sessions[session]['to'] + msg['to'] = self.sessions[session]['from'] + msg['failure']['seqnr'] = self.sessions[session]['seqnr'] + msg['failure']['error']['text'] = "Timeout" + msg['failure']['error']['nodeId'] = nodeId + msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat() + + # Drop communication with this device and check if we are done + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + msg['failure']['done'] = 'true' + msg.send() + # The session is complete, delete it + #print("del session " + session + " due to timeout") + del self.sessions[session] + + def _event_delayed_req(self, session, process_fields, req_flags): + """ + Triggered when the timer from a delayed request fires. + + Arguments: + session -- The request session id + process_fields -- The fields to request from the devices + flags -- [optional] flags to pass to the devices, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + """ + msg = self.xmpp.Message() + msg['from'] = self.sessions[session]['to'] + msg['to'] = self.sessions[session]['from'] + msg['started']['seqnr'] = self.sessions[session]['seqnr'] + msg.send() + + if self.threaded: + tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields, req_flags)) + tr_req.start() + else: + self._threaded_node_request(session, process_fields, req_flags) + + def _all_nodes_done(self, session): + """ + Checks wheter all devices are done replying to the readout. + + Arguments: + session -- The request session id + """ + for n in self.sessions[session]["nodeDone"]: + if not self.sessions[session]["nodeDone"][n]: + return False + return True + + def _device_field_request_callback(self, session, nodeId, result, timestamp_block, error_msg=None): + """ + Callback function called by the devices when they have any additional data. + Composes a message with the data and sends it back to the client, and resets + the timeout timer for the device. + + Arguments: + session -- The request session id + nodeId -- The device id which initiated the callback + result -- The current result status of the readout. Valid values are: + "error" - Readout failed. + "fields" - Contains readout data. + "done" - Indicates that the readout is complete. May contain + readout data. + timestamp_block -- [optional] Only applies when result != "error" + The readout data. Structured as a dictionary: + { + timestamp: timestamp for this datablock, + fields: list of field dictionary (one per readout field). + readout field dictionary format: + { + type: The field type (numeric, boolean, dateTime, timeSpan, string, enum) + name: The field name + value: The field value + unit: The unit of the field. Only applies to type numeric. + dataType: The datatype of the field. Only applies to type enum. + flags: [optional] data classifier flags for the field, e.g. momentary + Formatted as a dictionary like { "flag name": "flag value" ... } + } + } + error_msg -- [optional] Only applies when result == "error". + Error details when a request failed. + """ + if not session in self.sessions: + # This can happend if a session was deleted, like in a cancellation. Just drop the data. + return + + if result == "error": + self.sessions[session]["commTimers"][nodeId].cancel() + + msg = self.xmpp.Message() + msg['from'] = self.sessions[session]['to'] + msg['to'] = self.sessions[session]['from'] + msg['failure']['seqnr'] = self.sessions[session]['seqnr'] + msg['failure']['error']['text'] = error_msg + msg['failure']['error']['nodeId'] = nodeId + msg['failure']['error']['timestamp'] = datetime.datetime.now().replace(microsecond=0).isoformat() + + # Drop communication with this device and check if we are done + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + msg['failure']['done'] = 'true' + # The session is complete, delete it + # print("del session " + session + " due to error") + del self.sessions[session] + msg.send() + else: + msg = self.xmpp.Message() + msg['from'] = self.sessions[session]['to'] + msg['to'] = self.sessions[session]['from'] + msg['fields']['seqnr'] = self.sessions[session]['seqnr'] + + if timestamp_block is not None and len(timestamp_block) > 0: + node = msg['fields'].add_node(nodeId) + ts = node.add_timestamp(timestamp_block["timestamp"]) + + for f in timestamp_block["fields"]: + data = ts.add_data( typename=f['type'], + name=f['name'], + value=f['value'], + unit=f['unit'], + dataType=f['dataType'], + flags=f['flags']) + + if result == "done": + self.sessions[session]["commTimers"][nodeId].cancel() + self.sessions[session]["nodeDone"][nodeId] = True + msg['fields']['done'] = 'true' + if (self._all_nodes_done(session)): + # The session is complete, delete it + # print("del session " + session + " due to complete") + del self.sessions[session] + else: + # Restart comm timer + self.sessions[session]["commTimers"][nodeId].reset() + + msg.send() + + def _handle_event_cancel(self, iq): + """ Received Iq with cancel - this is a cancel request. + Delete the session and confirm. """ + + seqnr = iq['cancel']['seqnr'] + # Find the session + for s in self.sessions: + if self.sessions[s]['from'] == iq['from'] and self.sessions[s]['to'] == iq['to'] and self.sessions[s]['seqnr'] == seqnr: + # found it. Cancel all timers + for n in self.sessions[s]["commTimers"]: + self.sessions[s]["commTimers"][n].cancel() + + # Confirm + iq.reply() + iq['type'] = 'result' + iq['cancelled']['seqnr'] = seqnr + iq.send(block=False) + + # Delete session + del self.sessions[s] + return + + # Could not find session, send reject + iq.reply() + iq['type'] = 'error' + iq['rejected']['seqnr'] = seqnr + iq['rejected']['error'] = "Cancel request received, no matching request is active." + iq.send(block=False) + + # ================================================================= + # Client side (data retriever) API + + def request_data(self, from_jid, to_jid, callback, nodeIds=None, fields=None, flags=None): + """ + Called on the client side to initiade a data readout. + Composes a message with the request and sends it to the device(s). + Does not block, the callback will be called when data is available. + + Arguments: + from_jid -- The jid of the requester + to_jid -- The jid of the device(s) + callback -- The callback function to call when data is availble. + + The callback function must support the following arguments: + + from_jid -- The jid of the responding device(s) + result -- The current result status of the readout. Valid values are: + "accepted" - Readout request accepted + "queued" - Readout request accepted and queued + "rejected" - Readout request rejected + "failure" - Readout failed. + "cancelled" - Confirmation of request cancellation. + "started" - Previously queued request is now started + "fields" - Contains readout data. + "done" - Indicates that the readout is complete. + + nodeId -- [optional] Mandatory when result == "fields" or "failure". + The node Id of the responding device. One callback will only + contain data from one device. + timestamp -- [optional] Mandatory when result == "fields". + The timestamp of data in this callback. One callback will only + contain data from one timestamp. + fields -- [optional] Mandatory when result == "fields". + List of field dictionaries representing the readout data. + Dictionary format: + { + typename: The field type (numeric, boolean, dateTime, timeSpan, string, enum) + name: The field name + value: The field value + unit: The unit of the field. Only applies to type numeric. + dataType: The datatype of the field. Only applies to type enum. + flags: [optional] data classifier flags for the field, e.g. momentary. + Formatted as a dictionary like { "flag name": "flag value" ... } + } + + error_msg -- [optional] Mandatory when result == "rejected" or "failure". + Details about why the request is rejected or failed. + "rejected" means that the request is stopped, but note that the + request will continue even after a "failure". "failure" only means + that communication was stopped to that specific device, other + device(s) (if any) will continue their readout. + + nodeIds -- [optional] Limits the request to the node Ids in this list. + fields -- [optional] Limits the request to the field names in this list. + flags -- [optional] Limits the request according to the flags, or sets + readout conditions such as timing. + + Return value: + session -- Session identifier. Client can use this as a reference to cancel + the request. + """ + iq = self.xmpp.Iq() + iq['from'] = from_jid + iq['to'] = to_jid + iq['type'] = "get" + seqnr = self._get_new_seqnr() + iq['id'] = seqnr + iq['req']['seqnr'] = seqnr + if nodeIds is not None: + for nodeId in nodeIds: + iq['req'].add_node(nodeId) + if fields is not None: + for field in fields: + iq['req'].add_field(field) + + iq['req']._set_flags(flags) + + self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "seqnr": seqnr, "callback": callback} + iq.send(block=False) + + return seqnr + + def cancel_request(self, session): + """ + Called on the client side to cancel a request for data readout. + Composes a message with the cancellation and sends it to the device(s). + Does not block, the callback will be called when cancellation is + confirmed. + + Arguments: + session -- The session id of the request to cancel + """ + seqnr = session + iq = self.xmpp.Iq() + iq['from'] = self.sessions[seqnr]['from'] + iq['to'] = self.sessions[seqnr]['to'] + iq['type'] = "get" + iq['id'] = seqnr + iq['cancel']['seqnr'] = seqnr + iq.send(block=False) + + def _get_new_seqnr(self): + """ Returns a unique sequence number (unique across threads) """ + self.seqnr_lock.acquire() + self.last_seqnr += 1 + self.seqnr_lock.release() + return str(self.last_seqnr) + + def _handle_event_accepted(self, iq): + """ Received Iq with accepted - request was accepted """ + seqnr = iq['accepted']['seqnr'] + result = "accepted" + if iq['accepted']['queued'] == 'true': + result = "queued" + + callback = self.sessions[seqnr]["callback"] + callback(from_jid=iq['from'], result=result) + + def _handle_event_rejected(self, iq): + """ Received Iq with rejected - this is a reject. + Delete the session. """ + seqnr = iq['rejected']['seqnr'] + callback = self.sessions[seqnr]["callback"] + callback(from_jid=iq['from'], result="rejected", error_msg=iq['rejected']['error']) + # Session terminated + del self.sessions[seqnr] + + def _handle_event_cancelled(self, iq): + """ + Received Iq with cancelled - this is a cancel confirm. + Delete the session. + """ + #print("Got cancelled") + seqnr = iq['cancelled']['seqnr'] + callback = self.sessions[seqnr]["callback"] + callback(from_jid=iq['from'], result="cancelled") + # Session cancelled + del self.sessions[seqnr] + + def _handle_event_fields(self, msg): + """ + Received Msg with fields - this is a data reponse to a request. + If this is the last data block, issue a "done" callback. + """ + seqnr = msg['fields']['seqnr'] + callback = self.sessions[seqnr]["callback"] + for node in msg['fields']['nodes']: + for ts in node['timestamps']: + fields = [] + for d in ts['datas']: + field_block = {} + field_block["name"] = d['name'] + field_block["typename"] = d._get_typename() + field_block["value"] = d['value'] + if not d['unit'] == "": field_block["unit"] = d['unit']; + if not d['dataType'] == "": field_block["dataType"] = d['dataType']; + flags = d._get_flags() + if not len(flags) == 0: + field_block["flags"] = flags + fields.append(field_block) + + callback(from_jid=msg['from'], result="fields", nodeId=node['nodeId'], timestamp=ts['value'], fields=fields) + + if msg['fields']['done'] == "true": + callback(from_jid=msg['from'], result="done") + # Session done + del self.sessions[seqnr] + + def _handle_event_failure(self, msg): + """ + Received Msg with failure - our request failed + Delete the session. + """ + seqnr = msg['failure']['seqnr'] + callback = self.sessions[seqnr]["callback"] + callback(from_jid=msg['from'], result="failure", nodeId=msg['failure']['error']['nodeId'], timestamp=msg['failure']['error']['timestamp'], error_msg=msg['failure']['error']['text']) + + # Session failed + del self.sessions[seqnr] + + def _handle_event_started(self, msg): + """ + Received Msg with started - our request was queued and is now started. + """ + seqnr = msg['started']['seqnr'] + callback = self.sessions[seqnr]["callback"] + callback(from_jid=msg['from'], result="started") + + diff --git a/sleekxmpp/plugins/xep_0323/stanza/__init__.py b/sleekxmpp/plugins/xep_0323/stanza/__init__.py new file mode 100644 index 00000000..c039cefa --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/stanza/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0323.stanza.sensordata import * + diff --git a/sleekxmpp/plugins/xep_0323/stanza/base.py b/sleekxmpp/plugins/xep_0323/stanza/base.py new file mode 100644 index 00000000..1dadcf46 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/stanza/base.py @@ -0,0 +1,13 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ET + +pass diff --git a/sleekxmpp/plugins/xep_0323/stanza/sensordata.py b/sleekxmpp/plugins/xep_0323/stanza/sensordata.py new file mode 100644 index 00000000..e8718161 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/stanza/sensordata.py @@ -0,0 +1,792 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp import Iq, Message +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from re import match + +class Sensordata(ElementBase): + """ Placeholder for the namespace, not used as a stanza """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'sensordata' + plugin_attrib = name + interfaces = set(tuple()) + +class FieldTypes(): + """ + All field types are optional booleans that default to False + """ + field_types = set([ 'momentary','peak','status','computed','identity','historicalSecond','historicalMinute','historicalHour', \ + 'historicalDay','historicalWeek','historicalMonth','historicalQuarter','historicalYear','historicalOther']) + +class FieldStatus(): + """ + All field statuses are optional booleans that default to False + """ + field_status = set([ 'missing','automaticEstimate','manualEstimate','manualReadout','automaticReadout','timeOffset','warning','error', \ + 'signed','invoiced','endOfSeries','powerFailure','invoiceConfirmed']) + +class Request(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'req' + plugin_attrib = name + interfaces = set(['seqnr','nodes','fields','serviceToken','deviceToken','userToken','from','to','when','historical','all']) + interfaces.update(FieldTypes.field_types) + _flags = set(['serviceToken','deviceToken','userToken','from','to','when','historical','all']) + _flags.update(FieldTypes.field_types) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._nodes = set() + self._fields = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._nodes = set([node['nodeId'] for node in self['nodes']]) + self._fields = set([field['name'] for field in self['fields']]) + + def _get_flags(self): + """ + Helper function for getting of flags. Returns all flags in + dictionary format: { "flag name": "flag value" ... } + """ + flags = {} + for f in self._flags: + if not self[f] == "": + flags[f] = self[f] + return flags + + def _set_flags(self, flags): + """ + Helper function for setting of flags. + + Arguments: + flags -- Flags in dictionary format: { "flag name": "flag value" ... } + """ + for f in self._flags: + if flags is not None and f in flags: + self[f] = flags[f] + else: + self[f] = None + + def add_node(self, nodeId, sourceId=None, cacheType=None): + """ + Add a new node element. Each item is required to have a + nodeId, but may also specify a sourceId value and cacheType. + + Arguments: + nodeId -- The ID for the node. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + if nodeId not in self._nodes: + self._nodes.add((nodeId)) + node = RequestNode(parent=self) + node['nodeId'] = nodeId + node['sourceId'] = sourceId + node['cacheType'] = cacheType + self.iterables.append(node) + return node + return None + + def del_node(self, nodeId): + """ + Remove a single node. + + Arguments: + nodeId -- Node ID of the item to remove. + """ + if nodeId in self._nodes: + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + if node['nodeId'] == nodeId: + self.xml.remove(node.xml) + self.iterables.remove(node) + return True + return False + + def get_nodes(self): + """Return all nodes.""" + nodes = [] + for node in self['substanzas']: + if isinstance(node, RequestNode): + nodes.append(node) + return nodes + + def set_nodes(self, nodes): + """ + Set or replace all nodes. The given nodes must be in a + list or set where each item is a tuple of the form: + (nodeId, sourceId, cacheType) + + Arguments: + nodes -- A series of nodes in tuple format. + """ + self.del_nodes() + for node in nodes: + if isinstance(node, RequestNode): + self.add_node(node['nodeId'], node['sourceId'], node['cacheType']) + else: + nodeId, sourceId, cacheType = node + self.add_node(nodeId, sourceId, cacheType) + + def del_nodes(self): + """Remove all nodes.""" + self._nodes = set() + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + self.xml.remove(node.xml) + self.iterables.remove(node) + + + def add_field(self, name): + """ + Add a new field element. Each item is required to have a + name. + + Arguments: + name -- The name of the field. + """ + if name not in self._fields: + self._fields.add((name)) + field = RequestField(parent=self) + field['name'] = name + self.iterables.append(field) + return field + return None + + def del_field(self, name): + """ + Remove a single field. + + Arguments: + name -- name of field to remove. + """ + if name in self._fields: + fields = [i for i in self.iterables if isinstance(i, RequestField)] + for field in fields: + if field['name'] == name: + self.xml.remove(field.xml) + self.iterables.remove(field) + return True + return False + + def get_fields(self): + """Return all fields.""" + fields = [] + for field in self['substanzas']: + if isinstance(field, RequestField): + fields.append(field) + return fields + + def set_fields(self, fields): + """ + Set or replace all fields. The given fields must be in a + list or set where each item is RequestField or string + + Arguments: + fields -- A series of fields in RequestField or string format. + """ + self.del_fields() + for field in fields: + if isinstance(field, RequestField): + self.add_field(field['name']) + else: + self.add_field(field) + + def del_fields(self): + """Remove all fields.""" + self._fields = set() + fields = [i for i in self.iterables if isinstance(i, RequestField)] + for field in fields: + self.xml.remove(field.xml) + self.iterables.remove(field) + + +class RequestNode(ElementBase): + """ Node element in a request """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'node' + plugin_attrib = name + interfaces = set(['nodeId','sourceId','cacheType']) + +class RequestField(ElementBase): + """ Field element in a request """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'field' + plugin_attrib = name + interfaces = set(['name']) + +class Accepted(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'accepted' + plugin_attrib = name + interfaces = set(['seqnr','queued']) + +class Started(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'started' + plugin_attrib = name + interfaces = set(['seqnr']) + +class Failure(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'failure' + plugin_attrib = name + interfaces = set(['seqnr','done']) + +class Error(ElementBase): + """ Error element in a request failure """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'error' + plugin_attrib = name + interfaces = set(['nodeId','timestamp','sourceId','cacheType','text']) + + def get_text(self): + """Return then contents inside the XML tag.""" + return self.xml.text + + def set_text(self, value): + """Set then contents inside the XML tag. + + :param value: string + """ + + self.xml.text = value + return self + + def del_text(self): + """Remove the contents inside the XML tag.""" + self.xml.text = "" + return self + +class Rejected(ElementBase): + namespace = 'urn:xmpp:iot:sensordata' + name = 'rejected' + plugin_attrib = name + interfaces = set(['seqnr','error']) + sub_interfaces = set(['error']) + +class Fields(ElementBase): + """ Fields element, top level in a response message with data """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'fields' + plugin_attrib = name + interfaces = set(['seqnr','done','nodes']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._nodes = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._nodes = set([node['nodeId'] for node in self['nodes']]) + + + def add_node(self, nodeId, sourceId=None, cacheType=None, substanzas=None): + """ + Add a new node element. Each item is required to have a + nodeId, but may also specify a sourceId value and cacheType. + + Arguments: + nodeId -- The ID for the node. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + if nodeId not in self._nodes: + self._nodes.add((nodeId)) + node = FieldsNode(parent=self) + node['nodeId'] = nodeId + node['sourceId'] = sourceId + node['cacheType'] = cacheType + if substanzas is not None: + node.set_timestamps(substanzas) + + self.iterables.append(node) + return node + return None + + def del_node(self, nodeId): + """ + Remove a single node. + + Arguments: + nodeId -- Node ID of the item to remove. + """ + if nodeId in self._nodes: + nodes = [i for i in self.iterables if isinstance(i, FieldsNode)] + for node in nodes: + if node['nodeId'] == nodeId: + self.xml.remove(node.xml) + self.iterables.remove(node) + return True + return False + + def get_nodes(self): + """Return all nodes.""" + nodes = [] + for node in self['substanzas']: + if isinstance(node, FieldsNode): + nodes.append(node) + return nodes + + def set_nodes(self, nodes): + """ + Set or replace all nodes. The given nodes must be in a + list or set where each item is a tuple of the form: + (nodeId, sourceId, cacheType) + + Arguments: + nodes -- A series of nodes in tuple format. + """ + #print(str(id(self)) + " set_nodes: got " + str(nodes)) + self.del_nodes() + for node in nodes: + if isinstance(node, FieldsNode): + self.add_node(node['nodeId'], node['sourceId'], node['cacheType'], substanzas=node['substanzas']) + else: + nodeId, sourceId, cacheType = node + self.add_node(nodeId, sourceId, cacheType) + + def del_nodes(self): + """Remove all nodes.""" + self._nodes = set() + nodes = [i for i in self.iterables if isinstance(i, FieldsNode)] + for node in nodes: + self.xml.remove(node.xml) + self.iterables.remove(node) + + +class FieldsNode(ElementBase): + """ Node element in response fields """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'node' + plugin_attrib = name + interfaces = set(['nodeId','sourceId','cacheType','timestamps']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._timestamps = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._timestamps = set([ts['value'] for ts in self['timestamps']]) + + def add_timestamp(self, timestamp, substanzas=None): + """ + Add a new timestamp element. + + Arguments: + timestamp -- The timestamp in ISO format. + """ + #print(str(id(self)) + " add_timestamp: " + str(timestamp)) + + if timestamp not in self._timestamps: + self._timestamps.add((timestamp)) + ts = Timestamp(parent=self) + ts['value'] = timestamp + if not substanzas is None: + ts.set_datas(substanzas) + #print("add_timestamp with substanzas: " + str(substanzas)) + self.iterables.append(ts) + #print(str(id(self)) + " added_timestamp: " + str(id(ts))) + return ts + return None + + def del_timestamp(self, timestamp): + """ + Remove a single timestamp. + + Arguments: + timestamp -- timestamp (in ISO format) of the item to remove. + """ + #print("del_timestamp: ") + if timestamp in self._timestamps: + timestamps = [i for i in self.iterables if isinstance(i, Timestamp)] + for ts in timestamps: + if ts['value'] == timestamp: + self.xml.remove(ts.xml) + self.iterables.remove(ts) + return True + return False + + def get_timestamps(self): + """Return all timestamps.""" + #print(str(id(self)) + " get_timestamps: ") + timestamps = [] + for timestamp in self['substanzas']: + if isinstance(timestamp, Timestamp): + timestamps.append(timestamp) + return timestamps + + def set_timestamps(self, timestamps): + """ + Set or replace all timestamps. The given timestamps must be in a + list or set where each item is a timestamp + + Arguments: + timestamps -- A series of timestamps. + """ + #print(str(id(self)) + " set_timestamps: got " + str(timestamps)) + self.del_timestamps() + for timestamp in timestamps: + #print("set_timestamps: subset " + str(timestamp)) + #print("set_timestamps: subset.substanzas " + str(timestamp['substanzas'])) + if isinstance(timestamp, Timestamp): + self.add_timestamp(timestamp['value'], substanzas=timestamp['substanzas']) + else: + #print("set_timestamps: got " + str(timestamp)) + self.add_timestamp(timestamp) + + def del_timestamps(self): + """Remove all timestamps.""" + #print(str(id(self)) + " del_timestamps: ") + self._timestamps = set() + timestamps = [i for i in self.iterables if isinstance(i, Timestamp)] + for timestamp in timestamps: + self.xml.remove(timestamp.xml) + self.iterables.remove(timestamp) + +class Field(ElementBase): + """ + Field element in response Timestamp. This is a base class, + all instances of fields added to Timestamp must be of types: + DataNumeric + DataString + DataBoolean + DataDateTime + DataTimeSpan + DataEnum + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'field' + plugin_attrib = name + interfaces = set(['name','module','stringIds']) + interfaces.update(FieldTypes.field_types) + interfaces.update(FieldStatus.field_status) + + _flags = set() + _flags.update(FieldTypes.field_types) + _flags.update(FieldStatus.field_status) + + def set_stringIds(self, value): + """Verifies stringIds according to regexp from specification XMPP-0323. + + :param value: string + """ + + pattern = re.compile("^\d+([|]\w+([.]\w+)*([|][^,]*)?)?(,\d+([|]\w+([.]\w+)*([|][^,]*)?)?)*$") + if pattern.match(value) is not None: + self.xml.stringIds = value + else: + # Bad content, add nothing + pass + + return self + + def _get_flags(self): + """ + Helper function for getting of flags. Returns all flags in + dictionary format: { "flag name": "flag value" ... } + """ + flags = {} + for f in self._flags: + if not self[f] == "": + flags[f] = self[f] + return flags + + def _set_flags(self, flags): + """ + Helper function for setting of flags. + + Arguments: + flags -- Flags in dictionary format: { "flag name": "flag value" ... } + """ + for f in self._flags: + if flags is not None and f in flags: + self[f] = flags[f] + else: + self[f] = None + + def _get_typename(self): + return "invalid type, use subclasses!" + + +class Timestamp(ElementBase): + """ Timestamp element in response Node """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'timestamp' + plugin_attrib = name + interfaces = set(['value','datas']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._datas = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._datas = set([data['name'] for data in self['datas']]) + + def add_data(self, typename, name, value, module=None, stringIds=None, unit=None, dataType=None, flags=None): + """ + Add a new data element. + + Arguments: + typename -- The type of data element (numeric, string, boolean, dateTime, timeSpan or enum) + value -- The value of the data element + module -- [optional] language module to use for the data element + stringIds -- [optional] The stringIds used to find associated text in the language module + unit -- [optional] The unit. Only applicable for type numeric + dataType -- [optional] The dataType. Only applicable for type enum + """ + if name not in self._datas: + dataObj = None + if typename == "numeric": + dataObj = DataNumeric(parent=self) + dataObj['unit'] = unit + elif typename == "string": + dataObj = DataString(parent=self) + elif typename == "boolean": + dataObj = DataBoolean(parent=self) + elif typename == "dateTime": + dataObj = DataDateTime(parent=self) + elif typename == "timeSpan": + dataObj = DataTimeSpan(parent=self) + elif typename == "enum": + dataObj = DataEnum(parent=self) + dataObj['dataType'] = dataType + + dataObj['name'] = name + dataObj['value'] = value + dataObj['module'] = module + dataObj['stringIds'] = stringIds + + if flags is not None: + dataObj._set_flags(flags) + + self._datas.add(name) + self.iterables.append(dataObj) + return dataObj + return None + + def del_data(self, name): + """ + Remove a single data element. + + Arguments: + data_name -- The data element name to remove. + """ + if name in self._datas: + datas = [i for i in self.iterables if isinstance(i, Field)] + for data in datas: + if data['name'] == name: + self.xml.remove(data.xml) + self.iterables.remove(data) + return True + return False + + def get_datas(self): + """ Return all data elements. """ + datas = [] + for data in self['substanzas']: + if isinstance(data, Field): + datas.append(data) + return datas + + def set_datas(self, datas): + """ + Set or replace all data elements. The given elements must be in a + list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum) + + Arguments: + datas -- A series of data elements. + """ + self.del_datas() + for data in datas: + self.add_data(typename=data._get_typename(), name=data['name'], value=data['value'], module=data['module'], stringIds=data['stringIds'], unit=data['unit'], dataType=data['dataType'], flags=data._get_flags()) + + def del_datas(self): + """Remove all data elements.""" + self._datas = set() + datas = [i for i in self.iterables if isinstance(i, Field)] + for data in datas: + self.xml.remove(data.xml) + self.iterables.remove(data) + +class DataNumeric(Field): + """ + Field data of type numeric. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'numeric' + plugin_attrib = name + interfaces = set(['value', 'unit']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "numeric" + +class DataString(Field): + """ + Field data of type string + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'string' + plugin_attrib = name + interfaces = set(['value']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "string" + +class DataBoolean(Field): + """ + Field data of type boolean. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'boolean' + plugin_attrib = name + interfaces = set(['value']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "boolean" + +class DataDateTime(Field): + """ + Field data of type dateTime. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'dateTime' + plugin_attrib = name + interfaces = set(['value']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "dateTime" + +class DataTimeSpan(Field): + """ + Field data of type timeSpan. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'timeSpan' + plugin_attrib = name + interfaces = set(['value']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "timeSpan" + +class DataEnum(Field): + """ + Field data of type enum. + Note that the value is expressed as a string. + """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'enum' + plugin_attrib = name + interfaces = set(['value', 'dataType']) + interfaces.update(Field.interfaces) + + def _get_typename(self): + return "enum" + +class Done(ElementBase): + """ Done element used to signal that all data has been transferred """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'done' + plugin_attrib = name + interfaces = set(['seqnr']) + +class Cancel(ElementBase): + """ Cancel element used to signal that a request shall be cancelled """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'cancel' + plugin_attrib = name + interfaces = set(['seqnr']) + +class Cancelled(ElementBase): + """ Cancelled element used to signal that cancellation is confirmed """ + namespace = 'urn:xmpp:iot:sensordata' + name = 'cancelled' + plugin_attrib = name + interfaces = set(['seqnr']) + + +register_stanza_plugin(Iq, Request) +register_stanza_plugin(Request, RequestNode, iterable=True) +register_stanza_plugin(Request, RequestField, iterable=True) + +register_stanza_plugin(Iq, Accepted) +register_stanza_plugin(Message, Failure) +register_stanza_plugin(Failure, Error) + +register_stanza_plugin(Iq, Rejected) + +register_stanza_plugin(Message, Fields) +register_stanza_plugin(Fields, FieldsNode, iterable=True) +register_stanza_plugin(FieldsNode, Timestamp, iterable=True) +register_stanza_plugin(Timestamp, Field, iterable=True) +register_stanza_plugin(Timestamp, DataNumeric, iterable=True) +register_stanza_plugin(Timestamp, DataString, iterable=True) +register_stanza_plugin(Timestamp, DataBoolean, iterable=True) +register_stanza_plugin(Timestamp, DataDateTime, iterable=True) +register_stanza_plugin(Timestamp, DataTimeSpan, iterable=True) +register_stanza_plugin(Timestamp, DataEnum, iterable=True) + +register_stanza_plugin(Message, Started) + +register_stanza_plugin(Iq, Cancel) +register_stanza_plugin(Iq, Cancelled) diff --git a/sleekxmpp/plugins/xep_0323/timerreset.py b/sleekxmpp/plugins/xep_0323/timerreset.py new file mode 100644 index 00000000..398b47c1 --- /dev/null +++ b/sleekxmpp/plugins/xep_0323/timerreset.py @@ -0,0 +1,69 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" +from threading import Thread, Event, Timer +import time + +def TimerReset(*args, **kwargs): + """ Global function for Timer """ + return _TimerReset(*args, **kwargs) + + +class _TimerReset(Thread): + """Call a function after a specified number of seconds: + + t = TimerReset(30.0, f, args=[], kwargs={}) + t.start() + t.cancel() # stop the timer's action if it's still waiting + """ + + def __init__(self, interval, function, args=None, kwargs=None): + if not kwargs: + kwargs = {} + if not args: + args = [] + + Thread.__init__(self) + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.finished = Event() + self.resetted = True + + def cancel(self): + """Stop the timer if it hasn't finished yet""" + self.finished.set() + + def run(self): + #print "Time: %s - timer running..." % time.asctime() + + while self.resetted: + #print "Time: %s - timer waiting for timeout in %.2f..." % (time.asctime(), self.interval) + self.resetted = False + self.finished.wait(self.interval) + + if not self.finished.isSet(): + self.function(*self.args, **self.kwargs) + self.finished.set() + #print "Time: %s - timer finished!" % time.asctime() + + def reset(self, interval=None): + """ Reset the timer """ + + if interval: + #print "Time: %s - timer resetting to %.2f..." % (time.asctime(), interval) + self.interval = interval + else: + #print "Time: %s - timer resetting..." % time.asctime() + pass + + self.resetted = True + self.finished.set() + self.finished.clear() diff --git a/sleekxmpp/plugins/xep_0325/__init__.py b/sleekxmpp/plugins/xep_0325/__init__.py new file mode 100644 index 00000000..01c38dce --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/__init__.py @@ -0,0 +1,18 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0325.control import XEP_0325 +from sleekxmpp.plugins.xep_0325 import stanza + +register_plugin(XEP_0325) + +xep_0325=XEP_0325 diff --git a/sleekxmpp/plugins/xep_0325/control.py b/sleekxmpp/plugins/xep_0325/control.py new file mode 100644 index 00000000..11e7a045 --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/control.py @@ -0,0 +1,569 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import time +from threading import Thread, Timer, Lock + +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.base import BasePlugin +from sleekxmpp.plugins.xep_0325 import stanza +from sleekxmpp.plugins.xep_0325.stanza import Control + + +log = logging.getLogger(__name__) + + +class XEP_0325(BasePlugin): + + """ + XEP-0325: IoT Control + + + Actuators are devices in sensor networks that can be controlled through + the network and act with the outside world. In sensor networks and + Internet of Things applications, actuators make it possible to automate + real-world processes. + This plugin implements a mechanism whereby actuators can be controlled + in XMPP-based sensor networks, making it possible to integrate sensors + and actuators of different brands, makes and models into larger + Internet of Things applications. + + Also see <http://xmpp.org/extensions/xep-0325.html> + + Configuration Values: + threaded -- Indicates if communication with sensors should be threaded. + Defaults to True. + + Events: + Sensor side + ----------- + Control Event:DirectSet -- Received a control message + Control Event:SetReq -- Received a control request + + Client side + ----------- + Control Event:SetResponse -- Received a response to a + control request, type result + Control Event:SetResponseError -- Received a response to a + control request, type error + + Attributes: + threaded -- Indicates if command events should be threaded. + Defaults to True. + sessions -- A dictionary or equivalent backend mapping + session IDs to dictionaries containing data + relevant to a request's session. This dictionary is used + both by the client and sensor side. On client side, seqnr + is used as key, while on sensor side, a session_id is used + as key. This ensures that the two will not collide, so + one instance can be both client and sensor. + Sensor side + ----------- + nodes -- A dictionary mapping sensor nodes that are serviced through + this XMPP instance to their device handlers ("drivers"). + Client side + ----------- + last_seqnr -- The last used sequence number (integer). One sequence of + communication (e.g. -->request, <--accept, <--fields) + between client and sensor is identified by a unique + sequence number (unique between the client/sensor pair) + + Methods: + plugin_init -- Overrides base_plugin.plugin_init + post_init -- Overrides base_plugin.post_init + plugin_end -- Overrides base_plugin.plugin_end + + Sensor side + ----------- + register_node -- Register a sensor as available from this XMPP + instance. + + Client side + ----------- + set_request -- Initiates a control request to modify data in + sensor(s). Non-blocking, a callback function will + be called when the sensor has responded. + set_command -- Initiates a control command to modify data in + sensor(s). Non-blocking. The sensor(s) will not + respond regardless of the result of the command, + so no callback is made. + + """ + + name = 'xep_0325' + description = 'XEP-0325 Internet of Things - Control' + dependencies = set(['xep_0030']) + stanza = stanza + + + default_config = { + 'threaded': True +# 'session_db': None + } + + def plugin_init(self): + """ Start the XEP-0325 plugin """ + + self.xmpp.register_handler( + Callback('Control Event:DirectSet', + StanzaPath('message/set'), + self._handle_direct_set)) + + self.xmpp.register_handler( + Callback('Control Event:SetReq', + StanzaPath('iq@type=set/set'), + self._handle_set_req)) + + self.xmpp.register_handler( + Callback('Control Event:SetResponse', + StanzaPath('iq@type=result/setResponse'), + self._handle_set_response)) + + self.xmpp.register_handler( + Callback('Control Event:SetResponseError', + StanzaPath('iq@type=error/setResponse'), + self._handle_set_response)) + + # Server side dicts + self.nodes = {} + self.sessions = {} + + self.last_seqnr = 0 + self.seqnr_lock = Lock() + + ## For testning only + self.test_authenticated_from = "" + + def post_init(self): + """ Init complete. Register our features in Serivce discovery. """ + BasePlugin.post_init(self) + self.xmpp['xep_0030'].add_feature(Control.namespace) + self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple()) + + def _new_session(self): + """ Return a new session ID. """ + return str(time.time()) + '-' + self.xmpp.new_id() + + def plugin_end(self): + """ Stop the XEP-0325 plugin """ + self.sessions.clear() + self.xmpp.remove_handler('Control Event:DirectSet') + self.xmpp.remove_handler('Control Event:SetReq') + self.xmpp.remove_handler('Control Event:SetResponse') + self.xmpp.remove_handler('Control Event:SetResponseError') + self.xmpp['xep_0030'].del_feature(feature=Control.namespace) + self.xmpp['xep_0030'].set_items(node=Control.namespace, items=tuple()) + + + # ================================================================= + # Sensor side (data provider) API + + def register_node(self, nodeId, device, commTimeout, sourceId=None, cacheType=None): + """ + Register a sensor/device as available for control requests/commands + through this XMPP instance. + + The device object may by any custom implementation to support + specific devices, but it must implement the functions: + has_control_field + set_control_fields + according to the interfaces shown in the example device.py file. + + Arguments: + nodeId -- The identifier for the device + device -- The device object + commTimeout -- Time in seconds to wait between each callback from device during + a data readout. Float. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + self.nodes[nodeId] = {"device": device, + "commTimeout": commTimeout, + "sourceId": sourceId, + "cacheType": cacheType} + + def _set_authenticated(self, auth=''): + """ Internal testing function """ + self.test_authenticated_from = auth + + def _get_new_seqnr(self): + """ Returns a unique sequence number (unique across threads) """ + self.seqnr_lock.acquire() + self.last_seqnr += 1 + self.seqnr_lock.release() + return str(self.last_seqnr) + + def _handle_set_req(self, iq): + """ + Event handler for reception of an Iq with set req - this is a + control request. + + Verifies that + - all the requested nodes are available + (if no nodes are specified in the request, assume all nodes) + - all the control fields are available from all requested nodes + (if no nodes are specified in the request, assume all nodes) + + If the request passes verification, the control request is passed + to the devices (in a separate thread). + If the verification fails, a setResponse with error indication + is sent. + """ + + error_msg = '' + req_ok = True + missing_node = None + missing_field = None + + # Authentication + if len(self.test_authenticated_from) > 0 and not iq['from'] == self.test_authenticated_from: + # Invalid authentication + req_ok = False + error_msg = "Access denied" + + # Nodes + if len(iq['set']['nodes']) > 0: + for n in iq['set']['nodes']: + if not n['nodeId'] in self.nodes: + req_ok = False + missing_node = n['nodeId'] + error_msg = "Invalid nodeId " + n['nodeId'] + process_nodes = [n['nodeId'] for n in iq['set']['nodes']] + else: + process_nodes = self.nodes.keys() + + # Fields - for control we need to find all in all devices, otherwise we reject + process_fields = [] + if len(iq['set']['datas']) > 0: + for f in iq['set']['datas']: + for node in self.nodes: + if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()): + req_ok = False + missing_field = f['name'] + error_msg = "Invalid field " + f['name'] + break + process_fields = [(f['name'], f._get_typename(), f['value']) for f in iq['set']['datas']] + + if req_ok: + session = self._new_session() + self.sessions[session] = {"from": iq['from'], "to": iq['to'], "seqnr": iq['id']} + self.sessions[session]["commTimers"] = {} + self.sessions[session]["nodeDone"] = {} + # Flag that a reply is exected when we are done + self.sessions[session]["reply"] = True + + self.sessions[session]["node_list"] = process_nodes + if self.threaded: + #print("starting thread") + tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields)) + tr_req.start() + #print("started thread") + else: + self._threaded_node_request(session, process_fields) + + else: + iq.reply() + iq['type'] = 'error' + iq['setResponse']['responseCode'] = "NotFound" + if missing_node is not None: + iq['setResponse'].add_node(missing_node) + if missing_field is not None: + iq['setResponse'].add_data(missing_field) + iq['setResponse']['error']['var'] = "Output" + iq['setResponse']['error']['text'] = error_msg + iq.send(block=False) + + def _handle_direct_set(self, msg): + """ + Event handler for reception of a Message with set command - this is a + direct control command. + + Verifies that + - all the requested nodes are available + (if no nodes are specified in the request, assume all nodes) + - all the control fields are available from all requested nodes + (if no nodes are specified in the request, assume all nodes) + + If the request passes verification, the control request is passed + to the devices (in a separate thread). + If the verification fails, do nothing. + """ + req_ok = True + + # Nodes + if len(msg['set']['nodes']) > 0: + for n in msg['set']['nodes']: + if not n['nodeId'] in self.nodes: + req_ok = False + error_msg = "Invalid nodeId " + n['nodeId'] + process_nodes = [n['nodeId'] for n in msg['set']['nodes']] + else: + process_nodes = self.nodes.keys() + + # Fields - for control we need to find all in all devices, otherwise we reject + process_fields = [] + if len(msg['set']['datas']) > 0: + for f in msg['set']['datas']: + for node in self.nodes: + if not self.nodes[node]["device"].has_control_field(f['name'], f._get_typename()): + req_ok = False + missing_field = f['name'] + error_msg = "Invalid field " + f['name'] + break + process_fields = [(f['name'], f._get_typename(), f['value']) for f in msg['set']['datas']] + + if req_ok: + session = self._new_session() + self.sessions[session] = {"from": msg['from'], "to": msg['to']} + self.sessions[session]["commTimers"] = {} + self.sessions[session]["nodeDone"] = {} + self.sessions[session]["reply"] = False + + self.sessions[session]["node_list"] = process_nodes + if self.threaded: + #print("starting thread") + tr_req = Thread(target=self._threaded_node_request, args=(session, process_fields)) + tr_req.start() + #print("started thread") + else: + self._threaded_node_request(session, process_fields) + + + def _threaded_node_request(self, session, process_fields): + """ + Helper function to handle the device control in a separate thread. + + Arguments: + session -- The request session id + process_fields -- The fields to set in the devices. List of tuple format: + (name, datatype, value) + """ + for node in self.sessions[session]["node_list"]: + self.sessions[session]["nodeDone"][node] = False + + for node in self.sessions[session]["node_list"]: + timer = Timer(self.nodes[node]['commTimeout'], self._event_comm_timeout, args=(session, node)) + self.sessions[session]["commTimers"][node] = timer + timer.start() + self.nodes[node]['device'].set_control_fields(process_fields, session=session, callback=self._device_set_command_callback) + + def _event_comm_timeout(self, session, nodeId): + """ + Triggered if any of the control operations timeout. + Stop communicating with the failing device. + If the control command was an Iq request, sends a failure + message back to the client. + + Arguments: + session -- The request session id + nodeId -- The id of the device which timed out + """ + + if self.sessions[session]["reply"]: + # Reply is exected when we are done + iq = self.xmpp.Iq() + iq['from'] = self.sessions[session]['to'] + iq['to'] = self.sessions[session]['from'] + iq['type'] = "error" + iq['id'] = self.sessions[session]['seqnr'] + iq['setResponse']['responseCode'] = "OtherError" + iq['setResponse'].add_node(nodeId) + iq['setResponse']['error']['var'] = "Output" + iq['setResponse']['error']['text'] = "Timeout." + iq.send(block=False) + + ## TODO - should we send one timeout per node?? + + # Drop communication with this device and check if we are done + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + # The session is complete, delete it + del self.sessions[session] + + def _all_nodes_done(self, session): + """ + Checks wheter all devices are done replying to the control command. + + Arguments: + session -- The request session id + """ + for n in self.sessions[session]["nodeDone"]: + if not self.sessions[session]["nodeDone"][n]: + return False + return True + + def _device_set_command_callback(self, session, nodeId, result, error_field=None, error_msg=None): + """ + Callback function called by the devices when the control command is + complete or failed. + If needed, composes a message with the result and sends it back to the + client. + + Arguments: + session -- The request session id + nodeId -- The device id which initiated the callback + result -- The current result status of the control command. Valid values are: + "error" - Set fields failed. + "ok" - All fields were set. + error_field -- [optional] Only applies when result == "error" + The field name that failed (usually means it is missing) + error_msg -- [optional] Only applies when result == "error". + Error details when a request failed. + """ + + if not session in self.sessions: + # This can happend if a session was deleted, like in a timeout. Just drop the data. + return + + if result == "error": + self.sessions[session]["commTimers"][nodeId].cancel() + + if self.sessions[session]["reply"]: + # Reply is exected when we are done + iq = self.xmpp.Iq() + iq['from'] = self.sessions[session]['to'] + iq['to'] = self.sessions[session]['from'] + iq['type'] = "error" + iq['id'] = self.sessions[session]['seqnr'] + iq['setResponse']['responseCode'] = "OtherError" + iq['setResponse'].add_node(nodeId) + if error_field is not None: + iq['setResponse'].add_data(error_field) + iq['setResponse']['error']['var'] = error_field + iq['setResponse']['error']['text'] = error_msg + iq.send(block=False) + + # Drop communication with this device and check if we are done + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + # The session is complete, delete it + del self.sessions[session] + else: + self.sessions[session]["commTimers"][nodeId].cancel() + + self.sessions[session]["nodeDone"][nodeId] = True + if (self._all_nodes_done(session)): + if self.sessions[session]["reply"]: + # Reply is exected when we are done + iq = self.xmpp.Iq() + iq['from'] = self.sessions[session]['to'] + iq['to'] = self.sessions[session]['from'] + iq['type'] = "result" + iq['id'] = self.sessions[session]['seqnr'] + iq['setResponse']['responseCode'] = "OK" + iq.send(block=False) + + # The session is complete, delete it + del self.sessions[session] + + + # ================================================================= + # Client side (data controller) API + + def set_request(self, from_jid, to_jid, callback, fields, nodeIds=None): + """ + Called on the client side to initiade a control request. + Composes a message with the request and sends it to the device(s). + Does not block, the callback will be called when the device(s) + has responded. + + Arguments: + from_jid -- The jid of the requester + to_jid -- The jid of the device(s) + callback -- The callback function to call when data is availble. + + The callback function must support the following arguments: + + from_jid -- The jid of the responding device(s) + result -- The result of the control request. Valid values are: + "OK" - Control request completed successfully + "NotFound" - One or more nodes or fields are missing + "InsufficientPrivileges" - Not authorized. + "Locked" - Field(s) is locked and cannot + be changed at the moment. + "NotImplemented" - Request feature not implemented. + "FormError" - Error while setting with + a form (not implemented). + "OtherError" - Indicates other types of + errors, such as timeout. + Details in the error_msg. + + + nodeId -- [optional] Only applicable when result == "error" + List of node Ids of failing device(s). + + fields -- [optional] Only applicable when result == "error" + List of fields that failed.[optional] Mandatory when result == "rejected" or "failure". + + error_msg -- Details about why the request failed. + + fields -- Fields to set. List of tuple format: (name, typename, value). + nodeIds -- [optional] Limits the request to the node Ids in this list. + """ + iq = self.xmpp.Iq() + iq['from'] = from_jid + iq['to'] = to_jid + seqnr = self._get_new_seqnr() + iq['id'] = seqnr + iq['type'] = "set" + if nodeIds is not None: + for nodeId in nodeIds: + iq['set'].add_node(nodeId) + if fields is not None: + for name, typename, value in fields: + iq['set'].add_data(name=name, typename=typename, value=value) + + self.sessions[seqnr] = {"from": iq['from'], "to": iq['to'], "callback": callback} + iq.send(block=False) + + def set_command(self, from_jid, to_jid, fields, nodeIds=None): + """ + Called on the client side to initiade a control command. + Composes a message with the set commandand sends it to the device(s). + Does not block. Device(s) will not respond, regardless of result. + + Arguments: + from_jid -- The jid of the requester + to_jid -- The jid of the device(s) + + fields -- Fields to set. List of tuple format: (name, typename, value). + nodeIds -- [optional] Limits the request to the node Ids in this list. + """ + msg = self.xmpp.Message() + msg['from'] = from_jid + msg['to'] = to_jid + msg['type'] = "set" + if nodeIds is not None: + for nodeId in nodeIds: + msg['set'].add_node(nodeId) + if fields is not None: + for name, typename, value in fields: + msg['set'].add_data(name, typename, value) + + # We won't get any reply, so don't create a session + msg.send() + + def _handle_set_response(self, iq): + """ Received response from device(s) """ + #print("ooh") + seqnr = iq['id'] + from_jid = str(iq['from']) + result = iq['setResponse']['responseCode'] + nodeIds = [n['name'] for n in iq['setResponse']['nodes']] + fields = [f['name'] for f in iq['setResponse']['datas']] + error_msg = None + + if not iq['setResponse'].find('error') is None and not iq['setResponse']['error']['text'] == "": + error_msg = iq['setResponse']['error']['text'] + + callback = self.sessions[seqnr]["callback"] + callback(from_jid=from_jid, result=result, nodeIds=nodeIds, fields=fields, error_msg=error_msg) diff --git a/sleekxmpp/plugins/xep_0325/device.py b/sleekxmpp/plugins/xep_0325/device.py new file mode 100644 index 00000000..f1ed0733 --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/device.py @@ -0,0 +1,125 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime + +class Device(object): + """ + Example implementation of a device control object. + + The device object may by any custom implementation to support + specific devices, but it must implement the functions: + has_control_field + set_control_fields + """ + + def __init__(self, nodeId): + self.nodeId = nodeId + self.control_fields = {} + + def has_control_field(self, field, typename): + """ + Returns true if the supplied field name exists + and the type matches for control in this device. + + Arguments: + field -- The field name + typename -- The expected type + """ + if field in self.control_fields and self.control_fields[field]["type"] == typename: + return True + return False + + def set_control_fields(self, fields, session, callback): + """ + Starts a control setting procedure. Verifies the fields, + sets the data and (if needed) and calls the callback. + + Arguments: + fields -- List of control fields in tuple format: + (name, typename, value) + session -- Session id, only used in the callback as identifier + callback -- Callback function to call when control set is complete. + + The callback function must support the following arguments: + + session -- Session id, as supplied in the + request_fields call + nodeId -- Identifier for this device + result -- The current result status of the readout. + Valid values are: + "error" - Set fields failed. + "ok" - All fields were set. + error_field -- [optional] Only applies when result == "error" + The field name that failed + (usually means it is missing) + error_msg -- [optional] Only applies when result == "error". + Error details when a request failed. + """ + + if len(fields) > 0: + # Check availiability + for name, typename, value in fields: + if not self.has_control_field(name, typename): + self._send_control_reject(session, name, "NotFound", callback) + return False + + for name, typename, value in fields: + self._set_field_value(name, value) + + callback(session, result="ok", nodeId=self.nodeId) + return True + + def _send_control_reject(self, session, field, message, callback): + """ + Sends a reject to the caller + + Arguments: + session -- Session id, see definition in + set_control_fields function + callback -- Callback function, see definition in + set_control_fields function + """ + callback(session, result="error", nodeId=self.nodeId, error_field=field, error_msg=message) + + def _add_control_field(self, name, typename, value): + """ + Adds a control field to the device + + Arguments: + name -- Name of the field + typename -- Type of the field, one of: + (boolean, color, string, date, dateTime, + double, duration, int, long, time) + value -- Field value + """ + self.control_fields[name] = {"type": typename, "value": value} + + def _set_field_value(self, name, value): + """ + Set the value of a control field + + Arguments: + name -- Name of the field + value -- New value for the field + """ + if name in self.control_fields: + self.control_fields[name]["value"] = value + + def _get_field_value(self, name): + """ + Get the value of a control field. Only used for unit testing. + + Arguments: + name -- Name of the field + """ + if name in self.control_fields: + return self.control_fields[name]["value"] + return None diff --git a/sleekxmpp/plugins/xep_0325/stanza/__init__.py b/sleekxmpp/plugins/xep_0325/stanza/__init__.py new file mode 100644 index 00000000..746c2033 --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/stanza/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0325.stanza.control import * + diff --git a/sleekxmpp/plugins/xep_0325/stanza/base.py b/sleekxmpp/plugins/xep_0325/stanza/base.py new file mode 100644 index 00000000..1dadcf46 --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/stanza/base.py @@ -0,0 +1,13 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ET + +pass diff --git a/sleekxmpp/plugins/xep_0325/stanza/control.py b/sleekxmpp/plugins/xep_0325/stanza/control.py new file mode 100644 index 00000000..1fd5c35d --- /dev/null +++ b/sleekxmpp/plugins/xep_0325/stanza/control.py @@ -0,0 +1,527 @@ +""" + SleekXMPP: The Sleek XMPP Library + Implementation of xeps for Internet of Things + http://wiki.xmpp.org/web/Tech_pages/IoT_systems + Copyright (C) 2013 Sustainable Innovation, Joachim.lindborg@sust.se, bjorn.westrom@consoden.se + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp import Iq, Message +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from re import match + +class Control(ElementBase): + """ Placeholder for the namespace, not used as a stanza """ + namespace = 'urn:xmpp:iot:control' + name = 'control' + plugin_attrib = name + interfaces = set(tuple()) + +class ControlSet(ElementBase): + namespace = 'urn:xmpp:iot:control' + name = 'set' + plugin_attrib = name + interfaces = set(['nodes','datas']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._nodes = set() + self._datas = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._nodes = set([node['nodeId'] for node in self['nodes']]) + self._datas = set([data['name'] for data in self['datas']]) + + def add_node(self, nodeId, sourceId=None, cacheType=None): + """ + Add a new node element. Each item is required to have a + nodeId, but may also specify a sourceId value and cacheType. + + Arguments: + nodeId -- The ID for the node. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + if nodeId not in self._nodes: + self._nodes.add((nodeId)) + node = RequestNode(parent=self) + node['nodeId'] = nodeId + node['sourceId'] = sourceId + node['cacheType'] = cacheType + self.iterables.append(node) + return node + return None + + def del_node(self, nodeId): + """ + Remove a single node. + + Arguments: + nodeId -- Node ID of the item to remove. + """ + if nodeId in self._nodes: + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + if node['nodeId'] == nodeId: + self.xml.remove(node.xml) + self.iterables.remove(node) + return True + return False + + def get_nodes(self): + """Return all nodes.""" + nodes = [] + for node in self['substanzas']: + if isinstance(node, RequestNode): + nodes.append(node) + return nodes + + def set_nodes(self, nodes): + """ + Set or replace all nodes. The given nodes must be in a + list or set where each item is a tuple of the form: + (nodeId, sourceId, cacheType) + + Arguments: + nodes -- A series of nodes in tuple format. + """ + self.del_nodes() + for node in nodes: + if isinstance(node, RequestNode): + self.add_node(node['nodeId'], node['sourceId'], node['cacheType']) + else: + nodeId, sourceId, cacheType = node + self.add_node(nodeId, sourceId, cacheType) + + def del_nodes(self): + """Remove all nodes.""" + self._nodes = set() + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + self.xml.remove(node.xml) + self.iterables.remove(node) + + + def add_data(self, name, typename, value): + """ + Add a new data element. + + Arguments: + name -- The name of the data element + typename -- The type of data element + (boolean, color, string, date, dateTime, + double, duration, int, long, time) + value -- The value of the data element + """ + if name not in self._datas: + dataObj = None + if typename == "boolean": + dataObj = BooleanParameter(parent=self) + elif typename == "color": + dataObj = ColorParameter(parent=self) + elif typename == "string": + dataObj = StringParameter(parent=self) + elif typename == "date": + dataObj = DateParameter(parent=self) + elif typename == "dateTime": + dataObj = DateTimeParameter(parent=self) + elif typename == "double": + dataObj = DoubleParameter(parent=self) + elif typename == "duration": + dataObj = DurationParameter(parent=self) + elif typename == "int": + dataObj = IntParameter(parent=self) + elif typename == "long": + dataObj = LongParameter(parent=self) + elif typename == "time": + dataObj = TimeParameter(parent=self) + + dataObj['name'] = name + dataObj['value'] = value + + self._datas.add(name) + self.iterables.append(dataObj) + return dataObj + return None + + def del_data(self, name): + """ + Remove a single data element. + + Arguments: + data_name -- The data element name to remove. + """ + if name in self._datas: + datas = [i for i in self.iterables if isinstance(i, BaseParameter)] + for data in datas: + if data['name'] == name: + self.xml.remove(data.xml) + self.iterables.remove(data) + return True + return False + + def get_datas(self): + """ Return all data elements. """ + datas = [] + for data in self['substanzas']: + if isinstance(data, BaseParameter): + datas.append(data) + return datas + + def set_datas(self, datas): + """ + Set or replace all data elements. The given elements must be in a + list or set where each item is a data element (numeric, string, boolean, dateTime, timeSpan or enum) + + Arguments: + datas -- A series of data elements. + """ + self.del_datas() + for data in datas: + self.add_data(name=data['name'], typename=data._get_typename(), value=data['value']) + + def del_datas(self): + """Remove all data elements.""" + self._datas = set() + datas = [i for i in self.iterables if isinstance(i, BaseParameter)] + for data in datas: + self.xml.remove(data.xml) + self.iterables.remove(data) + + +class RequestNode(ElementBase): + """ Node element in a request """ + namespace = 'urn:xmpp:iot:control' + name = 'node' + plugin_attrib = name + interfaces = set(['nodeId','sourceId','cacheType']) + + +class ControlSetResponse(ElementBase): + namespace = 'urn:xmpp:iot:control' + name = 'setResponse' + plugin_attrib = name + interfaces = set(['responseCode']) + + def __init__(self, xml=None, parent=None): + ElementBase.__init__(self, xml, parent) + self._nodes = set() + self._datas = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._nodes = set([node['nodeId'] for node in self['nodes']]) + self._datas = set([data['name'] for data in self['datas']]) + + def add_node(self, nodeId, sourceId=None, cacheType=None): + """ + Add a new node element. Each item is required to have a + nodeId, but may also specify a sourceId value and cacheType. + + Arguments: + nodeId -- The ID for the node. + sourceId -- [optional] identifying the data source controlling the device + cacheType -- [optional] narrowing down the search to a specific kind of node + """ + if nodeId not in self._nodes: + self._nodes.add(nodeId) + node = RequestNode(parent=self) + node['nodeId'] = nodeId + node['sourceId'] = sourceId + node['cacheType'] = cacheType + self.iterables.append(node) + return node + return None + + def del_node(self, nodeId): + """ + Remove a single node. + + Arguments: + nodeId -- Node ID of the item to remove. + """ + if nodeId in self._nodes: + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + if node['nodeId'] == nodeId: + self.xml.remove(node.xml) + self.iterables.remove(node) + return True + return False + + def get_nodes(self): + """Return all nodes.""" + nodes = [] + for node in self['substanzas']: + if isinstance(node, RequestNode): + nodes.append(node) + return nodes + + def set_nodes(self, nodes): + """ + Set or replace all nodes. The given nodes must be in a + list or set where each item is a tuple of the form: + (nodeId, sourceId, cacheType) + + Arguments: + nodes -- A series of nodes in tuple format. + """ + self.del_nodes() + for node in nodes: + if isinstance(node, RequestNode): + self.add_node(node['nodeId'], node['sourceId'], node['cacheType']) + else: + nodeId, sourceId, cacheType = node + self.add_node(nodeId, sourceId, cacheType) + + def del_nodes(self): + """Remove all nodes.""" + self._nodes = set() + nodes = [i for i in self.iterables if isinstance(i, RequestNode)] + for node in nodes: + self.xml.remove(node.xml) + self.iterables.remove(node) + + + def add_data(self, name): + """ + Add a new ResponseParameter element. + + Arguments: + name -- Name of the parameter + """ + if name not in self._datas: + self._datas.add(name) + data = ResponseParameter(parent=self) + data['name'] = name + self.iterables.append(data) + return data + return None + + def del_data(self, name): + """ + Remove a single ResponseParameter element. + + Arguments: + name -- The data element name to remove. + """ + if name in self._datas: + datas = [i for i in self.iterables if isinstance(i, ResponseParameter)] + for data in datas: + if data['name'] == name: + self.xml.remove(data.xml) + self.iterables.remove(data) + return True + return False + + def get_datas(self): + """ Return all ResponseParameter elements. """ + datas = set() + for data in self['substanzas']: + if isinstance(data, ResponseParameter): + datas.add(data) + return datas + + def set_datas(self, datas): + """ + Set or replace all data elements. The given elements must be in a + list or set of ResponseParameter elements + + Arguments: + datas -- A series of data element names. + """ + self.del_datas() + for data in datas: + self.add_data(name=data['name']) + + def del_datas(self): + """Remove all ResponseParameter elements.""" + self._datas = set() + datas = [i for i in self.iterables if isinstance(i, ResponseParameter)] + for data in datas: + self.xml.remove(data.xml) + self.iterables.remove(data) + + +class Error(ElementBase): + namespace = 'urn:xmpp:iot:control' + name = 'error' + plugin_attrib = name + interfaces = set(['var','text']) + + def get_text(self): + """Return then contents inside the XML tag.""" + return self.xml.text + + def set_text(self, value): + """Set then contents inside the XML tag. + + Arguments: + value -- string + """ + + self.xml.text = value + return self + + def del_text(self): + """Remove the contents inside the XML tag.""" + self.xml.text = "" + return self + +class ResponseParameter(ElementBase): + """ + Parameter element in ControlSetResponse. + """ + namespace = 'urn:xmpp:iot:control' + name = 'parameter' + plugin_attrib = name + interfaces = set(['name']) + + +class BaseParameter(ElementBase): + """ + Parameter element in SetCommand. This is a base class, + all instances of parameters added to SetCommand must be of types: + BooleanParameter + ColorParameter + StringParameter + DateParameter + DateTimeParameter + DoubleParameter + DurationParameter + IntParameter + LongParameter + TimeParameter + """ + namespace = 'urn:xmpp:iot:control' + name = 'baseParameter' + plugin_attrib = name + interfaces = set(['name','value']) + + def _get_typename(self): + return self.name + + +class BooleanParameter(BaseParameter): + """ + Field data of type boolean. + Note that the value is expressed as a string. + """ + name = 'boolean' + plugin_attrib = name + +class ColorParameter(BaseParameter): + """ + Field data of type color. + Note that the value is expressed as a string. + """ + name = 'color' + plugin_attrib = name + +class StringParameter(BaseParameter): + """ + Field data of type string. + """ + name = 'string' + plugin_attrib = name + +class DateParameter(BaseParameter): + """ + Field data of type date. + Note that the value is expressed as a string. + """ + name = 'date' + plugin_attrib = name + +class DateTimeParameter(BaseParameter): + """ + Field data of type dateTime. + Note that the value is expressed as a string. + """ + name = 'dateTime' + plugin_attrib = name + +class DoubleParameter(BaseParameter): + """ + Field data of type double. + Note that the value is expressed as a string. + """ + name = 'double' + plugin_attrib = name + +class DurationParameter(BaseParameter): + """ + Field data of type duration. + Note that the value is expressed as a string. + """ + name = 'duration' + plugin_attrib = name + +class IntParameter(BaseParameter): + """ + Field data of type int. + Note that the value is expressed as a string. + """ + name = 'int' + plugin_attrib = name + +class LongParameter(BaseParameter): + """ + Field data of type long (64-bit int). + Note that the value is expressed as a string. + """ + name = 'long' + plugin_attrib = name + +class TimeParameter(BaseParameter): + """ + Field data of type time. + Note that the value is expressed as a string. + """ + name = 'time' + plugin_attrib = name + +register_stanza_plugin(Iq, ControlSet) +register_stanza_plugin(Message, ControlSet) + +register_stanza_plugin(ControlSet, RequestNode, iterable=True) + +register_stanza_plugin(ControlSet, BooleanParameter, iterable=True) +register_stanza_plugin(ControlSet, ColorParameter, iterable=True) +register_stanza_plugin(ControlSet, StringParameter, iterable=True) +register_stanza_plugin(ControlSet, DateParameter, iterable=True) +register_stanza_plugin(ControlSet, DateTimeParameter, iterable=True) +register_stanza_plugin(ControlSet, DoubleParameter, iterable=True) +register_stanza_plugin(ControlSet, DurationParameter, iterable=True) +register_stanza_plugin(ControlSet, IntParameter, iterable=True) +register_stanza_plugin(ControlSet, LongParameter, iterable=True) +register_stanza_plugin(ControlSet, TimeParameter, iterable=True) + +register_stanza_plugin(Iq, ControlSetResponse) +register_stanza_plugin(ControlSetResponse, Error) +register_stanza_plugin(ControlSetResponse, RequestNode, iterable=True) +register_stanza_plugin(ControlSetResponse, ResponseParameter, iterable=True) + diff --git a/sleekxmpp/roster/__init__.py b/sleekxmpp/roster/__init__.py index 4335d367..18b380c9 100644 --- a/sleekxmpp/roster/__init__.py +++ b/sleekxmpp/roster/__init__.py @@ -6,7 +6,6 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.xmlstream import JID from sleekxmpp.roster.item import RosterItem from sleekxmpp.roster.single import RosterNode from sleekxmpp.roster.multi import Roster diff --git a/sleekxmpp/roster/single.py b/sleekxmpp/roster/single.py index f080ae8a..e9ce4f21 100644 --- a/sleekxmpp/roster/single.py +++ b/sleekxmpp/roster/single.py @@ -237,8 +237,7 @@ class RosterNode(object): if not self.xmpp.is_component: return self.update(jid, subscription='remove') - def update(self, jid, name=None, subscription=None, groups=[], - block=True, timeout=None, callback=None): + def update(self, jid, name=None, subscription=None, groups=None, block=True, timeout=None, callback=None): """ Update a JID's subscription information. @@ -258,6 +257,9 @@ class RosterNode(object): Will be executed when the roster is received. Implies block=False. """ + if not groups: + groups = [] + self[jid]['name'] = name self[jid]['groups'] = groups self[jid].save() diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py index d21a74e1..c43178f2 100644 --- a/sleekxmpp/stanza/htmlim.py +++ b/sleekxmpp/stanza/htmlim.py @@ -7,78 +7,13 @@ """ from sleekxmpp.stanza import Message -from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin - - -class HTMLIM(ElementBase): - - """ - XEP-0071: XHTML-IM defines a method for embedding XHTML content - within a <message> stanza so that lightweight markup can be used - to format the message contents and to create links. - - Only a subset of XHTML is recommended for use with XHTML-IM. - See the full spec at 'http://xmpp.org/extensions/xep-0071.html' - for more information. - - Example stanza: - <message to="user@example.com"> - <body>Non-html message content.</body> - <html xmlns="http://jabber.org/protocol/xhtml-im"> - <body xmlns="http://www.w3.org/1999/xhtml"> - <p><b>HTML!</b></p> - </body> - </html> - </message> - - Stanza Interface: - body -- The contents of the HTML body tag. - - Methods: - setup -- Overrides ElementBase.setup. - get_body -- Return the HTML body contents. - set_body -- Set the HTML body contents. - del_body -- Remove the HTML body contents. - """ - - namespace = 'http://jabber.org/protocol/xhtml-im' - name = 'html' - interfaces = set(('body',)) - plugin_attrib = name - - def set_body(self, html): - """ - Set the contents of the HTML body. - - Arguments: - html -- Either a string or XML object. If the top level - element is not <body> with a namespace of - 'http://www.w3.org/1999/xhtml', it will be wrapped. - """ - if isinstance(html, str): - html = ET.XML(html) - if html.tag != '{http://www.w3.org/1999/xhtml}body': - body = ET.Element('{http://www.w3.org/1999/xhtml}body') - body.append(html) - self.xml.append(body) - else: - self.xml.append(html) - - def get_body(self): - """Return the contents of the HTML body.""" - html = self.xml.find('{http://www.w3.org/1999/xhtml}body') - if html is None: - return '' - return html - - def del_body(self): - """Remove the HTML body contents.""" - if self.parent is not None: - self.parent().xml.remove(self.xml) +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0071 import XHTML_IM as HTMLIM register_stanza_plugin(Message, HTMLIM) + # To comply with PEP8, method names now use underscores. # Deprecated method names are re-mapped for backwards compatibility. HTMLIM.setBody = HTMLIM.set_body diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index 71c0444d..088de4c0 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -9,7 +9,7 @@ from sleekxmpp.stanza.rootstanza import RootStanza from sleekxmpp.xmlstream import StanzaBase, ET from sleekxmpp.xmlstream.handler import Waiter, Callback -from sleekxmpp.xmlstream.matcher import MatcherId +from sleekxmpp.xmlstream.matcher import MatchIDSender, MatcherId from sleekxmpp.exceptions import IqTimeout, IqError @@ -115,9 +115,13 @@ class Iq(RootStanza): """ query = self.xml.find("{%s}query" % value) if query is None and value: - self.clear() - query = ET.Element("{%s}query" % value) - self.xml.append(query) + plugin = self.plugin_tag_map.get('{%s}query' % value, None) + if plugin: + self.enable(plugin.plugin_attrib) + else: + self.clear() + query = ET.Element("{%s}query" % value) + self.xml.append(query) return self def get_query(self): @@ -182,36 +186,46 @@ class Iq(RootStanza): the stanza immediately. Used during stream initialization. Defaults to False. timeout_callback -- Optional reference to a stream handler function. - Will be executed when the timeout expires before a - response has been received with the originally-sent IQ + Will be executed when the timeout expires before a + response has been received with the originally-sent IQ stanza. Only called if there is a callback parameter (and therefore are in async mode). """ if timeout is None: timeout = self.stream.response_timeout + + if self.stream.session_bind_event.is_set(): + matcher = MatchIDSender({ + 'id': self['id'], + 'self': self.stream.boundjid, + 'peer': self['to'] + }) + else: + matcher = MatcherId(self['id']) + if callback is not None and self['type'] in ('get', 'set'): handler_name = 'IqCallback_%s' % self['id'] if timeout_callback: self.callback = callback self.timeout_callback = timeout_callback - self.stream.schedule('IqTimeout_%s' % self['id'], - timeout, - self._fire_timeout, - repeat=False) + self.stream.schedule('IqTimeout_%s' % self['id'], + timeout, + self._fire_timeout, + repeat=False) handler = Callback(handler_name, - MatcherId(self['id']), + matcher, self._handle_result, once=True) else: handler = Callback(handler_name, - MatcherId(self['id']), + matcher, callback, once=True) self.stream.register_handler(handler) StanzaBase.send(self, now=now) return handler_name elif block and self['type'] in ('get', 'set'): - waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) + waitfor = Waiter('IqWait_%s' % self['id'], matcher) self.stream.register_handler(waitfor) StanzaBase.send(self, now=now) result = waitfor.wait(timeout) diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index a7c2b218..52b807e5 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -60,7 +60,9 @@ class RootStanza(StanzaBase): self.send() elif isinstance(e, XMPPError): # We raised this deliberately + keep_id = self['id'] self.reply(clear=e.clear) + self['id'] = keep_id self['error']['condition'] = e.condition self['error']['text'] = e.text self['error']['type'] = e.etype @@ -72,7 +74,9 @@ class RootStanza(StanzaBase): self.send() else: # We probably didn't raise this on purpose, so send an error stanza + keep_id = self['id'] self.reply() + self['id'] = keep_id self['error']['condition'] = 'undefined-condition' self['error']['text'] = "SleekXMPP got into trouble." self['error']['type'] = 'cancel' diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index 901c3a56..e26f99ce 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -9,16 +9,14 @@ import unittest from xml.parsers.expat import ExpatError -import sleekxmpp from sleekxmpp import ClientXMPP, ComponentXMPP from sleekxmpp.util import Queue from sleekxmpp.stanza import Message, Iq, Presence from sleekxmpp.test import TestSocket, TestLiveSocket -from sleekxmpp.exceptions import XMPPError, IqTimeout, IqError -from sleekxmpp.xmlstream import ET, register_stanza_plugin -from sleekxmpp.xmlstream import ElementBase, StanzaBase +from sleekxmpp.xmlstream import ET +from sleekxmpp.xmlstream import ElementBase from sleekxmpp.xmlstream.tostring import tostring -from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId +from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId, MatchIDSender from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath @@ -214,6 +212,7 @@ class SleekTest(unittest.TestCase): matchers = {'stanzapath': StanzaPath, 'xpath': MatchXPath, 'mask': MatchXMLMask, + 'idsender': MatchIDSender, 'id': MatcherId} Matcher = matchers.get(method, None) if Matcher is None: @@ -289,11 +288,8 @@ class SleekTest(unittest.TestCase): if self.xmpp: self.xmpp.socket.disconnect_error() - def stream_start(self, mode='client', skip=True, header=None, - socket='mock', jid='tester@localhost', - password='test', server='localhost', - port=5222, sasl_mech=None, - plugins=None, plugin_config={}): + def stream_start(self, mode='client', skip=True, header=None, socket='mock', jid='tester@localhost', + password='test', server='localhost', port=5222, sasl_mech=None, plugins=None, plugin_config=None): """ Initialize an XMPP client or component using a dummy XML stream. @@ -316,6 +312,9 @@ class SleekTest(unittest.TestCase): plugins -- List of plugins to register. By default, all plugins are loaded. """ + if not plugin_config: + plugin_config = {} + if mode == 'client': self.xmpp = ClientXMPP(jid, password, sasl_mech=sasl_mech, @@ -377,6 +376,7 @@ class SleekTest(unittest.TestCase): if skip: if socket != 'live': # Mark send queue as usable + self.xmpp.session_bind_event.set() self.xmpp.session_started_event.set() # Clear startup stanzas self.xmpp.socket.next_sent(timeout=1) @@ -425,8 +425,7 @@ class SleekTest(unittest.TestCase): parts.append('xmlns="%s"' % default_ns) return header % ' '.join(parts) - def recv(self, data, defaults=[], method='exact', - use_values=True, timeout=1): + def recv(self, data, defaults=None, method='exact', use_values=True, timeout=1): """ Pass data to the dummy XMPP client as if it came from an XMPP server. @@ -447,6 +446,9 @@ class SleekTest(unittest.TestCase): timeout -- Time to wait in seconds for data to be received by a live connection. """ + if not defaults: + defaults = [] + if self.xmpp.socket.is_live: # we are working with a live connection, so we should # verify what has been received instead of simulating diff --git a/sleekxmpp/thirdparty/mini_dateutil.py b/sleekxmpp/thirdparty/mini_dateutil.py index 93f26312..e751a448 100644 --- a/sleekxmpp/thirdparty/mini_dateutil.py +++ b/sleekxmpp/thirdparty/mini_dateutil.py @@ -108,7 +108,7 @@ except: def __init__(self, name, offset): self._name = name - self._offset = datetime.timedelta(seconds=offset) + self._offset = datetime.timedelta(minutes=offset) def utcoffset(self, dt): return self._offset @@ -154,7 +154,7 @@ except: absoff = offsetmins name = "UTC%s%02d:%02d" % (sign, int(absoff / 60), absoff % 60) - inst = tzoffset(offsetmins, name) + inst = tzoffset(name,offsetmins) _fixed_offset_tzs[offsetmins] = inst return _fixed_offset_tzs[offsetmins] diff --git a/sleekxmpp/thirdparty/socks.py b/sleekxmpp/thirdparty/socks.py index a6c0d70e..34090d51 100644 --- a/sleekxmpp/thirdparty/socks.py +++ b/sleekxmpp/thirdparty/socks.py @@ -13,7 +13,7 @@ are permitted provided that the following conditions are met: 3. Neither the name of Dan Haim nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission. - + THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO @@ -38,6 +38,8 @@ for use in PyLoris (http://pyloris.sourceforge.net/) Minor modifications made by Mario Vilas (http://breakingcode.wordpress.com/) mainly to merge bug fixes found in Sourceforge +Minor modifications made by Eugene Dementiev (http://www.dementiev.eu/) + """ import socket @@ -212,12 +214,12 @@ class socksocket(socket.socket): if self.__proxy[3]: # Resolve remotely ipaddr = None - req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr.encode() else: # Resolve locally ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) req = req + chr(0x01).encode() + ipaddr - req = req + struct.pack(">H", destport) + req += struct.pack(">H", destport) self.sendall(req) # Get the response resp = self.__recvall(4) @@ -286,7 +288,7 @@ class socksocket(socket.socket): # The username parameter is considered userid for SOCKS4 if self.__proxy[4] != None: req = req + self.__proxy[4] - req = req + chr(0x00).encode() + req += chr(0x00).encode() # DNS name if remote resolving is required # NOTE: This is actually an extension to the SOCKS4 protocol # called SOCKS4A and may not be supported in all cases. @@ -327,7 +329,10 @@ class socksocket(socket.socket): # We read the response until we get the string "\r\n\r\n" resp = self.recv(1) while resp.find("\r\n\r\n".encode()) == -1: - resp = resp + self.recv(1) + recv = self.recv(1) + if not recv: + raise GeneralProxyError((1, _generalerrors[1])) + resp = resp + recv # We just need the first line to check if the connection # was successful statusline = resp.splitlines()[0].split(" ".encode(), 2) diff --git a/sleekxmpp/thirdparty/statemachine.py b/sleekxmpp/thirdparty/statemachine.py index 4b5ecd6b..6c504dce 100644 --- a/sleekxmpp/thirdparty/statemachine.py +++ b/sleekxmpp/thirdparty/statemachine.py @@ -15,7 +15,8 @@ log = logging.getLogger(__name__) class StateMachine(object): - def __init__(self, states=[]): + def __init__(self, states=None): + if not states: states = [] self.lock = threading.Condition() self.__states = [] self.addStates(states) @@ -33,7 +34,7 @@ class StateMachine(object): self.lock.release() - def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={}): + def transition(self, from_state, to_state, wait=0.0, func=None, args=None, kwargs=None): ''' Transition from the given `from_state` to the given `to_state`. This method will return `True` if the state machine is now in `to_state`. It @@ -64,15 +65,23 @@ class StateMachine(object): values for `args` and `kwargs` are provided, they are expanded and passed like so: `func( *args, **kwargs )`. ''' + if not args: + args = [] + if not kwargs: + kwargs = {} return self.transition_any((from_state,), to_state, wait=wait, func=func, args=args, kwargs=kwargs) - def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={}): + def transition_any(self, from_states, to_state, wait=0.0, func=None, args=None, kwargs=None): ''' Transition from any of the given `from_states` to the given `to_state`. ''' + if not args: + args = [] + if not kwargs: + kwargs = {} if not isinstance(from_states, (tuple, list, set)): raise ValueError("from_states should be a list, tuple, or set") diff --git a/sleekxmpp/util/__init__.py b/sleekxmpp/util/__init__.py index 1e4af02d..47a935af 100644 --- a/sleekxmpp/util/__init__.py +++ b/sleekxmpp/util/__init__.py @@ -11,21 +11,38 @@ from sleekxmpp.util.misc_ops import bytes, unicode, hashes, hash, \ - num_to_bytes, bytes_to_num, quote, XOR + num_to_bytes, bytes_to_num, quote, \ + XOR, safedict # ===================================================================== # Standardize import of Queue class: import sys -if 'gevent' in sys.modules: + +def _gevent_threads_enabled(): + if not 'gevent' in sys.modules: + return False + try: + from gevent import thread as green_thread + thread = __import__('thread') + return thread.LockType is green_thread.LockType + except ImportError: + return False + +if _gevent_threads_enabled(): import gevent.queue as queue - Queue = queue.JoinableQueue + _queue = queue.JoinableQueue else: try: import queue except ImportError: import Queue as queue - Queue = queue.Queue + _queue = queue.Queue +class Queue(_queue): + def put(self, item, block=True, timeout=None): + if _queue.full(self): + _queue.get(self) + return _queue.put(self, item, block, timeout) QueueEmpty = queue.Empty diff --git a/sleekxmpp/util/misc_ops.py b/sleekxmpp/util/misc_ops.py index 3b246625..18c919a8 100644 --- a/sleekxmpp/util/misc_ops.py +++ b/sleekxmpp/util/misc_ops.py @@ -8,7 +8,10 @@ def unicode(text): text = text.decode('utf-8') import __builtin__ return __builtin__.unicode(text) - return str(text) + elif not isinstance(text, str): + return text.decode('utf-8') + else: + return text def bytes(text): @@ -126,6 +129,7 @@ def hashes(): hashes = ['SHA-' + h[3:] for h in dir(hashlib) if h.startswith('sha')] return t + hashes + def setdefaultencoding(encoding): """ Set the current default string encoding used by the Unicode implementation. @@ -148,4 +152,14 @@ def setdefaultencoding(encoding): if func is None: raise RuntimeError("Could not find setdefaultencoding") sys.setdefaultencoding = func - return func(encoding)
\ No newline at end of file + return func(encoding) + + +def safedict(data): + if sys.version_info < (2, 7): + safe = {} + for key in data: + safe[key.encode('utf8')] = data[key] + return safe + else: + return data diff --git a/sleekxmpp/util/sasl/__init__.py b/sleekxmpp/util/sasl/__init__.py index d054ce09..2d344e9b 100644 --- a/sleekxmpp/util/sasl/__init__.py +++ b/sleekxmpp/util/sasl/__init__.py @@ -7,7 +7,9 @@ Part of SleekXMPP: The Sleek XMPP Library - :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :copryight: (c) 2004-2013 David Alan Cridland + :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details """ diff --git a/sleekxmpp/util/sasl/client.py b/sleekxmpp/util/sasl/client.py index 0bfb63f8..fd685547 100644 --- a/sleekxmpp/util/sasl/client.py +++ b/sleekxmpp/util/sasl/client.py @@ -7,7 +7,9 @@ Part of SleekXMPP: The Sleek XMPP Library - :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :copryight: (c) 2004-2013 David Alan Cridland + :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details """ diff --git a/sleekxmpp/util/sasl/mechanisms.py b/sleekxmpp/util/sasl/mechanisms.py index 55ae44dd..7a7ebf7b 100644 --- a/sleekxmpp/util/sasl/mechanisms.py +++ b/sleekxmpp/util/sasl/mechanisms.py @@ -9,7 +9,9 @@ Part of SleekXMPP: The Sleek XMPP Library - :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :copryight: (c) 2004-2013 David Alan Cridland + :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details """ @@ -21,7 +23,8 @@ from base64 import b64encode, b64decode from sleekxmpp.util import bytes, hash, XOR, quote, num_to_bytes from sleekxmpp.util.sasl.client import sasl_mech, Mech, \ - SASLCancelled, SASLFailed + SASLCancelled, SASLFailed, \ + SASLMutualAuthFailed @sasl_mech(0) @@ -86,7 +89,7 @@ class EXTERNAL(Mech): return self.credentials['authzid'] -@sasl_mech(30) +@sasl_mech(31) class X_FACEBOOK_PLATFORM(Mech): name = 'X-FACEBOOK-PLATFORM' @@ -108,7 +111,7 @@ class X_FACEBOOK_PLATFORM(Mech): b'api_key': self.credentials['api_key'] } - resp = '&'.join(['%s=%s' % (k, v) for k, v in resp_data.items()]) + resp = '&'.join(['%s=%s' % (k.decode("utf-8"), v.decode("utf-8")) for k, v in resp_data.items()]) return bytes(resp) return b'' @@ -220,17 +223,16 @@ class SCRAM(Mech): return self.hash(text).digest() def saslname(self, value): - escaped = b'' - for char in bytes(value): - if char == b',': - escaped += b'=2C' - elif char == b'=': - escaped += b'=3D' + value = value.decode("utf-8") + escaped = [] + for char in value: + if char == ',': + escaped += '=2C' + elif char == '=': + escaped += '=3D' else: - if isinstance(char, int): - char = chr(char) - escaped += bytes(char) - return escaped + escaped += char + return "".join(escaped).encode("utf-8") def parse(self, challenge): items = {} @@ -284,7 +286,9 @@ class SCRAM(Mech): if nonce[:len(self.cnonce)] != self.cnonce: raise SASLCancelled('Invalid nonce') - cbind_data = self.credentials['channel_binding'] + cbind_data = b'' + if self.use_channel_binding: + cbind_data = self.credentials['channel_binding'] cbind_input = self.gs2_header + cbind_data channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'') @@ -467,7 +471,8 @@ class DIGEST(Mech): 'qop': self.qop, 'digest-uri': quote(self.digest_uri()), 'response': self.response(b'AUTHENTICATE'), - 'maxbuf': self.maxbuf + 'maxbuf': self.maxbuf, + 'charset': 'utf-8' } resp = b'' for key, value in data.items(): @@ -480,7 +485,7 @@ class DIGEST(Mech): if self.cnonce and self.nonce and self.nonce_count and self.qop: self.nonce_count += 1 return self.respond() - return b'' + return None data = self.parse(challenge) if 'rspauth' in data: @@ -526,6 +531,9 @@ else: result = kerberos.authGSSClientStep(self.gss, b64_challenge) if result != kerberos.AUTH_GSS_CONTINUE: self.step = 1 + elif not challenge: + kerberos.authGSSClientClean(self.gss) + return b'' elif self.step == 1: username = self.credentials['username'] @@ -535,7 +543,7 @@ else: resp = kerberos.authGSSClientResponse(self.gss) except kerberos.GSSError as e: - raise SASLCancelled('Kerberos error: %s' % e.message) + raise SASLCancelled('Kerberos error: %s' % e) if not resp: return b'' else: diff --git a/sleekxmpp/util/stringprep_profiles.py b/sleekxmpp/util/stringprep_profiles.py index ad89d4cc..84326bc3 100644 --- a/sleekxmpp/util/stringprep_profiles.py +++ b/sleekxmpp/util/stringprep_profiles.py @@ -16,9 +16,8 @@ from __future__ import unicode_literals -import sys import stringprep -import unicodedata +from unicodedata import ucd_3_2_0 as unicodedata from sleekxmpp.util import unicode diff --git a/sleekxmpp/version.py b/sleekxmpp/version.py index 010f425b..acea9334 100644 --- a/sleekxmpp/version.py +++ b/sleekxmpp/version.py @@ -9,5 +9,5 @@ # We don't want to have to import the entire library # just to get the version info for setup.py -__version__ = '1.1.10' -__version_info__ = (1, 1, 10, '', 0) +__version__ = '1.4.0' +__version_info__ = (1, 4, 0, '', 0) diff --git a/sleekxmpp/xmlstream/cert.py b/sleekxmpp/xmlstream/cert.py index fa12f794..71146f36 100644 --- a/sleekxmpp/xmlstream/cert.py +++ b/sleekxmpp/xmlstream/cert.py @@ -1,6 +1,10 @@ import logging from datetime import datetime, timedelta +# Make a call to strptime before starting threads to +# prevent thread safety issues. +datetime.strptime('1970-01-01 12:00:00', "%Y-%m-%d %H:%M:%S") + try: from pyasn1.codec.der import decoder, encoder diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py index d4537998..53b83bc7 100644 --- a/sleekxmpp/xmlstream/filesocket.py +++ b/sleekxmpp/xmlstream/filesocket.py @@ -13,6 +13,7 @@ """ from socket import _fileobject +import errno import socket @@ -29,7 +30,13 @@ class FileSocket(_fileobject): """Read data from the socket as if it were a file.""" if self._sock is None: return None - data = self._sock.recv(size) + while True: + try: + data = self._sock.recv(size) + break + except socket.error as serr: + if serr.errno != errno.EINTR: + raise if data is not None: return data diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py index 1038d1bd..aa74c434 100644 --- a/sleekxmpp/xmlstream/matcher/__init__.py +++ b/sleekxmpp/xmlstream/matcher/__init__.py @@ -7,6 +7,7 @@ """ from sleekxmpp.xmlstream.matcher.id import MatcherId +from sleekxmpp.xmlstream.matcher.idsender import MatchIDSender from sleekxmpp.xmlstream.matcher.many import MatchMany from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask diff --git a/sleekxmpp/xmlstream/matcher/idsender.py b/sleekxmpp/xmlstream/matcher/idsender.py new file mode 100644 index 00000000..5c2c1f51 --- /dev/null +++ b/sleekxmpp/xmlstream/matcher/idsender.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.xmlstream.matcher.id + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details +""" + +from sleekxmpp.xmlstream.matcher.base import MatcherBase + + +class MatchIDSender(MatcherBase): + + """ + The IDSender matcher selects stanzas that have the same stanza 'id' + interface value as the desired ID, and that the 'from' value is one + of a set of approved entities that can respond to a request. + """ + + def match(self, xml): + """Compare the given stanza's ``'id'`` attribute to the stored + ``id`` value, and verify the sender's JID. + + :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + stanza to compare against. + """ + + selfjid = self._criteria['self'] + peerjid = self._criteria['peer'] + + allowed = {} + allowed[''] = True + allowed[selfjid.bare] = True + allowed[selfjid.host] = True + allowed[peerjid.full] = True + allowed[peerjid.bare] = True + allowed[peerjid.host] = True + + _from = xml['from'] + + try: + return xml['id'] == self._criteria['id'] and allowed[_from] + except KeyError: + return False diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index a0568f08..56f728e1 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -14,12 +14,6 @@ from sleekxmpp.xmlstream.stanzabase import ET from sleekxmpp.xmlstream.matcher.base import MatcherBase -# Flag indicating if the builtin XPath matcher should be used, which -# uses namespaces, or a custom matcher that ignores namespaces. -# Changing this will affect ALL XMLMask matchers. -IGNORE_NS = False - - log = logging.getLogger(__name__) @@ -39,19 +33,15 @@ class MatchXMLMask(MatcherBase): :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` should be used instead. - The use of namespaces in the mask comparison is controlled by - ``IGNORE_NS``. Setting ``IGNORE_NS`` to ``True`` will disable namespace - based matching for ALL XMLMask matchers. - :param criteria: Either an :class:`~xml.etree.ElementTree.Element` XML object or XML string to use as a mask. """ - def __init__(self, criteria): + def __init__(self, criteria, default_ns='jabber:client'): MatcherBase.__init__(self, criteria) if isinstance(criteria, str): self._criteria = ET.fromstring(self._criteria) - self.default_ns = 'jabber:client' + self.default_ns = default_ns def setDefaultNS(self, ns): """Set the default namespace to use during comparisons. @@ -84,8 +74,6 @@ class MatchXMLMask(MatcherBase): do not have a specified namespace. Defaults to ``"__no_ns__"``. """ - use_ns = not IGNORE_NS - if source is None: # If the element was not found. May happend during recursive calls. return False @@ -96,17 +84,10 @@ class MatchXMLMask(MatcherBase): mask = ET.fromstring(mask) except ExpatError: log.warning("Expat error: %s\nIn parsing: %s", '', mask) - if not use_ns: - # Compare the element without using namespaces. - source_tag = source.tag.split('}', 1)[-1] - mask_tag = mask.tag.split('}', 1)[-1] - if source_tag != mask_tag: - return False - else: - # Compare the element using namespaces - mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag) - if source.tag not in [mask.tag, mask_ns_tag]: - return False + + mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag) + if source.tag not in [mask.tag, mask_ns_tag]: + return False # If the mask includes text, compare it. if mask.text and source.text and \ @@ -122,37 +103,15 @@ class MatchXMLMask(MatcherBase): # Recursively check subelements. matched_elements = {} for subelement in mask: - if use_ns: - matched = False - for other in source.findall(subelement.tag): - matched_elements[other] = False - if self._mask_cmp(other, subelement, use_ns): - if not matched_elements.get(other, False): - matched_elements[other] = True - matched = True - if not matched: - return False - else: - if not self._mask_cmp(self._get_child(source, subelement.tag), - subelement, use_ns): - return False + matched = False + for other in source.findall(subelement.tag): + matched_elements[other] = False + if self._mask_cmp(other, subelement, use_ns): + if not matched_elements.get(other, False): + matched_elements[other] = True + matched = True + if not matched: + return False # Everything matches. return True - - def _get_child(self, xml, tag): - """Return a child element given its tag, ignoring namespace values. - - Returns ``None`` if the child was not found. - - :param xml: The :class:`~xml.etree.ElementTree.Element` XML object - to search for the given child tag. - :param tag: The name of the subelement to find. - """ - tag = tag.split('}')[-1] - try: - children = [c.tag.split('}')[-1] for c in xml] - index = children.index(tag) - except ValueError: - return None - return list(xml)[index] diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index 3f03e68e..f3d28429 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -9,16 +9,10 @@ :license: MIT, see LICENSE for more details """ -from sleekxmpp.xmlstream.stanzabase import ET +from sleekxmpp.xmlstream.stanzabase import ET, fix_ns from sleekxmpp.xmlstream.matcher.base import MatcherBase -# Flag indicating if the builtin XPath matcher should be used, which -# uses namespaces, or a custom matcher that ignores namespaces. -# Changing this will affect ALL XPath matchers. -IGNORE_NS = False - - class MatchXPath(MatcherBase): """ @@ -38,6 +32,9 @@ class MatchXPath(MatcherBase): expressions will be matched without using namespaces. """ + def __init__(self, criteria): + self._criteria = fix_ns(criteria) + def match(self, xml): """ Compare a stanza's XML contents to an XPath expression. @@ -59,28 +56,4 @@ class MatchXPath(MatcherBase): x = ET.Element('x') x.append(xml) - if not IGNORE_NS: - # Use builtin, namespace respecting, XPath matcher. - if x.find(self._criteria) is not None: - return True - return False - else: - # Remove namespaces from the XPath expression. - criteria = [] - for ns_block in self._criteria.split('{'): - criteria.extend(ns_block.split('}')[-1].split('/')) - - # Walk the XPath expression. - xml = x - for tag in criteria: - if not tag: - # Skip empty tag name artifacts from the cleanup phase. - continue - - children = [c.tag.split('}')[-1] for c in xml] - try: - index = children.index(tag) - except ValueError: - return False - xml = list(xml)[index] - return True + return x.find(self._criteria) is not None diff --git a/sleekxmpp/xmlstream/resolver.py b/sleekxmpp/xmlstream/resolver.py index 394daa64..188e5ac7 100644 --- a/sleekxmpp/xmlstream/resolver.py +++ b/sleekxmpp/xmlstream/resolver.py @@ -32,10 +32,10 @@ log = logging.getLogger(__name__) #: cd dnspython #: git checkout python3 #: python3 setup.py install -USE_DNSPYTHON = False +DNSPYTHON_AVAILABLE = False try: import dns.resolver - USE_DNSPYTHON = True + DNSPYTHON_AVAILABLE = True except ImportError as e: log.debug("Could not find dnspython package. " + \ "Not all features will be available") @@ -47,13 +47,13 @@ def default_resolver(): :returns: A :class:`dns.resolver.Resolver` object if dnspython is available. Otherwise, ``None``. """ - if USE_DNSPYTHON: + if DNSPYTHON_AVAILABLE: return dns.resolver.get_default_resolver() return None def resolve(host, port=None, service=None, proto='tcp', - resolver=None, use_ipv6=True): + resolver=None, use_ipv6=True, use_dnspython=True): """Peform DNS resolution for a given hostname. Resolution may perform SRV record lookups if a service and protocol @@ -77,6 +77,9 @@ def resolve(host, port=None, service=None, proto='tcp', :param use_ipv6: Optionally control the use of IPv6 in situations where it is either not available, or performance is degraded. Defaults to ``True``. + :param use_dnspython: Optionally control if dnspython is used to make + the DNS queries instead of the built-in DNS + library. :type host: string :type port: int @@ -84,14 +87,22 @@ def resolve(host, port=None, service=None, proto='tcp', :type proto: string :type resolver: :class:`dns.resolver.Resolver` :type use_ipv6: bool + :type use_dnspython: bool :return: An iterable of IP address, port pairs in the order dictated by SRV priorities and weights, if applicable. """ + + if not use_dnspython: + if DNSPYTHON_AVAILABLE: + log.debug("DNS: Not using dnspython, but dnspython is installed.") + else: + log.debug("DNS: Not using dnspython.") + if not use_ipv6: log.debug("DNS: Use of IPv6 has been disabled.") - if resolver is None and USE_DNSPYTHON: + if resolver is None and DNSPYTHON_AVAILABLE and use_dnspython: resolver = dns.resolver.get_default_resolver() # An IPv6 literal is allowed to be enclosed in square brackets, but @@ -113,7 +124,7 @@ def resolve(host, port=None, service=None, proto='tcp', if hasattr(socket, 'inet_pton'): ipv6 = socket.inet_pton(socket.AF_INET6, host) yield (host, host, port) - except socket.error: + except (socket.error, ValueError): pass # If no service was provided, then we can just do A/AAAA lookups on the @@ -122,7 +133,9 @@ def resolve(host, port=None, service=None, proto='tcp', if not service: hosts = [(host, port)] else: - hosts = get_SRV(host, port, service, proto, resolver=resolver) + hosts = get_SRV(host, port, service, proto, + resolver=resolver, + use_dnspython=use_dnspython) for host, port in hosts: results = [] @@ -131,16 +144,18 @@ def resolve(host, port=None, service=None, proto='tcp', results.append((host, '::1', port)) results.append((host, '127.0.0.1', port)) if use_ipv6: - for address in get_AAAA(host, resolver=resolver): + for address in get_AAAA(host, resolver=resolver, + use_dnspython=use_dnspython): results.append((host, address, port)) - for address in get_A(host, resolver=resolver): + for address in get_A(host, resolver=resolver, + use_dnspython=use_dnspython): results.append((host, address, port)) for host, address, port in results: yield host, address, port -def get_A(host, resolver=None): +def get_A(host, resolver=None, use_dnspython=True): """Lookup DNS A records for a given host. If ``resolver`` is not provided, or is ``None``, then resolution will @@ -148,9 +163,13 @@ def get_A(host, resolver=None): :param host: The hostname to resolve for A record IPv4 addresses. :param resolver: Optional DNS resolver object to use for the query. + :param use_dnspython: Optionally control if dnspython is used to make + the DNS queries instead of the built-in DNS + library. :type host: string :type resolver: :class:`dns.resolver.Resolver` or ``None`` + :type use_dnspython: bool :return: A list of IPv4 literals. """ @@ -158,7 +177,7 @@ def get_A(host, resolver=None): # If not using dnspython, attempt lookup using the OS level # getaddrinfo() method. - if resolver is None: + if resolver is None or not use_dnspython: try: recs = socket.getaddrinfo(host, None, socket.AF_INET, socket.SOCK_STREAM) @@ -183,7 +202,7 @@ def get_A(host, resolver=None): return [] -def get_AAAA(host, resolver=None): +def get_AAAA(host, resolver=None, use_dnspython=True): """Lookup DNS AAAA records for a given host. If ``resolver`` is not provided, or is ``None``, then resolution will @@ -191,9 +210,13 @@ def get_AAAA(host, resolver=None): :param host: The hostname to resolve for AAAA record IPv6 addresses. :param resolver: Optional DNS resolver object to use for the query. + :param use_dnspython: Optionally control if dnspython is used to make + the DNS queries instead of the built-in DNS + library. :type host: string :type resolver: :class:`dns.resolver.Resolver` or ``None`` + :type use_dnspython: bool :return: A list of IPv6 literals. """ @@ -201,12 +224,15 @@ def get_AAAA(host, resolver=None): # If not using dnspython, attempt lookup using the OS level # getaddrinfo() method. - if resolver is None: + if resolver is None or not use_dnspython: + if not socket.has_ipv6: + log.debug("Unable to query %s for AAAA records: IPv6 is not supported", host) + return [] try: recs = socket.getaddrinfo(host, None, socket.AF_INET6, socket.SOCK_STREAM) return [rec[4][0] for rec in recs] - except socket.gaierror: + except (OSError, socket.gaierror): log.debug("DNS: Error retreiving AAAA address " + \ "info for %s." % host) return [] @@ -227,7 +253,7 @@ def get_AAAA(host, resolver=None): return [] -def get_SRV(host, port, service, proto='tcp', resolver=None): +def get_SRV(host, port, service, proto='tcp', resolver=None, use_dnspython=True): """Perform SRV record resolution for a given host. .. note:: @@ -253,7 +279,7 @@ def get_SRV(host, port, service, proto='tcp', resolver=None): :return: A list of hostname, port pairs in the order dictacted by SRV priorities and weights. """ - if resolver is None: + if resolver is None or not use_dnspython: log.warning("DNS: dnspython not found. Can not use SRV lookup.") return [(host, port)] diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py index b3e50983..e6fae37a 100644 --- a/sleekxmpp/xmlstream/scheduler.py +++ b/sleekxmpp/xmlstream/scheduler.py @@ -20,6 +20,11 @@ import itertools from sleekxmpp.util import Queue, QueueEmpty +#: The time in seconds to wait for events from the event queue, and also the +#: time between checks for the process stop signal. +WAIT_TIMEOUT = 1.0 + + log = logging.getLogger(__name__) @@ -76,7 +81,7 @@ class Task(object): """ if self.qpointer is not None: self.qpointer.put(('schedule', self.callback, - self.args, self.name)) + self.args, self.kwargs, self.name)) else: self.callback(*self.args, **self.kwargs) self.reset() @@ -120,6 +125,10 @@ class Scheduler(object): #: Lock for accessing the task queue. self.schedule_lock = threading.RLock() + #: The time in seconds to wait for events from the event queue, + #: and also the time between checks for the process stop signal. + self.wait_timeout = WAIT_TIMEOUT + def process(self, threaded=True, daemon=False): """Begin accepting and processing scheduled tasks. @@ -139,24 +148,25 @@ class Scheduler(object): self.run = True try: while self.run and not self.stop.is_set(): - wait = 0.1 updated = False if self.schedule: wait = self.schedule[0].next - time.time() + else: + wait = self.wait_timeout try: if wait <= 0.0: newtask = self.addq.get(False) else: - if wait >= 3.0: - wait = 3.0 newtask = None - elapsed = 0 - while not self.stop.is_set() and \ + while self.run and \ + not self.stop.is_set() and \ newtask is None and \ - elapsed < wait: - newtask = self.addq.get(True, 0.1) - elapsed += 0.1 - except QueueEmpty: + wait > 0: + try: + newtask = self.addq.get(True, min(wait, self.wait_timeout)) + except QueueEmpty: # Nothing to add, nothing to do. Check run flags and continue waiting. + wait -= self.wait_timeout + except QueueEmpty: # Time to run some tasks, and no new tasks to add. self.schedule_lock.acquire() # select only those tasks which are to be executed now relevant = itertools.takewhile( @@ -174,11 +184,11 @@ class Scheduler(object): # only need to resort tasks if a repeated task has # been kept in the list. updated = True - else: - updated = True + else: # Add new task self.schedule_lock.acquire() if newtask is not None: self.schedule.append(newtask) + updated = True finally: if updated: self.schedule.sort(key=lambda task: task.next) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 122d7eb4..11c8dd67 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -3,7 +3,7 @@ sleekxmpp.xmlstream.stanzabase ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - This module implements a wrapper layer for XML objects + module implements a wrapper layer for XML objects that allows them to be treated like dictionaries. Part of SleekXMPP: The Sleek XMPP Library @@ -19,6 +19,7 @@ import logging import weakref from xml.etree import cElementTree as ET +from sleekxmpp.util import safedict from sleekxmpp.xmlstream import JID from sleekxmpp.xmlstream.tostring import tostring from sleekxmpp.thirdparty import OrderedDict @@ -141,7 +142,7 @@ def multifactory(stanza, plugin_attrib): parent.loaded_plugins.remove(plugin_attrib) try: parent.xml.remove(self.xml) - except: + except ValueError: pass else: for stanza in list(res): @@ -192,7 +193,7 @@ def fix_ns(xpath, split=False, propagate_ns=True, default_ns=''): for element in elements: if element: # Skip empty entry artifacts from splitting. - if propagate_ns: + if propagate_ns and element[0] != '*': tag = '{%s}%s' % (namespace, element) else: tag = element @@ -565,7 +566,10 @@ class ElementBase(object): values = {} values['lang'] = self['lang'] for interface in self.interfaces: - values[interface] = self[interface] + if isinstance(self[interface], JID): + values[interface] = self[interface].jid + else: + values[interface] = self[interface] if interface in self.lang_interfaces: values['%s|*' % interface] = self['%s|*' % interface] for plugin, stanza in self.plugins.items(): @@ -596,31 +600,39 @@ class ElementBase(object): iterable_interfaces = [p.plugin_attrib for \ p in self.plugin_iterables] + if 'lang' in values: + self['lang'] = values['lang'] + + if 'substanzas' in values: + # Remove existing substanzas + for stanza in self.iterables: + try: + self.xml.remove(stanza.xml) + except ValueError: + pass + self.iterables = [] + + # Add new substanzas + for subdict in values['substanzas']: + if '__childtag__' in subdict: + for subclass in self.plugin_iterables: + child_tag = "{%s}%s" % (subclass.namespace, + subclass.name) + if subdict['__childtag__'] == child_tag: + sub = subclass(parent=self) + sub.values = subdict + self.iterables.append(sub) + for interface, value in values.items(): full_interface = interface interface_lang = ('%s|' % interface).split('|') interface = interface_lang[0] lang = interface_lang[1] or self.get_lang() - if interface == 'substanzas': - # Remove existing substanzas - for stanza in self.iterables: - self.xml.remove(stanza.xml) - self.iterables = [] - - # Add new substanzas - for subdict in value: - if '__childtag__' in subdict: - for subclass in self.plugin_iterables: - child_tag = "{%s}%s" % (subclass.namespace, - subclass.name) - if subdict['__childtag__'] == child_tag: - sub = subclass(parent=self) - sub.values = subdict - self.iterables.append(sub) - break - elif interface == 'lang': - self[interface] = value + if interface == 'lang': + continue + elif interface == 'substanzas': + continue elif interface in self.interfaces: self[full_interface] = value elif interface in self.plugin_attrib_map: @@ -668,6 +680,8 @@ class ElementBase(object): if lang and attrib in self.lang_interfaces: kwargs['lang'] = lang + kwargs = safedict(kwargs) + if attrib == 'substanzas': return self.iterables elif attrib in self.interfaces or attrib == 'lang': @@ -744,6 +758,8 @@ class ElementBase(object): if lang and attrib in self.lang_interfaces: kwargs['lang'] = lang + kwargs = safedict(kwargs) + if attrib in self.interfaces or attrib == 'lang': if value is not None: set_method = "set_%s" % attrib.lower() @@ -830,6 +846,8 @@ class ElementBase(object): if lang and attrib in self.lang_interfaces: kwargs['lang'] = lang + kwargs = safedict(kwargs) + if attrib in self.interfaces or attrib == 'lang': del_method = "del_%s" % attrib.lower() del_method2 = "del%s" % attrib.title() @@ -866,7 +884,7 @@ class ElementBase(object): self.loaded_plugins.remove(attrib) try: self.xml.remove(plugin.xml) - except: + except ValueError: pass return self diff --git a/sleekxmpp/xmlstream/tostring.py b/sleekxmpp/xmlstream/tostring.py index 08d7ad02..c49abd3e 100644 --- a/sleekxmpp/xmlstream/tostring.py +++ b/sleekxmpp/xmlstream/tostring.py @@ -24,8 +24,8 @@ if sys.version_info < (3, 0): XML_NS = 'http://www.w3.org/XML/1998/namespace' -def tostring(xml=None, xmlns='', stream=None, - outbuffer='', top_level=False, open_only=False): +def tostring(xml=None, xmlns='', stream=None, outbuffer='', + top_level=False, open_only=False, namespaces=None): """Serialize an XML object to a Unicode string. If an outer xmlns is provided using ``xmlns``, then the current element's @@ -41,7 +41,8 @@ def tostring(xml=None, xmlns='', stream=None, during recursive calls. :param bool top_level: Indicates that the element is the outermost element. - + :param set namespaces: Track which namespaces are in active use so + that new ones can be declared when needed. :type xml: :py:class:`~xml.etree.ElementTree.Element` :type stream: :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` @@ -63,6 +64,7 @@ def tostring(xml=None, xmlns='', stream=None, default_ns = '' stream_ns = '' use_cdata = False + if stream: default_ns = stream.default_ns stream_ns = stream.stream_ns @@ -82,6 +84,7 @@ def tostring(xml=None, xmlns='', stream=None, output.append(namespace) # Output escaped attribute values. + new_namespaces = set() for attrib, value in xml.attrib.items(): value = escape(value, use_cdata) if '}' not in attrib: @@ -89,14 +92,20 @@ def tostring(xml=None, xmlns='', stream=None, else: attrib_ns = attrib.split('}')[0][1:] attrib = attrib.split('}')[1] - if stream and attrib_ns in stream.namespace_map: + if attrib_ns == XML_NS: + output.append(' xml:%s="%s"' % (attrib, value)) + elif stream and attrib_ns in stream.namespace_map: mapped_ns = stream.namespace_map[attrib_ns] if mapped_ns: - output.append(' %s:%s="%s"' % (mapped_ns, - attrib, - value)) - elif attrib_ns == XML_NS: - output.append(' xml:%s="%s"' % (attrib, value)) + if namespaces is None: + namespaces = set() + if attrib_ns not in namespaces: + namespaces.add(attrib_ns) + new_namespaces.add(attrib_ns) + output.append(' xmlns:%s="%s"' % ( + mapped_ns, attrib_ns)) + output.append(' %s:%s="%s"' % ( + mapped_ns, attrib, value)) if open_only: # Only output the opening tag, regardless of content. @@ -110,7 +119,8 @@ def tostring(xml=None, xmlns='', stream=None, output.append(escape(xml.text, use_cdata)) if len(xml): for child in xml: - output.append(tostring(child, tag_xmlns, stream)) + output.append(tostring(child, tag_xmlns, stream, + namespaces=namespaces)) output.append("</%s>" % tag_name) elif xml.text: # If we only have text content. @@ -121,6 +131,11 @@ def tostring(xml=None, xmlns='', stream=None, if xml.tail: # If there is additional text after the element. output.append(escape(xml.tail, use_cdata)) + for ns in new_namespaces: + # Remove namespaces introduced in this context. This is necessary + # because the namespaces object continues to be shared with other + # contexts. + namespaces.remove(ns) return ''.join(output) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index bea6e88f..f9ec4947 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -26,11 +26,12 @@ import time import random import weakref import uuid +import errno from xml.parsers.expat import ExpatError import sleekxmpp -from sleekxmpp.util import Queue, QueueEmpty +from sleekxmpp.util import Queue, QueueEmpty, safedict from sleekxmpp.thirdparty.statemachine import StateMachine from sleekxmpp.xmlstream import Scheduler, tostring, cert from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET, ElementBase @@ -49,7 +50,7 @@ RESPONSE_TIMEOUT = 30 #: The time in seconds to wait for events from the event queue, and also the #: time between checks for the process stop signal. -WAIT_TIMEOUT = 0.1 +WAIT_TIMEOUT = 1.0 #: The number of threads to use to handle XML stream events. This is not the #: same as the number of custom event handling threads. @@ -122,6 +123,11 @@ class XMLStream(object): #: xmpp.ssl_version = ssl.PROTOCOL_SSLv23 self.ssl_version = ssl.PROTOCOL_TLSv1 + #: The list of accepted ciphers, in OpenSSL Format. + #: It might be useful to override it for improved security + #: over the python defaults. + self.ciphers = None + #: Path to a file containing certificates for verifying the #: server SSL certificate. A non-``None`` value will trigger #: certificate checking. @@ -218,6 +224,11 @@ class XMLStream(object): #: If set to ``True``, attempt to use IPv6. self.use_ipv6 = True + #: If set to ``True``, allow using the ``dnspython`` DNS library + #: if available. If set to ``False``, the builtin DNS resolver + #: will be used, even if ``dnspython`` is installed. + self.use_dnspython = True + #: Use CDATA for escaping instead of XML entities. Defaults #: to ``False``. self.use_cdata = False @@ -280,7 +291,7 @@ class XMLStream(object): self.event_queue = Queue() #: A queue of string data to be sent over the stream. - self.send_queue = Queue() + self.send_queue = Queue(maxsize=256) self.send_queue_lock = threading.Lock() self.send_lock = threading.RLock() @@ -449,9 +460,11 @@ class XMLStream(object): def _connect(self, reattempt=True): self.scheduler.remove('Session timeout check') - if self.reconnect_delay is None or not reattempt: + if self.reconnect_delay is None: delay = 1.0 - else: + self.reconnect_delay = delay + + if reattempt: delay = min(self.reconnect_delay * 2, self.reconnect_max_delay) delay = random.normalvariate(delay, delay * 0.1) log.debug('Waiting %s seconds before connecting.', delay) @@ -461,10 +474,10 @@ class XMLStream(object): time.sleep(0.1) elapsed += 0.1 except KeyboardInterrupt: - self.stop.set() + self.set_stop() return False except SystemExit: - self.stop.set() + self.set_stop() return False if self.default_domain: @@ -507,12 +520,19 @@ class XMLStream(object): else: cert_policy = ssl.CERT_REQUIRED - ssl_socket = ssl.wrap_socket(self.socket, - certfile=self.certfile, - keyfile=self.keyfile, - ca_certs=self.ca_certs, - cert_reqs=cert_policy, - do_handshake_on_connect=False) + ssl_args = safedict({ + 'certfile': self.certfile, + 'keyfile': self.keyfile, + 'ca_certs': self.ca_certs, + 'cert_reqs': cert_policy, + 'do_handshake_on_connect': False, + "ssl_version": self.ssl_version + }) + + if sys.version_info >= (2, 7): + ssl_args['ciphers'] = self.ciphers + + ssl_socket = ssl.wrap_socket(self.socket, **ssl_args) if hasattr(self.socket, 'socket'): # We are using a testing socket, so preserve the top @@ -550,7 +570,7 @@ class XMLStream(object): cert.verify(self._expected_server_name, self._der_cert) except cert.CertificateError as err: if not self.event_handled('ssl_invalid_cert'): - log.error(err.message) + log.error(err) self.disconnect(send_close=False) else: self.event('ssl_invalid_cert', @@ -559,8 +579,7 @@ class XMLStream(object): self.set_socket(self.socket, ignore=True) #this event is where you should set your application state - self.event("connected", direct=True) - self.reconnect_delay = 1.0 + self.event('connected', direct=True) return True except (Socket.error, ssl.SSLError) as serr: error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" @@ -600,7 +619,7 @@ class XMLStream(object): headers = '\r\n'.join(headers) + '\r\n\r\n' try: - log.debug("Connecting to proxy: %s:%s", address) + log.debug("Connecting to proxy: %s:%s", *address) self.socket.connect(address) self.send_raw(headers, now=True) resp = '' @@ -611,6 +630,7 @@ class XMLStream(object): lines = resp.split('\r\n') if '200' not in lines[0]: self.event('proxy_error', resp) + self.event('connection_failed', direct=True) log.error('Proxy Error: %s', lines[0]) return False @@ -706,7 +726,7 @@ class XMLStream(object): self.stream_end_event.set() if not self.auto_reconnect: - self.stop.set() + self.set_stop() if self._disconnect_wait_for_threads: self._wait_for_threads() @@ -718,12 +738,12 @@ class XMLStream(object): self.event('socket_error', serr, direct=True) finally: #clear your application state - self.event("disconnected", direct=True) + self.event('disconnected', direct=True) return True def abort(self): self.session_started_event.clear() - self.stop.set() + self.set_stop() if self._disconnect_wait_for_threads: self._wait_for_threads() try: @@ -818,19 +838,26 @@ class XMLStream(object): to be restarted. """ log.info("Negotiating TLS") - log.info("Using SSL version: %s", str(self.ssl_version)) + ssl_versions = {3: 'TLS 1.0', 1: 'SSL 3', 2: 'SSL 2/3'} + log.info("Using SSL version: %s", ssl_versions[self.ssl_version]) if self.ca_certs is None: cert_policy = ssl.CERT_NONE else: cert_policy = ssl.CERT_REQUIRED - ssl_socket = ssl.wrap_socket(self.socket, - certfile=self.certfile, - keyfile=self.keyfile, - ssl_version=self.ssl_version, - do_handshake_on_connect=False, - ca_certs=self.ca_certs, - cert_reqs=cert_policy) + ssl_args = safedict({ + 'certfile': self.certfile, + 'keyfile': self.keyfile, + 'ca_certs': self.ca_certs, + 'cert_reqs': cert_policy, + 'do_handshake_on_connect': False, + "ssl_version": self.ssl_version + }) + + if sys.version_info >= (2, 7): + ssl_args['ciphers'] = self.ciphers + + ssl_socket = ssl.wrap_socket(self.socket, **ssl_args) if hasattr(self.socket, 'socket'): # We are using a testing socket, so preserve the top @@ -859,7 +886,7 @@ class XMLStream(object): cert.verify(self._expected_server_name, self._der_cert) except cert.CertificateError as err: if not self.event_handled('ssl_invalid_cert'): - log.error(err.message) + log.error(err) self.disconnect(self.auto_reconnect, send_close=False) else: self.event('ssl_invalid_cert', pem_cert, direct=True) @@ -915,12 +942,13 @@ class XMLStream(object): self.whitespace_keepalive_interval = 300 """ - self.schedule('Whitespace Keepalive', - self.whitespace_keepalive_interval, - self.send_raw, - args=(' ',), - kwargs={'now': True}, - repeat=True) + if self.whitespace_keepalive: + self.schedule('Whitespace Keepalive', + self.whitespace_keepalive_interval, + self.send_raw, + args=(' ',), + kwargs={'now': True}, + repeat=True) def _remove_schedules(self, event): """Remove whitespace keepalive and certificate expiration schedules.""" @@ -1016,9 +1044,13 @@ class XMLStream(object): # and handler classes here. if name is None: - name = 'add_handler_%s' % self.getNewId() - self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, - once=disposable, instream=instream)) + name = 'add_handler_%s' % self.new_id() + self.register_handler( + XMLCallback(name, + MatchXMLMask(mask, self.default_ns), + pointer, + once=disposable, + instream=instream)) def register_handler(self, handler, before=None, after=None): """Add a stream event handler that will be executed when a matching @@ -1059,7 +1091,8 @@ class XMLStream(object): return resolve(domain, port, service=self.dns_service, resolver=resolver, - use_ipv6=self.use_ipv6) + use_ipv6=self.use_ipv6, + use_dnspython=self.use_dnspython) def pick_dns_answer(self, domain, port=None): """Pick a server and port from DNS answers. @@ -1120,7 +1153,7 @@ class XMLStream(object): """ return len(self.__event_handlers.get(name, [])) - def event(self, name, data={}, direct=False): + def event(self, name, data=None, direct=False): """Manually trigger a custom event. :param name: The name of the event to trigger. @@ -1131,6 +1164,11 @@ class XMLStream(object): event queue. All event handlers will run in the same thread. """ + if not data: + data = {} + + log.debug("Event triggered: " + name) + handlers = self.__event_handlers.get(name, []) for handler in handlers: #TODO: Data should not be copied, but should be read only, @@ -1302,6 +1340,9 @@ class XMLStream(object): if not self.stop.is_set(): time.sleep(self.ssl_retry_delay) tries += 1 + except Socket.error as serr: + if serr.errno != errno.EINTR: + raise if count > 1: log.debug('SENT: %d chunks', count) except (Socket.error, ssl.SSLError) as serr: @@ -1316,12 +1357,12 @@ class XMLStream(object): return True def _start_thread(self, name, target, track=True): - self.__active_threads.add(name) self.__thread[name] = threading.Thread(name=name, target=target) self.__thread[name].daemon = self._use_daemons self.__thread[name].start() if track: + self.__active_threads.add(name) with self.__thread_cond: self.__thread_count += 1 @@ -1350,6 +1391,13 @@ class XMLStream(object): if self.__thread_count == 0: self.__thread_cond.notify() + def set_stop(self): + self.stop.set() + + # Unlock queues + self.event_queue.put(None) + self.send_queue.put(None) + def _wait_for_threads(self): with self.__thread_cond: if self.__thread_count != 0: @@ -1493,6 +1541,10 @@ class XMLStream(object): # as handshakes. self.stream_end_event.clear() self.start_stream_handler(root) + + # We have a successful stream connection, so reset + # exponential backoff for new reconnect attempts. + self.reconnect_delay = 1.0 depth += 1 if event == b'end': depth -= 1 @@ -1618,11 +1670,7 @@ class XMLStream(object): log.debug("Loading event runner") try: while not self.stop.is_set(): - try: - wait = self.wait_timeout - event = self.event_queue.get(True, timeout=wait) - except QueueEmpty: - event = None + event = self.event_queue.get() if event is None: continue @@ -1638,10 +1686,10 @@ class XMLStream(object): log.exception(error_msg, handler.name) orig.exception(e) elif etype == 'schedule': - name = args[1] + name = args[2] try: log.debug('Scheduled event: %s: %s', name, args[0]) - handler(*args[0]) + handler(*args[0], **args[1]) except Exception as e: log.exception('Error processing scheduled task') self.exception(e) @@ -1683,14 +1731,13 @@ class XMLStream(object): while not self.stop.is_set(): while not self.stop.is_set() and \ not self.session_started_event.is_set(): - self.session_started_event.wait(timeout=0.1) + self.session_started_event.wait(timeout=0.1) # Wait for session start if self.__failed_send_stanza is not None: data = self.__failed_send_stanza self.__failed_send_stanza = None else: - try: - data = self.send_queue.get(True, 1) - except QueueEmpty: + data = self.send_queue.get() # Wait for data to send + if data is None: continue log.debug("SEND: %s", data) enc_data = data.encode('utf-8') @@ -1717,6 +1764,9 @@ class XMLStream(object): if not self.stop.is_set(): time.sleep(self.ssl_retry_delay) tries += 1 + except Socket.error as serr: + if serr.errno != errno.EINTR: + raise if count > 1: log.debug('SENT: %d chunks', count) self.send_queue.task_done() |