Merge branch 'develop' into stream_features

Conflicts:
	sleekxmpp/xmlstream/stanzabase.py
This commit is contained in:
Lance Stout 2011-02-14 16:26:23 -05:00
commit d5b3a52827
41 changed files with 2050 additions and 393 deletions

137
examples/ping.py Executable file
View file

@ -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.")

44
examples/rpc_async.py Normal file
View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -45,10 +45,13 @@ packages = [ 'sleekxmpp',
'sleekxmpp/xmlstream/handler', 'sleekxmpp/xmlstream/handler',
'sleekxmpp/thirdparty', 'sleekxmpp/thirdparty',
'sleekxmpp/plugins', 'sleekxmpp/plugins',
'sleekxmpp/plugins/xep_0009',
'sleekxmpp/plugins/xep_0009/stanza',
'sleekxmpp/plugins/xep_0030', 'sleekxmpp/plugins/xep_0030',
'sleekxmpp/plugins/xep_0030/stanza', 'sleekxmpp/plugins/xep_0030/stanza',
'sleekxmpp/plugins/xep_0059', 'sleekxmpp/plugins/xep_0059',
'sleekxmpp/plugins/xep_0092', 'sleekxmpp/plugins/xep_0092',
'sleekxmpp/plugins/xep_0199',
] ]
if sys.version_info < (3, 0): if sys.version_info < (3, 0):

View file

@ -90,20 +90,6 @@ class BaseXMPP(XMLStream):
# To comply with PEP8, method names now use underscores. # To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility. # 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.default_ns = default_ns
self.stream_ns = 'http://etherx.jabber.org/streams' self.stream_ns = 'http://etherx.jabber.org/streams'
self.namespace_map[self.stream_ns] = 'stream' self.namespace_map[self.stream_ns] = 'stream'
@ -704,3 +690,19 @@ class BaseXMPP(XMLStream):
# Restore the old, lowercased name for backwards compatibility. # Restore the old, lowercased name for backwards compatibility.
basexmpp = BaseXMPP 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

View file

@ -70,13 +70,6 @@ class ClientXMPP(BaseXMPP):
""" """
BaseXMPP.__init__(self, 'jabber:client') 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.set_jid(jid)
self.password = password self.password = password
self.escape_quotes = escape_quotes self.escape_quotes = escape_quotes
@ -504,3 +497,11 @@ class ClientXMPP(BaseXMPP):
iq.reply() iq.reply()
iq.enable('roster') iq.enable('roster')
iq.send() 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

View file

@ -21,7 +21,8 @@ class XMPPError(Exception):
""" """
def __init__(self, condition='undefined-condition', text=None, etype=None, 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. Create a new XMPPError exception.
@ -37,6 +38,9 @@ class XMPPError(Exception):
extension_args -- Content and attributes for the extension extension_args -- Content and attributes for the extension
element. Same as the additional arguments to element. Same as the additional arguments to
the ET.Element constructor. 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: if extension_args is None:
extension_args = {} extension_args = {}
@ -44,6 +48,7 @@ class XMPPError(Exception):
self.condition = condition self.condition = condition
self.text = text self.text = text
self.etype = etype self.etype = etype
self.clear = clear
self.extension = extension self.extension = extension
self.extension_ns = extension_ns self.extension_ns = extension_ns
self.extension_args = extension_args self.extension_args = extension_args

View file

@ -57,6 +57,7 @@ class Form(ElementBase):
return field return field
def getXML(self, type='submit'): def getXML(self, type='submit'):
self['type'] = type
log.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py") log.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py")
return self.xml return self.xml

View file

@ -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

View file

@ -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()

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -90,10 +90,6 @@ class xep_0030(base_plugin):
self.description = 'Service Discovery' self.description = 'Service Discovery'
self.stanza = sleekxmpp.plugins.xep_0030.stanza 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( self.xmpp.register_handler(
Callback('Disco Info', Callback('Disco Info',
StanzaPath('iq/disco_info'), StanzaPath('iq/disco_info'),
@ -124,7 +120,8 @@ class xep_0030(base_plugin):
"""Handle cross-plugin dependencies.""" """Handle cross-plugin dependencies."""
base_plugin.post_init(self) base_plugin.post_init(self)
if self.xmpp['xep_0059']: 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): 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['type'] = 'get'
iq['disco_info']['node'] = node if node else '' iq['disco_info']['node'] = node if node else ''
return iq.send(timeout=kwargs.get('timeout', None), return iq.send(timeout=kwargs.get('timeout', None),
block=kwargs.get('block', None), block=kwargs.get('block', True),
callback=kwargs.get('callback', None)) callback=kwargs.get('callback', None))
def get_items(self, jid=None, node=None, local=False, **kwargs): 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') return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
else: else:
return iq.send(timeout=kwargs.get('timeout', None), return iq.send(timeout=kwargs.get('timeout', None),
block=kwargs.get('block', None), block=kwargs.get('block', True),
callback=kwargs.get('callback', None)) callback=kwargs.get('callback', None))
def set_items(self, jid=None, node=None, **kwargs): 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) 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. Add a new identity to the given JID/node combination.
@ -607,3 +605,7 @@ class xep_0030(base_plugin):
info.add_feature(info.namespace) info.add_feature(info.namespace)
return info return info
# Retain some backwards compatibility
xep_0030.getInfo = xep_0030.get_info
xep_0030.getItems = xep_0030.get_items

View file

@ -262,4 +262,3 @@ class StaticDisco(object):
self.nodes[(jid, node)]['items'].del_item( self.nodes[(jid, node)]['items'].del_item(
data.get('ijid', ''), data.get('ijid', ''),
node=data.get('inode', None)) node=data.get('inode', None))

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -27,10 +27,12 @@ class EntityTime(ElementBase):
interfaces = set(('tzo', 'utc')) interfaces = set(('tzo', 'utc'))
sub_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 #pass
def set_tzo(self, tzo): # TODO: support datetime.tzinfo objects? def set_tzo(self, tzo):
if isinstance(tzo, tzinfo): if isinstance(tzo, tzinfo):
td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here' 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 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? # Returns a datetime object instead the string. Is this a good idea?
value = self._get_sub_text('utc') value = self._get_sub_text('utc')
if '.' in value: 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: else:
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')

View file

@ -77,15 +77,6 @@ class Error(ElementBase):
Arguments: Arguments:
xml -- Use an existing XML object for the stanza's values. 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 ElementBase.setup(self, xml):
#If we had to generate XML then set default values. #If we had to generate XML then set default values.
self['type'] = 'cancel' self['type'] = 'cancel'
@ -139,3 +130,13 @@ class Error(ElementBase):
"""Remove the <text> element.""" """Remove the <text> element."""
self._del_sub('{%s}text' % self.condition_ns) self._del_sub('{%s}text' % self.condition_ns)
return self 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

View file

@ -46,23 +46,6 @@ class HTMLIM(ElementBase):
interfaces = set(('body',)) interfaces = set(('body',))
plugin_attrib = name 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): def set_body(self, html):
""" """
Set the contents of the HTML body. Set the contents of the HTML body.
@ -95,3 +78,9 @@ class HTMLIM(ElementBase):
register_stanza_plugin(Message, HTMLIM) register_stanza_plugin(Message, HTMLIM)
# To comply with PEP8, method names now use underscores.
# Deprecated method names are re-mapped for backwards compatibility.
HTMLIM.setBody = HTMLIM.set_body
HTMLIM.getBody = HTMLIM.get_body
HTMLIM.delBody = HTMLIM.del_body

View file

@ -75,13 +75,6 @@ class Iq(RootStanza):
Overrides StanzaBase.__init__. Overrides StanzaBase.__init__.
""" """
StanzaBase.__init__(self, *args, **kwargs) 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['id'] == '':
if self.stream is not None: if self.stream is not None:
self['id'] = self.stream.getNewId() self['id'] = self.stream.getNewId()
@ -144,7 +137,7 @@ class Iq(RootStanza):
self.xml.remove(child) self.xml.remove(child)
return self return self
def reply(self): def reply(self, clear=True):
""" """
Send a reply <iq> stanza. Send a reply <iq> stanza.
@ -152,9 +145,13 @@ class Iq(RootStanza):
Sets the 'type' to 'result' in addition to the default Sets the 'type' to 'result' in addition to the default
StanzaBase.reply behavior. StanzaBase.reply behavior.
Arguments:
clear -- Indicates if existing content should be
removed before replying. Defaults to True.
""" """
self['type'] = 'result' self['type'] = 'result'
StanzaBase.reply(self) StanzaBase.reply(self, clear)
return self return self
def send(self, block=True, timeout=None, callback=None): def send(self, block=True, timeout=None, callback=None):
@ -185,13 +182,14 @@ class Iq(RootStanza):
if timeout is None: if timeout is None:
timeout = self.stream.response_timeout timeout = self.stream.response_timeout
if callback is not None and self['type'] in ('get', 'set'): 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']), MatcherId(self['id']),
callback, callback,
once=True) once=True)
self.stream.register_handler(handler) self.stream.register_handler(handler)
StanzaBase.send(self) StanzaBase.send(self)
return None return handler_name
elif block and self['type'] in ('get', 'set'): elif block and self['type'] in ('get', 'set'):
waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id'])) waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
self.stream.register_handler(waitfor) self.stream.register_handler(waitfor)
@ -224,3 +222,11 @@ class Iq(RootStanza):
else: else:
StanzaBase._set_stanza_values(self, values) StanzaBase._set_stanza_values(self, values)
return self 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

View file

@ -63,27 +63,6 @@ class Message(RootStanza):
plugin_attrib = name plugin_attrib = name
types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat')) 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): def get_type(self):
""" """
Return the message type. Return the message type.
@ -104,7 +83,7 @@ class Message(RootStanza):
self['type'] = 'normal' self['type'] = 'normal'
return self return self
def reply(self, body=None): def reply(self, body=None, clear=True):
""" """
Create a message reply. Create a message reply.
@ -115,6 +94,8 @@ class Message(RootStanza):
Arguments: 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) StanzaBase.reply(self)
if self['type'] == 'groupchat': if self['type'] == 'groupchat':
@ -163,3 +144,14 @@ class Message(RootStanza):
def del_mucnick(self): def del_mucnick(self):
"""Dummy method to prevent deletion.""" """Dummy method to prevent deletion."""
pass 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

View file

@ -49,23 +49,6 @@ class Nick(ElementBase):
plugin_attrib = name plugin_attrib = name
interfaces = set(('nick',)) 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): def set_nick(self, nick):
""" """
Add a <nick> element with the given nickname. Add a <nick> element with the given nickname.
@ -87,3 +70,9 @@ class Nick(ElementBase):
register_stanza_plugin(Message, Nick) register_stanza_plugin(Message, Nick)
register_stanza_plugin(Presence, 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

View file

@ -72,26 +72,6 @@ class Presence(RootStanza):
'subscribed', 'unsubscribe', 'unsubscribed')) 'subscribed', 'unsubscribe', 'unsubscribed'))
showtypes = set(('dnd', 'chat', 'xa', 'away')) 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): def exception(self, e):
""" """
Override exception passback for presence. 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 # The priority is not a number: we consider it 0 as a default
return 0 return 0
def reply(self): def reply(self, clear=True):
""" """
Set the appropriate presence reply type. Set the appropriate presence reply type.
Overrides StanzaBase.reply. Overrides StanzaBase.reply.
Arguments:
clear -- Indicates if the stanza contents should be removed
before replying. Defaults to True.
""" """
if self['type'] == 'unsubscribe': if self['type'] == 'unsubscribe':
self['type'] = 'unsubscribed' self['type'] = 'unsubscribed'
elif self['type'] == 'subscribe': elif self['type'] == 'subscribe':
self['type'] = 'subscribed' 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

View file

@ -43,8 +43,8 @@ class RootStanza(StanzaBase):
Arguments: Arguments:
e -- Exception object e -- Exception object
""" """
self.reply()
if isinstance(e, XMPPError): if isinstance(e, XMPPError):
self.reply(clear=e.clear)
# We raised this deliberately # We raised this deliberately
self['error']['condition'] = e.condition self['error']['condition'] = e.condition
self['error']['text'] = e.text self['error']['text'] = e.text
@ -56,6 +56,7 @@ class RootStanza(StanzaBase):
self['error']['type'] = e.etype self['error']['type'] = e.etype
self.send() self.send()
else: else:
self.reply()
# We probably didn't raise this on purpose, so send an error stanza # We probably didn't raise this on purpose, so send an error stanza
self['error']['condition'] = 'undefined-condition' self['error']['condition'] = 'undefined-condition'
self['error']['text'] = "SleekXMPP got into trouble." self['error']['text'] = "SleekXMPP got into trouble."

View file

@ -38,23 +38,6 @@ class Roster(ElementBase):
plugin_attrib = 'roster' plugin_attrib = 'roster'
interfaces = set(('items',)) 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): def set_items(self, items):
""" """
Set the roster entries in the <roster> stanza. Set the roster entries in the <roster> stanza.
@ -123,3 +106,9 @@ class Roster(ElementBase):
register_stanza_plugin(Iq, Roster) 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

View file

@ -42,8 +42,6 @@ class BaseHandler(object):
this handler. this handler.
stream -- The XMLStream instance the handler should monitor. stream -- The XMLStream instance the handler should monitor.
""" """
self.checkDelete = self.check_delete
self.name = name self.name = name
self.stream = stream self.stream = stream
self._destroy = False self._destroy = False
@ -87,3 +85,8 @@ class BaseHandler(object):
handlers. handlers.
""" """
return self._destroy 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

View file

@ -61,7 +61,8 @@ class Callback(BaseHandler):
Arguments: Arguments:
payload -- The matched stanza object. payload -- The matched stanza object.
""" """
BaseHandler.prerun(self, payload) if self._once:
self._destroy = True
if self._instream: if self._instream:
self.run(payload, True) self.run(payload, True)
@ -78,7 +79,7 @@ class Callback(BaseHandler):
Defaults to False. Defaults to False.
""" """
if not self._instream or instream: if not self._instream or instream:
BaseHandler.run(self, payload)
self._pointer(payload) self._pointer(payload)
if self._once: if self._once:
self._destroy = True self._destroy = True
del self._pointer

View file

@ -117,7 +117,8 @@ class MatchXMLMask(MatcherBase):
return False return False
# If the mask includes text, compare it. # 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 return False
# Compare attributes. The stanza must include the attributes # Compare attributes. The stanza must include the attributes

View file

@ -140,7 +140,8 @@ class Scheduler(object):
"""Process scheduled tasks.""" """Process scheduled tasks."""
self.run = True self.run = True
try: 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 wait = 1
updated = False updated = False
if self.schedule: if self.schedule:

View file

@ -218,18 +218,6 @@ class ElementBase(object):
xml -- Initialize the stanza with optional existing XML. xml -- Initialize the stanza with optional existing XML.
parent -- Optional stanza object that contains this stanza. 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.xml = xml
self.plugins = OrderedDict() self.plugins = OrderedDict()
self.iterables = [] self.iterables = []
@ -1078,17 +1066,6 @@ class StanzaBase(ElementBase):
sfrom -- Optional string or JID object of the sender's JID. sfrom -- Optional string or JID object of the sender's JID.
sid -- Optional ID value for the stanza. 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 self.stream = stream
if stream is not None: if stream is not None:
self.namespace = stream.default_ns self.namespace = stream.default_ns
@ -1163,12 +1140,17 @@ class StanzaBase(ElementBase):
self.clear() self.clear()
return self return self
def reply(self): def reply(self, clear=True):
""" """
Reset the stanza and swap its 'from' and 'to' attributes to prepare Swap the 'from' and 'to' attributes to prepare the stanza for
for sending a reply stanza. 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. 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 it's a component, use from
if self.stream and hasattr(self.stream, "is_component") and \ if self.stream and hasattr(self.stream, "is_component") and \
@ -1177,6 +1159,7 @@ class StanzaBase(ElementBase):
else: else:
self['to'] = self['from'] self['to'] = self['from']
del self['from'] del self['from']
if clear:
self.clear() self.clear()
return self return self
@ -1221,3 +1204,25 @@ class StanzaBase(ElementBase):
stanza_ns=self.namespace, stanza_ns=self.namespace,
stream=self.stream, stream=self.stream,
top_level = True) top_level = True)
# 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

View file

@ -149,19 +149,6 @@ class XMLStream(object):
port -- The port to use for the connection. port -- The port to use for the connection.
Defaults to 0. 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_support = SSL_SUPPORT
self.ssl_version = ssl.PROTOCOL_TLSv1 self.ssl_version = ssl.PROTOCOL_TLSv1
self.ca_certs = None self.ca_certs = None
@ -826,13 +813,7 @@ class XMLStream(object):
# Convert the raw XML object into a stanza object. If no registered # Convert the raw XML object into a stanza object. If no registered
# stanza type applies, a generic StanzaBase stanza will be used. # stanza type applies, a generic StanzaBase stanza will be used.
stanza_type = StanzaBase stanza = self._build_stanza(xml)
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)
# Match the stanza against registered handlers. Handlers marked # Match the stanza against registered handlers. Handlers marked
# to run "in stream" will be executed immediately; the rest will # to run "in stream" will be executed immediately; the rest will
@ -840,12 +821,12 @@ class XMLStream(object):
unhandled = True unhandled = True
for handler in self.__handlers: for handler in self.__handlers:
if handler.match(stanza): if handler.match(stanza):
stanza_copy = stanza_type(self, copy.deepcopy(xml)) stanza_copy = copy.copy(stanza)
handler.prerun(stanza_copy) handler.prerun(stanza_copy)
self.event_queue.put(('stanza', handler, stanza_copy)) self.event_queue.put(('stanza', handler, stanza_copy))
try: try:
if handler.check_delete(): if handler.check_delete():
self.__handlers.pop(self.__handlers.index(handler)) self.__handlers.remove(handler)
except: except:
pass # not thread safe pass # not thread safe
unhandled = False unhandled = False
@ -970,9 +951,11 @@ class XMLStream(object):
is not caught. is not caught.
""" """
init_old = threading.Thread.__init__ init_old = threading.Thread.__init__
def init(self, *args, **kwargs): def init(self, *args, **kwargs):
init_old(self, *args, **kwargs) init_old(self, *args, **kwargs)
run_old = self.run run_old = self.run
def run_with_except_hook(*args, **kw): def run_with_except_hook(*args, **kw):
try: try:
run_old(*args, **kw) run_old(*args, **kw)
@ -982,3 +965,17 @@ class XMLStream(object):
sys.excepthook(*sys.exc_info()) sys.excepthook(*sys.exc_info())
self.run = run_with_except_hook self.run = run_with_except_hook
threading.Thread.__init__ = init 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

View file

@ -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)

View file

@ -181,7 +181,7 @@ class TestPubsubStanzas(SleekTest):
<pubsub xmlns="http://jabber.org/protocol/pubsub"> <pubsub xmlns="http://jabber.org/protocol/pubsub">
<subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp"> <subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp">
<options 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"> <field var="pubsub#title" type="text-single">
<value>this thing is awesome</value> <value>this thing is awesome</value>
</field> </field>

View file

@ -1,5 +1,7 @@
import sys import sys
import sleekxmpp import sleekxmpp
from sleekxmpp.xmlstream.matcher import MatchXPath
from sleekxmpp.xmlstream.handler import Callback
from sleekxmpp.exceptions import XMPPError from sleekxmpp.exceptions import XMPPError
from sleekxmpp.test import * from sleekxmpp.test import *
@ -46,6 +48,41 @@ class TestStreamExceptions(SleekTest):
</message> </message>
""", use_values=False) """, 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): def testThreadedXMPPErrorException(self):
"""Test raising an XMPPError exception in a threaded handler.""" """Test raising an XMPPError exception in a threaded handler."""

185
todo1.0
View file

@ -1,123 +1,62 @@
ElementBase sub_items not subitem? Plugins:
0004
*XMPP needs to use JID class instead of lots of fields. PEP8
Stream/Unit tests
BaseXMPP set_jid, makeIqQuery, getjidresource, getjidbare not needed Fix serialization issue
Use OrderedDict for fields/values
Why CamelCase and underscore_names? Document semantics. 0009
Review contribution from dannmartens
conn_tests and sleekxmpp/tests and sleekxmpp/xmlstresm/test.* -> convert to either unit tests, or at least put in same place 0012
PEP8
Update setup.py - github url, version # Documentation
Stream/Unit tests
scheduler needs unit tests 0030
Done
ClientXMPP stream:features handler should use new state machine 0033
PEP8
Write stream tests for startls, features, etc. Documentation
Stream/Unit tests
0045
PEP8
-- PEP8 - all files Documentation
Stream/Unit tests
Need to use spaces 0050
Review replacement in github.com/legastero/adhoc
Docstrings are lacking. Need to document attributes and return values. 0059
Done
Organize imports 0060
PEP8
Use absolute, not relative imports Documentation
Stream/Unit tests
Fix one-liner if statements 0078
Will require new stream features handling, see stream_features branch.
Line length limit of 79 characters PEP8
Documentation
Stream/Unit tests
0085
-- Plugins PEP8
Documentation
--- xep_0004 Stream/Unit tests
0086
Need more unit tests PEP8
Documentation
--- xep_0009 Consider any simplifications.
0092
Need stanza objects Done
0128
Need unit tests Needs complete rewrite to work with new 0030 plugin.
0199
--- xep_0045 PEP8
Documentation
Need to use stanza objects Stream/Unit tests
Needs to use scheduler instead of its own thread.
A few TODO comments for checking roles and using defaults 0202
PEP8
Need unit tests Documentation
Stream/Unit tests
--- xep_0050 0249
Review, minor cleanup
Need unit tests gmail_notify
PEP8
Need stanza objects - use new xep_0004 Documentation
Stream/Unit tests
--- 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