mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-11-27 19:19:54 +00:00
Merge branch 'develop' into stream_features
Conflicts: sleekxmpp/xmlstream/stanzabase.py
This commit is contained in:
commit
d5b3a52827
41 changed files with 2050 additions and 393 deletions
137
examples/ping.py
Executable file
137
examples/ping.py
Executable 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
44
examples/rpc_async.py
Normal 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()
|
||||
|
53
examples/rpc_client_side.py
Normal file
53
examples/rpc_client_side.py
Normal 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()
|
||||
|
52
examples/rpc_server_side.py
Normal file
52
examples/rpc_server_side.py
Normal 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()
|
||||
|
3
setup.py
3
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):
|
||||
|
|
|
@ -90,20 +90,6 @@ class BaseXMPP(XMLStream):
|
|||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.registerPlugin = self.register_plugin
|
||||
self.makeIq = self.make_iq
|
||||
self.makeIqGet = self.make_iq_get
|
||||
self.makeIqResult = self.make_iq_result
|
||||
self.makeIqSet = self.make_iq_set
|
||||
self.makeIqError = self.make_iq_error
|
||||
self.makeIqQuery = self.make_iq_query
|
||||
self.makeQueryRoster = self.make_query_roster
|
||||
self.makeMessage = self.make_message
|
||||
self.makePresence = self.make_presence
|
||||
self.sendMessage = self.send_message
|
||||
self.sendPresence = self.send_presence
|
||||
self.sendPresenceSubscription = self.send_presence_subscription
|
||||
|
||||
self.default_ns = default_ns
|
||||
self.stream_ns = 'http://etherx.jabber.org/streams'
|
||||
self.namespace_map[self.stream_ns] = 'stream'
|
||||
|
@ -704,3 +690,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
|
||||
|
|
|
@ -70,13 +70,6 @@ class ClientXMPP(BaseXMPP):
|
|||
"""
|
||||
BaseXMPP.__init__(self, 'jabber:client')
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
self.updateRoster = self.update_roster
|
||||
self.delRosterItem = self.del_roster_item
|
||||
self.getRoster = self.get_roster
|
||||
self.registerFeature = self.register_feature
|
||||
|
||||
self.set_jid(jid)
|
||||
self.password = password
|
||||
self.escape_quotes = escape_quotes
|
||||
|
@ -504,3 +497,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
11
sleekxmpp/plugins/xep_0009/__init__.py
Normal file
11
sleekxmpp/plugins/xep_0009/__init__.py
Normal 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
|
166
sleekxmpp/plugins/xep_0009/binding.py
Normal file
166
sleekxmpp/plugins/xep_0009/binding.py
Normal 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()
|
739
sleekxmpp/plugins/xep_0009/remote.py
Normal file
739
sleekxmpp/plugins/xep_0009/remote.py
Normal 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)
|
||||
|
221
sleekxmpp/plugins/xep_0009/rpc.py
Normal file
221
sleekxmpp/plugins/xep_0009/rpc.py
Normal 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
|
||||
|
64
sleekxmpp/plugins/xep_0009/stanza/RPC.py
Normal file
64
sleekxmpp/plugins/xep_0009/stanza/RPC.py
Normal 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)
|
9
sleekxmpp/plugins/xep_0009/stanza/__init__.py
Normal file
9
sleekxmpp/plugins/xep_0009/stanza/__init__.py
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -262,4 +262,3 @@ class StaticDisco(object):
|
|||
self.nodes[(jid, node)]['items'].del_item(
|
||||
data.get('ijid', ''),
|
||||
node=data.get('inode', None))
|
||||
|
||||
|
|
|
@ -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
|
10
sleekxmpp/plugins/xep_0199/__init__.py
Normal file
10
sleekxmpp/plugins/xep_0199/__init__.py
Normal 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
|
163
sleekxmpp/plugins/xep_0199/ping.py
Normal file
163
sleekxmpp/plugins/xep_0199/ping.py
Normal 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
|
36
sleekxmpp/plugins/xep_0199/stanza.py
Normal file
36
sleekxmpp/plugins/xep_0199/stanza.py
Normal 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()
|
|
@ -27,10 +27,12 @@ class EntityTime(ElementBase):
|
|||
interfaces = set(('tzo', 'utc'))
|
||||
sub_interfaces = set(('tzo', 'utc'))
|
||||
|
||||
#def get_utc(self): # TODO: return a datetime.tzinfo object?
|
||||
#def get_tzo(self):
|
||||
# TODO: Right now it returns a string but maybe it should
|
||||
# return a datetime.tzinfo object or maybe a datetime.timedelta?
|
||||
#pass
|
||||
|
||||
def set_tzo(self, tzo): # TODO: support datetime.tzinfo objects?
|
||||
def set_tzo(self, tzo):
|
||||
if isinstance(tzo, tzinfo):
|
||||
td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here'
|
||||
seconds = td.seconds + td.days * 24 * 3600
|
||||
|
@ -45,7 +47,7 @@ class EntityTime(ElementBase):
|
|||
# Returns a datetime object instead the string. Is this a good idea?
|
||||
value = self._get_sub_text('utc')
|
||||
if '.' in value:
|
||||
return datetime.strptime(value, '%Y-%m-%d.%fT%H:%M:%SZ')
|
||||
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
|
||||
else:
|
||||
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
@ -115,6 +94,8 @@ class Message(RootStanza):
|
|||
|
||||
Arguments:
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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.
|
||||
|
@ -123,3 +106,9 @@ class Roster(ElementBase):
|
|||
|
||||
|
||||
register_stanza_plugin(Iq, Roster)
|
||||
|
||||
# To comply with PEP8, method names now use underscores.
|
||||
# Deprecated method names are re-mapped for backwards compatibility.
|
||||
Roster.setItems = Roster.set_items
|
||||
Roster.getItems = Roster.get_items
|
||||
Roster.delItems = Roster.del_items
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 = []
|
||||
|
@ -1078,17 +1066,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
|
||||
|
@ -1163,12 +1140,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 \
|
||||
|
@ -1177,6 +1159,7 @@ class StanzaBase(ElementBase):
|
|||
else:
|
||||
self['to'] = self['from']
|
||||
del self['from']
|
||||
if clear:
|
||||
self.clear()
|
||||
return self
|
||||
|
||||
|
@ -1221,3 +1204,25 @@ class StanzaBase(ElementBase):
|
|||
stanza_ns=self.namespace,
|
||||
stream=self.stream,
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
55
tests/test_stanza_xep_0009.py
Normal file
55
tests/test_stanza_xep_0009.py
Normal 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)
|
||||
|
|
@ -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>
|
||||
|
|
|
@ -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't do things that way here.
|
||||
</text>
|
||||
</error>
|
||||
</iq>
|
||||
""", use_values=False)
|
||||
|
||||
def testThreadedXMPPErrorException(self):
|
||||
"""Test raising an XMPPError exception in a threaded handler."""
|
||||
|
||||
|
|
185
todo1.0
185
todo1.0
|
@ -1,123 +1,62 @@
|
|||
ElementBase sub_items not subitem?
|
||||
|
||||
*XMPP needs to use JID class instead of lots of fields.
|
||||
|
||||
BaseXMPP set_jid, makeIqQuery, getjidresource, getjidbare not needed
|
||||
|
||||
Why CamelCase and underscore_names? Document semantics.
|
||||
|
||||
conn_tests and sleekxmpp/tests and sleekxmpp/xmlstresm/test.* -> convert to either unit tests, or at least put in same place
|
||||
|
||||
Update setup.py - github url, version #
|
||||
|
||||
scheduler needs unit tests
|
||||
|
||||
ClientXMPP stream:features handler should use new state machine
|
||||
|
||||
Write stream tests for startls, features, etc.
|
||||
|
||||
|
||||
|
||||
-- PEP8 - all files
|
||||
|
||||
Need to use spaces
|
||||
|
||||
Docstrings are lacking. Need to document attributes and return values.
|
||||
|
||||
Organize imports
|
||||
|
||||
Use absolute, not relative imports
|
||||
|
||||
Fix one-liner if statements
|
||||
|
||||
Line length limit of 79 characters
|
||||
|
||||
|
||||
|
||||
-- Plugins
|
||||
|
||||
--- xep_0004
|
||||
|
||||
Need more unit tests
|
||||
|
||||
--- xep_0009
|
||||
|
||||
Need stanza objects
|
||||
|
||||
Need unit tests
|
||||
|
||||
--- xep_0045
|
||||
|
||||
Need to use stanza objects
|
||||
|
||||
A few TODO comments for checking roles and using defaults
|
||||
|
||||
Need unit tests
|
||||
|
||||
--- xep_0050
|
||||
|
||||
Need unit tests
|
||||
|
||||
Need stanza objects - use new xep_0004
|
||||
|
||||
--- xep_0060
|
||||
|
||||
Need unit tests
|
||||
|
||||
Need to use existing stanza objects
|
||||
|
||||
--- xep_0078
|
||||
|
||||
Is it useful still?
|
||||
|
||||
Need stanza objects/unit tests
|
||||
|
||||
--- xep_0086
|
||||
|
||||
Is there a way to automate setting error codes?
|
||||
|
||||
Seems like this should be part of the error stanza by default
|
||||
|
||||
Use stanza objects
|
||||
|
||||
--- xep_0092
|
||||
|
||||
Stanza objects
|
||||
|
||||
Unit tests
|
||||
|
||||
--- xep_0199
|
||||
|
||||
Stanza objects
|
||||
|
||||
Unit tests
|
||||
|
||||
Clean commented code
|
||||
|
||||
Use the new scheduler
|
||||
|
||||
|
||||
|
||||
-- Documentation
|
||||
|
||||
Document the Zen/Tao/Whatever of SleekXMPP to explain design goals and decisions
|
||||
|
||||
Write architecture description
|
||||
|
||||
XMPP:TDG needs to be rewritten.
|
||||
|
||||
Need to update docs that reference old JID attributes of sleekxmpp objects
|
||||
|
||||
Page describing new JID class
|
||||
|
||||
Message page needs updating
|
||||
|
||||
Iq page needs to be written
|
||||
|
||||
Make guides to go with example.py and component_example.py
|
||||
|
||||
Page on xmlstream.matchers
|
||||
|
||||
Page on xmlstream.handlers, especially waiters
|
||||
|
||||
Page on using xmlstream.scheduler
|
||||
Plugins:
|
||||
0004
|
||||
PEP8
|
||||
Stream/Unit tests
|
||||
Fix serialization issue
|
||||
Use OrderedDict for fields/values
|
||||
0009
|
||||
Review contribution from dannmartens
|
||||
0012
|
||||
PEP8
|
||||
Documentation
|
||||
Stream/Unit tests
|
||||
0030
|
||||
Done
|
||||
0033
|
||||
PEP8
|
||||
Documentation
|
||||
Stream/Unit tests
|
||||
0045
|
||||
PEP8
|
||||
Documentation
|
||||
Stream/Unit tests
|
||||
0050
|
||||
Review replacement in github.com/legastero/adhoc
|
||||
0059
|
||||
Done
|
||||
0060
|
||||
PEP8
|
||||
Documentation
|
||||
Stream/Unit tests
|
||||
0078
|
||||
Will require new stream features handling, see stream_features branch.
|
||||
PEP8
|
||||
Documentation
|
||||
Stream/Unit tests
|
||||
0085
|
||||
PEP8
|
||||
Documentation
|
||||
Stream/Unit tests
|
||||
0086
|
||||
PEP8
|
||||
Documentation
|
||||
Consider any simplifications.
|
||||
0092
|
||||
Done
|
||||
0128
|
||||
Needs complete rewrite to work with new 0030 plugin.
|
||||
0199
|
||||
PEP8
|
||||
Documentation
|
||||
Stream/Unit tests
|
||||
Needs to use scheduler instead of its own thread.
|
||||
0202
|
||||
PEP8
|
||||
Documentation
|
||||
Stream/Unit tests
|
||||
0249
|
||||
Review, minor cleanup
|
||||
gmail_notify
|
||||
PEP8
|
||||
Documentation
|
||||
Stream/Unit tests
|
||||
|
|
Loading…
Reference in a new issue