summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xexamples/ping.py137
-rw-r--r--examples/rpc_async.py44
-rw-r--r--examples/rpc_client_side.py53
-rw-r--r--examples/rpc_server_side.py52
-rw-r--r--setup.py3
-rw-r--r--sleekxmpp/basexmpp.py30
-rw-r--r--sleekxmpp/clientxmpp.py16
-rw-r--r--sleekxmpp/exceptions.py7
-rw-r--r--sleekxmpp/plugins/old_0009.py (renamed from sleekxmpp/plugins/xep_0009.py)0
-rw-r--r--sleekxmpp/plugins/xep_0004.py1
-rw-r--r--sleekxmpp/plugins/xep_0009/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0009/binding.py166
-rw-r--r--sleekxmpp/plugins/xep_0009/remote.py739
-rw-r--r--sleekxmpp/plugins/xep_0009/rpc.py221
-rw-r--r--sleekxmpp/plugins/xep_0009/stanza/RPC.py64
-rw-r--r--sleekxmpp/plugins/xep_0009/stanza/__init__.py9
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py18
-rw-r--r--sleekxmpp/plugins/xep_0030/static.py1
-rw-r--r--sleekxmpp/plugins/xep_0199.py63
-rw-r--r--sleekxmpp/plugins/xep_0199/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0199/ping.py163
-rw-r--r--sleekxmpp/plugins/xep_0199/stanza.py36
-rw-r--r--sleekxmpp/stanza/error.py19
-rw-r--r--sleekxmpp/stanza/htmlim.py23
-rw-r--r--sleekxmpp/stanza/iq.py28
-rw-r--r--sleekxmpp/stanza/message.py38
-rw-r--r--sleekxmpp/stanza/nick.py23
-rw-r--r--sleekxmpp/stanza/presence.py38
-rw-r--r--sleekxmpp/stanza/rootstanza.py3
-rw-r--r--sleekxmpp/stanza/roster.py23
-rw-r--r--sleekxmpp/xmlstream/handler/base.py7
-rw-r--r--sleekxmpp/xmlstream/handler/callback.py5
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py3
-rw-r--r--sleekxmpp/xmlstream/scheduler.py3
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py59
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py41
-rw-r--r--tests/test_stanza_xep_0009.py55
-rw-r--r--tests/test_stanza_xep_0060.py2
-rw-r--r--tests/test_stream_exceptions.py37
39 files changed, 1984 insertions, 267 deletions
diff --git a/examples/ping.py b/examples/ping.py
new file mode 100755
index 00000000..70066e3c
--- /dev/null
+++ b/examples/ping.py
@@ -0,0 +1,137 @@
+#!/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 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
+
+ # 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['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! RTT: %s" % str(result))
+ 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.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:
+ # 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/examples/rpc_async.py b/examples/rpc_async.py
new file mode 100644
index 00000000..0b6d1936
--- /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 00000000..ca1084f0
--- /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 00000000..0af8af43
--- /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
diff --git a/setup.py b/setup.py
index 45682679..ae8cf682 100644
--- a/setup.py
+++ b/setup.py
@@ -45,10 +45,13 @@ 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',
'sleekxmpp/plugins/xep_0059',
'sleekxmpp/plugins/xep_0092',
+ 'sleekxmpp/plugins/xep_0199',
]
if sys.version_info < (3, 0):
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index bd953afe..460c01f3 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -91,20 +91,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'
@@ -703,3 +689,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 2554d299..02e1b390 100644
--- a/sleekxmpp/clientxmpp.py
+++ b/sleekxmpp/clientxmpp.py
@@ -68,13 +68,7 @@ class ClientXMPP(BaseXMPP):
"""
BaseXMPP.__init__(self, jid, '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
self.plugin_config = plugin_config
@@ -438,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/exceptions.py b/sleekxmpp/exceptions.py
index d3988b4a..4727f0c6 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/plugins/xep_0009.py b/sleekxmpp/plugins/old_0009.py
index 625b03fb..625b03fb 100644
--- a/sleekxmpp/plugins/xep_0009.py
+++ b/sleekxmpp/plugins/old_0009.py
diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py
index 5d41d269..5a49d70f 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
diff --git a/sleekxmpp/plugins/xep_0009/__init__.py b/sleekxmpp/plugins/xep_0009/__init__.py
new file mode 100644
index 00000000..2cd14170
--- /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 00000000..30f02d36
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009/binding.py
@@ -0,0 +1,166 @@
+"""
+ 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("{%s}param" % _namespace)
+ param.append(_py2xml(x))
+ params.append(param) #<params><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 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}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:
+ 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 00000000..8c534118
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009/remote.py
@@ -0,0 +1,739 @@
+"""
+ 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 '<fqn>.<method>'.
+ '''
+ __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:
+ log.debug("[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)
+
diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py
new file mode 100644
index 00000000..fc306d31
--- /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 sleekxmpp.plugins import base
+from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.xmlstream.handler.callback import Callback
+from sleekxmpp.xmlstream.matcher.xpath import MatchXPath
+from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin
+from xml.etree import cElementTree as ET
+import logging
+
+
+
+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
+
diff --git a/sleekxmpp/plugins/xep_0009/stanza/RPC.py b/sleekxmpp/plugins/xep_0009/stanza/RPC.py
new file mode 100644
index 00000000..3d1c77a2
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0009/stanza/RPC.py
@@ -0,0 +1,64 @@
+"""
+ 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 00000000..5dcbf330
--- /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 sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, MethodResponse
diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py
index a976b988..45d6931b 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):
"""
@@ -271,7 +268,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):
@@ -318,7 +315,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):
@@ -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 f957c84c..654a9bd0 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.py b/sleekxmpp/plugins/xep_0199.py
deleted file mode 100644
index 16e79e26..00000000
--- 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("<iq type='get' xmlns='%s'><ping xmlns='urn:xmpp:ping'/></iq>" % 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):
- """ 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:
- #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 00000000..3444fe94
--- /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 00000000..064af4ca
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0199/ping.py
@@ -0,0 +1,163 @@
+"""
+ 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 <http://www.xmpp.org/extensions/xep-0199.html>.
+
+ 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
+
+ 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
+
+
+# Backwards compatibility for names
+Ping.sendPing = Ping.send_ping
diff --git a/sleekxmpp/plugins/xep_0199/stanza.py b/sleekxmpp/plugins/xep_0199/stanza.py
new file mode 100644
index 00000000..6586a763
--- /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:
+ <iq type="get">
+ <ping xmlns="urn:xmpp:ping" />
+ </iq>
+
+ Stanza Interface:
+ None
+
+ Methods:
+ None
+ """
+
+ name = 'ping'
+ namespace = 'urn:xmpp:ping'
+ plugin_attrib = 'ping'
+ interfaces = set()
diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py
index 09229bc6..5d1ce50d 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 <text> 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 45868287..d21a74e1 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 c6aa64d0..2bfbc7b1 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()
@@ -144,7 +137,7 @@ class Iq(RootStanza):
self.xml.remove(child)
return self
- def reply(self):
+ def reply(self, clear=True):
"""
Send a reply <iq> stanza.
@@ -152,9 +145,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):
@@ -185,13 +182,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 None
+ 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)
@@ -224,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 66c74d8a..cb3d344c 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.
@@ -104,7 +83,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 +93,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':
@@ -163,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 dce41d14..1e23d34f 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 <nick> 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 7dcd8f90..c8706233 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.
@@ -173,14 +153,28 @@ 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)
+
+
+# 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/rootstanza.py b/sleekxmpp/stanza/rootstanza.py
index 8123c5f8..bc11476e 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/stanza/roster.py b/sleekxmpp/stanza/roster.py
index 57ea62ba..3fcdbebc 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 <roster> stanza.
@@ -125,3 +108,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 9c704ec6..6ec9b6a3 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/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py
index f0a72853..7fadab43 100644
--- a/sleekxmpp/xmlstream/handler/callback.py
+++ b/sleekxmpp/xmlstream/handler/callback.py
@@ -61,7 +61,8 @@ class Callback(BaseHandler):
Arguments:
payload -- The matched stanza object.
"""
- BaseHandler.prerun(self, payload)
+ if self._once:
+ self._destroy = True
if self._instream:
self.run(payload, True)
@@ -78,7 +79,7 @@ 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
+ del self._pointer
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
index 60e19495..53ccc9ba 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 14359102..0e711b4b 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 3937a7a9..753977c1 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
@@ -1161,12 +1138,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 +1157,8 @@ class StanzaBase(ElementBase):
else:
self['to'] = self['from']
del self['from']
- self.clear()
+ if clear:
+ self.clear()
return self
def error(self):
@@ -1218,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 1cd23fba..a5151d7b 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
@@ -826,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
@@ -840,12 +821,12 @@ 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:
if handler.check_delete():
- self.__handlers.pop(self.__handlers.index(handler))
+ self.__handlers.remove(handler)
except:
pass # not thread safe
unhandled = False
@@ -970,9 +951,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 +965,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
diff --git a/tests/test_stanza_xep_0009.py b/tests/test_stanza_xep_0009.py
new file mode 100644
index 00000000..6186dd90
--- /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.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
+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, """
+ <iq>
+ <query xmlns="jabber:iq:rpc">
+ <methodCall>
+ <methodName>system.exit</methodName>
+ <params />
+ </methodCall>
+ </query>
+ </iq>
+ """, use_values=False)
+
+ def testMethodResponse(self):
+ iq = self.Iq()
+ iq['rpc_query']['method_response']['params'] = py2xml(*())
+ self.check(iq, """
+ <iq>
+ <query xmlns="jabber:iq:rpc">
+ <methodResponse>
+ <params />
+ </methodResponse>
+ </query>
+ </iq>
+ """, use_values=False)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberRPC)
+
diff --git a/tests/test_stanza_xep_0060.py b/tests/test_stanza_xep_0060.py
index 5d455236..8e6e820d 100644
--- a/tests/test_stanza_xep_0060.py
+++ b/tests/test_stanza_xep_0060.py
@@ -181,7 +181,7 @@ class TestPubsubStanzas(SleekTest):
<pubsub xmlns="http://jabber.org/protocol/pubsub">
<subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp">
<options node="cheese" jid="fritzy@netflint.net/sleekxmpp">
- <x xmlns="jabber:x:data" type="form">
+ <x xmlns="jabber:x:data" type="submit">
<field var="pubsub#title" type="text-single">
<value>this thing is awesome</value>
</field>
diff --git a/tests/test_stream_exceptions.py b/tests/test_stream_exceptions.py
index e1b70d39..a4598a10 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):
</message>
""", 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("""
+ <iq type="get" id="0">
+ <query xmlns="test" />
+ </iq>
+ """)
+
+ self.send("""
+ <iq type="error" id="0">
+ <query xmlns="test" />
+ <error type="cancel">
+ <feature-not-implemented
+ xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ We don&apos;t do things that way here.
+ </text>
+ </error>
+ </iq>
+ """, use_values=False)
+
def testThreadedXMPPErrorException(self):
"""Test raising an XMPPError exception in a threaded handler."""