From 3a12cdbd131e5bb98f192c077faa6bdda8fd95c7 Mon Sep 17 00:00:00 2001 From: Dann Martens Date: Thu, 13 Jan 2011 08:40:53 +0100 Subject: [PATCH 01/26] Introduced new XEP-0009 into develop. --- .../plugins/{xep_0009.py => old_0009.py} | 0 sleekxmpp/plugins/xep_0009/__init__.py | 11 + sleekxmpp/plugins/xep_0009/binding.py | 281 +++++++ sleekxmpp/plugins/xep_0009/remote.py | 752 ++++++++++++++++++ sleekxmpp/plugins/xep_0009/rpc.py | 221 +++++ sleekxmpp/plugins/xep_0009/stanza/RPC.py | 68 ++ sleekxmpp/plugins/xep_0009/stanza/__init__.py | 9 + tests/test_stanza_xep_0009.py | 55 ++ 8 files changed, 1397 insertions(+) rename sleekxmpp/plugins/{xep_0009.py => old_0009.py} (100%) create mode 100644 sleekxmpp/plugins/xep_0009/__init__.py create mode 100644 sleekxmpp/plugins/xep_0009/binding.py create mode 100644 sleekxmpp/plugins/xep_0009/remote.py create mode 100644 sleekxmpp/plugins/xep_0009/rpc.py create mode 100644 sleekxmpp/plugins/xep_0009/stanza/RPC.py create mode 100644 sleekxmpp/plugins/xep_0009/stanza/__init__.py create mode 100644 tests/test_stanza_xep_0009.py diff --git a/sleekxmpp/plugins/xep_0009.py b/sleekxmpp/plugins/old_0009.py similarity index 100% rename from sleekxmpp/plugins/xep_0009.py rename to sleekxmpp/plugins/old_0009.py diff --git a/sleekxmpp/plugins/xep_0009/__init__.py b/sleekxmpp/plugins/xep_0009/__init__.py new file mode 100644 index 0000000..2cd1417 --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0009 import stanza +from sleekxmpp.plugins.xep_0009.rpc import xep_0009 +from sleekxmpp.plugins.xep_0009.stanza import RPCQuery, MethodCall, MethodResponse diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py new file mode 100644 index 0000000..6b50d99 --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/binding.py @@ -0,0 +1,281 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from xml.etree import cElementTree as ET +import base64 +import logging +import time + + + +log = logging.getLogger(__name__) + +_namespace = 'jabber:iq:rpc' + +def fault2xml(fault): + value = dict() + value['faultCode'] = fault['code'] + value['faultString'] = fault['string'] + fault = ET.Element("fault", {'xmlns': _namespace}) + fault.append(_py2xml((value))) + return fault + +def xml2fault(params): + vals = [] + for value in params.findall('{%s}value' % _namespace): + vals.append(_xml2py(value)) + fault = dict() + fault['code'] = vals[0]['faultCode'] + fault['string'] = vals[0]['faultString'] + return fault + +def py2xml(*args): + params = ET.Element("{%s}params" % _namespace) + for x in args: + param = ET.Element("param") + param.append(_py2xml(x)) + params.append(param) #... + return params + +def _py2xml(*args): + for x in args: + val = ET.Element("value") + if x is None: + nil = ET.Element("nil") + val.append(nil) + elif type(x) is int: + i4 = ET.Element("i4") + i4.text = str(x) + val.append(i4) + elif 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) in (list, tuple): + 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 py2xml(*args): +# params = ET.Element("{%s}params" % _namespace) +# for x in args: +# param = ET.Element("{%s}param" % _namespace) +# param.append(_py2xml(x)) +# params.append(param) #... +# return params +# +#def _py2xml(*args): +# for x in args: +# val = ET.Element("{%s}value" % _namespace) +# if x is None: +# nil = ET.Element("{%s}nil" % _namespace) +# val.append(nil) +# elif type(x) is int: +# i4 = ET.Element("{%s}i4" % _namespace) +# i4.text = str(x) +# val.append(i4) +# elif type(x) is bool: +# boolean = ET.Element("{%s}boolean" % _namespace) +# boolean.text = str(int(x)) +# val.append(boolean) +# elif type(x) is str: +# string = ET.Element("{%s}string" % _namespace) +# string.text = x +# val.append(string) +# elif type(x) is float: +# double = ET.Element("{%s}double" % _namespace) +# double.text = str(x) +# val.append(double) +# elif type(x) is rpcbase64: +# b64 = ET.Element("{%s}Base64" % _namespace) +# b64.text = x.encoded() +# val.append(b64) +# elif type(x) is rpctime: +# iso = ET.Element("{%s}dateTime.iso8601" % _namespace) +# iso.text = str(x) +# val.append(iso) +# elif type(x) in (list, tuple): +# array = ET.Element("{%s}array" % _namespace) +# data = ET.Element("{%s}data" % _namespace) +# for y in x: +# data.append(_py2xml(y)) +# array.append(data) +# val.append(array) +# elif type(x) is dict: +# struct = ET.Element("{%s}struct" % _namespace) +# for y in x.keys(): +# member = ET.Element("{%s}member" % _namespace) +# name = ET.Element("{%s}name" % _namespace) +# name.text = y +# member.append(name) +# member.append(_py2xml(x[y])) +# struct.append(member) +# val.append(struct) +# return val + + +#def py2xml(*args): +# params = ET.Element("params", {'xmlns': _namespace}) +# for x in args: +# param = ET.Element("param", {'xmlns': _namespace}) +# param.append(_py2xml(x)) +# params.append(param) #... +# return params +# +#def _py2xml(*args): +# for x in args: +# val = ET.Element("value", {'xmlns': _namespace}) +# if x is None: +# nil = ET.Element("nil", {'xmlns': _namespace}) +# val.append(nil) +# elif type(x) is int: +# i4 = ET.Element("i4", {'xmlns': _namespace}) +# i4.text = str(x) +# val.append(i4) +# elif type(x) is bool: +# boolean = ET.Element("boolean", {'xmlns': _namespace}) +# boolean.text = str(int(x)) +# val.append(boolean) +# elif type(x) is str: +# string = ET.Element("string", {'xmlns': _namespace}) +# string.text = x +# val.append(string) +# elif type(x) is float: +# double = ET.Element("double", {'xmlns': _namespace}) +# double.text = str(x) +# val.append(double) +# elif type(x) is rpcbase64: +# b64 = ET.Element("Base64", {'xmlns': _namespace}) +# b64.text = x.encoded() +# val.append(b64) +# elif type(x) is rpctime: +# iso = ET.Element("dateTime.iso8601", {'xmlns': _namespace}) +# iso.text = str(x) +# val.append(iso) +# elif type(x) in (list, tuple): +# array = ET.Element("array", {'xmlns': _namespace}) +# data = ET.Element("data", {'xmlns': _namespace}) +# for y in x: +# data.append(_py2xml(y)) +# array.append(data) +# val.append(array) +# elif type(x) is dict: +# struct = ET.Element("struct", {'xmlns': _namespace}) +# for y in x.keys(): +# member = ET.Element("member", {'xmlns': _namespace}) +# name = ET.Element("name", {'xmlns': _namespace}) +# name.text = y +# member.append(name) +# member.append(_py2xml(x[y])) +# struct.append(member) +# val.append(struct) +# return val + +def xml2py(params): + namespace = 'jabber:iq:rpc' + vals = [] + for param in params.findall('{%s}param' % namespace): + vals.append(_xml2py(param.find('{%s}value' % namespace))) + return vals + +def _xml2py(value): + namespace = 'jabber:iq:rpc' + if value.find('{%s}i4' % namespace) is not None: + return int(value.find('{%s}i4' % namespace).text) + if value.find('{%s}int' % namespace) is not None: + return int(value.find('{%s}int' % namespace).text) + if value.find('{%s}boolean' % namespace) is not None: + return bool(value.find('{%s}boolean' % namespace).text) + if value.find('{%s}string' % namespace) is not None: + return value.find('{%s}string' % namespace).text + if value.find('{%s}double' % namespace) is not None: + return float(value.find('{%s}double' % namespace).text) + if value.find('{%s}Base64') is not None: + return rpcbase64(value.find('Base64' % namespace).text) + if value.find('{%s}dateTime.iso8601') is not None: + return rpctime(value.find('{%s}dateTime.iso8601')) + if value.find('{%s}struct' % namespace) is not None: + struct = {} + for member in value.find('{%s}struct' % namespace).findall('{%s}member' % namespace): + struct[member.find('{%s}name' % namespace).text] = _xml2py(member.find('{%s}value' % namespace)) + return struct + if value.find('{%s}array' % namespace) is not None: + array = [] + for val in value.find('{%s}array' % namespace).find('{%s}data' % namespace).findall('{%s}value' % namespace): + 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(self.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() diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py new file mode 100644 index 0000000..bd931c0 --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/remote.py @@ -0,0 +1,752 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from binding import py2xml, xml2py, xml2fault, fault2xml +from threading import RLock +import abc +import inspect +import logging +import sleekxmpp +import sys +import threading +import traceback + + + +log = logging.getLogger(__name__) + + + +def _intercept(method, name, public): + def _resolver(instance, *args, **kwargs): + log.debug("Locally calling %s.%s with arguments %s." % (instance.FQN(), method.__name__, args)) + try: + value = method(instance, *args, **kwargs) + if value == NotImplemented: + raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__)) + return value + except InvocationException: + raise + except Exception as e: + raise InvocationException("A problem occured calling %s.%s!" % (instance.FQN(), method.__name__), e) + _resolver._rpc = public + _resolver._rpc_name = method.__name__ if name is None else name + return _resolver + +def remote(function_argument, public = True): + ''' + Decorator for methods which are remotely callable. This decorator + works in conjunction with classes which extend ABC Endpoint. + Example: + + @remote + def remote_method(arg1, arg2) + + Arguments: + function_argument -- a stand-in for either the actual method + OR a new name (string) for the method. In that case the + method is considered mapped: + Example: + + @remote("new_name") + def remote_method(arg1, arg2) + + public -- A flag which indicates if this method should be part + of the known dictionary of remote methods. Defaults to True. + Example: + + @remote(False) + def remote_method(arg1, arg2) + + Note: renaming and revising (public vs. private) can be combined. + Example: + + @remote("new_name", False) + def remote_method(arg1, arg2) + ''' + if hasattr(function_argument, '__call__'): + return _intercept(function_argument, None, public) + else: + if not isinstance(function_argument, basestring): + if not isinstance(function_argument, bool): + raise Exception('Expected an RPC method name or visibility modifier!') + else: + def _wrap_revised(function): + function = _intercept(function, None, function_argument) + return function + return _wrap_revised + def _wrap_remapped(function): + function = _intercept(function, function_argument, public) + return function + return _wrap_remapped + + + +class ACL: + ''' + An Access Control List (ACL) is a list of rules, which are evaluated + in order until a match is found. The policy of the matching rule + is then applied. + + Rules are 3-tuples, consisting of a policy enumerated type, a JID + expression and a RCP resource expression. + + Examples: + [ (ACL.ALLOW, '*', '*') ] allow everyone everything, no restrictions + [ (ACL.DENY, '*', '*') ] deny everyone everything, no restrictions + [ (ACL.ALLOW, 'test@xmpp.org/unit', 'test.*'), + (ACL.DENY, '*', '*') ] deny everyone everything, except named + JID, which is allowed access to endpoint 'test' only. + + The use of wildcards is allowed in expressions, as follows: + '*' everyone, or everything (= all endpoints and methods) + 'test@xmpp.org/*' every JID regardless of JID resource + '*@xmpp.org/rpc' every JID from domain xmpp.org with JID res 'rpc' + 'frank@*' every 'frank', regardless of domain or JID res + 'system.*' all methods of endpoint 'system' + '*.reboot' all methods reboot regardless of endpoint + ''' + ALLOW = True + DENY = False + + @classmethod + def check(cls, rules, jid, resource): + if rules is None: + return cls.DENY # No rules means no access! + for rule in rules: + policy = cls._check(rule, jid, resource) + if policy is not None: + return policy + return cls.DENY # By default if not rule matches, deny access. + + @classmethod + def _check(cls, rule, jid, resource): + if cls._match(jid, rule[1]) and cls._match(resource, rule[2]): + return rule[0] + else: + return None + + @classmethod + def _next_token(cls, expression, index): + new_index = expression.find('*', index) + if new_index == 0: + return '' + else: + if new_index == -1: + return expression[index : ] + else: + return expression[index : new_index] + + @classmethod + def _match(cls, value, expression): + #! print "_match [VALUE] %s [EXPR] %s" % (value, expression) + index = 0 + position = 0 + while index < len(expression): + token = cls._next_token(expression, index) + #! print "[TOKEN] '%s'" % token + size = len(token) + if size > 0: + token_index = value.find(token, position) + if token_index == -1: + return False + else: + #! print "[INDEX-OF] %s" % token_index + position = token_index + len(token) + pass + if size == 0: + index += 1 + else: + index += size + #! print "index %s position %s" % (index, position) + return True + +ANY_ALL = [ (ACL.ALLOW, '*', '*') ] + + + +class RemoteException(Exception): + ''' + Base exception for RPC. This exception is raised when a problem + occurs in the network layer. + ''' + + def __init__(self, message="", cause=None): + ''' + Initializes a new RemoteException. + + Arguments: + message -- The message accompanying this exception. + cause -- The underlying cause of this exception. + ''' + self._message = message + self._cause = cause + pass + + def __str__(self): + return repr(self._message) + + def get_message(self): + return self._message + + def get_cause(self): + return self._cause + + + +class InvocationException(RemoteException): + ''' + Exception raised when a problem occurs during the remote invocation + of a method. + ''' + pass + + + +class AuthorizationException(RemoteException): + ''' + Exception raised when the caller is not authorized to invoke the + remote method. + ''' + pass + + + +class TimeoutException(Exception): + ''' + Exception raised when the synchronous execution of a method takes + longer than the given threshold because an underlying asynchronous + reply did not arrive in time. + ''' + pass + + + +class Callback(object): + ''' + A base class for callback handlers. + ''' + __metaclass__ = abc.ABCMeta + + + @abc.abstractproperty + def set_value(self, value): + return NotImplemented + + @abc.abstractproperty + def cancel_with_error(self, exception): + return NotImplemented + + + +class Future(Callback): + ''' + Represents the result of an asynchronous computation. + ''' + + def __init__(self): + ''' + Initializes a new Future. + ''' + self._value = None + self._exception = None + self._event = threading.Event() + pass + + def set_value(self, value): + ''' + Sets the value of this Future. Once the value is set, a caller + blocked on get_value will be able to continue. + ''' + self._value = value + self._event.set() + + def get_value(self, timeout=None): + ''' + Gets the value of this Future. This call will block until + the result is available, or until an optional timeout expires. + When this Future is cancelled with an error, + + Arguments: + timeout -- The maximum waiting time to obtain the value. + ''' + self._event.wait(timeout) + if self._exception: + raise self._exception + if not self._event.is_set(): + raise TimeoutException + return self._value + + def is_done(self): + ''' + Returns true if a value has been returned. + ''' + return self._event.is_set() + + def cancel_with_error(self, exception): + ''' + Cancels the Future because of an error. Once cancelled, a + caller blocked on get_value will be able to continue. + ''' + self._exception = exception + self._event.set() + + + +class Endpoint(object): + ''' + The Endpoint class is an abstract base class for all objects + participating in an RPC-enabled XMPP network. + + A user subclassing this class is required to implement the method: + FQN(self) + where FQN stands for Fully Qualified Name, an unambiguous name + which specifies which object an RPC call refers to. It is the + first part in a RPC method name '.'. + ''' + __metaclass__ = abc.ABCMeta + + + def __init__(self, session, target_jid): + ''' + Initialize a new Endpoint. This constructor should never be + invoked by a user, instead it will be called by the factories + which instantiate the RPC-enabled objects, of which only + the classes are provided by the user. + + Arguments: + session -- An RPC session instance. + target_jid -- the identity of the remote XMPP entity. + ''' + self.session = session + self.target_jid = target_jid + + @abc.abstractproperty + def FQN(self): + return NotImplemented + + def get_methods(self): + ''' + Returns a dictionary of all RPC method names provided by this + class. This method returns the actual method names as found + in the class definition which have been decorated with: + + @remote + def some_rpc_method(arg1, arg2) + + + Unless: + (1) the name has been remapped, in which case the new + name will be returned. + + @remote("new_name") + def some_rpc_method(arg1, arg2) + + (2) the method is set to hidden + + @remote(False) + def some_hidden_method(arg1, arg2) + ''' + result = dict() + for function in dir(self): + test_attr = getattr(self, function, None) + try: + if test_attr._rpc: + result[test_attr._rpc_name] = test_attr + except Exception: + pass + return result + + + +class Proxy(Endpoint): + ''' + Implementation of the Proxy pattern which is intended to wrap + around Endpoints in order to intercept calls, marshall them and + forward them to the remote object. + ''' + + def __init__(self, endpoint, callback = None): + ''' + Initializes a new Proxy. + + Arguments: + endpoint -- The endpoint which is proxified. + ''' + self._endpoint = endpoint + self._callback = callback + + def __getattribute__(self, name, *args): + if name in ('__dict__', '_endpoint', 'async', '_callback'): + return object.__getattribute__(self, name) + else: + attribute = self._endpoint.__getattribute__(name) + if hasattr(attribute, '__call__'): + try: + if attribute._rpc: + def _remote_call(*args, **kwargs): + log.debug("Remotely calling '%s.%s' with arguments %s." % (self._endpoint.FQN(), attribute._rpc_name, args)) + return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs) + return _remote_call + except: + pass # If the attribute doesn't exist, don't care! + return attribute + + def async(self, callback): + return Proxy(self._endpoint, callback) + + def get_endpoint(self): + ''' + Returns the proxified endpoint. + ''' + return self._endpoint + + def FQN(self): + return self._endpoint.FQN() + + + +class JabberRPCEntry(object): + + + def __init__(self, endpoint_FQN, call): + self._endpoint_FQN = endpoint_FQN + self._call = call + + def call_method(self, args): + return_value = self._call(*args) + if return_value is None: + return return_value + else: + return self._return(return_value) + + def get_endpoint_FQN(self): + return self._endpoint_FQN + + def _return(self, *args): + return args + + + +class RemoteSession(object): + ''' + A context object for a Jabber-RPC session. + ''' + + + def __init__(self, client, session_close_callback): + ''' + Initializes a new RPC session. + + Arguments: + client -- The SleekXMPP client associated with this session. + session_close_callback -- A callback called when the + session is closed. + ''' + self._client = client + self._session_close_callback = session_close_callback + self._event = threading.Event() + self._entries = {} + self._callbacks = {} + self._acls = {} + self._lock = RLock() + + def _wait(self): + self._event.wait() + + def _notify(self, event): + log.debug("RPC Session as %s started." % self._client.boundjid.full) + self._client.sendPresence() + self._event.set() + pass + + def _register_call(self, endpoint, method, name=None): + ''' + Registers a method from an endpoint as remotely callable. + ''' + if name is None: + name = method.__name__ + key = "%s.%s" % (endpoint, name) + log.debug("Registering call handler for %s (%s)." % (key, method)) + with self._lock: + if self._entries.has_key(key): + raise KeyError("A handler for %s has already been regisered!" % endpoint) + self._entries[key] = JabberRPCEntry(endpoint, method) + return key + + def _register_acl(self, endpoint, acl): + log.debug("Registering ACL %s for endpoint %s." % (repr(acl), endpoint)) + with self._lock: + self._acls[endpoint] = acl + + def _register_callback(self, pid, callback): + with self._lock: + self._callbacks[pid] = callback + + def forget_callback(self, callback): + with self._lock: + pid = self._find_key(self._callbacks, callback) + if pid is not None: + del self._callback[pid] + else: + raise ValueError("Unknown callback!") + pass + + 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] + if len(search) == 0: + return None + else: + return search[0] + + def _unregister_call(self, key): + #removes the registered call + with self._lock: + if self._entries[key]: + del self._entries[key] + else: + raise ValueError() + + def new_proxy(self, target_jid, endpoint_cls): + ''' + Instantiates a new proxy object, which proxies to a remote + endpoint. This method uses a class reference without + constructor arguments to instantiate the proxy. + + Arguments: + target_jid -- the XMPP entity ID hosting the endpoint. + endpoint_cls -- The remote (duck) type. + ''' + try: + argspec = inspect.getargspec(endpoint_cls.__init__) + args = [None] * (len(argspec[0]) - 1) + result = endpoint_cls(*args) + Endpoint.__init__(result, self, target_jid) + return Proxy(result) + except: + traceback.print_exc(file=sys.stdout) + + def new_handler(self, acl, handler_cls, *args, **kwargs): + ''' + Instantiates a new handler object, which is called remotely + by others. The user can control the effect of the call by + implementing the remote method in the local endpoint class. The + returned reference can be called locally and will behave as a + regular instance. + + Arguments: + acl -- Access control list (see ACL class) + handler_clss -- The local (duck) type. + *args -- Constructor arguments for the local type. + **kwargs -- Constructor keyworded arguments for the local + type. + ''' + argspec = inspect.getargspec(handler_cls.__init__) + base_argspec = inspect.getargspec(Endpoint.__init__) + if(argspec == base_argspec): + result = handler_cls(self, self._client.boundjid.full) + else: + 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(): + #!!! 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) + return result + +# def is_available(self, targetCls, pto): +# return self._client.is_available(pto) + + def _call_remote(self, pto, pmethod, callback, *arguments): + iq = self._client.plugin['xep_0009'].make_iq_method_call(pto, pmethod, py2xml(*arguments)) + pid = iq['id'] + if callback is None: + future = Future() + self._register_callback(pid, future) + iq.send() + return future.get_value(30) + else: + print "[RemoteSession] _call_remote %s" % callback + self._register_callback(pid, callback) + iq.send() + + def close(self): + ''' + Closes this session. + ''' + self._client.disconnect(False) + self._session_close_callback() + + def _on_jabber_rpc_method_call(self, iq): + iq.enable('rpc_query') + params = iq['rpc_query']['method_call']['params'] + args = xml2py(params) + pmethod = iq['rpc_query']['method_call']['method_name'] + try: + with self._lock: + entry = self._entries[pmethod] + rules = self._acls[entry.get_endpoint_FQN()] + if ACL.check(rules, iq['from'], pmethod): + return_value = entry.call_method(args) + else: + raise AuthorizationException("Unauthorized access to %s from %s!" % (pmethod, iq['from'])) + if return_value is None: + return_value = () + response = self._client.plugin['xep_0009'].make_iq_method_response(iq['id'], iq['from'], py2xml(*return_value)) + response.send() + except InvocationException as ie: + fault = dict() + fault['code'] = 500 + fault['string'] = ie.get_message() + self._client.plugin['xep_0009']._send_fault(iq, fault2xml(fault)) + except AuthorizationException as ae: + log.error(ae.get_message()) + error = self._client.plugin['xep_0009']._forbidden(iq) + error.send() + except Exception as e: + if isinstance(e, KeyError): + log.error("No handler available for %s!" % pmethod) + error = self._client.plugin['xep_0009']._item_not_found(iq) + else: + traceback.print_exc(file=sys.stderr) + log.error("An unexpected problem occurred invoking method %s!" % pmethod) + error = self._client.plugin['xep_0009']._undefined_condition(iq) + #! print "[REMOTE.PY] _handle_remote_procedure_call AN ERROR SHOULD BE SENT NOW %s " % e + error.send() + + def _on_jabber_rpc_method_response(self, iq): + iq.enable('rpc_query') + args = xml2py(iq['rpc_query']['method_response']['params']) + pid = iq['id'] + with self._lock: + callback = self._callbacks[pid] + del self._callbacks[pid] + if(len(args) > 0): + callback.set_value(args[0]) + else: + callback.set_value(None) + pass + + def _on_jabber_rpc_method_response2(self, iq): + iq.enable('rpc_query') + if iq['rpc_query']['method_response']['fault'] is not None: + self._on_jabber_rpc_method_fault(iq) + else: + args = xml2py(iq['rpc_query']['method_response']['params']) + pid = iq['id'] + with self._lock: + callback = self._callbacks[pid] + del self._callbacks[pid] + if(len(args) > 0): + callback.set_value(args[0]) + else: + callback.set_value(None) + pass + + def _on_jabber_rpc_method_fault(self, iq): + iq.enable('rpc_query') + fault = xml2fault(iq['rpc_query']['method_response']['fault']) + pid = iq['id'] + with self._lock: + callback = self._callbacks[pid] + del self._callbacks[pid] + e = { + 500: InvocationException + }[fault['code']](fault['string']) + callback.cancel_with_error(e) + + def _on_jabber_rpc_error(self, iq): + pid = iq['id'] + pmethod = self._client.plugin['xep_0009']._extract_method(iq['rpc_query']) + code = iq['error']['code'] + type = iq['error']['type'] + condition = iq['error']['condition'] + #! print("['REMOTE.PY']._BINDING_handle_remote_procedure_error -> ERROR! ERROR! ERROR! Condition is '%s'" % condition) + with self._lock: + callback = self._callbacks[pid] + del self._callbacks[pid] + e = { + 'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])), + 'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])), + 'undefined-condition': RemoteException("An unexpected problem occured trying to invoke %s at %s!" % (pmethod, iq['from'])), + }[condition] + if e is None: + RemoteException("An unexpected exception occurred at %s!" % iq['from']) + callback.cancel_with_error(e) + + + +class Remote(object): + ''' + Bootstrap class for Jabber-RPC sessions. New sessions are openend + with an existing XMPP client, or one is instantiated on demand. + ''' + _instance = None + _sessions = dict() + _lock = threading.RLock() + + @classmethod + def new_session_with_client(cls, client, callback=None): + ''' + Opens a new session with a given client. + + Arguments: + client -- An XMPP client. + callback -- An optional callback which can be used to track + the starting state of the session. + ''' + with Remote._lock: + 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; + def _session_close_callback(): + with Remote._lock: + del cls._sessions[client.boundjid.bare] + result = RemoteSession(client, _session_close_callback) + client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call) + client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response) + client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault) + client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error) + if callback is None: + start_event_handler = result._notify + else: + start_event_handler = callback + client.add_event_handler("session_start", start_event_handler) + if client.connect(): + client.process(threaded=True) + else: + raise RemoteException("Could not connect to XMPP server!") + pass + if callback is None: + result._wait() + return result + + @classmethod + def new_session(cls, jid, password, callback=None): + ''' + Opens a new session and instantiates a new XMPP client. + + Arguments: + jid -- The XMPP JID for logging in. + password -- The password for logging in. + callback -- An optional callback which can be used to track + the starting state of the session. + ''' + client = sleekxmpp.ClientXMPP(jid, password) + #? Register plug-ins. + client.registerPlugin('xep_0004') # Data Forms + client.registerPlugin('xep_0009') # Jabber-RPC + client.registerPlugin('xep_0030') # Service Discovery + client.registerPlugin('xep_0060') # PubSub + client.registerPlugin('xep_0199') # XMPP Ping + return cls.new_session_with_client(client, callback) + + \ No newline at end of file diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py new file mode 100644 index 0000000..84afeb5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/rpc.py @@ -0,0 +1,221 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from .. import base +from stanza.RPC import RPCQuery, MethodCall, MethodResponse +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.matcher.xpath import MatchXPath +from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin +from xml.etree import cElementTree as ET +import logging + + + +log = logging.getLogger(__name__) + + + +class xep_0009(base.base_plugin): + + def plugin_init(self): + self.xep = '0009' + self.description = 'Jabber-RPC' + #self.stanza = sleekxmpp.plugins.xep_0009.stanza + + register_stanza_plugin(Iq, RPCQuery) + register_stanza_plugin(RPCQuery, MethodCall) + register_stanza_plugin(RPCQuery, MethodResponse) + + self.xmpp.registerHandler( + 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( + 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( + Callback('RPC Call', MatchXPath('{%s}iq/{%s}error' % (self.xmpp.default_ns, self.xmpp.default_ns)), + self._handle_error) + ) + self.xmpp.add_event_handler('jabber_rpc_method_call', self._on_jabber_rpc_method_call) + self.xmpp.add_event_handler('jabber_rpc_method_response', self._on_jabber_rpc_method_response) + self.xmpp.add_event_handler('jabber_rpc_method_fault', self._on_jabber_rpc_method_fault) + self.xmpp.add_event_handler('jabber_rpc_error', self._on_jabber_rpc_error) + self.xmpp.add_event_handler('error', self._handle_error) + #self.activeCalls = [] + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc') + self.xmpp.plugin['xep_0030'].add_identity('automation','rpc') + + def make_iq_method_call(self, pto, pmethod, params): + iq = self.xmpp.makeIqSet() + iq.attrib['to'] = pto + iq.attrib['from'] = self.xmpp.boundjid.full + iq.enable('rpc_query') + iq['rpc_query']['method_call']['method_name'] = pmethod + iq['rpc_query']['method_call']['params'] = params + return iq; + + def make_iq_method_response(self, pid, pto, params): + iq = self.xmpp.makeIqResult(pid) + iq.attrib['to'] = pto + iq.attrib['from'] = self.xmpp.boundjid.full + iq.enable('rpc_query') + iq['rpc_query']['method_response']['params'] = params + return iq + + def make_iq_method_response_fault(self, pid, pto, params): + iq = self.xmpp.makeIqResult(pid) + iq.attrib['to'] = pto + iq.attrib['from'] = self.xmpp.boundjid.full + iq.enable('rpc_query') + iq['rpc_query']['method_response']['params'] = None + iq['rpc_query']['method_response']['fault'] = params + return iq + +# def make_iq_method_error(self, pto, pid, pmethod, params, code, type, condition): +# iq = self.xmpp.makeIqError(pid) +# iq.attrib['to'] = pto +# iq.attrib['from'] = self.xmpp.boundjid.full +# iq['error']['code'] = code +# iq['error']['type'] = type +# iq['error']['condition'] = condition +# iq['rpc_query']['method_call']['method_name'] = pmethod +# iq['rpc_query']['method_call']['params'] = params +# return iq + + def _item_not_found(self, iq): + payload = iq.get_payload() + iq.reply().error().set_payload(payload); + iq['error']['code'] = '404' + iq['error']['type'] = 'cancel' + iq['error']['condition'] = 'item-not-found' + return iq + + def _undefined_condition(self, iq): + payload = iq.get_payload() + iq.reply().error().set_payload(payload) + iq['error']['code'] = '500' + iq['error']['type'] = 'cancel' + iq['error']['condition'] = 'undefined-condition' + return iq + + def _forbidden(self, iq): + payload = iq.get_payload() + iq.reply().error().set_payload(payload) + iq['error']['code'] = '403' + iq['error']['type'] = 'auth' + iq['error']['condition'] = 'forbidden' + return iq + + def _recipient_unvailable(self, iq): + payload = iq.get_payload() + iq.reply().error().set_payload(payload) + iq['error']['code'] = '404' + iq['error']['type'] = 'wait' + iq['error']['condition'] = 'recipient-unavailable' + return iq + + def _handle_method_call(self, iq): + type = iq['type'] + if type == 'set': + log.debug("Incoming Jabber-RPC call from %s" % iq['from']) + self.xmpp.event('jabber_rpc_method_call', iq) + else: + if type == 'error' and ['rpc_query'] is None: + self.handle_error(iq) + else: + log.debug("Incoming Jabber-RPC error from %s" % iq['from']) + self.xmpp.event('jabber_rpc_error', iq) + + def _handle_method_response(self, iq): + if iq['rpc_query']['method_response']['fault'] is not None: + log.debug("Incoming Jabber-RPC fault from %s" % iq['from']) + #self._on_jabber_rpc_method_fault(iq) + self.xmpp.event('jabber_rpc_method_fault', iq) + else: + log.debug("Incoming Jabber-RPC response from %s" % iq['from']) + self.xmpp.event('jabber_rpc_method_response', iq) + + def _handle_error(self, iq): + print("['XEP-0009']._handle_error -> ERROR! Iq is '%s'" % iq) + print("#######################") + print("### NOT IMPLEMENTED ###") + print("#######################") + + def _on_jabber_rpc_method_call(self, iq, forwarded=False): + """ + A default handler for Jabber-RPC method call. If another + handler is registered, this one will defer and not run. + + If this handler is called by your own custom handler with + forwarded set to True, then it will run as normal. + """ + if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1: + return + # Reply with error by default + error = self.client.plugin['xep_0009']._item_not_found(iq) + error.send() + + def _on_jabber_rpc_method_response(self, iq, forwarded=False): + """ + A default handler for Jabber-RPC method response. If another + handler is registered, this one will defer and not run. + + If this handler is called by your own custom handler with + forwarded set to True, then it will run as normal. + """ + if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1: + return + error = self.client.plugin['xep_0009']._recpient_unavailable(iq) + error.send() + + def _on_jabber_rpc_method_fault(self, iq, forwarded=False): + """ + A default handler for Jabber-RPC fault response. If another + handler is registered, this one will defer and not run. + + If this handler is called by your own custom handler with + forwarded set to True, then it will run as normal. + """ + if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1: + return + error = self.client.plugin['xep_0009']._recpient_unavailable(iq) + error.send() + + def _on_jabber_rpc_error(self, iq, forwarded=False): + """ + A default handler for Jabber-RPC error response. If another + handler is registered, this one will defer and not run. + + If this handler is called by your own custom handler with + forwarded set to True, then it will run as normal. + """ + if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1: + return + error = self.client.plugin['xep_0009']._recpient_unavailable(iq, iq.get_payload()) + error.send() + + def _send_fault(self, iq, fault_xml): # + fault = self.make_iq_method_response_fault(iq['id'], iq['from'], fault_xml) + fault.send() + + def _send_error(self, iq): + print("['XEP-0009']._send_error -> ERROR! Iq is '%s'" % iq) + print("#######################") + print("### NOT IMPLEMENTED ###") + print("#######################") + + def _extract_method(self, stanza): + xml = ET.fromstring("%s" % stanza) + return xml.find("./methodCall/methodName").text + \ No newline at end of file diff --git a/sleekxmpp/plugins/xep_0009/stanza/RPC.py b/sleekxmpp/plugins/xep_0009/stanza/RPC.py new file mode 100644 index 0000000..24f2efd --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/stanza/RPC.py @@ -0,0 +1,68 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream.stanzabase import ElementBase +from xml.etree import cElementTree as ET + + +class RPCQuery(ElementBase): + name = 'query' + namespace = 'jabber:iq:rpc' + plugin_attrib = 'rpc_query' + interfaces = set(()) + subinterfaces = set(()) + plugin_attrib_map = {} + plugin_tag_map = {} + + + +class MethodCall(ElementBase): + name = 'methodCall' + namespace = 'jabber:iq:rpc' + plugin_attrib = 'method_call' + interfaces = set(('method_name', 'params')) + subinterfaces = set(()) + plugin_attrib_map = {} + plugin_tag_map = {} + + + def get_method_name(self): + return self._get_sub_text('methodName') + + def set_method_name(self, value): + return self._set_sub_text('methodName', value) + + def get_params(self): + return self.xml.find('{%s}params' % self.namespace) + + def set_params(self, params): + self.append(params) + + + +class MethodResponse(ElementBase): + name = 'methodResponse' + namespace = 'jabber:iq:rpc' + plugin_attrib = 'method_response' + interfaces = set(('params', 'fault')) + subinterfaces = set(()) + plugin_attrib_map = {} + plugin_tag_map = {} + + + def get_params(self): + return self.xml.find('{%s}params' % self.namespace) + + def set_params(self, params): + self.append(params) + + def get_fault(self): + return self.xml.find('{%s}fault' % self.namespace) + + def set_fault(self, fault): + self.append(fault) diff --git a/sleekxmpp/plugins/xep_0009/stanza/__init__.py b/sleekxmpp/plugins/xep_0009/stanza/__init__.py new file mode 100644 index 0000000..0b90223 --- /dev/null +++ b/sleekxmpp/plugins/xep_0009/stanza/__init__.py @@ -0,0 +1,9 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from RPC import RPCQuery, MethodCall, MethodResponse diff --git a/tests/test_stanza_xep_0009.py b/tests/test_stanza_xep_0009.py new file mode 100644 index 0000000..f6ab7ed --- /dev/null +++ b/tests/test_stanza_xep_0009.py @@ -0,0 +1,55 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.old_0009 import py2xml +from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, \ + MethodResponse +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.test.sleektest import SleekTest +from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin +import unittest + + + +class TestJabberRPC(SleekTest): + + def setUp(self): + register_stanza_plugin(Iq, RPCQuery) + register_stanza_plugin(RPCQuery, MethodCall) + register_stanza_plugin(RPCQuery, MethodResponse) + + def testMethodCall(self): + iq = self.Iq() + iq['rpc_query']['method_call']['method_name'] = 'system.exit' + iq['rpc_query']['method_call']['params'] = py2xml(*()) + self.check(iq, """ + + + + system.exit + + + + + """, use_values=False) + + def testMethodResponse(self): + iq = self.Iq() + iq['rpc_query']['method_response']['params'] = py2xml(*()) + self.check(iq, """ + + + + + + + + """, use_values=False) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberRPC) + \ No newline at end of file From 0a3a7b5a70dda56ffae4e85dc161b95072d3f085 Mon Sep 17 00:00:00 2001 From: Dann Martens Date: Thu, 13 Jan 2011 11:37:58 +0100 Subject: [PATCH 02/26] Removed binding XML namespace experiments. --- sleekxmpp/plugins/xep_0009/binding.py | 115 -------------------------- 1 file changed, 115 deletions(-) diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py index 6b50d99..464081f 100644 --- a/sleekxmpp/plugins/xep_0009/binding.py +++ b/sleekxmpp/plugins/xep_0009/binding.py @@ -91,121 +91,6 @@ def _py2xml(*args): val.append(struct) return val -#def py2xml(*args): -# params = ET.Element("{%s}params" % _namespace) -# for x in args: -# param = ET.Element("{%s}param" % _namespace) -# param.append(_py2xml(x)) -# params.append(param) #... -# return params -# -#def _py2xml(*args): -# for x in args: -# val = ET.Element("{%s}value" % _namespace) -# if x is None: -# nil = ET.Element("{%s}nil" % _namespace) -# val.append(nil) -# elif type(x) is int: -# i4 = ET.Element("{%s}i4" % _namespace) -# i4.text = str(x) -# val.append(i4) -# elif type(x) is bool: -# boolean = ET.Element("{%s}boolean" % _namespace) -# boolean.text = str(int(x)) -# val.append(boolean) -# elif type(x) is str: -# string = ET.Element("{%s}string" % _namespace) -# string.text = x -# val.append(string) -# elif type(x) is float: -# double = ET.Element("{%s}double" % _namespace) -# double.text = str(x) -# val.append(double) -# elif type(x) is rpcbase64: -# b64 = ET.Element("{%s}Base64" % _namespace) -# b64.text = x.encoded() -# val.append(b64) -# elif type(x) is rpctime: -# iso = ET.Element("{%s}dateTime.iso8601" % _namespace) -# iso.text = str(x) -# val.append(iso) -# elif type(x) in (list, tuple): -# array = ET.Element("{%s}array" % _namespace) -# data = ET.Element("{%s}data" % _namespace) -# for y in x: -# data.append(_py2xml(y)) -# array.append(data) -# val.append(array) -# elif type(x) is dict: -# struct = ET.Element("{%s}struct" % _namespace) -# for y in x.keys(): -# member = ET.Element("{%s}member" % _namespace) -# name = ET.Element("{%s}name" % _namespace) -# name.text = y -# member.append(name) -# member.append(_py2xml(x[y])) -# struct.append(member) -# val.append(struct) -# return val - - -#def py2xml(*args): -# params = ET.Element("params", {'xmlns': _namespace}) -# for x in args: -# param = ET.Element("param", {'xmlns': _namespace}) -# param.append(_py2xml(x)) -# params.append(param) #... -# return params -# -#def _py2xml(*args): -# for x in args: -# val = ET.Element("value", {'xmlns': _namespace}) -# if x is None: -# nil = ET.Element("nil", {'xmlns': _namespace}) -# val.append(nil) -# elif type(x) is int: -# i4 = ET.Element("i4", {'xmlns': _namespace}) -# i4.text = str(x) -# val.append(i4) -# elif type(x) is bool: -# boolean = ET.Element("boolean", {'xmlns': _namespace}) -# boolean.text = str(int(x)) -# val.append(boolean) -# elif type(x) is str: -# string = ET.Element("string", {'xmlns': _namespace}) -# string.text = x -# val.append(string) -# elif type(x) is float: -# double = ET.Element("double", {'xmlns': _namespace}) -# double.text = str(x) -# val.append(double) -# elif type(x) is rpcbase64: -# b64 = ET.Element("Base64", {'xmlns': _namespace}) -# b64.text = x.encoded() -# val.append(b64) -# elif type(x) is rpctime: -# iso = ET.Element("dateTime.iso8601", {'xmlns': _namespace}) -# iso.text = str(x) -# val.append(iso) -# elif type(x) in (list, tuple): -# array = ET.Element("array", {'xmlns': _namespace}) -# data = ET.Element("data", {'xmlns': _namespace}) -# for y in x: -# data.append(_py2xml(y)) -# array.append(data) -# val.append(array) -# elif type(x) is dict: -# struct = ET.Element("struct", {'xmlns': _namespace}) -# for y in x.keys(): -# member = ET.Element("member", {'xmlns': _namespace}) -# name = ET.Element("name", {'xmlns': _namespace}) -# name.text = y -# member.append(name) -# member.append(_py2xml(x[y])) -# struct.append(member) -# val.append(struct) -# return val - def xml2py(params): namespace = 'jabber:iq:rpc' vals = [] From 2e6c27f665d04f9132cbf845175d050e92aa0882 Mon Sep 17 00:00:00 2001 From: Dann Martens Date: Thu, 13 Jan 2011 11:58:20 +0100 Subject: [PATCH 03/26] Added examples. --- examples/rpc_async.py | 44 ++++++++++++++++++++++++++++++ examples/rpc_client_side.py | 53 +++++++++++++++++++++++++++++++++++++ examples/rpc_server_side.py | 52 ++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 examples/rpc_async.py create mode 100644 examples/rpc_client_side.py create mode 100644 examples/rpc_server_side.py diff --git a/examples/rpc_async.py b/examples/rpc_async.py new file mode 100644 index 0000000..0b6d193 --- /dev/null +++ b/examples/rpc_async.py @@ -0,0 +1,44 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Dann Martens + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \ + ANY_ALL, Future +import time + +class Boomerang(Endpoint): + + def FQN(self): + return 'boomerang' + + @remote + def throw(self): + print "Duck!" + + + +def main(): + + session = Remote.new_session('kangaroo@xmpp.org/rpc', '*****') + + session.new_handler(ANY_ALL, Boomerang) + + boomerang = session.new_proxy('kangaroo@xmpp.org/rpc', Boomerang) + + callback = Future() + + boomerang.async(callback).throw() + + time.sleep(10) + + session.close() + + + +if __name__ == '__main__': + main() + \ No newline at end of file diff --git a/examples/rpc_client_side.py b/examples/rpc_client_side.py new file mode 100644 index 0000000..ca1084f --- /dev/null +++ b/examples/rpc_client_side.py @@ -0,0 +1,53 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Dann Martens + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \ + ANY_ALL +import threading +import time + +class Thermostat(Endpoint): + + def FQN(self): + return 'thermostat' + + def __init(self, initial_temperature): + self._temperature = initial_temperature + self._event = threading.Event() + + @remote + def set_temperature(self, temperature): + return NotImplemented + + @remote + def get_temperature(self): + return NotImplemented + + @remote(False) + def release(self): + return NotImplemented + + + +def main(): + + session = Remote.new_session('operator@xmpp.org/rpc', '*****') + + thermostat = session.new_proxy('thermostat@xmpp.org/rpc', Thermostat) + + print("Current temperature is %s" % thermostat.get_temperature()) + + thermostat.set_temperature(20) + + time.sleep(10) + + session.close() + +if __name__ == '__main__': + main() + \ No newline at end of file diff --git a/examples/rpc_server_side.py b/examples/rpc_server_side.py new file mode 100644 index 0000000..0af8af4 --- /dev/null +++ b/examples/rpc_server_side.py @@ -0,0 +1,52 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Dann Martens + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0009.remote import Endpoint, remote, Remote, \ + ANY_ALL +import threading + +class Thermostat(Endpoint): + + def FQN(self): + return 'thermostat' + + def __init(self, initial_temperature): + self._temperature = initial_temperature + self._event = threading.Event() + + @remote + def set_temperature(self, temperature): + print("Setting temperature to %s" % temperature) + self._temperature = temperature + + @remote + def get_temperature(self): + return self._temperature + + @remote(False) + def release(self): + self._event.set() + + def wait_for_release(self): + self._event.wait() + + + +def main(): + + session = Remote.new_session('sleek@xmpp.org/rpc', '*****') + + thermostat = session.new_handler(ANY_ALL, Thermostat, 18) + + thermostat.wait_for_release() + + session.close() + +if __name__ == '__main__': + main() + \ No newline at end of file From a21178007faa4e2975ae07ef48762a74eb84eda5 Mon Sep 17 00:00:00 2001 From: Dann Martens Date: Thu, 13 Jan 2011 12:53:17 +0100 Subject: [PATCH 04/26] Updated setup.py to include XEP-0009. --- setup.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.py b/setup.py index 93c9ab3..b030f4a 100644 --- a/setup.py +++ b/setup.py @@ -45,6 +45,8 @@ packages = [ 'sleekxmpp', 'sleekxmpp/xmlstream/handler', 'sleekxmpp/thirdparty', 'sleekxmpp/plugins', + 'sleekxmpp/plugins/xep_0009', + 'sleekxmpp/plugins/xep_0009/stanza', 'sleekxmpp/plugins/xep_0030', 'sleekxmpp/plugins/xep_0030/stanza' ] From 4be6482ff3278612365863575dceeda9fd9a88c3 Mon Sep 17 00:00:00 2001 From: Dann Martens Date: Thu, 13 Jan 2011 13:42:01 +0100 Subject: [PATCH 05/26] Fixed 'nil' bug in unmarshalling. --- sleekxmpp/plugins/xep_0009/binding.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py index 464081f..61ef146 100644 --- a/sleekxmpp/plugins/xep_0009/binding.py +++ b/sleekxmpp/plugins/xep_0009/binding.py @@ -100,6 +100,8 @@ def xml2py(params): def _xml2py(value): namespace = 'jabber:iq:rpc' + if value.find('{%s}nil' % namespace) is not None: + return None if value.find('{%s}i4' % namespace) is not None: return int(value.find('{%s}i4' % namespace).text) if value.find('{%s}int' % namespace) is not None: From b68e7bed403924dc4ebf7294854d4892c48ce0ab Mon Sep 17 00:00:00 2001 From: Dann Martens Date: Thu, 13 Jan 2011 15:04:16 +0100 Subject: [PATCH 06/26] Fixed typo. --- sleekxmpp/basexmpp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 42cbaa0..e3c7bc5 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -245,7 +245,7 @@ class BaseXMPP(XMLStream): """Create a Presence stanza associated with this stream.""" return Presence(self, *args, **kwargs) - def make_iq(self, id=0, ifrom=None, ito=None, type=None, query=None): + def make_iq(self, id=0, ifrom=None, ito=None, itype=None, query=None): """ Create a new Iq stanza with a given Id and from JID. From 5313338c3ac3350e0c4a524c974021972f10ab94 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 31 Jan 2011 15:40:00 -0500 Subject: [PATCH 07/26] Fixes for XEP-0202 --- sleekxmpp/plugins/xep_0202.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sleekxmpp/plugins/xep_0202.py b/sleekxmpp/plugins/xep_0202.py index fe1191e..3b31c97 100644 --- a/sleekxmpp/plugins/xep_0202.py +++ b/sleekxmpp/plugins/xep_0202.py @@ -27,10 +27,12 @@ class EntityTime(ElementBase): interfaces = set(('tzo', 'utc')) sub_interfaces = set(('tzo', 'utc')) - #def get_utc(self): # TODO: return a datetime.tzinfo object? + #def get_tzo(self): + # TODO: Right now it returns a string but maybe it should + # return a datetime.tzinfo object or maybe a datetime.timedelta? #pass - def set_tzo(self, tzo): # TODO: support datetime.tzinfo objects? + def set_tzo(self, tzo): if isinstance(tzo, tzinfo): td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here' seconds = td.seconds + td.days * 24 * 3600 @@ -45,7 +47,7 @@ class EntityTime(ElementBase): # Returns a datetime object instead the string. Is this a good idea? value = self._get_sub_text('utc') if '.' in value: - return datetime.strptime(value, '%Y-%m-%d.%fT%H:%M:%SZ') + return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ') else: return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') From 8dbe6f65462ec9b1a0506a00316415996f4d53d8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 31 Jan 2011 15:54:44 -0500 Subject: [PATCH 08/26] Updated todo list for 1.0 release. --- todo1.0 | 185 +++++++++++++++++++------------------------------------- 1 file changed, 62 insertions(+), 123 deletions(-) diff --git a/todo1.0 b/todo1.0 index 191c0e2..3212a31 100644 --- a/todo1.0 +++ b/todo1.0 @@ -1,123 +1,62 @@ -ElementBase sub_items not subitem? - -*XMPP needs to use JID class instead of lots of fields. - -BaseXMPP set_jid, makeIqQuery, getjidresource, getjidbare not needed - -Why CamelCase and underscore_names? Document semantics. - -conn_tests and sleekxmpp/tests and sleekxmpp/xmlstresm/test.* -> convert to either unit tests, or at least put in same place - -Update setup.py - github url, version # - -scheduler needs unit tests - -ClientXMPP stream:features handler should use new state machine - -Write stream tests for startls, features, etc. - - - --- PEP8 - all files - -Need to use spaces - -Docstrings are lacking. Need to document attributes and return values. - -Organize imports - -Use absolute, not relative imports - -Fix one-liner if statements - -Line length limit of 79 characters - - - --- Plugins - ---- xep_0004 - -Need more unit tests - ---- xep_0009 - -Need stanza objects - -Need unit tests - ---- xep_0045 - -Need to use stanza objects - -A few TODO comments for checking roles and using defaults - -Need unit tests - ---- xep_0050 - -Need unit tests - -Need stanza objects - use new xep_0004 - ---- xep_0060 - -Need unit tests - -Need to use existing stanza objects - ---- xep_0078 - -Is it useful still? - -Need stanza objects/unit tests - ---- xep_0086 - -Is there a way to automate setting error codes? - -Seems like this should be part of the error stanza by default - -Use stanza objects - ---- xep_0092 - -Stanza objects - -Unit tests - ---- xep_0199 - -Stanza objects - -Unit tests - -Clean commented code - -Use the new scheduler - - - --- Documentation - -Document the Zen/Tao/Whatever of SleekXMPP to explain design goals and decisions - -Write architecture description - -XMPP:TDG needs to be rewritten. - -Need to update docs that reference old JID attributes of sleekxmpp objects - -Page describing new JID class - -Message page needs updating - -Iq page needs to be written - -Make guides to go with example.py and component_example.py - -Page on xmlstream.matchers - -Page on xmlstream.handlers, especially waiters - -Page on using xmlstream.scheduler +Plugins: + 0004 + PEP8 + Stream/Unit tests + Fix serialization issue + Use OrderedDict for fields/values + 0009 + Review contribution from dannmartens + 0012 + PEP8 + Documentation + Stream/Unit tests + 0030 + Done + 0033 + PEP8 + Documentation + Stream/Unit tests + 0045 + PEP8 + Documentation + Stream/Unit tests + 0050 + Review replacement in github.com/legastero/adhoc + 0059 + Done + 0060 + PEP8 + Documentation + Stream/Unit tests + 0078 + Will require new stream features handling, see stream_features branch. + PEP8 + Documentation + Stream/Unit tests + 0085 + PEP8 + Documentation + Stream/Unit tests + 0086 + PEP8 + Documentation + Consider any simplifications. + 0092 + Done + 0128 + Needs complete rewrite to work with new 0030 plugin. + 0199 + PEP8 + Documentation + Stream/Unit tests + Needs to use scheduler instead of its own thread. + 0202 + PEP8 + Documentation + Stream/Unit tests + 0249 + Review, minor cleanup + gmail_notify + PEP8 + Documentation + Stream/Unit tests From aa1996eba60229f5c843f8d4aae8421a80b41981 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 7 Feb 2011 10:18:15 -0500 Subject: [PATCH 09/26] Fixed failing tests from new XEP-0009 plugin --- sleekxmpp/plugins/xep_0009/binding.py | 2 +- tests/test_stanza_xep_0009.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py index 61ef146..700beb2 100644 --- a/sleekxmpp/plugins/xep_0009/binding.py +++ b/sleekxmpp/plugins/xep_0009/binding.py @@ -37,7 +37,7 @@ def xml2fault(params): def py2xml(*args): params = ET.Element("{%s}params" % _namespace) for x in args: - param = ET.Element("param") + param = ET.Element("{%s}param" % _namespace) param.append(_py2xml(x)) params.append(param) #... return params diff --git a/tests/test_stanza_xep_0009.py b/tests/test_stanza_xep_0009.py index f6ab7ed..6186dd9 100644 --- a/tests/test_stanza_xep_0009.py +++ b/tests/test_stanza_xep_0009.py @@ -6,9 +6,9 @@ See the file LICENSE for copying permission. """ -from sleekxmpp.plugins.old_0009 import py2xml from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, \ MethodResponse +from sleekxmpp.plugins.xep_0009.binding import py2xml from sleekxmpp.stanza.iq import Iq from sleekxmpp.test.sleektest import SleekTest from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin @@ -52,4 +52,4 @@ class TestJabberRPC(SleekTest): """, use_values=False) suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberRPC) - \ No newline at end of file + From 1ed06bebcdbe82e0c528c98134e381f5b4dcccad Mon Sep 17 00:00:00 2001 From: Stefan de Konink Date: Wed, 2 Feb 2011 22:05:52 +0800 Subject: [PATCH 10/26] This fixes the configuration stuff, because type is form not submit with setNodeConfiguration. --- sleekxmpp/plugins/xep_0004.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py index 5d41d26..5a49d70 100644 --- a/sleekxmpp/plugins/xep_0004.py +++ b/sleekxmpp/plugins/xep_0004.py @@ -57,6 +57,7 @@ class Form(ElementBase): return field def getXML(self, type='submit'): + self['type'] = type log.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py") return self.xml From 4b71fba64c8675aa891fd6a92eb6f0f045e4a48e Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Wed, 9 Feb 2011 09:45:45 +0800 Subject: [PATCH 11/26] Fix the xep_0009 import (no more relatives) Also, remove trailing spaces in all files of this plugin --- sleekxmpp/plugins/xep_0009/binding.py | 12 +- sleekxmpp/plugins/xep_0009/remote.py | 253 +++++++++--------- sleekxmpp/plugins/xep_0009/rpc.py | 88 +++--- sleekxmpp/plugins/xep_0009/stanza/RPC.py | 24 +- sleekxmpp/plugins/xep_0009/stanza/__init__.py | 2 +- 5 files changed, 180 insertions(+), 199 deletions(-) diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py index 700beb2..30f02d3 100644 --- a/sleekxmpp/plugins/xep_0009/binding.py +++ b/sleekxmpp/plugins/xep_0009/binding.py @@ -11,11 +11,9 @@ import base64 import logging import time - - log = logging.getLogger(__name__) -_namespace = 'jabber:iq:rpc' +_namespace = 'jabber:iq:rpc' def fault2xml(fault): value = dict() @@ -25,7 +23,7 @@ def fault2xml(fault): fault.append(_py2xml((value))) return fault -def xml2fault(params): +def xml2fault(params): vals = [] for value in params.findall('{%s}value' % _namespace): vals.append(_xml2py(value)) @@ -101,7 +99,7 @@ def xml2py(params): def _xml2py(value): namespace = 'jabber:iq:rpc' if value.find('{%s}nil' % namespace) is not None: - return None + return None if value.find('{%s}i4' % namespace) is not None: return int(value.find('{%s}i4' % namespace).text) if value.find('{%s}int' % namespace) is not None: @@ -131,7 +129,7 @@ def _xml2py(value): class rpcbase64(object): - + def __init__(self, data): #base 64 encoded string self.data = data @@ -148,7 +146,7 @@ class rpcbase64(object): class rpctime(object): - + def __init__(self,data=None): #assume string data is in iso format YYYYMMDDTHH:MM:SS if type(data) is str: diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py index bd931c0..ea867fd 100644 --- a/sleekxmpp/plugins/xep_0009/remote.py +++ b/sleekxmpp/plugins/xep_0009/remote.py @@ -16,19 +16,15 @@ import sys import threading import traceback - - log = logging.getLogger(__name__) - - def _intercept(method, name, public): def _resolver(instance, *args, **kwargs): log.debug("Locally calling %s.%s with arguments %s." % (instance.FQN(), method.__name__, args)) - try: + try: value = method(instance, *args, **kwargs) if value == NotImplemented: - raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__)) + raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__)) return value except InvocationException: raise @@ -43,50 +39,49 @@ def remote(function_argument, public = True): Decorator for methods which are remotely callable. This decorator works in conjunction with classes which extend ABC Endpoint. Example: - + @remote def remote_method(arg1, arg2) - + Arguments: function_argument -- a stand-in for either the actual method OR a new name (string) for the method. In that case the method is considered mapped: Example: - + @remote("new_name") def remote_method(arg1, arg2) - + public -- A flag which indicates if this method should be part of the known dictionary of remote methods. Defaults to True. Example: - + @remote(False) def remote_method(arg1, arg2) - + Note: renaming and revising (public vs. private) can be combined. Example: - + @remote("new_name", False) def remote_method(arg1, arg2) ''' if hasattr(function_argument, '__call__'): return _intercept(function_argument, None, public) - else: + else: if not isinstance(function_argument, basestring): - if not isinstance(function_argument, bool): + if not isinstance(function_argument, bool): raise Exception('Expected an RPC method name or visibility modifier!') else: def _wrap_revised(function): function = _intercept(function, None, function_argument) return function - return _wrap_revised + return _wrap_revised def _wrap_remapped(function): function = _intercept(function, function_argument, public) return function return _wrap_remapped - - - + + class ACL: ''' An Access Control List (ACL) is a list of rules, which are evaluated @@ -102,7 +97,7 @@ class ACL: [ (ACL.ALLOW, 'test@xmpp.org/unit', 'test.*'), (ACL.DENY, '*', '*') ] deny everyone everything, except named JID, which is allowed access to endpoint 'test' only. - + The use of wildcards is allowed in expressions, as follows: '*' everyone, or everything (= all endpoints and methods) 'test@xmpp.org/*' every JID regardless of JID resource @@ -113,7 +108,7 @@ class ACL: ''' ALLOW = True DENY = False - + @classmethod def check(cls, rules, jid, resource): if rules is None: @@ -121,9 +116,9 @@ class ACL: for rule in rules: policy = cls._check(rule, jid, resource) if policy is not None: - return policy + return policy return cls.DENY # By default if not rule matches, deny access. - + @classmethod def _check(cls, rule, jid, resource): if cls._match(jid, rule[1]) and cls._match(resource, rule[2]): @@ -138,13 +133,13 @@ class ACL: return '' else: if new_index == -1: - return expression[index : ] + return expression[index : ] else: return expression[index : new_index] @classmethod - def _match(cls, value, expression): - #! print "_match [VALUE] %s [EXPR] %s" % (value, expression) + def _match(cls, value, expression): + #! print "_match [VALUE] %s [EXPR] %s" % (value, expression) index = 0 position = 0 while index < len(expression): @@ -169,24 +164,23 @@ class ACL: ANY_ALL = [ (ACL.ALLOW, '*', '*') ] - class RemoteException(Exception): ''' Base exception for RPC. This exception is raised when a problem occurs in the network layer. ''' - + def __init__(self, message="", cause=None): ''' Initializes a new RemoteException. - + Arguments: message -- The message accompanying this exception. cause -- The underlying cause of this exception. - ''' + ''' self._message = message - self._cause = cause - pass + self._cause = cause + pass def __str__(self): return repr(self._message) @@ -202,7 +196,7 @@ class RemoteException(Exception): class InvocationException(RemoteException): ''' Exception raised when a problem occurs during the remote invocation - of a method. + of a method. ''' pass @@ -216,48 +210,45 @@ class AuthorizationException(RemoteException): pass - class TimeoutException(Exception): ''' Exception raised when the synchronous execution of a method takes longer than the given threshold because an underlying asynchronous reply did not arrive in time. ''' - pass - + pass class Callback(object): ''' - A base class for callback handlers. + A base class for callback handlers. ''' __metaclass__ = abc.ABCMeta - - + + @abc.abstractproperty def set_value(self, value): return NotImplemented - + @abc.abstractproperty def cancel_with_error(self, exception): return NotImplemented - class Future(Callback): ''' Represents the result of an asynchronous computation. ''' - + def __init__(self): ''' Initializes a new Future. ''' self._value = None - self._exception = None + self._exception = None self._event = threading.Event() pass - + def set_value(self, value): ''' Sets the value of this Future. Once the value is set, a caller @@ -265,13 +256,13 @@ class Future(Callback): ''' self._value = value self._event.set() - + def get_value(self, timeout=None): ''' Gets the value of this Future. This call will block until the result is available, or until an optional timeout expires. - When this Future is cancelled with an error, - + When this Future is cancelled with an error, + Arguments: timeout -- The maximum waiting time to obtain the value. ''' @@ -281,7 +272,7 @@ class Future(Callback): if not self._event.is_set(): raise TimeoutException return self._value - + def is_done(self): ''' Returns true if a value has been returned. @@ -290,23 +281,23 @@ class Future(Callback): def cancel_with_error(self, exception): ''' - Cancels the Future because of an error. Once cancelled, a + Cancels the Future because of an error. Once cancelled, a caller blocked on get_value will be able to continue. ''' - self._exception = exception - self._event.set() + self._exception = exception + self._event.set() + - class Endpoint(object): ''' The Endpoint class is an abstract base class for all objects participating in an RPC-enabled XMPP network. - + A user subclassing this class is required to implement the method: - FQN(self) - where FQN stands for Fully Qualified Name, an unambiguous name - which specifies which object an RPC call refers to. It is the + FQN(self) + where FQN stands for Fully Qualified Name, an unambiguous name + which specifies which object an RPC call refers to. It is the first part in a RPC method name '.'. ''' __metaclass__ = abc.ABCMeta @@ -317,38 +308,38 @@ class Endpoint(object): Initialize a new Endpoint. This constructor should never be invoked by a user, instead it will be called by the factories which instantiate the RPC-enabled objects, of which only - the classes are provided by the user. - + the classes are provided by the user. + Arguments: session -- An RPC session instance. target_jid -- the identity of the remote XMPP entity. ''' self.session = session self.target_jid = target_jid - + @abc.abstractproperty def FQN(self): return NotImplemented - + def get_methods(self): ''' Returns a dictionary of all RPC method names provided by this class. This method returns the actual method names as found in the class definition which have been decorated with: - + @remote def some_rpc_method(arg1, arg2) - - + + Unless: (1) the name has been remapped, in which case the new name will be returned. - + @remote("new_name") def some_rpc_method(arg1, arg2) - + (2) the method is set to hidden - + @remote(False) def some_hidden_method(arg1, arg2) ''' @@ -360,7 +351,7 @@ class Endpoint(object): result[test_attr._rpc_name] = test_attr except Exception: pass - return result + return result @@ -374,13 +365,13 @@ class Proxy(Endpoint): def __init__(self, endpoint, callback = None): ''' Initializes a new Proxy. - + Arguments: endpoint -- The endpoint which is proxified. ''' self._endpoint = endpoint self._callback = callback - + def __getattribute__(self, name, *args): if name in ('__dict__', '_endpoint', 'async', '_callback'): return object.__getattribute__(self, name) @@ -389,31 +380,30 @@ class Proxy(Endpoint): if hasattr(attribute, '__call__'): try: if attribute._rpc: - def _remote_call(*args, **kwargs): + def _remote_call(*args, **kwargs): log.debug("Remotely calling '%s.%s' with arguments %s." % (self._endpoint.FQN(), attribute._rpc_name, args)) return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs) return _remote_call except: - pass # If the attribute doesn't exist, don't care! + pass # If the attribute doesn't exist, don't care! return attribute - - def async(self, callback): + + def async(self, callback): return Proxy(self._endpoint, callback) - + def get_endpoint(self): ''' Returns the proxified endpoint. ''' return self._endpoint - + def FQN(self): return self._endpoint.FQN() - class JabberRPCEntry(object): - - + + def __init__(self, endpoint_FQN, call): self._endpoint_FQN = endpoint_FQN self._call = call @@ -424,28 +414,27 @@ class JabberRPCEntry(object): return return_value else: return self._return(return_value) - + def get_endpoint_FQN(self): return self._endpoint_FQN def _return(self, *args): return args - - - + + class RemoteSession(object): ''' - A context object for a Jabber-RPC session. + A context object for a Jabber-RPC session. ''' - - + + def __init__(self, client, session_close_callback): ''' Initializes a new RPC session. - + Arguments: client -- The SleekXMPP client associated with this session. - session_close_callback -- A callback called when the + session_close_callback -- A callback called when the session is closed. ''' self._client = client @@ -455,16 +444,16 @@ class RemoteSession(object): self._callbacks = {} self._acls = {} self._lock = RLock() - + def _wait(self): self._event.wait() - + def _notify(self, event): log.debug("RPC Session as %s started." % self._client.boundjid.full) self._client.sendPresence() self._event.set() pass - + def _register_call(self, endpoint, method, name=None): ''' Registers a method from an endpoint as remotely callable. @@ -487,8 +476,8 @@ class RemoteSession(object): def _register_callback(self, pid, callback): with self._lock: self._callbacks[pid] = callback - - def forget_callback(self, callback): + + def forget_callback(self, callback): with self._lock: pid = self._find_key(self._callbacks, callback) if pid is not None: @@ -496,7 +485,7 @@ class RemoteSession(object): else: raise ValueError("Unknown callback!") pass - + 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] @@ -504,7 +493,7 @@ class RemoteSession(object): return None else: return search[0] - + def _unregister_call(self, key): #removes the registered call with self._lock: @@ -512,18 +501,18 @@ class RemoteSession(object): del self._entries[key] else: raise ValueError() - + def new_proxy(self, target_jid, endpoint_cls): ''' Instantiates a new proxy object, which proxies to a remote - endpoint. This method uses a class reference without + endpoint. This method uses a class reference without constructor arguments to instantiate the proxy. - + Arguments: target_jid -- the XMPP entity ID hosting the endpoint. endpoint_cls -- The remote (duck) type. ''' - try: + try: argspec = inspect.getargspec(endpoint_cls.__init__) args = [None] * (len(argspec[0]) - 1) result = endpoint_cls(*args) @@ -531,7 +520,7 @@ class RemoteSession(object): return Proxy(result) except: traceback.print_exc(file=sys.stdout) - + def new_handler(self, acl, handler_cls, *args, **kwargs): ''' Instantiates a new handler object, which is called remotely @@ -539,7 +528,7 @@ class RemoteSession(object): implementing the remote method in the local endpoint class. The returned reference can be called locally and will behave as a regular instance. - + Arguments: acl -- Access control list (see ACL class) handler_clss -- The local (duck) type. @@ -556,11 +545,11 @@ class RemoteSession(object): Endpoint.__init__(result, self, self._client.boundjid.full) method_dict = result.get_methods() for method_name, method in method_dict.iteritems(): - #!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name) + #!!! 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) return result - + # def is_available(self, targetCls, pto): # return self._client.is_available(pto) @@ -571,19 +560,19 @@ class RemoteSession(object): future = Future() self._register_callback(pid, future) iq.send() - return future.get_value(30) + return future.get_value(30) else: print "[RemoteSession] _call_remote %s" % callback self._register_callback(pid, callback) iq.send() - + def close(self): ''' Closes this session. ''' self._client.disconnect(False) self._session_close_callback() - + def _on_jabber_rpc_method_call(self, iq): iq.enable('rpc_query') params = iq['rpc_query']['method_call']['params'] @@ -609,15 +598,15 @@ class RemoteSession(object): except AuthorizationException as ae: log.error(ae.get_message()) error = self._client.plugin['xep_0009']._forbidden(iq) - error.send() - except Exception as e: + error.send() + except Exception as e: if isinstance(e, KeyError): log.error("No handler available for %s!" % pmethod) error = self._client.plugin['xep_0009']._item_not_found(iq) else: traceback.print_exc(file=sys.stderr) log.error("An unexpected problem occurred invoking method %s!" % pmethod) - error = self._client.plugin['xep_0009']._undefined_condition(iq) + error = self._client.plugin['xep_0009']._undefined_condition(iq) #! print "[REMOTE.PY] _handle_remote_procedure_call AN ERROR SHOULD BE SENT NOW %s " % e error.send() @@ -632,7 +621,7 @@ class RemoteSession(object): callback.set_value(args[0]) else: callback.set_value(None) - pass + pass def _on_jabber_rpc_method_response2(self, iq): iq.enable('rpc_query') @@ -648,41 +637,40 @@ class RemoteSession(object): callback.set_value(args[0]) else: callback.set_value(None) - pass + pass def _on_jabber_rpc_method_fault(self, iq): iq.enable('rpc_query') fault = xml2fault(iq['rpc_query']['method_response']['fault']) - pid = iq['id'] + pid = iq['id'] with self._lock: callback = self._callbacks[pid] del self._callbacks[pid] e = { - 500: InvocationException + 500: InvocationException }[fault['code']](fault['string']) - callback.cancel_with_error(e) + callback.cancel_with_error(e) def _on_jabber_rpc_error(self, iq): pid = iq['id'] pmethod = self._client.plugin['xep_0009']._extract_method(iq['rpc_query']) - code = iq['error']['code'] + code = iq['error']['code'] type = iq['error']['type'] condition = iq['error']['condition'] #! print("['REMOTE.PY']._BINDING_handle_remote_procedure_error -> ERROR! ERROR! ERROR! Condition is '%s'" % condition) with self._lock: callback = self._callbacks[pid] del self._callbacks[pid] - e = { + e = { 'item-not-found': RemoteException("No remote handler available for %s at %s!" % (pmethod, iq['from'])), 'forbidden': AuthorizationException("Forbidden to invoke remote handler for %s at %s!" % (pmethod, iq['from'])), - 'undefined-condition': RemoteException("An unexpected problem occured trying to invoke %s at %s!" % (pmethod, iq['from'])), + 'undefined-condition': RemoteException("An unexpected problem occured trying to invoke %s at %s!" % (pmethod, iq['from'])), }[condition] if e is None: RemoteException("An unexpected exception occurred at %s!" % iq['from']) callback.cancel_with_error(e) - class Remote(object): ''' Bootstrap class for Jabber-RPC sessions. New sessions are openend @@ -691,18 +679,18 @@ class Remote(object): _instance = None _sessions = dict() _lock = threading.RLock() - + @classmethod def new_session_with_client(cls, client, callback=None): ''' Opens a new session with a given client. - + Arguments: client -- An XMPP client. callback -- An optional callback which can be used to track the starting state of the session. ''' - with Remote._lock: + with Remote._lock: if(client.boundjid.bare in cls._sessions): raise RemoteException("There already is a session associated with these credentials!") else: @@ -710,21 +698,21 @@ class Remote(object): def _session_close_callback(): with Remote._lock: del cls._sessions[client.boundjid.bare] - result = RemoteSession(client, _session_close_callback) + result = RemoteSession(client, _session_close_callback) client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_call', result._on_jabber_rpc_method_call) client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_response', result._on_jabber_rpc_method_response) - client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault) + client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_method_fault', result._on_jabber_rpc_method_fault) client.plugin['xep_0009'].xmpp.add_event_handler('jabber_rpc_error', result._on_jabber_rpc_error) if callback is None: - start_event_handler = result._notify + start_event_handler = result._notify else: - start_event_handler = callback - client.add_event_handler("session_start", start_event_handler) + start_event_handler = callback + client.add_event_handler("session_start", start_event_handler) if client.connect(): client.process(threaded=True) else: raise RemoteException("Could not connect to XMPP server!") - pass + pass if callback is None: result._wait() return result @@ -733,20 +721,19 @@ class Remote(object): def new_session(cls, jid, password, callback=None): ''' Opens a new session and instantiates a new XMPP client. - + Arguments: jid -- The XMPP JID for logging in. password -- The password for logging in. callback -- An optional callback which can be used to track - the starting state of the session. - ''' + the starting state of the session. + ''' client = sleekxmpp.ClientXMPP(jid, password) - #? Register plug-ins. + #? Register plug-ins. client.registerPlugin('xep_0004') # Data Forms client.registerPlugin('xep_0009') # Jabber-RPC client.registerPlugin('xep_0030') # Service Discovery client.registerPlugin('xep_0060') # PubSub - client.registerPlugin('xep_0199') # XMPP Ping - return cls.new_session_with_client(client, callback) + client.registerPlugin('xep_0199') # XMPP Ping + return cls.new_session_with_client(client, callback) - \ No newline at end of file diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py index 84afeb5..fc306d3 100644 --- a/sleekxmpp/plugins/xep_0009/rpc.py +++ b/sleekxmpp/plugins/xep_0009/rpc.py @@ -6,8 +6,8 @@ See the file LICENSE for copying permission. """ -from .. import base -from stanza.RPC import RPCQuery, MethodCall, MethodResponse +from sleekxmpp.plugins import base +from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse from sleekxmpp.stanza.iq import Iq from sleekxmpp.xmlstream.handler.callback import Callback from sleekxmpp.xmlstream.matcher.xpath import MatchXPath @@ -27,15 +27,15 @@ class xep_0009(base.base_plugin): self.xep = '0009' self.description = 'Jabber-RPC' #self.stanza = sleekxmpp.plugins.xep_0009.stanza - + register_stanza_plugin(Iq, RPCQuery) - register_stanza_plugin(RPCQuery, MethodCall) + register_stanza_plugin(RPCQuery, MethodCall) register_stanza_plugin(RPCQuery, MethodResponse) - + self.xmpp.registerHandler( 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( Callback('RPC Call', MatchXPath('{%s}iq/{%s}query/{%s}methodResponse' % (self.xmpp.default_ns, RPCQuery.namespace, RPCQuery.namespace)), self._handle_method_response) @@ -46,7 +46,7 @@ class xep_0009(base.base_plugin): ) self.xmpp.add_event_handler('jabber_rpc_method_call', self._on_jabber_rpc_method_call) self.xmpp.add_event_handler('jabber_rpc_method_response', self._on_jabber_rpc_method_response) - self.xmpp.add_event_handler('jabber_rpc_method_fault', self._on_jabber_rpc_method_fault) + self.xmpp.add_event_handler('jabber_rpc_method_fault', self._on_jabber_rpc_method_fault) self.xmpp.add_event_handler('jabber_rpc_error', self._on_jabber_rpc_error) self.xmpp.add_event_handler('error', self._handle_error) #self.activeCalls = [] @@ -54,7 +54,7 @@ class xep_0009(base.base_plugin): def post_init(self): base.base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc') - self.xmpp.plugin['xep_0030'].add_identity('automation','rpc') + self.xmpp.plugin['xep_0030'].add_identity('automation','rpc') def make_iq_method_call(self, pto, pmethod, params): iq = self.xmpp.makeIqSet() @@ -64,7 +64,7 @@ class xep_0009(base.base_plugin): iq['rpc_query']['method_call']['method_name'] = pmethod iq['rpc_query']['method_call']['params'] = params return iq; - + def make_iq_method_response(self, pid, pto, params): iq = self.xmpp.makeIqResult(pid) iq.attrib['to'] = pto @@ -78,7 +78,7 @@ class xep_0009(base.base_plugin): iq.attrib['to'] = pto iq.attrib['from'] = self.xmpp.boundjid.full iq.enable('rpc_query') - iq['rpc_query']['method_response']['params'] = None + iq['rpc_query']['method_response']['params'] = None iq['rpc_query']['method_response']['fault'] = params return iq @@ -100,58 +100,58 @@ class xep_0009(base.base_plugin): iq['error']['type'] = 'cancel' iq['error']['condition'] = 'item-not-found' return iq - + def _undefined_condition(self, iq): - payload = iq.get_payload() - iq.reply().error().set_payload(payload) + payload = iq.get_payload() + iq.reply().error().set_payload(payload) iq['error']['code'] = '500' iq['error']['type'] = 'cancel' iq['error']['condition'] = 'undefined-condition' - return iq + return iq def _forbidden(self, iq): payload = iq.get_payload() - iq.reply().error().set_payload(payload) + iq.reply().error().set_payload(payload) iq['error']['code'] = '403' iq['error']['type'] = 'auth' iq['error']['condition'] = 'forbidden' - return iq + return iq def _recipient_unvailable(self, iq): - payload = iq.get_payload() - iq.reply().error().set_payload(payload) + payload = iq.get_payload() + iq.reply().error().set_payload(payload) iq['error']['code'] = '404' iq['error']['type'] = 'wait' iq['error']['condition'] = 'recipient-unavailable' - return iq + return iq def _handle_method_call(self, iq): type = iq['type'] if type == 'set': log.debug("Incoming Jabber-RPC call from %s" % iq['from']) - self.xmpp.event('jabber_rpc_method_call', iq) + self.xmpp.event('jabber_rpc_method_call', iq) else: if type == 'error' and ['rpc_query'] is None: self.handle_error(iq) else: - log.debug("Incoming Jabber-RPC error from %s" % iq['from']) - self.xmpp.event('jabber_rpc_error', iq) - + log.debug("Incoming Jabber-RPC error from %s" % iq['from']) + self.xmpp.event('jabber_rpc_error', iq) + def _handle_method_response(self, iq): if iq['rpc_query']['method_response']['fault'] is not None: - log.debug("Incoming Jabber-RPC fault from %s" % iq['from']) + log.debug("Incoming Jabber-RPC fault from %s" % iq['from']) #self._on_jabber_rpc_method_fault(iq) self.xmpp.event('jabber_rpc_method_fault', iq) else: - log.debug("Incoming Jabber-RPC response from %s" % iq['from']) - self.xmpp.event('jabber_rpc_method_response', iq) - + log.debug("Incoming Jabber-RPC response from %s" % iq['from']) + self.xmpp.event('jabber_rpc_method_response', iq) + def _handle_error(self, iq): print("['XEP-0009']._handle_error -> ERROR! Iq is '%s'" % iq) print("#######################") - print("### NOT IMPLEMENTED ###") + print("### NOT IMPLEMENTED ###") print("#######################") - + def _on_jabber_rpc_method_call(self, iq, forwarded=False): """ A default handler for Jabber-RPC method call. If another @@ -161,7 +161,7 @@ class xep_0009(base.base_plugin): forwarded set to True, then it will run as normal. """ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_call') > 1: - return + return # Reply with error by default error = self.client.plugin['xep_0009']._item_not_found(iq) error.send() @@ -175,7 +175,7 @@ class xep_0009(base.base_plugin): forwarded set to True, then it will run as normal. """ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_response') > 1: - return + return error = self.client.plugin['xep_0009']._recpient_unavailable(iq) error.send() @@ -186,12 +186,12 @@ class xep_0009(base.base_plugin): If this handler is called by your own custom handler with forwarded set to True, then it will run as normal. - """ + """ if not forwarded and self.xmpp.event_handled('jabber_rpc_method_fault') > 1: - return + return error = self.client.plugin['xep_0009']._recpient_unavailable(iq) error.send() - + def _on_jabber_rpc_error(self, iq, forwarded=False): """ A default handler for Jabber-RPC error response. If another @@ -199,23 +199,23 @@ class xep_0009(base.base_plugin): If this handler is called by your own custom handler with forwarded set to True, then it will run as normal. - """ + """ if not forwarded and self.xmpp.event_handled('jabber_rpc_error') > 1: - return + return error = self.client.plugin['xep_0009']._recpient_unavailable(iq, iq.get_payload()) error.send() - - def _send_fault(self, iq, fault_xml): # + + def _send_fault(self, iq, fault_xml): # fault = self.make_iq_method_response_fault(iq['id'], iq['from'], fault_xml) - fault.send() - + fault.send() + def _send_error(self, iq): - print("['XEP-0009']._send_error -> ERROR! Iq is '%s'" % iq) + print("['XEP-0009']._send_error -> ERROR! Iq is '%s'" % iq) print("#######################") - print("### NOT IMPLEMENTED ###") + print("### NOT IMPLEMENTED ###") print("#######################") - + def _extract_method(self, stanza): xml = ET.fromstring("%s" % stanza) return xml.find("./methodCall/methodName").text - \ No newline at end of file + diff --git a/sleekxmpp/plugins/xep_0009/stanza/RPC.py b/sleekxmpp/plugins/xep_0009/stanza/RPC.py index 24f2efd..3d1c77a 100644 --- a/sleekxmpp/plugins/xep_0009/stanza/RPC.py +++ b/sleekxmpp/plugins/xep_0009/stanza/RPC.py @@ -14,55 +14,51 @@ class RPCQuery(ElementBase): name = 'query' namespace = 'jabber:iq:rpc' plugin_attrib = 'rpc_query' - interfaces = set(()) + interfaces = set(()) subinterfaces = set(()) plugin_attrib_map = {} plugin_tag_map = {} - class MethodCall(ElementBase): name = 'methodCall' namespace = 'jabber:iq:rpc' plugin_attrib = 'method_call' - interfaces = set(('method_name', 'params')) + interfaces = set(('method_name', 'params')) subinterfaces = set(()) plugin_attrib_map = {} - plugin_tag_map = {} - + plugin_tag_map = {} def get_method_name(self): return self._get_sub_text('methodName') def set_method_name(self, value): return self._set_sub_text('methodName', value) - + def get_params(self): return self.xml.find('{%s}params' % self.namespace) - + def set_params(self, params): self.append(params) - - + class MethodResponse(ElementBase): name = 'methodResponse' namespace = 'jabber:iq:rpc' plugin_attrib = 'method_response' - interfaces = set(('params', 'fault')) + interfaces = set(('params', 'fault')) subinterfaces = set(()) plugin_attrib_map = {} plugin_tag_map = {} - - + def get_params(self): return self.xml.find('{%s}params' % self.namespace) - + def set_params(self, params): self.append(params) def get_fault(self): return self.xml.find('{%s}fault' % self.namespace) - + def set_fault(self, fault): self.append(fault) diff --git a/sleekxmpp/plugins/xep_0009/stanza/__init__.py b/sleekxmpp/plugins/xep_0009/stanza/__init__.py index 0b90223..5dcbf33 100644 --- a/sleekxmpp/plugins/xep_0009/stanza/__init__.py +++ b/sleekxmpp/plugins/xep_0009/stanza/__init__.py @@ -6,4 +6,4 @@ See the file LICENSE for copying permission. """ -from RPC import RPCQuery, MethodCall, MethodResponse +from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse From 72ead3d598394fd478097de48cbef49800220ea2 Mon Sep 17 00:00:00 2001 From: Florent Le Coz Date: Wed, 9 Feb 2011 09:48:45 +0800 Subject: [PATCH 12/26] Replace the print statement by a log.debug call This print syntax is deprecated in python3, so the plugin was working only with python2 --- sleekxmpp/plugins/xep_0009/remote.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py index ea867fd..8c53411 100644 --- a/sleekxmpp/plugins/xep_0009/remote.py +++ b/sleekxmpp/plugins/xep_0009/remote.py @@ -562,7 +562,7 @@ class RemoteSession(object): iq.send() return future.get_value(30) else: - print "[RemoteSession] _call_remote %s" % callback + log.debug("[RemoteSession] _call_remote %s" % callback) self._register_callback(pid, callback) iq.send() From 30da68f47bb35e67739063bea47679b9a777b16b Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Tue, 8 Feb 2011 21:09:49 -0500 Subject: [PATCH 13/26] Update XEP-0060 test. --- tests/test_stanza_xep_0060.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_stanza_xep_0060.py b/tests/test_stanza_xep_0060.py index 5d45523..8e6e820 100644 --- a/tests/test_stanza_xep_0060.py +++ b/tests/test_stanza_xep_0060.py @@ -181,7 +181,7 @@ class TestPubsubStanzas(SleekTest): - + this thing is awesome From 145f577bde699f9f732348983d55169bbdaf733e Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 9 Feb 2011 08:58:00 -0500 Subject: [PATCH 14/26] Fix get_items default behaviour. --- sleekxmpp/plugins/xep_0030/disco.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index a976b98..26d6ec3 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -318,7 +318,7 @@ class xep_0030(base_plugin): return self.xmpp['xep_0059'].iterate(iq, 'disco_items') else: return iq.send(timeout=kwargs.get('timeout', None), - block=kwargs.get('block', None), + block=kwargs.get('block', True), callback=kwargs.get('callback', None)) def set_items(self, jid=None, node=None, **kwargs): From 13a01beb07329e3d87cdd53d167edd137b310668 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 9 Feb 2011 09:12:44 -0500 Subject: [PATCH 15/26] Fix same error for get_info default behaviour. --- sleekxmpp/plugins/xep_0030/disco.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 26d6ec3..96aac69 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -271,7 +271,7 @@ class xep_0030(base_plugin): iq['type'] = 'get' iq['disco_info']['node'] = node if node else '' return iq.send(timeout=kwargs.get('timeout', None), - block=kwargs.get('block', None), + block=kwargs.get('block', True), callback=kwargs.get('callback', None)) def get_items(self, jid=None, node=None, local=False, **kwargs): From 3463bf46c65f091f42643bc3f777ac05620192b6 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Thu, 10 Feb 2011 13:45:35 -0800 Subject: [PATCH 16/26] added option to return false on ping error, added ping example --- examples/ping.py | 134 ++++++++++++++++++++++++++++++++++ sleekxmpp/plugins/xep_0199.py | 4 +- 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100755 examples/ping.py diff --git a/examples/ping.py b/examples/ping.py new file mode 100755 index 0000000..a476e1d --- /dev/null +++ b/examples/ping.py @@ -0,0 +1,134 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import sys +import logging +import time +from optparse import OptionParser + +import sleekxmpp + +# Python versions before 3.0 do not use UTF-8 encoding +# by default. To ensure that Unicode is handled properly +# throughout SleekXMPP, we will set the default encoding +# ourselves to UTF-8. +if sys.version_info < (3, 0): + reload(sys) + sys.setdefaultencoding('utf8') + + +class PingTest(sleekxmpp.ClientXMPP): + + """ + A simple SleekXMPP bot that will echo messages it + receives, along with a short thank you message. + """ + + def __init__(self, jid, password, pingjid): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + if pingjid is None: + pingjid = self.jid + self.pingjid = pingjid + + # The session_start event will be triggered when + # the bot establishes its connection with the server + # and the XML streams are ready for use. We want to + # listen for this event so that we we can intialize + # our roster. + self.add_event_handler("session_start", self.start) + + def start(self, event): + """ + Process the session_start event. + + Typical actions for the session_start event are + requesting the roster and broadcasting an intial + presence stanza. + + Arguments: + event -- An empty dictionary. The session_start + event does not provide any additional + data. + """ + self.sendPresence() + result = self.plugin['xep_0199'].sendPing(self.pingjid, timeout=10, errorfalse=True) + logging.info("Pinging...") + if result is False: + logging.info("Couldn't ping.") + self.disconnect() + sys.exit(1) + else: + logging.info("Success!") + self.disconnect() + + +if __name__ == '__main__': + # Setup the command line arguments. + optp = OptionParser() + + # Output verbosity options. + optp.add_option('-q', '--quiet', help='set logging to ERROR', + action='store_const', dest='loglevel', + const=logging.ERROR, default=logging.INFO) + optp.add_option('-d', '--debug', help='set logging to DEBUG', + action='store_const', dest='loglevel', + const=logging.DEBUG, default=logging.INFO) + optp.add_option('-v', '--verbose', help='set logging to COMM', + action='store_const', dest='loglevel', + const=5, default=logging.INFO) + optp.add_option('-t', '--pingto', help='set jid to ping', + action='store', type='string', dest='pingjid', default=None) + + # JID and password options. + optp.add_option("-j", "--jid", dest="jid", + help="JID to use") + optp.add_option("-p", "--password", dest="password", + help="password to use") + + opts, args = optp.parse_args() + + # Setup logging. + logging.basicConfig(level=opts.loglevel, + format='%(levelname)-8s %(message)s') + + if None in [opts.jid, opts.password]: + optp.print_help() + sys.exit(1) + + # Setup the PingTest and register plugins. Note that while plugins may + # have interdependencies, the order in which you register them does + # not matter. + xmpp = PingTest(opts.jid, opts.password, opts.pingjid) + xmpp.registerPlugin('xep_0030') # Service Discovery + xmpp.registerPlugin('xep_0004') # Data Forms + xmpp.registerPlugin('xep_0060') # PubSub + xmpp.registerPlugin('xep_0199') # XMPP Ping + + # If you are working with an OpenFire server, you may need + # to adjust the SSL version used: + # xmpp.ssl_version = ssl.PROTOCOL_SSLv3 + + # If you want to verify the SSL certificates offered by a server: + # xmpp.ca_certs = "path/to/ca/cert" + + # Connect to the XMPP server and start processing XMPP stanzas. + if xmpp.connect(): + # If you do not have the pydns library installed, you will need + # to manually specify the name of the server if it does not match + # the one in the JID. For example, to use Google Talk you would + # need to use: + # + # if xmpp.connect(('talk.google.com', 5222)): + # ... + xmpp.process(threaded=False) + print("Done") + else: + print("Unable to connect.") diff --git a/sleekxmpp/plugins/xep_0199.py b/sleekxmpp/plugins/xep_0199.py index 16e79e2..e7ec5c4 100644 --- a/sleekxmpp/plugins/xep_0199.py +++ b/sleekxmpp/plugins/xep_0199.py @@ -42,7 +42,7 @@ class xep_0199(base.base_plugin): iq.attrib['to'] = xml.get('from', self.xmpp.boundjid.domain) self.xmpp.send(iq) - def sendPing(self, jid, timeout = 30): + def sendPing(self, jid, timeout = 30, errorfalse=False): """ sendPing(jid, timeout) Sends a ping to the specified jid, returning the time (in seconds) to receive a reply, or None if no reply is received in timeout seconds. @@ -57,7 +57,7 @@ class xep_0199(base.base_plugin): #pingresult = self.xmpp.send(iq, self.xmpp.makeIq(id), timeout) pingresult = iq.send() endTime = time.clock() - if pingresult == False: + if pingresult == False or (errorfalse and pingresult['type'] == 'error'): #self.xmpp.disconnect(reconnect=True) return False return endTime - startTime From c4b1212c44e0758c6361ca46c6c3a90e27ac876f Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 11 Feb 2011 00:30:45 -0500 Subject: [PATCH 17/26] Updated XEP-0199 plugin. Now has docs and uses the new plugin format. --- examples/ping.py | 23 ++-- setup.py | 1 + sleekxmpp/plugins/xep_0199.py | 63 ---------- sleekxmpp/plugins/xep_0199/__init__.py | 10 ++ sleekxmpp/plugins/xep_0199/ping.py | 162 +++++++++++++++++++++++++ sleekxmpp/plugins/xep_0199/stanza.py | 36 ++++++ 6 files changed, 222 insertions(+), 73 deletions(-) delete mode 100644 sleekxmpp/plugins/xep_0199.py create mode 100644 sleekxmpp/plugins/xep_0199/__init__.py create mode 100644 sleekxmpp/plugins/xep_0199/ping.py create mode 100644 sleekxmpp/plugins/xep_0199/stanza.py diff --git a/examples/ping.py b/examples/ping.py index a476e1d..70066e3 100755 --- a/examples/ping.py +++ b/examples/ping.py @@ -28,15 +28,15 @@ if sys.version_info < (3, 0): class PingTest(sleekxmpp.ClientXMPP): """ - A simple SleekXMPP bot that will echo messages it - receives, along with a short thank you message. + A simple SleekXMPP bot that will send a ping request + to a given JID. """ def __init__(self, jid, password, pingjid): sleekxmpp.ClientXMPP.__init__(self, jid, password) if pingjid is None: pingjid = self.jid - self.pingjid = pingjid + self.pingjid = pingjid # The session_start event will be triggered when # the bot establishes its connection with the server @@ -59,14 +59,16 @@ class PingTest(sleekxmpp.ClientXMPP): data. """ self.sendPresence() - result = self.plugin['xep_0199'].sendPing(self.pingjid, timeout=10, errorfalse=True) + result = self['xep_0199'].send_ping(self.pingjid, + timeout=10, + errorfalse=True) logging.info("Pinging...") if result is False: logging.info("Couldn't ping.") self.disconnect() sys.exit(1) else: - logging.info("Success!") + logging.info("Success! RTT: %s" % str(result)) self.disconnect() @@ -85,7 +87,8 @@ if __name__ == '__main__': action='store_const', dest='loglevel', const=5, default=logging.INFO) optp.add_option('-t', '--pingto', help='set jid to ping', - action='store', type='string', dest='pingjid', default=None) + action='store', type='string', dest='pingjid', + default=None) # JID and password options. optp.add_option("-j", "--jid", dest="jid", @@ -107,10 +110,10 @@ if __name__ == '__main__': # have interdependencies, the order in which you register them does # not matter. xmpp = PingTest(opts.jid, opts.password, opts.pingjid) - xmpp.registerPlugin('xep_0030') # Service Discovery - xmpp.registerPlugin('xep_0004') # Data Forms - xmpp.registerPlugin('xep_0060') # PubSub - xmpp.registerPlugin('xep_0199') # XMPP Ping + xmpp.register_plugin('xep_0030') # Service Discovery + xmpp.register_plugin('xep_0004') # Data Forms + xmpp.register_plugin('xep_0060') # PubSub + xmpp.register_plugin('xep_0199') # XMPP Ping # If you are working with an OpenFire server, you may need # to adjust the SSL version used: diff --git a/setup.py b/setup.py index d6d8d6d..ae8cf68 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0030/stanza', 'sleekxmpp/plugins/xep_0059', 'sleekxmpp/plugins/xep_0092', + 'sleekxmpp/plugins/xep_0199', ] if sys.version_info < (3, 0): diff --git a/sleekxmpp/plugins/xep_0199.py b/sleekxmpp/plugins/xep_0199.py deleted file mode 100644 index e7ec5c4..0000000 --- a/sleekxmpp/plugins/xep_0199.py +++ /dev/null @@ -1,63 +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 xml.etree import cElementTree as ET -from . import base -import time -import logging - - -log = logging.getLogger(__name__) - - -class xep_0199(base.base_plugin): - """XEP-0199 XMPP Ping""" - - def plugin_init(self): - self.description = "XMPP Ping" - self.xep = "0199" - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_ping, name='XMPP Ping') - if self.config.get('keepalive', True): - self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True) - - def post_init(self): - base.base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:ping') - - def handler_pingserver(self, xml): - self.xmpp.schedule("xep-0119 ping", float(self.config.get('frequency', 300)), self.scheduled_ping, repeat=True) - - def scheduled_ping(self): - log.debug("pinging...") - if self.sendPing(self.xmpp.boundjid.host, self.config.get('timeout', 30)) is False: - log.debug("Did not recieve ping back in time. Requesting Reconnect.") - self.xmpp.reconnect() - - def handler_ping(self, xml): - iq = self.xmpp.makeIqResult(xml.get('id', 'unknown')) - iq.attrib['to'] = xml.get('from', self.xmpp.boundjid.domain) - self.xmpp.send(iq) - - def sendPing(self, jid, timeout = 30, errorfalse=False): - """ sendPing(jid, timeout) - Sends a ping to the specified jid, returning the time (in seconds) - to receive a reply, or None if no reply is received in timeout seconds. - """ - id = self.xmpp.getNewId() - iq = self.xmpp.makeIq(id) - iq.attrib['type'] = 'get' - iq.attrib['to'] = jid - ping = ET.Element('{urn:xmpp:ping}ping') - iq.append(ping) - startTime = time.clock() - #pingresult = self.xmpp.send(iq, self.xmpp.makeIq(id), timeout) - pingresult = iq.send() - endTime = time.clock() - if pingresult == False or (errorfalse and pingresult['type'] == 'error'): - #self.xmpp.disconnect(reconnect=True) - return False - return endTime - startTime diff --git a/sleekxmpp/plugins/xep_0199/__init__.py b/sleekxmpp/plugins/xep_0199/__init__.py new file mode 100644 index 0000000..3444fe9 --- /dev/null +++ b/sleekxmpp/plugins/xep_0199/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0199.stanza import Ping +from sleekxmpp.plugins.xep_0199.ping import xep_0199 diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py new file mode 100644 index 0000000..cde2f82 --- /dev/null +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -0,0 +1,162 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import time +import logging + +import sleekxmpp +from sleekxmpp import Iq +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins.xep_0199 import stanza, Ping + + +log = logging.getLogger(__name__) + + +class xep_0199(base_plugin): + + """ + XEP-0199: XMPP Ping + + Given that XMPP is based on TCP connections, it is possible for the + underlying connection to be terminated without the application's + awareness. Ping stanzas provide an alternative to whitespace based + keepalive methods for detecting lost connections. + + Also see . + + Attributes: + 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. + Defaults to 300 seconds. + timeout -- Time in seconds to wait for a ping response. + Defaults to 30 seconds. + Methods: + send_ping -- Send a ping to a given JID, returning the + round trip time. + """ + + def plugin_init(self): + """ + Start the XEP-0199 plugin. + """ + self.description = 'XMPP Ping' + self.xep = '0199' + self.stanza = stanza + + # Backwards compatibility for names + self.sendPing = self.send_ping + + self.keepalive = self.config.get('keepalive', True) + self.frequency = float(self.config.get('frequency', 300)) + self.timeout = self.config.get('timeout', 30) + + register_stanza_plugin(Iq, Ping) + + self.xmpp.register_handler( + Callback('Ping', + StanzaPath('iq@type=get/ping'), + self._handle_ping)) + + if self.keepalive: + self.xmpp.add_event_handler('session_start', + self._handle_keepalive, + threaded=True) + + def post_init(self): + """Handle cross-plugin dependencies.""" + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature(Ping.namespace) + + def _handle_keepalive(self, event): + """ + 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. + + Arguments: + event -- The session_start event. + """ + def scheduled_ping(): + """Send ping request to the server.""" + log.debug("Pinging...") + resp = self.send_ping(self.xmpp.boundjid.host, self.timeout) + if not resp: + log.debug("Did not recieve ping back in time." + \ + "Requesting Reconnect.") + self.xmpp.reconnect() + + self.xmpp.schedule('Ping Keep Alive', + self.frequency, + scheduled_ping, + repeat=True) + + def _handle_ping(self, iq): + """ + Automatically reply to ping requests. + + Arguments: + iq -- The ping request. + """ + log.debug("Pinged by %s" % iq['from']) + iq.reply().enable('ping').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. + + 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. + 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: + timeout = self.timeout + + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['to'] = jid + if ifrom: + iq['from'] = ifrom + iq.enable('ping') + + start_time = time.clock() + resp = iq.send(block=block, + timeout=timeout, + callback=callback) + end_time = time.clock() + + delay = end_time - start_time + + if not block: + return None + + if not resp or resp['type'] == 'error': + return False + + log.debug("Pong: %s %f" % (jid, delay)) + return delay diff --git a/sleekxmpp/plugins/xep_0199/stanza.py b/sleekxmpp/plugins/xep_0199/stanza.py new file mode 100644 index 0000000..6586a76 --- /dev/null +++ b/sleekxmpp/plugins/xep_0199/stanza.py @@ -0,0 +1,36 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import sleekxmpp +from sleekxmpp.xmlstream import ElementBase + + +class Ping(ElementBase): + + """ + Given that XMPP is based on TCP connections, it is possible for the + underlying connection to be terminated without the application's + awareness. Ping stanzas provide an alternative to whitespace based + keepalive methods for detecting lost connections. + + Example ping stanza: + + + + + Stanza Interface: + None + + Methods: + None + """ + + name = 'ping' + namespace = 'urn:xmpp:ping' + plugin_attrib = 'ping' + interfaces = set() From 0d326383799a7d7bb69fec9dcd1eaf9e1a64eab8 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Fri, 11 Feb 2011 15:20:26 -0500 Subject: [PATCH 18/26] XMPPError exceptions can keep a stanza's contents. This allows exceptions to include the original content of a stanza in the error response by including the parameter clear=False when raising the exception. --- sleekxmpp/exceptions.py | 7 +++++- sleekxmpp/stanza/iq.py | 8 +++++-- sleekxmpp/stanza/message.py | 6 +++-- sleekxmpp/stanza/presence.py | 8 +++++-- sleekxmpp/stanza/rootstanza.py | 3 ++- sleekxmpp/xmlstream/stanzabase.py | 14 ++++++++---- tests/test_stream_exceptions.py | 37 +++++++++++++++++++++++++++++++ 7 files changed, 71 insertions(+), 12 deletions(-) diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py index d3988b4..4727f0c 100644 --- a/sleekxmpp/exceptions.py +++ b/sleekxmpp/exceptions.py @@ -21,7 +21,8 @@ class XMPPError(Exception): """ def __init__(self, condition='undefined-condition', text=None, etype=None, - extension=None, extension_ns=None, extension_args=None): + extension=None, extension_ns=None, extension_args=None, + clear=True): """ Create a new XMPPError exception. @@ -37,6 +38,9 @@ class XMPPError(Exception): extension_args -- Content and attributes for the extension element. Same as the additional arguments to the ET.Element constructor. + clear -- Indicates if the stanza's contents should be + removed before replying with an error. + Defaults to True. """ if extension_args is None: extension_args = {} @@ -44,6 +48,7 @@ class XMPPError(Exception): self.condition = condition self.text = text self.etype = etype + self.clear = clear self.extension = extension self.extension_ns = extension_ns self.extension_args = extension_args diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index c6aa64d..841d282 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -144,7 +144,7 @@ class Iq(RootStanza): self.xml.remove(child) return self - def reply(self): + def reply(self, clear=True): """ Send a reply stanza. @@ -152,9 +152,13 @@ class Iq(RootStanza): Sets the 'type' to 'result' in addition to the default StanzaBase.reply behavior. + + Arguments: + clear -- Indicates if existing content should be + removed before replying. Defaults to True. """ self['type'] = 'result' - StanzaBase.reply(self) + StanzaBase.reply(self, clear) return self def send(self, block=True, timeout=None, callback=None): diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py index 66c74d8..6f0cf21 100644 --- a/sleekxmpp/stanza/message.py +++ b/sleekxmpp/stanza/message.py @@ -104,7 +104,7 @@ class Message(RootStanza): self['type'] = 'normal' return self - def reply(self, body=None): + def reply(self, body=None, clear=True): """ Create a message reply. @@ -114,7 +114,9 @@ class Message(RootStanza): adds a message body if one is given. Arguments: - body -- Optional text content for the message. + body -- Optional text content for the message. + clear -- Indicates if existing content should be removed + before replying. Defaults to True. """ StanzaBase.reply(self) if self['type'] == 'groupchat': diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py index 7dcd8f9..60dddf6 100644 --- a/sleekxmpp/stanza/presence.py +++ b/sleekxmpp/stanza/presence.py @@ -173,14 +173,18 @@ class Presence(RootStanza): # The priority is not a number: we consider it 0 as a default return 0 - def reply(self): + def reply(self, clear=True): """ Set the appropriate presence reply type. Overrides StanzaBase.reply. + + Arguments: + clear -- Indicates if the stanza contents should be removed + before replying. Defaults to True. """ if self['type'] == 'unsubscribe': self['type'] = 'unsubscribed' elif self['type'] == 'subscribe': self['type'] = 'subscribed' - return StanzaBase.reply(self) + return StanzaBase.reply(self, clear) diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index 8123c5f..bc11476 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -43,8 +43,8 @@ class RootStanza(StanzaBase): Arguments: e -- Exception object """ - self.reply() if isinstance(e, XMPPError): + self.reply(clear=e.clear) # We raised this deliberately self['error']['condition'] = e.condition self['error']['text'] = e.text @@ -56,6 +56,7 @@ class RootStanza(StanzaBase): self['error']['type'] = e.etype self.send() else: + self.reply() # We probably didn't raise this on purpose, so send an error stanza self['error']['condition'] = 'undefined-condition' self['error']['text'] = "SleekXMPP got into trouble." diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 3937a7a..1f229ce 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -1161,12 +1161,17 @@ class StanzaBase(ElementBase): self.clear() return self - def reply(self): + def reply(self, clear=True): """ - Reset the stanza and swap its 'from' and 'to' attributes to prepare - for sending a reply stanza. + Swap the 'from' and 'to' attributes to prepare the stanza for + sending a reply. If clear=True, then also remove the stanza's + contents to make room for the reply content. For client streams, the 'from' attribute is removed. + + Arguments: + clear -- Indicates if the stanza's contents should be + removed. Defaults to True """ # if it's a component, use from if self.stream and hasattr(self.stream, "is_component") and \ @@ -1175,7 +1180,8 @@ class StanzaBase(ElementBase): else: self['to'] = self['from'] del self['from'] - self.clear() + if clear: + self.clear() return self def error(self): diff --git a/tests/test_stream_exceptions.py b/tests/test_stream_exceptions.py index e1b70d3..a4598a1 100644 --- a/tests/test_stream_exceptions.py +++ b/tests/test_stream_exceptions.py @@ -1,5 +1,7 @@ import sys import sleekxmpp +from sleekxmpp.xmlstream.matcher import MatchXPath +from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.exceptions import XMPPError from sleekxmpp.test import * @@ -46,6 +48,41 @@ class TestStreamExceptions(SleekTest): """, use_values=False) + def testIqErrorException(self): + """Test using error exceptions with Iq stanzas.""" + + def handle_iq(iq): + raise XMPPError(condition='feature-not-implemented', + text="We don't do things that way here.", + etype='cancel', + clear=False) + + self.stream_start() + self.xmpp.register_handler( + Callback( + 'Test Iq', + MatchXPath('{%s}iq/{test}query' % self.xmpp.default_ns), + handle_iq)) + + self.recv(""" + + + + """) + + self.send(""" + + + + + + We don't do things that way here. + + + + """, use_values=False) + def testThreadedXMPPErrorException(self): """Test raising an XMPPError exception in a threaded handler.""" From ca2b4a188ac31d1bdf45ec244c950f7675414b38 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sat, 12 Feb 2011 11:01:43 -0500 Subject: [PATCH 19/26] Return the registered callback when using iq.send(callback=foo). Allows for a callback to be canceled by unregistering the returned handler. --- sleekxmpp/stanza/iq.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index 841d282..7a8b997 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -195,7 +195,7 @@ class Iq(RootStanza): once=True) self.stream.register_handler(handler) StanzaBase.send(self) - return None + return handler elif block and self['type'] in ('get', 'set'): waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) self.stream.register_handler(waitfor) From 70af52d74c3f4253791120be4016f108aa966b4d Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 13 Feb 2011 16:28:06 -0500 Subject: [PATCH 20/26] Make one-off Callbacks ready for deletion after the prerun step. Waiting until the actual run step means that the handler is not marked for deletion when checked in the __spawn_event() thread, causing the callback to stay in the handler list. --- sleekxmpp/xmlstream/handler/callback.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index f0a7285..cfd3c43 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -62,6 +62,8 @@ class Callback(BaseHandler): payload -- The matched stanza object. """ BaseHandler.prerun(self, payload) + if self._once: + self._destroy = True if self._instream: self.run(payload, True) From 34f6195ca5a042ac61aa20af0b339a6654e2ffdd Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 13 Feb 2011 16:30:57 -0500 Subject: [PATCH 21/26] Return the name of the registered callback. Instead of the actual callback object, return just the name of the callback object created when using iq.send(callback=..). This will help prevent memory leaks by not keeping an additional reference to the object, but still allows for the callback to be canceled by using self.remove_handler("handler_name"). --- sleekxmpp/stanza/iq.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index 7a8b997..330df6c 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -189,13 +189,14 @@ class Iq(RootStanza): if timeout is None: timeout = self.stream.response_timeout if callback is not None and self['type'] in ('get', 'set'): - handler = Callback('IqCallback_%s' % self['id'], + handler_name = 'IqCallback_%s' % self['id'] + handler = Callback(handler_name, MatcherId(self['id']), callback, once=True) self.stream.register_handler(handler) StanzaBase.send(self) - return handler + return handler_name elif block and self['type'] in ('get', 'set'): waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) self.stream.register_handler(waitfor) From 8b5511c7ec036aef6a243c3c5e801cdaa7a4863a Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Sun, 13 Feb 2011 16:40:04 -0500 Subject: [PATCH 22/26] Simplification when removing a deletable handler. --- sleekxmpp/xmlstream/xmlstream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 1cd23fb..560b80a 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -845,7 +845,7 @@ class XMLStream(object): self.event_queue.put(('stanza', handler, stanza_copy)) try: if handler.check_delete(): - self.__handlers.pop(self.__handlers.index(handler)) + self.__handlers.remove(handler) except: pass # not thread safe unhandled = False From 9004e8bbf2c29d23fad20467791d9a8224d4bdb3 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 14 Feb 2011 11:13:41 -0500 Subject: [PATCH 23/26] Break references that can prevent garbage collection. --- sleekxmpp/xmlstream/handler/callback.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index cfd3c43..f1fda80 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -84,3 +84,4 @@ class Callback(BaseHandler): self._pointer(payload) if self._once: self._destroy = True + del self._pointer From e0f9025e7c6fe51243222aeb684b94eda1a2be5f Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 14 Feb 2011 11:30:04 -0500 Subject: [PATCH 24/26] More attempts at fixing garbage collection. Don't keep a reference to stanzas in Callback objects. --- sleekxmpp/xmlstream/handler/callback.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index f1fda80..7fadab4 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -61,7 +61,6 @@ class Callback(BaseHandler): Arguments: payload -- The matched stanza object. """ - BaseHandler.prerun(self, payload) if self._once: self._destroy = True if self._instream: @@ -80,7 +79,6 @@ class Callback(BaseHandler): Defaults to False. """ if not self._instream or instream: - BaseHandler.run(self, payload) self._pointer(payload) if self._once: self._destroy = True From 75584d7ad74b284d30164cde0b5efec2c845d207 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 14 Feb 2011 13:49:43 -0500 Subject: [PATCH 25/26] Remap old method names in a better way. This should prevent some reference cycles that will cause garbage collection issues. --- sleekxmpp/basexmpp.py | 30 +++++++++-------- sleekxmpp/clientxmpp.py | 15 +++++---- sleekxmpp/plugins/xep_0030/disco.py | 14 ++++---- sleekxmpp/plugins/xep_0030/static.py | 1 - sleekxmpp/plugins/xep_0199/ping.py | 7 ++-- sleekxmpp/stanza/error.py | 19 +++++------ sleekxmpp/stanza/htmlim.py | 23 ++++--------- sleekxmpp/stanza/iq.py | 15 +++++---- sleekxmpp/stanza/message.py | 32 +++++++----------- sleekxmpp/stanza/nick.py | 23 ++++--------- sleekxmpp/stanza/presence.py | 30 ++++++----------- sleekxmpp/stanza/roster.py | 23 ++++--------- sleekxmpp/xmlstream/handler/base.py | 7 ++-- sleekxmpp/xmlstream/matcher/xmlmask.py | 3 +- sleekxmpp/xmlstream/scheduler.py | 3 +- sleekxmpp/xmlstream/stanzabase.py | 45 +++++++++++++------------- sleekxmpp/xmlstream/xmlstream.py | 29 +++++++++-------- 17 files changed, 140 insertions(+), 179 deletions(-) diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 3cf949a..8347bfe 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -90,20 +90,6 @@ class BaseXMPP(XMLStream): # To comply with PEP8, method names now use underscores. # Deprecated method names are re-mapped for backwards compatibility. - self.registerPlugin = self.register_plugin - self.makeIq = self.make_iq - self.makeIqGet = self.make_iq_get - self.makeIqResult = self.make_iq_result - self.makeIqSet = self.make_iq_set - self.makeIqError = self.make_iq_error - self.makeIqQuery = self.make_iq_query - self.makeQueryRoster = self.make_query_roster - self.makeMessage = self.make_message - self.makePresence = self.make_presence - self.sendMessage = self.send_message - self.sendPresence = self.send_presence - self.sendPresenceSubscription = self.send_presence_subscription - self.default_ns = default_ns self.stream_ns = 'http://etherx.jabber.org/streams' @@ -701,3 +687,19 @@ class BaseXMPP(XMLStream): # Restore the old, lowercased name for backwards compatibility. basexmpp = BaseXMPP + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +BaseXMPP.registerPlugin = BaseXMPP.register_plugin +BaseXMPP.makeIq = BaseXMPP.make_iq +BaseXMPP.makeIqGet = BaseXMPP.make_iq_get +BaseXMPP.makeIqResult = BaseXMPP.make_iq_result +BaseXMPP.makeIqSet = BaseXMPP.make_iq_set +BaseXMPP.makeIqError = BaseXMPP.make_iq_error +BaseXMPP.makeIqQuery = BaseXMPP.make_iq_query +BaseXMPP.makeQueryRoster = BaseXMPP.make_query_roster +BaseXMPP.makeMessage = BaseXMPP.make_message +BaseXMPP.makePresence = BaseXMPP.make_presence +BaseXMPP.sendMessage = BaseXMPP.send_message +BaseXMPP.sendPresence = BaseXMPP.send_presence +BaseXMPP.sendPresenceSubscription = BaseXMPP.send_presence_subscription diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index a181398..1e860ea 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -68,13 +68,6 @@ class ClientXMPP(BaseXMPP): """ BaseXMPP.__init__(self, 'jabber:client') - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.updateRoster = self.update_roster - self.delRosterItem = self.del_roster_item - self.getRoster = self.get_roster - self.registerFeature = self.register_feature - self.set_jid(jid) self.password = password self.escape_quotes = escape_quotes @@ -439,3 +432,11 @@ class ClientXMPP(BaseXMPP): iq.reply() iq.enable('roster') iq.send() + + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +ClientXMPP.updateRoster = ClientXMPP.update_roster +ClientXMPP.delRosterItem = ClientXMPP.del_roster_item +ClientXMPP.getRoster = ClientXMPP.get_roster +ClientXMPP.registerFeature = ClientXMPP.register_feature diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 96aac69..45d6931 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -90,10 +90,6 @@ class xep_0030(base_plugin): self.description = 'Service Discovery' self.stanza = sleekxmpp.plugins.xep_0030.stanza - # Retain some backwards compatibility - self.getInfo = self.get_info - self.getItems = self.get_items - self.xmpp.register_handler( Callback('Disco Info', StanzaPath('iq/disco_info'), @@ -124,7 +120,8 @@ class xep_0030(base_plugin): """Handle cross-plugin dependencies.""" base_plugin.post_init(self) if self.xmpp['xep_0059']: - register_stanza_plugin(DiscoItems, self.xmpp['xep_0059'].stanza.Set) + register_stanza_plugin(DiscoItems, + self.xmpp['xep_0059'].stanza.Set) def set_node_handler(self, htype, jid=None, node=None, handler=None): """ @@ -378,7 +375,8 @@ class xep_0030(base_plugin): """ self._run_node_handler('del_item', jid, node, kwargs) - def add_identity(self, category='', itype='', name='', node=None, jid=None, lang=None): + def add_identity(self, category='', itype='', name='', + node=None, jid=None, lang=None): """ Add a new identity to the given JID/node combination. @@ -607,3 +605,7 @@ class xep_0030(base_plugin): info.add_feature(info.namespace) return info + +# Retain some backwards compatibility +xep_0030.getInfo = xep_0030.get_info +xep_0030.getItems = xep_0030.get_items diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py index f957c84..654a9bd 100644 --- a/sleekxmpp/plugins/xep_0030/static.py +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -262,4 +262,3 @@ class StaticDisco(object): self.nodes[(jid, node)]['items'].del_item( data.get('ijid', ''), node=data.get('inode', None)) - diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py index cde2f82..064af4c 100644 --- a/sleekxmpp/plugins/xep_0199/ping.py +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -54,9 +54,6 @@ class xep_0199(base_plugin): self.xep = '0199' self.stanza = stanza - # Backwards compatibility for names - self.sendPing = self.send_ping - self.keepalive = self.config.get('keepalive', True) self.frequency = float(self.config.get('frequency', 300)) self.timeout = self.config.get('timeout', 30) @@ -160,3 +157,7 @@ class xep_0199(base_plugin): log.debug("Pong: %s %f" % (jid, delay)) return delay + + +# Backwards compatibility for names +Ping.sendPing = Ping.send_ping diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py index 09229bc..5d1ce50 100644 --- a/sleekxmpp/stanza/error.py +++ b/sleekxmpp/stanza/error.py @@ -77,15 +77,6 @@ class Error(ElementBase): Arguments: xml -- Use an existing XML object for the stanza's values. """ - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.getCondition = self.get_condition - self.setCondition = self.set_condition - self.delCondition = self.del_condition - self.getText = self.get_text - self.setText = self.set_text - self.delText = self.del_text - if ElementBase.setup(self, xml): #If we had to generate XML then set default values. self['type'] = 'cancel' @@ -139,3 +130,13 @@ class Error(ElementBase): """Remove the element.""" self._del_sub('{%s}text' % self.condition_ns) return self + + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +Error.getCondition = Error.get_condition +Error.setCondition = Error.set_condition +Error.delCondition = Error.del_condition +Error.getText = Error.get_text +Error.setText = Error.set_text +Error.delText = Error.del_text diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py index 4586828..d21a74e 100644 --- a/sleekxmpp/stanza/htmlim.py +++ b/sleekxmpp/stanza/htmlim.py @@ -46,23 +46,6 @@ class HTMLIM(ElementBase): interfaces = set(('body',)) plugin_attrib = name - def setup(self, xml=None): - """ - Populate the stanza object using an optional XML object. - - Overrides StanzaBase.setup. - - Arguments: - xml -- Use an existing XML object for the stanza's values. - """ - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.setBody = self.set_body - self.getBody = self.get_body - self.delBody = self.del_body - - return ElementBase.setup(self, xml) - def set_body(self, html): """ Set the contents of the HTML body. @@ -95,3 +78,9 @@ class HTMLIM(ElementBase): 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 +HTMLIM.getBody = HTMLIM.get_body +HTMLIM.delBody = HTMLIM.del_body diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index 330df6c..2bfbc7b 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -75,13 +75,6 @@ class Iq(RootStanza): Overrides StanzaBase.__init__. """ StanzaBase.__init__(self, *args, **kwargs) - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.setPayload = self.set_payload - self.getQuery = self.get_query - self.setQuery = self.set_query - self.delQuery = self.del_query - if self['id'] == '': if self.stream is not None: self['id'] = self.stream.getNewId() @@ -229,3 +222,11 @@ class Iq(RootStanza): else: StanzaBase._set_stanza_values(self, values) return self + + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +Iq.setPayload = Iq.set_payload +Iq.getQuery = Iq.get_query +Iq.setQuery = Iq.set_query +Iq.delQuery = Iq.del_query diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py index 6f0cf21..cb3d344 100644 --- a/sleekxmpp/stanza/message.py +++ b/sleekxmpp/stanza/message.py @@ -63,27 +63,6 @@ class Message(RootStanza): plugin_attrib = name types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat')) - def setup(self, xml=None): - """ - Populate the stanza object using an optional XML object. - - Overrides StanzaBase.setup. - - Arguments: - xml -- Use an existing XML object for the stanza's values. - """ - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.getType = self.get_type - self.getMucroom = self.get_mucroom - self.setMucroom = self.set_mucroom - self.delMucroom = self.del_mucroom - self.getMucnick = self.get_mucnick - self.setMucnick = self.set_mucnick - self.delMucnick = self.del_mucnick - - return StanzaBase.setup(self, xml) - def get_type(self): """ Return the message type. @@ -165,3 +144,14 @@ class Message(RootStanza): def del_mucnick(self): """Dummy method to prevent deletion.""" pass + + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +Message.getType = Message.get_type +Message.getMucroom = Message.get_mucroom +Message.setMucroom = Message.set_mucroom +Message.delMucroom = Message.del_mucroom +Message.getMucnick = Message.get_mucnick +Message.setMucnick = Message.set_mucnick +Message.delMucnick = Message.del_mucnick diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py index dce41d1..1e23d34 100644 --- a/sleekxmpp/stanza/nick.py +++ b/sleekxmpp/stanza/nick.py @@ -49,23 +49,6 @@ class Nick(ElementBase): plugin_attrib = name interfaces = set(('nick',)) - def setup(self, xml=None): - """ - Populate the stanza object using an optional XML object. - - Overrides StanzaBase.setup. - - Arguments: - xml -- Use an existing XML object for the stanza's values. - """ - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.setNick = self.set_nick - self.getNick = self.get_nick - self.delNick = self.del_nick - - return ElementBase.setup(self, xml) - def set_nick(self, nick): """ Add a element with the given nickname. @@ -87,3 +70,9 @@ class Nick(ElementBase): register_stanza_plugin(Message, Nick) register_stanza_plugin(Presence, Nick) + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +Nick.setNick = Nick.set_nick +Nick.getNick = Nick.get_nick +Nick.delNick = Nick.del_nick diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py index 60dddf6..c870623 100644 --- a/sleekxmpp/stanza/presence.py +++ b/sleekxmpp/stanza/presence.py @@ -72,26 +72,6 @@ class Presence(RootStanza): 'subscribed', 'unsubscribe', 'unsubscribed')) showtypes = set(('dnd', 'chat', 'xa', 'away')) - def setup(self, xml=None): - """ - Populate the stanza object using an optional XML object. - - Overrides ElementBase.setup. - - Arguments: - xml -- Use an existing XML object for the stanza's values. - """ - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.setShow = self.set_show - self.getType = self.get_type - self.setType = self.set_type - self.delType = self.get_type - self.getPriority = self.get_priority - self.setPriority = self.set_priority - - return StanzaBase.setup(self, xml) - def exception(self, e): """ Override exception passback for presence. @@ -188,3 +168,13 @@ class Presence(RootStanza): elif self['type'] == 'subscribe': self['type'] = 'subscribed' return StanzaBase.reply(self, clear) + + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +Presence.setShow = Presence.set_show +Presence.getType = Presence.get_type +Presence.setType = Presence.set_type +Presence.delType = Presence.get_type +Presence.getPriority = Presence.get_priority +Presence.setPriority = Presence.set_priority diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py index 8f154a2..afe7551 100644 --- a/sleekxmpp/stanza/roster.py +++ b/sleekxmpp/stanza/roster.py @@ -38,23 +38,6 @@ class Roster(ElementBase): plugin_attrib = 'roster' interfaces = set(('items',)) - def setup(self, xml=None): - """ - Populate the stanza object using an optional XML object. - - Overrides StanzaBase.setup. - - Arguments: - xml -- Use an existing XML object for the stanza's values. - """ - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.setItems = self.set_items - self.getItems = self.get_items - self.delItems = self.del_items - - return ElementBase.setup(self, xml) - def set_items(self, items): """ Set the roster entries in the stanza. @@ -123,3 +106,9 @@ class Roster(ElementBase): register_stanza_plugin(Iq, Roster) + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +Roster.setItems = Roster.set_items +Roster.getItems = Roster.get_items +Roster.delItems = Roster.del_items diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py index 9c704ec..6ec9b6a 100644 --- a/sleekxmpp/xmlstream/handler/base.py +++ b/sleekxmpp/xmlstream/handler/base.py @@ -42,8 +42,6 @@ class BaseHandler(object): this handler. stream -- The XMLStream instance the handler should monitor. """ - self.checkDelete = self.check_delete - self.name = name self.stream = stream self._destroy = False @@ -87,3 +85,8 @@ class BaseHandler(object): handlers. """ return self._destroy + + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +BaseHandler.checkDelete = BaseHandler.check_delete diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index 60e1949..53ccc9b 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -117,7 +117,8 @@ class MatchXMLMask(MatcherBase): return False # If the mask includes text, compare it. - if mask.text and source.text and source.text.strip() != mask.text.strip(): + if mask.text and source.text and \ + source.text.strip() != mask.text.strip(): return False # Compare attributes. The stanza must include the attributes diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py index 1435910..0e711b4 100644 --- a/sleekxmpp/xmlstream/scheduler.py +++ b/sleekxmpp/xmlstream/scheduler.py @@ -140,7 +140,8 @@ class Scheduler(object): """Process scheduled tasks.""" self.run = True try: - while self.run and (self.parentstop is None or not self.parentstop.isSet()): + while self.run and (self.parentstop is None or \ + not self.parentstop.isSet()): wait = 1 updated = False if self.schedule: diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 1f229ce..753977c 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -218,18 +218,6 @@ class ElementBase(object): xml -- Initialize the stanza with optional existing XML. parent -- Optional stanza object that contains this stanza. """ - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.initPlugin = self.init_plugin - self._getAttr = self._get_attr - self._setAttr = self._set_attr - self._delAttr = self._del_attr - self._getSubText = self._get_sub_text - self._setSubText = self._set_sub_text - self._delSub = self._del_sub - self.getStanzaValues = self._get_stanza_values - self.setStanzaValues = self._set_stanza_values - self.xml = xml self.plugins = OrderedDict() self.iterables = [] @@ -1076,17 +1064,6 @@ class StanzaBase(ElementBase): sfrom -- Optional string or JID object of the sender's JID. sid -- Optional ID value for the stanza. """ - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.setType = self.set_type - self.getTo = self.get_to - self.setTo = self.set_to - self.getFrom = self.get_from - self.setFrom = self.set_from - self.getPayload = self.get_payload - self.setPayload = self.set_payload - self.delPayload = self.del_payload - self.stream = stream if stream is not None: self.namespace = stream.default_ns @@ -1224,3 +1201,25 @@ class StanzaBase(ElementBase): return tostring(self.xml, xmlns='', stanza_ns=self.namespace, stream=self.stream) + + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +ElementBase.initPlugin = ElementBase.init_plugin +ElementBase._getAttr = ElementBase._get_attr +ElementBase._setAttr = ElementBase._set_attr +ElementBase._delAttr = ElementBase._del_attr +ElementBase._getSubText = ElementBase._get_sub_text +ElementBase._setSubText = ElementBase._set_sub_text +ElementBase._delSub = ElementBase._del_sub +ElementBase.getStanzaValues = ElementBase._get_stanza_values +ElementBase.setStanzaValues = ElementBase._set_stanza_values + +StanzaBase.setType = StanzaBase.set_type +StanzaBase.getTo = StanzaBase.get_to +StanzaBase.setTo = StanzaBase.set_to +StanzaBase.getFrom = StanzaBase.get_from +StanzaBase.setFrom = StanzaBase.set_from +StanzaBase.getPayload = StanzaBase.get_payload +StanzaBase.setPayload = StanzaBase.set_payload +StanzaBase.delPayload = StanzaBase.del_payload diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 560b80a..87771ad 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -149,19 +149,6 @@ class XMLStream(object): port -- The port to use for the connection. Defaults to 0. """ - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. - self.startTLS = self.start_tls - self.registerStanza = self.register_stanza - self.removeStanza = self.remove_stanza - self.registerHandler = self.register_handler - self.removeHandler = self.remove_handler - self.setSocket = self.set_socket - self.sendRaw = self.send_raw - self.getId = self.get_id - self.getNewId = self.new_id - self.sendXML = self.send_xml - self.ssl_support = SSL_SUPPORT self.ssl_version = ssl.PROTOCOL_TLSv1 self.ca_certs = None @@ -970,9 +957,11 @@ class XMLStream(object): is not caught. """ init_old = threading.Thread.__init__ + def init(self, *args, **kwargs): init_old(self, *args, **kwargs) run_old = self.run + def run_with_except_hook(*args, **kw): try: run_old(*args, **kw) @@ -982,3 +971,17 @@ class XMLStream(object): sys.excepthook(*sys.exc_info()) self.run = run_with_except_hook threading.Thread.__init__ = init + + +# To comply with PEP8, method names now use underscores. +# Deprecated method names are re-mapped for backwards compatibility. +XMLStream.startTLS = XMLStream.start_tls +XMLStream.registerStanza = XMLStream.register_stanza +XMLStream.removeStanza = XMLStream.remove_stanza +XMLStream.registerHandler = XMLStream.register_handler +XMLStream.removeHandler = XMLStream.remove_handler +XMLStream.setSocket = XMLStream.set_socket +XMLStream.sendRaw = XMLStream.send_raw +XMLStream.getId = XMLStream.get_id +XMLStream.getNewId = XMLStream.new_id +XMLStream.sendXML = XMLStream.send_xml From d709f8db657aa1d1314082d842dd29e2546739c4 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Mon, 14 Feb 2011 13:50:59 -0500 Subject: [PATCH 26/26] Use the _build_stanza method. --- sleekxmpp/xmlstream/xmlstream.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 87771ad..a5151d7 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -813,13 +813,7 @@ class XMLStream(object): # Convert the raw XML object into a stanza object. If no registered # stanza type applies, a generic StanzaBase stanza will be used. - stanza_type = StanzaBase - for stanza_class in self.__root_stanza: - if xml.tag == "{%s}%s" % (self.default_ns, stanza_class.name) or \ - xml.tag == stanza_class.tag_name(): - stanza_type = stanza_class - break - stanza = stanza_type(self, xml) + stanza = self._build_stanza(xml) # Match the stanza against registered handlers. Handlers marked # to run "in stream" will be executed immediately; the rest will @@ -827,7 +821,7 @@ class XMLStream(object): unhandled = True for handler in self.__handlers: if handler.match(stanza): - stanza_copy = stanza_type(self, copy.deepcopy(xml)) + stanza_copy = copy.copy(stanza) handler.prerun(stanza_copy) self.event_queue.put(('stanza', handler, stanza_copy)) try: