diff options
Diffstat (limited to 'slixmpp')
42 files changed, 1179 insertions, 81 deletions
diff --git a/slixmpp/basexmpp.py b/slixmpp/basexmpp.py index 80699319..83741bd7 100644 --- a/slixmpp/basexmpp.py +++ b/slixmpp/basexmpp.py @@ -22,7 +22,6 @@ from slixmpp.exceptions import IqError, IqTimeout from slixmpp.stanza import Message, Presence, Iq, StreamError from slixmpp.stanza.roster import Roster from slixmpp.stanza.nick import Nick -from slixmpp.stanza.htmlim import HTMLIM from slixmpp.xmlstream import XMLStream, JID from slixmpp.xmlstream import ET, register_stanza_plugin @@ -46,8 +45,8 @@ class BaseXMPP(XMLStream): is used during initialization. """ - def __init__(self, jid='', default_ns='jabber:client'): - XMLStream.__init__(self) + def __init__(self, jid='', default_ns='jabber:client', **kwargs): + XMLStream.__init__(self, **kwargs) self.default_ns = default_ns self.stream_ns = 'http://etherx.jabber.org/streams' @@ -221,7 +220,7 @@ class BaseXMPP(XMLStream): self.plugin[name].post_init() self.plugin[name].post_inited = True - 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/slixmpp/clientxmpp.py b/slixmpp/clientxmpp.py index d1fd65a9..40d20333 100644 --- a/slixmpp/clientxmpp.py +++ b/slixmpp/clientxmpp.py @@ -50,7 +50,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 @@ -58,9 +57,15 @@ class ClientXMPP(BaseXMPP): :param escape_quotes: **Deprecated.** """ - def __init__(self, jid, password, plugin_config={}, plugin_whitelist=[], - escape_quotes=True, sasl_mech=None, lang='en'): - BaseXMPP.__init__(self, jid, 'jabber:client') + def __init__(self, jid, password, plugin_config=None, + plugin_whitelist=None, escape_quotes=True, sasl_mech=None, + lang='en', **kwargs): + if not plugin_whitelist: + plugin_whitelist = [] + if not plugin_config: + plugin_config = {} + + BaseXMPP.__init__(self, jid, 'jabber:client', **kwargs) self.escape_quotes = escape_quotes self.plugin_config = plugin_config diff --git a/slixmpp/componentxmpp.py b/slixmpp/componentxmpp.py index 68669c06..868798d1 100644 --- a/slixmpp/componentxmpp.py +++ b/slixmpp/componentxmpp.py @@ -46,8 +46,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: diff --git a/slixmpp/features/feature_mechanisms/mechanisms.py b/slixmpp/features/feature_mechanisms/mechanisms.py index 5f947cfa..8e507afc 100644 --- a/slixmpp/features/feature_mechanisms/mechanisms.py +++ b/slixmpp/features/feature_mechanisms/mechanisms.py @@ -190,14 +190,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 sasl.SASLFailed: + self.attempted_mechs.add(self.mech.name) + self._send_auth() else: resp.send() @@ -210,13 +210,13 @@ 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() diff --git a/slixmpp/plugins/__init__.py b/slixmpp/plugins/__init__.py index cf24caed..d28cf281 100644 --- a/slixmpp/plugins/__init__.py +++ b/slixmpp/plugins/__init__.py @@ -47,6 +47,7 @@ __all__ = [ 'xep_0108', # User Activity 'xep_0115', # Entity Capabilities 'xep_0118', # User Tune + 'xep_0122', # Data Forms Validation 'xep_0128', # Extended Service Discovery 'xep_0131', # Standard Headers and Internet Metadata 'xep_0133', # Service Administration @@ -83,4 +84,5 @@ __all__ = [ 'xep_0319', # Last User Interaction in Presence 'xep_0323', # IoT Systems Sensor Data 'xep_0325', # IoT Systems Control + 'xep_0332', # HTTP Over XMPP Transport ] diff --git a/slixmpp/plugins/google/auth/stanza.py b/slixmpp/plugins/google/auth/stanza.py new file mode 100644 index 00000000..c5c693ee --- /dev/null +++ b/slixmpp/plugins/google/auth/stanza.py @@ -0,0 +1,47 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.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('') + + 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): + 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/slixmpp/plugins/google/gmail/notifications.py b/slixmpp/plugins/google/gmail/notifications.py new file mode 100644 index 00000000..e6785ccb --- /dev/null +++ b/slixmpp/plugins/google/gmail/notifications.py @@ -0,0 +1,90 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +import logging + +from slixmpp.stanza import Iq +from slixmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.matcher import MatchXPath +from slixmpp.xmlstream import register_stanza_plugin +from slixmpp.plugins import BasePlugin +from slixmpp.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, timeout=None, callback=None): + last_time = self._last_result_time + last_tid = self._last_result_tid + + callback = lambda iq: self._update_last_results(iq, callback) + + return self.search(newer_time=last_time, + newer_tid=last_tid, + timeout=timeout, + callback=callback) + + 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, + 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(timeout=timeout, callback=callback) diff --git a/slixmpp/plugins/google/nosave/stanza.py b/slixmpp/plugins/google/nosave/stanza.py new file mode 100644 index 00000000..b060a486 --- /dev/null +++ b/slixmpp/plugins/google/nosave/stanza.py @@ -0,0 +1,59 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.jid import JID +from slixmpp.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/slixmpp/plugins/google/settings/settings.py b/slixmpp/plugins/google/settings/settings.py new file mode 100644 index 00000000..84a8dfa9 --- /dev/null +++ b/slixmpp/plugins/google/settings/settings.py @@ -0,0 +1,63 @@ +""" + Slixmpp: The Slick XMPP Library + Copyright (C) 2013 Nathanael C. Fritz, Lance J.T. Stout + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.stanza import Iq +from slixmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.matcher import StanzaPath +from slixmpp.xmlstream import register_stanza_plugin +from slixmpp.plugins import BasePlugin +from slixmpp.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, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq.enable('google_settings') + return iq.send(timeout=timeout, callback=callback) + + def update(self, settings, 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(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/slixmpp/plugins/xep_0004/stanza/field.py b/slixmpp/plugins/xep_0004/stanza/field.py index ceddcd0e..42f1210b 100644 --- a/slixmpp/plugins/xep_0004/stanza/field.py +++ b/slixmpp/plugins/xep_0004/stanza/field.py @@ -13,8 +13,9 @@ class FormField(ElementBase): namespace = 'jabber:x:data' name = 'field' plugin_attrib = 'field' + plugin_multi_attrib = 'fields' interfaces = set(('answer', 'desc', 'required', 'value', - 'options', 'label', 'type', 'var')) + 'label', 'type', 'var')) sub_interfaces = set(('desc',)) plugin_tag_map = {} plugin_attrib_map = {} @@ -165,6 +166,7 @@ class FieldOption(ElementBase): plugin_attrib = 'option' interfaces = set(('label', 'value')) sub_interfaces = set(('value',)) + plugin_multi_attrib = 'options' FormField.addOption = FormField.add_option diff --git a/slixmpp/plugins/xep_0004/stanza/form.py b/slixmpp/plugins/xep_0004/stanza/form.py index 2f617e39..151e2ef1 100644 --- a/slixmpp/plugins/xep_0004/stanza/form.py +++ b/slixmpp/plugins/xep_0004/stanza/form.py @@ -10,6 +10,7 @@ import copy import logging from collections import OrderedDict +from slixmpp.thirdparty import OrderedSet from slixmpp.xmlstream import ElementBase, ET from slixmpp.plugins.xep_0004.stanza import FormField @@ -22,8 +23,7 @@ class Form(ElementBase): namespace = 'jabber:x:data' name = 'x' plugin_attrib = 'form' - interfaces = set(('fields', 'instructions', 'items', - 'reported', 'title', 'type', 'values')) + interfaces = OrderedSet(('instructions', 'reported', 'title', 'type', 'items', )) sub_interfaces = set(('title',)) form_types = set(('cancel', 'form', 'result', 'submit')) @@ -43,12 +43,12 @@ class Form(ElementBase): @property def field(self): - return self['fields'] + return self.get_fields() def set_type(self, ftype): self._set_attr('type', ftype) if ftype == 'submit': - fields = self['fields'] + fields = self.get_fields() for var in fields: field = fields[var] del field['type'] @@ -74,7 +74,8 @@ class Form(ElementBase): field['desc'] = desc field['required'] = required if options is not None: - field['options'] = options + for option in options: + field.add_option(**option) else: del field['type'] self.append(field) @@ -151,7 +152,6 @@ class Form(ElementBase): return fields def get_instructions(self): - instructions = '' instsXML = self.xml.findall('{%s}instructions' % self.namespace) return "\n".join([instXML.text for instXML in instsXML]) @@ -170,7 +170,7 @@ class Form(ElementBase): def get_reported(self): fields = OrderedDict() xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, - FormField.namespace)) + FormField.namespace)) for field in xml: field = FormField(xml=field) fields[field['var']] = field @@ -178,7 +178,7 @@ class Form(ElementBase): def get_values(self): values = OrderedDict() - fields = self['fields'] + fields = self.get_fields() for var in fields: values[var] = fields[var]['value'] return values @@ -195,7 +195,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'] @@ -213,17 +220,33 @@ class Form(ElementBase): self.add_item(item) def set_reported(self, reported): + """ + This either needs a dictionary of dictionaries or a dictionary of form fields. + :param reported: + :return: + """ for var in reported: field = reported[var] - field['var'] = var - self.add_reported(var, **field) + + if isinstance(field, dict): + self.add_reported(**field) + else: + reported = self.xml.find('{%s}reported' % self.namespace) + if reported is None: + reported = ET.Element('{%s}reported' % self.namespace) + self.xml.append(reported) + + fieldXML = ET.Element('{%s}field' % FormField.namespace) + reported.append(fieldXML) + new_field = FormField(xml=fieldXML) + new_field.values = field.values def set_values(self, values): - fields = self['fields'] + fields = self.get_fields() for field in values: - if field not in fields: + if field not in self.get_fields(): fields[field] = self.add_field(var=field) - fields[field]['value'] = values[field] + self.get_fields()[field]['value'] = values[field] def merge(self, other): new = copy.copy(self) diff --git a/slixmpp/plugins/xep_0009/remote.py b/slixmpp/plugins/xep_0009/remote.py index b7612c03..9675c88d 100644 --- a/slixmpp/plugins/xep_0009/remote.py +++ b/slixmpp/plugins/xep_0009/remote.py @@ -6,7 +6,7 @@ See the file LICENSE for copying permission. """ -from binding import py2xml, xml2py, xml2fault, fault2xml +from slixmpp.plugins.xep_0009.binding import py2xml, xml2py, xml2fault, fault2xml from threading import RLock import abc import inspect @@ -18,6 +18,38 @@ import traceback log = logging.getLogger(__name__) +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 +100,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 +254,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 +322,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 +334,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 +520,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 +576,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 +598,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 +726,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/slixmpp/plugins/xep_0009/rpc.py b/slixmpp/plugins/xep_0009/rpc.py index 786b1d2f..3ce156cf 100644 --- a/slixmpp/plugins/xep_0009/rpc.py +++ b/slixmpp/plugins/xep_0009/rpc.py @@ -220,3 +220,4 @@ class XEP_0009(BasePlugin): def _extract_method(self, stanza): xml = ET.fromstring("%s" % stanza) return xml.find("./methodCall/methodName").text + diff --git a/slixmpp/plugins/xep_0030/disco.py b/slixmpp/plugins/xep_0030/disco.py index f368bc12..e6286b92 100644 --- a/slixmpp/plugins/xep_0030/disco.py +++ b/slixmpp/plugins/xep_0030/disco.py @@ -609,7 +609,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. @@ -620,6 +620,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/slixmpp/plugins/xep_0045.py b/slixmpp/plugins/xep_0045.py index 66bd863c..f6f48891 100644 --- a/slixmpp/plugins/xep_0045.py +++ b/slixmpp/plugins/xep_0045.py @@ -403,6 +403,16 @@ class XEP_0045(BasePlugin): return None return self.rooms[room].keys() + def getUsersByAffiliation(cls, room, affiliation='member', ifrom=None): + if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): + raise TypeError + query = ET.Element('{http://jabber.org/protocol/muc#admin}query') + item = ET.Element('{http://jabber.org/protocol/muc#admin}item', {'affiliation': affiliation}) + query.append(item) + iq = cls.xmpp.Iq(sto=room, sfrom=ifrom, stype='get') + iq.append(query) + return iq.send() + xep_0045 = XEP_0045 register_plugin(XEP_0045) diff --git a/slixmpp/plugins/xep_0050/adhoc.py b/slixmpp/plugins/xep_0050/adhoc.py index 2ecbfea6..fa6017d5 100644 --- a/slixmpp/plugins/xep_0050/adhoc.py +++ b/slixmpp/plugins/xep_0050/adhoc.py @@ -94,7 +94,7 @@ class XEP_0050(BasePlugin): self._handle_command)) register_stanza_plugin(Iq, Command) - register_stanza_plugin(Command, Form) + register_stanza_plugin(Command, Form, iterable=True) self.xmpp.add_event_handler('command_execute', self._handle_command_start) @@ -415,12 +415,26 @@ class XEP_0050(BasePlugin): del self.sessions[sessionid] + payload = session['payload'] + if payload is None: + payload = [] + if not isinstance(payload, list): + payload = [payload] + + for item in payload: + register_stanza_plugin(Command, item.__class__, iterable=True) + iq = iq.reply() + iq['command']['node'] = node iq['command']['sessionid'] = sessionid iq['command']['actions'] = [] iq['command']['status'] = 'completed' iq['command']['notes'] = session['notes'] + + for item in payload: + iq['command'].append(item) + iq.send() else: raise XMPPError('item-not-found') diff --git a/slixmpp/plugins/xep_0054/stanza.py b/slixmpp/plugins/xep_0054/stanza.py index 13b36320..48a41432 100644 --- a/slixmpp/plugins/xep_0054/stanza.py +++ b/slixmpp/plugins/xep_0054/stanza.py @@ -128,7 +128,8 @@ class Telephone(ElementBase): def setup(self, xml=None): super(Telephone, self).setup(xml=xml) - self._set_sub_text('NUMBER', '', keep=True) + ## this blanks out numbers received from server + ##self._set_sub_text('NUMBER', '', keep=True) def set_number(self, value): self._set_sub_text('NUMBER', value, keep=True) diff --git a/slixmpp/plugins/xep_0065/proxy.py b/slixmpp/plugins/xep_0065/proxy.py index 3e75b710..c5d358dd 100644 --- a/slixmpp/plugins/xep_0065/proxy.py +++ b/slixmpp/plugins/xep_0065/proxy.py @@ -251,7 +251,6 @@ class XEP_0065(BasePlugin): host : The hostname or the IP of the proxy. <str> port : The port of the proxy. <str> or <int> """ - factory = lambda: Socks5Protocol(dest, 0, self.xmpp.event) return self.xmpp.loop.create_connection(factory, proxy, proxy_port) diff --git a/slixmpp/plugins/xep_0096/file_transfer.py b/slixmpp/plugins/xep_0096/file_transfer.py index 462e7c19..3c09a5b5 100644 --- a/slixmpp/plugins/xep_0096/file_transfer.py +++ b/slixmpp/plugins/xep_0096/file_transfer.py @@ -47,6 +47,7 @@ class XEP_0096(BasePlugin): data['size'] = size data['date'] = date data['desc'] = desc + data['hash'] = hash if allow_ranged: data.enable('range') diff --git a/slixmpp/plugins/xep_0122/__init__.py b/slixmpp/plugins/xep_0122/__init__.py new file mode 100644 index 00000000..76ca80b2 --- /dev/null +++ b/slixmpp/plugins/xep_0122/__init__.py @@ -0,0 +1,11 @@ + +from slixmpp.plugins.base import register_plugin +from slixmpp.plugins.xep_0122.stanza import FormValidation +from slixmpp.plugins.xep_0122.data_validation import XEP_0122 + + +register_plugin(XEP_0122) + + +# Retain some backwards compatibility +xep_0122 = XEP_0122 diff --git a/slixmpp/plugins/xep_0122/data_validation.py b/slixmpp/plugins/xep_0122/data_validation.py new file mode 100644 index 00000000..6129db51 --- /dev/null +++ b/slixmpp/plugins/xep_0122/data_validation.py @@ -0,0 +1,19 @@ +from slixmpp.xmlstream import register_stanza_plugin +from slixmpp.plugins import BasePlugin +from slixmpp.plugins.xep_0004 import stanza +from slixmpp.plugins.xep_0004.stanza import FormField +from slixmpp.plugins.xep_0122.stanza import FormValidation + + +class XEP_0122(BasePlugin): + """ + XEP-0122: Data Forms + """ + + name = 'xep_0122' + description = 'XEP-0122: Data Forms Validation' + dependencies = set(['xep_0004']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(FormField, FormValidation) diff --git a/slixmpp/plugins/xep_0122/stanza.py b/slixmpp/plugins/xep_0122/stanza.py new file mode 100644 index 00000000..9f1c423d --- /dev/null +++ b/slixmpp/plugins/xep_0122/stanza.py @@ -0,0 +1,93 @@ +from slixmpp.xmlstream import ElementBase, ET + + +class FormValidation(ElementBase): + """ + Validation values for form fields. + + Example: + + <field var='evt.date' type='text-single' label='Event Date/Time'> + <validate xmlns='http://jabber.org/protocol/xdata-validate' + datatype='xs:dateTime'/> + <value>2003-10-06T11:22:00-07:00</value> + </field> + + Questions: + Should this look at the datatype value and convert the range values as appropriate? + Should this stanza provide a pass/fail for a value from the field, or convert field value to datatype? + """ + + namespace = 'http://jabber.org/protocol/xdata-validate' + name = 'validate' + plugin_attrib = 'validate' + interfaces = {'datatype', 'basic', 'open', 'range', 'regex', } + sub_interfaces = {'basic', 'open', 'range', 'regex', } + plugin_attrib_map = {} + plugin_tag_map = {} + + def _add_field(self, name): + self.remove_all() + item_xml = ET.Element('{%s}%s' % (self.namespace, name)) + self.xml.append(item_xml) + return item_xml + + def set_basic(self, value): + if value: + self._add_field('basic') + else: + del self['basic'] + + def set_open(self, value): + if value: + self._add_field('open') + else: + del self['open'] + + def set_regex(self, regex): + if regex: + _regex = self._add_field('regex') + _regex.text = regex + else: + del self['regex'] + + def set_range(self, value, minimum=None, maximum=None): + if value: + _range = self._add_field('range') + _range.attrib['min'] = str(minimum) + _range.attrib['max'] = str(maximum) + else: + del self['range'] + + def remove_all(self, except_tag=None): + for a in self.sub_interfaces: + if a != except_tag: + del self[a] + + def get_basic(self): + present = self.xml.find('{%s}basic' % self.namespace) + return present is not None + + def get_open(self): + present = self.xml.find('{%s}open' % self.namespace) + return present is not None + + def get_regex(self): + present = self.xml.find('{%s}regex' % self.namespace) + if present is not None: + return present.text + + return False + + def get_range(self): + present = self.xml.find('{%s}range' % self.namespace) + if present is not None: + attributes = present.attrib + return_value = dict() + if 'min' in attributes: + return_value['minimum'] = attributes['min'] + if 'max' in attributes: + return_value['maximum'] = attributes['max'] + return return_value + + return False diff --git a/slixmpp/plugins/xep_0138.py b/slixmpp/plugins/xep_0138.py new file mode 100644 index 00000000..049060cf --- /dev/null +++ b/slixmpp/plugins/xep_0138.py @@ -0,0 +1,145 @@ +""" + slixmpp: The Slick XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +import logging +import zlib + + +from slixmpp.stanza import StreamFeatures +from slixmpp.xmlstream import RestartStream, register_stanza_plugin, ElementBase, StanzaBase +from slixmpp.xmlstream.matcher import * +from slixmpp.xmlstream.handler import * +from slixmpp.plugins import BasePlugin, register_plugin + +log = logging.getLogger(__name__) + + +class Compression(ElementBase): + name = 'compression' + namespace = 'http://jabber.org/features/compress' + interfaces = set(('methods',)) + plugin_attrib = 'compression' + plugin_tag_map = {} + plugin_attrib_map = {} + + def get_methods(self): + methods = [] + for method in self.xml.findall('{%s}method' % self.namespace): + methods.append(method.text) + return methods + + +class Compress(StanzaBase): + name = 'compress' + namespace = 'http://jabber.org/protocol/compress' + interfaces = set(('method',)) + sub_interfaces = interfaces + plugin_attrib = 'compress' + plugin_tag_map = {} + plugin_attrib_map = {} + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + +class Compressed(StanzaBase): + name = 'compressed' + namespace = 'http://jabber.org/protocol/compress' + interfaces = set() + plugin_tag_map = {} + plugin_attrib_map = {} + + def setup(self, xml): + StanzaBase.setup(self, xml) + self.xml.tag = self.tag_name() + + + + +class ZlibSocket(object): + + def __init__(self, socketobj): + self.__socket = socketobj + self.compressor = zlib.compressobj() + self.decompressor = zlib.decompressobj(zlib.MAX_WBITS) + + def __getattr__(self, name): + return getattr(self.__socket, name) + + def send(self, data): + sentlen = len(data) + data = self.compressor.compress(data) + data += self.compressor.flush(zlib.Z_SYNC_FLUSH) + log.debug(b'>>> (compressed)' + (data.encode("hex"))) + #return self.__socket.send(data) + sentactuallen = self.__socket.send(data) + assert(sentactuallen == len(data)) + + return sentlen + + def recv(self, *args, **kwargs): + data = self.__socket.recv(*args, **kwargs) + log.debug(b'<<< (compressed)' + data.encode("hex")) + return self.decompressor.decompress(self.decompressor.unconsumed_tail + data) + + +class XEP_0138(BasePlugin): + """ + XEP-0138: Compression + """ + name = "xep_0138" + description = "XEP-0138: Compression" + dependencies = set(["xep_0030"]) + + def plugin_init(self): + self.xep = '0138' + self.description = 'Stream Compression (Generic)' + + self.compression_methods = {'zlib': True} + + register_stanza_plugin(StreamFeatures, Compression) + self.xmpp.register_stanza(Compress) + self.xmpp.register_stanza(Compressed) + + self.xmpp.register_handler( + Callback('Compressed', + StanzaPath('compressed'), + self._handle_compressed, + instream=True)) + + self.xmpp.register_feature('compression', + self._handle_compression, + restart=True, + order=self.config.get('order', 5)) + + def register_compression_method(self, name, handler): + self.compression_methods[name] = handler + + def _handle_compression(self, features): + for method in features['compression']['methods']: + if method in self.compression_methods: + log.info('Attempting to use %s compression' % method) + c = Compress(self.xmpp) + c['method'] = method + c.send(now=True) + return True + return False + + def _handle_compressed(self, stanza): + self.xmpp.features.add('compression') + log.debug('Stream Compressed!') + compressed_socket = ZlibSocket(self.xmpp.socket) + self.xmpp.set_socket(compressed_socket) + raise RestartStream() + + def _handle_failure(self, stanza): + pass + +xep_0138 = XEP_0138 +register_plugin(XEP_0138) diff --git a/slixmpp/plugins/xep_0202/time.py b/slixmpp/plugins/xep_0202/time.py index fbf6b4f0..185200fc 100644 --- a/slixmpp/plugins/xep_0202/time.py +++ b/slixmpp/plugins/xep_0202/time.py @@ -96,3 +96,4 @@ class XEP_0202(BasePlugin): iq['from'] = ifrom iq.enable('entity_time') return iq.send(**iqargs) + diff --git a/slixmpp/plugins/xep_0323/device.py b/slixmpp/plugins/xep_0323/device.py index b4142003..994fc5ce 100644 --- a/slixmpp/plugins/xep_0323/device.py +++ b/slixmpp/plugins/xep_0323/device.py @@ -21,7 +21,10 @@ class Device(object): request_fields """ - def __init__(self, nodeId, fields={}): + def __init__(self, nodeId, fields=None): + if not fields: + fields = {} + self.nodeId = nodeId self.fields = fields # see fields described below # {'type':'numeric', diff --git a/slixmpp/plugins/xep_0323/sensordata.py b/slixmpp/plugins/xep_0323/sensordata.py index 21afb55a..c88deee9 100644 --- a/slixmpp/plugins/xep_0323/sensordata.py +++ b/slixmpp/plugins/xep_0323/sensordata.py @@ -22,7 +22,6 @@ from slixmpp.plugins.base import BasePlugin from slixmpp.plugins.xep_0323 import stanza from slixmpp.plugins.xep_0323.stanza import Sensordata - log = logging.getLogger(__name__) @@ -108,7 +107,6 @@ class XEP_0323(BasePlugin): default_config = { 'threaded': True -# 'session_db': None } def plugin_init(self): @@ -161,11 +159,11 @@ class XEP_0323(BasePlugin): self.last_seqnr = 0 self.seqnr_lock = Lock() - ## For testning only + ## For testing only self.test_authenticated_from = "" def post_init(self): - """ Init complete. Register our features in Serivce discovery. """ + """ Init complete. Register our features in Service discovery. """ BasePlugin.post_init(self) self.xmpp['xep_0030'].add_feature(Sensordata.namespace) self.xmpp['xep_0030'].set_items(node=Sensordata.namespace, items=tuple()) @@ -301,8 +299,6 @@ class XEP_0323(BasePlugin): self.sessions[session]["commTimers"] = {} self.sessions[session]["nodeDone"] = {} - #print("added session: " + str(self.sessions)) - iq = iq.reply() iq['accepted']['seqnr'] = seqnr if not request_delay_sec is None: @@ -319,10 +315,8 @@ class XEP_0323(BasePlugin): 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) @@ -349,7 +343,6 @@ class XEP_0323(BasePlugin): 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) @@ -377,7 +370,6 @@ class XEP_0323(BasePlugin): 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): @@ -404,7 +396,7 @@ class XEP_0323(BasePlugin): def _all_nodes_done(self, session): """ - Checks wheter all devices are done replying to the readout. + Checks whether all devices are done replying to the readout. Arguments: session -- The request session id @@ -448,7 +440,7 @@ class XEP_0323(BasePlugin): 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. + # This can happen if a session was deleted, like in a cancellation. Just drop the data. return if result == "error": @@ -467,7 +459,6 @@ class XEP_0323(BasePlugin): 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: @@ -491,11 +482,10 @@ class XEP_0323(BasePlugin): 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] + msg['fields']['done'] = 'true' else: # Restart comm timer self.sessions[session]["commTimers"][nodeId].reset() @@ -531,19 +521,19 @@ class XEP_0323(BasePlugin): iq['rejected']['error'] = "Cancel request received, no matching request is active." iq.send() - # ================================================================= + # ================================================================= # 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. + Called on the client side to initiate 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. + callback -- The callback function to call when data is available. The callback function must support the following arguments: @@ -636,7 +626,7 @@ class XEP_0323(BasePlugin): def _get_new_seqnr(self): """ Returns a unique sequence number (unique across threads) """ self.seqnr_lock.acquire() - self.last_seqnr = self.last_seqnr + 1 + self.last_seqnr += 1 self.seqnr_lock.release() return str(self.last_seqnr) @@ -664,7 +654,6 @@ class XEP_0323(BasePlugin): 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") @@ -673,7 +662,7 @@ class XEP_0323(BasePlugin): def _handle_event_fields(self, msg): """ - Received Msg with fields - this is a data reponse to a request. + Received Msg with fields - this is a data response to a request. If this is the last data block, issue a "done" callback. """ seqnr = msg['fields']['seqnr'] diff --git a/slixmpp/plugins/xep_0323/timerreset.py b/slixmpp/plugins/xep_0323/timerreset.py index baa80d41..616380e7 100644 --- a/slixmpp/plugins/xep_0323/timerreset.py +++ b/slixmpp/plugins/xep_0323/timerreset.py @@ -23,7 +23,12 @@ class _TimerReset(Thread): t.cancel() # stop the timer's action if it's still waiting """ - def __init__(self, interval, function, args=[], kwargs={}): + 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 diff --git a/slixmpp/plugins/xep_0325/control.py b/slixmpp/plugins/xep_0325/control.py index 0c6837f6..9a493b02 100644 --- a/slixmpp/plugins/xep_0325/control.py +++ b/slixmpp/plugins/xep_0325/control.py @@ -223,7 +223,6 @@ class XEP_0325(BasePlugin): error_msg = "Access denied" # Nodes - process_nodes = [] if len(iq['set']['nodes']) > 0: for n in iq['set']['nodes']: if not n['nodeId'] in self.nodes: @@ -286,7 +285,6 @@ class XEP_0325(BasePlugin): req_ok = True # Nodes - process_nodes = [] if len(msg['set']['nodes']) > 0: for n in msg['set']['nodes']: if not n['nodeId'] in self.nodes: @@ -548,4 +546,3 @@ class XEP_0325(BasePlugin): callback = self.sessions[seqnr]["callback"] callback(from_jid=from_jid, result=result, nodeIds=nodeIds, fields=fields, error_msg=error_msg) - diff --git a/slixmpp/plugins/xep_0332/__init__.py b/slixmpp/plugins/xep_0332/__init__.py new file mode 100644 index 00000000..8bf6b369 --- /dev/null +++ b/slixmpp/plugins/xep_0332/__init__.py @@ -0,0 +1,17 @@ +""" + Slixmpp: The Slick XMPP Library + Implementation of HTTP over XMPP transport + http://xmpp.org/extensions/xep-0332.html + Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.plugins.base import register_plugin + +from slixmpp.plugins.xep_0332 import stanza +from slixmpp.plugins.xep_0332.http import XEP_0332 + + +register_plugin(XEP_0332) diff --git a/slixmpp/plugins/xep_0332/http.py b/slixmpp/plugins/xep_0332/http.py new file mode 100644 index 00000000..7ad14dc8 --- /dev/null +++ b/slixmpp/plugins/xep_0332/http.py @@ -0,0 +1,159 @@ +""" + Slixmpp: The Slick XMPP Library + Implementation of HTTP over XMPP transport + http://xmpp.org/extensions/xep-0332.html + Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +import logging + +from slixmpp import Iq + +from slixmpp.xmlstream import register_stanza_plugin +from slixmpp.xmlstream.handler import Callback +from slixmpp.xmlstream.matcher import StanzaPath + +from slixmpp.plugins.base import BasePlugin +from slixmpp.plugins.xep_0332.stanza import ( + HTTPRequest, HTTPResponse, HTTPData +) +from slixmpp.plugins.xep_0131.stanza import Headers + + +log = logging.getLogger(__name__) + + +class XEP_0332(BasePlugin): + """ + XEP-0332: HTTP over XMPP transport + """ + + name = 'xep_0332' + description = 'XEP-0332: HTTP over XMPP transport' + + #: xep_0047 not included. + #: xep_0001, 0137 and 0166 are missing + dependencies = set(['xep_0030', 'xep_0131']) + + #: TODO: Do we really need to mention the supported_headers?! + default_config = { + 'supported_headers': set([ + 'Content-Length', 'Transfer-Encoding', 'DateTime', + 'Accept-Charset', 'Location', 'Content-ID', 'Description', + 'Content-Language', 'Content-Transfer-Encoding', 'Timestamp', + 'Expires', 'User-Agent', 'Host', 'Proxy-Authorization', 'Date', + 'WWW-Authenticate', 'Accept-Encoding', 'Server', 'Error-Info', + 'Identifier', 'Content-Location', 'Content-Encoding', 'Distribute', + 'Accept', 'Proxy-Authenticate', 'ETag', 'Expect', 'Content-Type' + ]) + } + + def plugin_init(self): + self.xmpp.register_handler( + Callback( + 'HTTP Request', + StanzaPath('iq/http-req'), + self._handle_request + ) + ) + self.xmpp.register_handler( + Callback( + 'HTTP Response', + StanzaPath('iq/http-resp'), + self._handle_response + ) + ) + register_stanza_plugin(Iq, HTTPRequest, iterable=True) + register_stanza_plugin(Iq, HTTPResponse, iterable=True) + register_stanza_plugin(HTTPRequest, Headers, iterable=True) + register_stanza_plugin(HTTPRequest, HTTPData, iterable=True) + register_stanza_plugin(HTTPResponse, Headers, iterable=True) + register_stanza_plugin(HTTPResponse, HTTPData, iterable=True) + # TODO: Should we register any api's here? self.api.register() + + def plugin_end(self): + self.xmpp.remove_handler('HTTP Request') + self.xmpp.remove_handler('HTTP Response') + self.xmpp['xep_0030'].del_feature('urn:xmpp:http') + for header in self.supported_headers: + self.xmpp['xep_0030'].del_feature( + feature='%s#%s' % (Headers.namespace, header) + ) + + def session_bind(self, jid): + self.xmpp['xep_0030'].add_feature('urn:xmpp:http') + for header in self.supported_headers: + self.xmpp['xep_0030'].add_feature( + '%s#%s' % (Headers.namespace, header) + ) + # TODO: Do we need to add the supported headers to xep_0131? + # self.xmpp['xep_0131'].supported_headers.add(header) + + def _handle_request(self, iq): + self.xmpp.event('http_request', iq) + + def _handle_response(self, iq): + self.xmpp.event('http_response', iq) + + def send_request(self, to=None, method=None, resource=None, headers=None, + data=None, **kwargs): + iq = self.xmpp.Iq() + iq['from'] = self.xmpp.boundjid + iq['to'] = to + iq['type'] = 'set' + iq['http-req']['headers'] = headers + iq['http-req']['method'] = method + iq['http-req']['resource'] = resource + iq['http-req']['version'] = '1.1' # TODO: set this implicitly + if 'id' in kwargs: + iq['id'] = kwargs["id"] + if data is not None: + iq['http-req']['data'] = data + return iq.send( + timeout=kwargs.get('timeout', None), + block=kwargs.get('block', True), + callback=kwargs.get('callback', None), + timeout_callback=kwargs.get('timeout_callback', None) + ) + + def send_response(self, to=None, code=None, message=None, headers=None, + data=None, **kwargs): + iq = self.xmpp.Iq() + iq['from'] = self.xmpp.boundjid + iq['to'] = to + iq['type'] = 'result' + iq['http-resp']['headers'] = headers + iq['http-resp']['code'] = code + iq['http-resp']['message'] = message + iq['http-resp']['version'] = '1.1' # TODO: set this implicitly + if 'id' in kwargs: + iq['id'] = kwargs["id"] + if data is not None: + iq['http-resp']['data'] = data + return iq.send( + timeout=kwargs.get('timeout', None), + block=kwargs.get('block', True), + callback=kwargs.get('callback', None), + timeout_callback=kwargs.get('timeout_callback', None) + ) + + def send_error(self, to=None, ecode='500', etype='wait', + econd='internal-server-error', **kwargs): + iq = self.xmpp.Iq() + iq['from'] = self.xmpp.boundjid + iq['to'] = to + iq['type'] = 'error' + iq['error']['code'] = ecode + iq['error']['type'] = etype + iq['error']['condition'] = econd + if 'id' in kwargs: + iq['id'] = kwargs["id"] + return iq.send( + timeout=kwargs.get('timeout', None), + block=kwargs.get('block', True), + callback=kwargs.get('callback', None), + timeout_callback=kwargs.get('timeout_callback', None) + ) diff --git a/slixmpp/plugins/xep_0332/stanza/__init__.py b/slixmpp/plugins/xep_0332/stanza/__init__.py new file mode 100644 index 00000000..f98375c6 --- /dev/null +++ b/slixmpp/plugins/xep_0332/stanza/__init__.py @@ -0,0 +1,13 @@ +""" + Slixmpp: The Slick XMPP Library + Implementation of HTTP over XMPP transport + http://xmpp.org/extensions/xep-0332.html + Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.plugins.xep_0332.stanza.request import HTTPRequest +from slixmpp.plugins.xep_0332.stanza.response import HTTPResponse +from slixmpp.plugins.xep_0332.stanza.data import HTTPData diff --git a/slixmpp/plugins/xep_0332/stanza/data.py b/slixmpp/plugins/xep_0332/stanza/data.py new file mode 100644 index 00000000..a19c94f5 --- /dev/null +++ b/slixmpp/plugins/xep_0332/stanza/data.py @@ -0,0 +1,30 @@ +""" + Slixmpp: The Slick XMPP Library + Implementation of HTTP over XMPP transport + http://xmpp.org/extensions/xep-0332.html + Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.xmlstream import ElementBase + + +class HTTPData(ElementBase): + """ + The data element. + """ + name = 'data' + namespace = 'urn:xmpp:http' + interfaces = set(['data']) + plugin_attrib = 'data' + is_extension = True + + def get_data(self, encoding='text'): + data = self._get_sub_text(encoding, None) + return str(data) if data is not None else data + + def set_data(self, data, encoding='text'): + self._set_sub_text(encoding, text=data) + diff --git a/slixmpp/plugins/xep_0332/stanza/request.py b/slixmpp/plugins/xep_0332/stanza/request.py new file mode 100644 index 00000000..e3e46361 --- /dev/null +++ b/slixmpp/plugins/xep_0332/stanza/request.py @@ -0,0 +1,71 @@ +""" + slixmpp: The Slick XMPP Library + Implementation of HTTP over XMPP transport + http://xmpp.org/extensions/xep-0332.html + Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.xmlstream import ElementBase + + +class HTTPRequest(ElementBase): + + """ + All HTTP communication is done using the `Request`/`Response` paradigm. + Each HTTP Request is made sending an `iq` stanza containing a `req` + element to the server. Each `iq` stanza sent is of type `set`. + + Examples: + <iq type='set' from='a@b.com/browser' to='x@y.com' id='1'> + <req xmlns='urn:xmpp:http' + method='GET' + resource='/api/users' + version='1.1'> + <headers xmlns='http://jabber.org/protocol/shim'> + <header name='Host'>b.com</header> + </headers> + </req> + </iq> + + <iq type='set' from='a@b.com/browser' to='x@y.com' id='2'> + <req xmlns='urn:xmpp:http' + method='PUT' + resource='/api/users' + version='1.1'> + <headers xmlns='http://jabber.org/protocol/shim'> + <header name='Host'>b.com</header> + <header name='Content-Type'>text/html</header> + <header name='Content-Length'>...</header> + </headers> + <data> + <text>...</text> + </data> + </req> + </iq> + """ + + name = 'request' + namespace = 'urn:xmpp:http' + interfaces = set(['method', 'resource', 'version']) + plugin_attrib = 'http-req' + + def get_method(self): + return self._get_attr('method', None) + + def set_method(self, method): + self._set_attr('method', method) + + def get_resource(self): + return self._get_attr('resource', None) + + def set_resource(self, resource): + self._set_attr('resource', resource) + + def get_version(self): + return self._get_attr('version', None) + + def set_version(self, version='1.1'): + self._set_attr('version', version) diff --git a/slixmpp/plugins/xep_0332/stanza/response.py b/slixmpp/plugins/xep_0332/stanza/response.py new file mode 100644 index 00000000..a0b8fe34 --- /dev/null +++ b/slixmpp/plugins/xep_0332/stanza/response.py @@ -0,0 +1,66 @@ +""" + Slixmpp: The Slick XMPP Library + Implementation of HTTP over XMPP transport + http://xmpp.org/extensions/xep-0332.html + Copyright (C) 2015 Riptide IO, sangeeth@riptideio.com + This file is part of slixmpp. + + See the file LICENSE for copying permission. +""" + +from slixmpp.xmlstream import ElementBase + + +class HTTPResponse(ElementBase): + + """ + When the HTTP Server responds, it does so by sending an `iq` stanza + response (type=`result`) back to the client containing the `resp` element. + Since response are asynchronous, and since multiple requests may be active + at the same time, responses may be returned in a different order than the + in which the original requests were made. + + Examples: + <iq type='result' + from='httpserver@clayster.com' + to='httpclient@clayster.com/browser' id='2'> + <resp xmlns='urn:xmpp:http' + version='1.1' + statusCode='200' + statusMessage='OK'> + <headers xmlns='http://jabber.org/protocol/shim'> + <header name='Date'>Fri, 03 May 2013 16:39:54GMT-4</header> + <header name='Server'>Clayster</header> + <header name='Content-Type'>text/turtle</header> + <header name='Content-Length'>...</header> + <header name='Connection'>Close</header> + </headers> + <data> + <text> + ... + </text> + </data> + </resp> + </iq> + """ + + name = 'response' + namespace = 'urn:xmpp:http' + interfaces = set(['code', 'message', 'version']) + plugin_attrib = 'http-resp' + + def get_code(self): + code = self._get_attr('statusCode', None) + return int(code) if code is not None else code + + def set_code(self, code): + self._set_attr('statusCode', str(code)) + + def get_message(self): + return self._get_attr('statusMessage', '') + + def set_message(self, message): + self._set_attr('statusMessage', message) + + def set_version(self, version='1.1'): + self._set_attr('version', version) diff --git a/slixmpp/roster/single.py b/slixmpp/roster/single.py index a37e3eb7..62fbca41 100644 --- a/slixmpp/roster/single.py +++ b/slixmpp/roster/single.py @@ -254,6 +254,9 @@ class RosterNode(object): callback -- Optional reference to a stream handler function. Will be executed when the roster is received. """ + if not groups: + groups = [] + self[jid]['name'] = name self[jid]['groups'] = groups self[jid].save() diff --git a/slixmpp/stanza/atom.py b/slixmpp/stanza/atom.py index 2c105685..ccded724 100644 --- a/slixmpp/stanza/atom.py +++ b/slixmpp/stanza/atom.py @@ -6,8 +6,7 @@ See the file LICENSE for copying permission. """ -from slixmpp.xmlstream import ElementBase - +from slixmpp.xmlstream import ElementBase, register_stanza_plugin class AtomEntry(ElementBase): @@ -22,5 +21,23 @@ class AtomEntry(ElementBase): namespace = 'http://www.w3.org/2005/Atom' name = 'entry' plugin_attrib = 'entry' - interfaces = set(('title', 'summary')) - sub_interfaces = set(('title', 'summary')) + interfaces = set(('title', 'summary', 'id', 'published', 'updated')) + sub_interfaces = set(('title', 'summary', 'id', 'published', + 'updated')) + +class AtomAuthor(ElementBase): + + """ + An Atom author. + + Stanza Interface: + name -- The printable author name + uri -- The bare jid of the author + """ + + name = 'author' + plugin_attrib = 'author' + interfaces = set(('name', 'uri')) + sub_interfaces = set(('name', 'uri')) + +register_stanza_plugin(AtomEntry, AtomAuthor) diff --git a/slixmpp/stanza/rootstanza.py b/slixmpp/stanza/rootstanza.py index ff139382..a6dd958e 100644 --- a/slixmpp/stanza/rootstanza.py +++ b/slixmpp/stanza/rootstanza.py @@ -60,7 +60,9 @@ class RootStanza(StanzaBase): reply.send() elif isinstance(e, XMPPError): # We raised this deliberately + keep_id = self['id'] reply = self.reply(clear=e.clear) + reply['id'] = keep_id reply['error']['condition'] = e.condition reply['error']['text'] = e.text reply['error']['type'] = e.etype @@ -72,7 +74,9 @@ class RootStanza(StanzaBase): reply.send() else: # We probably didn't raise this on purpose, so send an error stanza + keep_id = self['id'] reply = self.reply() + reply['id'] = keep_id reply['error']['condition'] = 'undefined-condition' reply['error']['text'] = "Slixmpp got into trouble." reply['error']['type'] = 'cancel' diff --git a/slixmpp/test/slixtest.py b/slixmpp/test/slixtest.py index 19ef9ae6..f66cf6be 100644 --- a/slixmpp/test/slixtest.py +++ b/slixmpp/test/slixtest.py @@ -319,6 +319,9 @@ class SlixTest(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, @@ -402,8 +405,7 @@ class SlixTest(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. diff --git a/slixmpp/thirdparty/__init__.py b/slixmpp/thirdparty/__init__.py index fe1056e6..d950f4f9 100644 --- a/slixmpp/thirdparty/__init__.py +++ b/slixmpp/thirdparty/__init__.py @@ -4,3 +4,4 @@ except: from slixmpp.thirdparty.gnupg import GPG from slixmpp.thirdparty.mini_dateutil import tzutc, tzoffset, parse_iso +from slixmpp.thirdparty.orderedset import OrderedSet diff --git a/slixmpp/thirdparty/orderedset.py b/slixmpp/thirdparty/orderedset.py new file mode 100644 index 00000000..f6642db3 --- /dev/null +++ b/slixmpp/thirdparty/orderedset.py @@ -0,0 +1,89 @@ +# Copyright (c) 2009 Raymond Hettinger +# +# 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. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +# OTHER DEALINGS IN THE SOFTWARE. + +import collections + +class OrderedSet(collections.MutableSet): + + def __init__(self, iterable=None): + self.end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.map = {} # key --> [key, prev, next] + if iterable is not None: + self |= iterable + + def __len__(self): + return len(self.map) + + def __contains__(self, key): + return key in self.map + + def add(self, key): + if key not in self.map: + end = self.end + curr = end[1] + curr[2] = end[1] = self.map[key] = [key, curr, end] + + def discard(self, key): + if key in self.map: + key, prev, next = self.map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): + end = self.end + curr = end[1] + while curr is not end: + yield curr[0] + curr = curr[1] + + def pop(self, last=True): + if not self: + raise KeyError('set is empty') + key = self.end[1][0] if last else self.end[2][0] + self.discard(key) + return key + + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self)) + + def __eq__(self, other): + if isinstance(other, OrderedSet): + return len(self) == len(other) and list(self) == list(other) + return set(self) == set(other) + + +if __name__ == '__main__': + s = OrderedSet('abracadaba') + t = OrderedSet('simsalabim') + print(s | t) + print(s & t) + print(s - t)
\ No newline at end of file diff --git a/slixmpp/xmlstream/cert.py b/slixmpp/xmlstream/cert.py index 71146f36..d357b326 100644 --- a/slixmpp/xmlstream/cert.py +++ b/slixmpp/xmlstream/cert.py @@ -181,4 +181,4 @@ def verify(expected, raw_cert): return True raise CertificateError( - 'Could not match certficate against hostname: %s' % expected) + 'Could not match certificate against hostname: %s' % expected) diff --git a/slixmpp/xmlstream/stanzabase.py b/slixmpp/xmlstream/stanzabase.py index 10c29782..1ddee825 100644 --- a/slixmpp/xmlstream/stanzabase.py +++ b/slixmpp/xmlstream/stanzabase.py @@ -558,10 +558,13 @@ class ElementBase(object): .. versionadded:: 1.0-Beta1 """ - values = {} + values = OrderedDict() 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(): @@ -672,6 +675,8 @@ class ElementBase(object): if lang and attrib in self.lang_interfaces: kwargs['lang'] = lang + kwargs = OrderedDict(kwargs) + if attrib == 'substanzas': return self.iterables elif attrib in self.interfaces or attrib == 'lang': @@ -748,6 +753,8 @@ class ElementBase(object): if lang and attrib in self.lang_interfaces: kwargs['lang'] = lang + kwargs = OrderedDict(kwargs) + if attrib in self.interfaces or attrib == 'lang': if value is not None: set_method = "set_%s" % attrib.lower() @@ -834,6 +841,8 @@ class ElementBase(object): if lang and attrib in self.lang_interfaces: kwargs['lang'] = lang + kwargs = OrderedDict(kwargs) + if attrib in self.interfaces or attrib == 'lang': del_method = "del_%s" % attrib.lower() del_method2 = "del%s" % attrib.title() |