mirror of
https://github.com/correl/SleekXMPP.git
synced 2024-12-22 03:00:16 +00:00
Merge remote branch 'tom/hacks'
This commit is contained in:
commit
686943a2ec
28 changed files with 1102 additions and 254 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
*.pyc
|
||||
.project
|
||||
build/
|
||||
*.swp
|
||||
|
|
9
README
9
README
|
@ -1,5 +1,8 @@
|
|||
SleekXMPP is an XMPP library written for Python 3.x (with 2.6 compatibility).
|
||||
SleekXMPP is an XMPP library written for Python 3.1+ (with 2.6 compatibility).
|
||||
Hosted at http://wiki.github.com/fritzy/SleekXMPP/
|
||||
|
||||
Featured in examples in XMPP: The Definitive Guide by Kevin Smith, Remko Tronçon, and Peter Saint-Andre
|
||||
If you're coming here from The Definitive Guide, please read http://wiki.github.com/fritzy/SleekXMPP/xmpp-the-definitive-guide
|
||||
|
||||
SleekXMPP has several design goals/philosophies:
|
||||
- Low number of dependencies.
|
||||
|
@ -31,7 +34,9 @@ Since 0.2, here's the Changelog:
|
|||
Credits
|
||||
----------------
|
||||
Main Author: Nathan Fritz fritz@netflint.net
|
||||
XEP-0045 original implementation: Kevin Smith
|
||||
Contributors: Kevin Smith & Lance Stout
|
||||
Patches: Remko Tronçon
|
||||
|
||||
Feel free to add fritzy@netflint.net to your roster for direct support and comments.
|
||||
Join sleekxmpp-discussion@googlegroups.com / http://groups.google.com/group/sleekxmpp-discussion for email discussion.
|
||||
Join sleek@conference.jabber.org for groupchat discussion.
|
||||
|
|
171
conn_tests/test_pubsubjobs.py
Normal file
171
conn_tests/test_pubsubjobs.py
Normal file
|
@ -0,0 +1,171 @@
|
|||
import logging
|
||||
import sleekxmpp
|
||||
from optparse import OptionParser
|
||||
from xml.etree import cElementTree as ET
|
||||
import os
|
||||
import time
|
||||
import sys
|
||||
import unittest
|
||||
import sleekxmpp.plugins.xep_0004
|
||||
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
|
||||
from sleekxmpp.xmlstream.handler.waiter import Waiter
|
||||
try:
|
||||
import configparser
|
||||
except ImportError:
|
||||
import ConfigParser as configparser
|
||||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
|
||||
class TestClient(sleekxmpp.ClientXMPP):
|
||||
def __init__(self, jid, password):
|
||||
sleekxmpp.ClientXMPP.__init__(self, jid, password)
|
||||
self.add_event_handler("session_start", self.start)
|
||||
#self.add_event_handler("message", self.message)
|
||||
self.waitforstart = queue.Queue()
|
||||
|
||||
def start(self, event):
|
||||
self.getRoster()
|
||||
self.sendPresence()
|
||||
self.waitforstart.put(True)
|
||||
|
||||
|
||||
class TestPubsubServer(unittest.TestCase):
|
||||
statev = {}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
unittest.TestCase.__init__(self, *args, **kwargs)
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
def test001getdefaultconfig(self):
|
||||
"""Get the default node config"""
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2')
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3')
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4')
|
||||
self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode5')
|
||||
result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost)
|
||||
self.statev['defaultconfig'] = result
|
||||
self.failUnless(isinstance(result, sleekxmpp.plugins.xep_0004.Form))
|
||||
|
||||
def test002createdefaultnode(self):
|
||||
"""Create a node without config"""
|
||||
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode1'))
|
||||
|
||||
def test003deletenode(self):
|
||||
"""Delete recently created node"""
|
||||
self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode1'))
|
||||
|
||||
def test004createnode(self):
|
||||
"""Create a node with a config"""
|
||||
self.statev['defaultconfig'].field['pubsub#access_model'].setValue('open')
|
||||
self.statev['defaultconfig'].field['pubsub#notify_retract'].setValue(True)
|
||||
self.statev['defaultconfig'].field['pubsub#persist_items'].setValue(True)
|
||||
self.statev['defaultconfig'].field['pubsub#presence_based_delivery'].setValue(True)
|
||||
p = self.xmpp2.Presence()
|
||||
p['to'] = self.pshost
|
||||
p.send()
|
||||
self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode2', self.statev['defaultconfig'], ntype='job'))
|
||||
|
||||
def test005reconfigure(self):
|
||||
"""Retrieving node config and reconfiguring"""
|
||||
nconfig = self.xmpp1['xep_0060'].getNodeConfig(self.pshost, 'testnode2')
|
||||
self.failUnless(nconfig, "No configuration returned")
|
||||
#print("\n%s ==\n %s" % (nconfig.getValues(), self.statev['defaultconfig'].getValues()))
|
||||
self.failUnless(nconfig.getValues() == self.statev['defaultconfig'].getValues(), "Configuration does not match")
|
||||
self.failUnless(self.xmpp1['xep_0060'].setNodeConfig(self.pshost, 'testnode2', nconfig))
|
||||
|
||||
def test006subscribetonode(self):
|
||||
"""Subscribe to node from account 2"""
|
||||
self.failUnless(self.xmpp2['xep_0060'].subscribe(self.pshost, "testnode2"))
|
||||
|
||||
def test007publishitem(self):
|
||||
"""Publishing item"""
|
||||
item = ET.Element('{http://netflint.net/protocol/test}test')
|
||||
w = Waiter('wait publish', StanzaPath('message/pubsub_event/items'))
|
||||
self.xmpp2.registerHandler(w)
|
||||
#result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test1', item),))
|
||||
result = self.xmpp1['jobs'].createJob(self.pshost, "testnode2", 'test1', item)
|
||||
msg = w.wait(5) # got to get a result in 5 seconds
|
||||
self.failUnless(msg != False, "Account #2 did not get message event")
|
||||
#result = self.xmpp1['xep_0060'].setItem(self.pshost, "testnode2", (('test2', item),))
|
||||
result = self.xmpp1['jobs'].createJob(self.pshost, "testnode2", 'test2', item)
|
||||
w = Waiter('wait publish2', StanzaPath('message/pubsub_event/items'))
|
||||
self.xmpp2.registerHandler(w)
|
||||
self.xmpp2['jobs'].claimJob(self.pshost, 'testnode2', 'test1')
|
||||
msg = w.wait(5) # got to get a result in 5 seconds
|
||||
self.xmpp2['jobs'].claimJob(self.pshost, 'testnode2', 'test2')
|
||||
self.xmpp2['jobs'].finishJob(self.pshost, 'testnode2', 'test1')
|
||||
self.xmpp2['jobs'].finishJob(self.pshost, 'testnode2', 'test2')
|
||||
print result
|
||||
#need to add check for update
|
||||
|
||||
def test900cleanup(self):
|
||||
"Cleaning up"
|
||||
#self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2'), "Could not delete test node.")
|
||||
time.sleep(10)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
#parse command line arguements
|
||||
optp = OptionParser()
|
||||
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("-c","--config", dest="configfile", default="config.xml", help="set config file to use")
|
||||
optp.add_option("-n","--nodenum", dest="nodenum", default="1", help="set node number to use")
|
||||
optp.add_option("-p","--pubsub", dest="pubsub", default="1", help="set pubsub host to use")
|
||||
opts,args = optp.parse_args()
|
||||
|
||||
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||
|
||||
#load xml config
|
||||
logging.info("Loading config file: %s" % opts.configfile)
|
||||
config = configparser.RawConfigParser()
|
||||
config.read(opts.configfile)
|
||||
|
||||
#init
|
||||
logging.info("Account 1 is %s" % config.get('account1', 'jid'))
|
||||
xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass'))
|
||||
logging.info("Account 2 is %s" % config.get('account2', 'jid'))
|
||||
xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass'))
|
||||
|
||||
xmpp1.registerPlugin('xep_0004')
|
||||
xmpp1.registerPlugin('xep_0030')
|
||||
xmpp1.registerPlugin('xep_0060')
|
||||
xmpp1.registerPlugin('xep_0199')
|
||||
xmpp1.registerPlugin('jobs')
|
||||
xmpp2.registerPlugin('xep_0004')
|
||||
xmpp2.registerPlugin('xep_0030')
|
||||
xmpp2.registerPlugin('xep_0060')
|
||||
xmpp2.registerPlugin('xep_0199')
|
||||
xmpp2.registerPlugin('jobs')
|
||||
|
||||
if not config.get('account1', 'server'):
|
||||
# we don't know the server, but the lib can probably figure it out
|
||||
xmpp1.connect()
|
||||
else:
|
||||
xmpp1.connect((config.get('account1', 'server'), 5222))
|
||||
xmpp1.process(threaded=True)
|
||||
|
||||
#init
|
||||
if not config.get('account2', 'server'):
|
||||
# we don't know the server, but the lib can probably figure it out
|
||||
xmpp2.connect()
|
||||
else:
|
||||
xmpp2.connect((config.get('account2', 'server'), 5222))
|
||||
xmpp2.process(threaded=True)
|
||||
|
||||
TestPubsubServer.xmpp1 = xmpp1
|
||||
TestPubsubServer.xmpp2 = xmpp2
|
||||
TestPubsubServer.pshost = config.get('settings', 'pubsub')
|
||||
xmpp1.waitforstart.get(True)
|
||||
xmpp2.waitforstart.get(True)
|
||||
testsuite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubServer)
|
||||
|
||||
alltests_suite = unittest.TestSuite([testsuite])
|
||||
result = unittest.TextTestRunner(verbosity=2).run(alltests_suite)
|
||||
xmpp1.disconnect()
|
||||
xmpp2.disconnect()
|
|
@ -5,7 +5,6 @@ from xml.etree import cElementTree as ET
|
|||
import os
|
||||
import time
|
||||
import sys
|
||||
import thread
|
||||
import unittest
|
||||
import sleekxmpp.plugins.xep_0004
|
||||
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
|
||||
|
|
|
@ -37,8 +37,8 @@ if __name__ == '__main__':
|
|||
|
||||
logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
|
||||
xmpp = Example('user@gmail.com/sleekxmpp', 'password')
|
||||
xmpp.registerPlugin('xep_0030')
|
||||
xmpp.registerPlugin('xep_0004')
|
||||
xmpp.registerPlugin('xep_0030')
|
||||
xmpp.registerPlugin('xep_0060')
|
||||
xmpp.registerPlugin('xep_0199')
|
||||
if xmpp.connect(('talk.google.com', 5222)):
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
#!/usr/bin/python2.5
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file license.txt for copying permission.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from . basexmpp import basexmpp
|
||||
|
@ -31,6 +31,7 @@ from . import plugins
|
|||
srvsupport = True
|
||||
try:
|
||||
import dns.resolver
|
||||
import dns.rdatatype
|
||||
except ImportError:
|
||||
srvsupport = False
|
||||
|
||||
|
@ -53,12 +54,14 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
self.plugin_config = plugin_config
|
||||
self.escape_quotes = escape_quotes
|
||||
self.set_jid(jid)
|
||||
self.server = None
|
||||
self.port = 5222 # not used if DNS SRV is used
|
||||
self.plugin_whitelist = plugin_whitelist
|
||||
self.auto_reconnect = True
|
||||
self.srvsupport = srvsupport
|
||||
self.password = password
|
||||
self.registered_features = []
|
||||
self.stream_header = """<stream:stream to='%s' xmlns:stream='http://etherx.jabber.org/streams' xmlns='%s' version='1.0'>""" % (self.server,self.default_ns)
|
||||
self.stream_header = """<stream:stream to='%s' xmlns:stream='http://etherx.jabber.org/streams' xmlns='%s' version='1.0'>""" % (self.domain,self.default_ns)
|
||||
self.stream_footer = "</stream:stream>"
|
||||
#self.map_namespace('http://etherx.jabber.org/streams', 'stream')
|
||||
#self.map_namespace('jabber:client', '')
|
||||
|
@ -87,16 +90,23 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
def get(self, key, default):
|
||||
return self.plugin.get(key, default)
|
||||
|
||||
def connect(self, address=tuple()):
|
||||
def connect(self, host=None, port=None):
|
||||
"""Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses
|
||||
the JID server."""
|
||||
if not address or len(address) < 2:
|
||||
|
||||
if self.state['connected']: return True
|
||||
|
||||
if host:
|
||||
self.server = host
|
||||
if port is None: port = self.port
|
||||
else:
|
||||
if not self.srvsupport:
|
||||
logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using server hostname from JID.")
|
||||
logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using domain from JID.")
|
||||
else:
|
||||
logging.debug("Since no address is supplied, attempting SRV lookup.")
|
||||
try:
|
||||
answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.server)
|
||||
answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.domain,
|
||||
dns.rdatatype.SRV )
|
||||
except dns.resolver.NXDOMAIN:
|
||||
logging.debug("No appropriate SRV record found. Using JID server name.")
|
||||
else:
|
||||
|
@ -113,12 +123,19 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
picked = random.randint(0, intmax)
|
||||
for priority in priorities:
|
||||
if picked <= priority:
|
||||
address = addresses[priority]
|
||||
(host,port) = addresses[priority]
|
||||
break
|
||||
if not address:
|
||||
# if SRV lookup was successful, we aren't using a particular server.
|
||||
self.server = None
|
||||
|
||||
if not host:
|
||||
# if all else fails take server from JID.
|
||||
address = (self.server, 5222)
|
||||
result = XMLStream.connect(self, address[0], address[1], use_tls=True)
|
||||
(host,port) = (self.domain, self.port)
|
||||
self.server = None
|
||||
|
||||
logging.debug('Attempting connection to %s:%d', host, port )
|
||||
#TODO option to not use TLS?
|
||||
result = XMLStream.connect(self, host, port, use_tls=True)
|
||||
if result:
|
||||
self.event("connected")
|
||||
else:
|
||||
|
@ -159,6 +176,7 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
self._handleRoster(iq, request=True)
|
||||
|
||||
def _handleStreamFeatures(self, features):
|
||||
logging.debug('handling stream features')
|
||||
self.features = []
|
||||
for sub in features.xml:
|
||||
self.features.append(sub.tag)
|
||||
|
@ -166,12 +184,16 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
for feature in self.registered_features:
|
||||
if feature[0].match(subelement):
|
||||
#if self.maskcmp(subelement, feature[0], True):
|
||||
# This calls the feature handler & optionally breaks
|
||||
if feature[1](subelement) and feature[2]: #if breaker, don't continue
|
||||
return True
|
||||
|
||||
def handler_starttls(self, xml):
|
||||
logging.debug( 'TLS start handler; SSL support: %s', self.ssl_support )
|
||||
if not self.authenticated and self.ssl_support:
|
||||
self.add_handler("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_tls_start, instream=True)
|
||||
_stanza = "<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />"
|
||||
if not self.event_handlers.get(_stanza,None): # don't add handler > once
|
||||
self.add_handler( _stanza, self.handler_tls_start, instream=True )
|
||||
self.sendXML(xml)
|
||||
return True
|
||||
else:
|
||||
|
@ -206,12 +228,13 @@ class ClientXMPP(basexmpp, XMLStream):
|
|||
return True
|
||||
|
||||
def handler_auth_success(self, xml):
|
||||
logging.debug("Authentication successful.")
|
||||
self.authenticated = True
|
||||
self.features = []
|
||||
raise RestartStream()
|
||||
|
||||
def handler_auth_fail(self, xml):
|
||||
logging.info("Authentication failed.")
|
||||
logging.warning("Authentication failed.")
|
||||
self.disconnect()
|
||||
self.event("failed_auth")
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ class basexmpp(object):
|
|||
self.resource = ''
|
||||
self.jid = ''
|
||||
self.username = ''
|
||||
self.server = ''
|
||||
self.domain = ''
|
||||
self.plugin = {}
|
||||
self.auto_authorize = True
|
||||
self.auto_subscribe = True
|
||||
|
@ -84,7 +84,12 @@ class basexmpp(object):
|
|||
self.resource = self.getjidresource(jid)
|
||||
self.jid = self.getjidbare(jid)
|
||||
self.username = jid.split('@', 1)[0]
|
||||
self.server = jid.split('@',1)[-1].split('/', 1)[0]
|
||||
self.domain = jid.split('@',1)[-1].split('/', 1)[0]
|
||||
|
||||
def process(self, *args, **kwargs):
|
||||
for idx in self.plugin:
|
||||
if not self.plugin[idx].post_inited: self.plugin[idx].post_init()
|
||||
return super(basexmpp, self).process(*args, **kwargs)
|
||||
|
||||
def registerPlugin(self, plugin, pconfig = {}):
|
||||
"""Register a plugin not in plugins.__init__.__all__ but in the plugins
|
||||
|
@ -109,7 +114,7 @@ class basexmpp(object):
|
|||
plugin_list = plugins.__all__
|
||||
for plugin in plugin_list:
|
||||
if plugin in plugins.__all__:
|
||||
self.registerPlugin(plugin, self.plugin_config.get(plugin, {}))
|
||||
self.registerPlugin(plugin, self.plugin_config.get(plugin, {}), False)
|
||||
else:
|
||||
raise NameError("No plugin by the name of %s listed in plugins.__all__." % plugin)
|
||||
# run post_init() for cross-plugin interaction
|
||||
|
@ -185,6 +190,19 @@ class basexmpp(object):
|
|||
self.event_handlers[name] = []
|
||||
self.event_handlers[name].append((pointer, threaded, disposable))
|
||||
|
||||
def del_event_handler(self, name, pointer):
|
||||
"""Remove a handler for an event."""
|
||||
if not name in self.event_handlers:
|
||||
return
|
||||
|
||||
# Need to keep handlers that do not use
|
||||
# the given function pointer
|
||||
def filter_pointers(handler):
|
||||
return handler[0] != pointer
|
||||
|
||||
self.event_handlers[name] = filter(filter_pointers,
|
||||
self.event_handlers[name])
|
||||
|
||||
def event(self, name, eventdata = {}): # called on an event
|
||||
for handler in self.event_handlers.get(name, []):
|
||||
if handler[1]: #if threaded
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/python2.5
|
||||
#!/usr/bin/python2.6
|
||||
|
||||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
|
@ -54,6 +54,16 @@ class ComponentXMPP(basexmpp, XMLStream):
|
|||
self.secret = secret
|
||||
self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake))
|
||||
|
||||
def __getitem__(self, key):
|
||||
if key in self.plugin:
|
||||
return self.plugin[key]
|
||||
else:
|
||||
logging.warning("""Plugin "%s" is not loaded.""" % key)
|
||||
return False
|
||||
|
||||
def get(self, key, default):
|
||||
return self.plugin.get(key, default)
|
||||
|
||||
def incoming_filter(self, xmlobj):
|
||||
if xmlobj.tag.startswith('{jabber:client}'):
|
||||
xmlobj.tag = xmlobj.tag.replace('jabber:client', self.default_ns)
|
||||
|
|
|
@ -24,6 +24,7 @@ class base_plugin(object):
|
|||
self.description = 'Base Plugin'
|
||||
self.xmpp = xmpp
|
||||
self.config = config
|
||||
self.post_inited = False
|
||||
self.enable = config.get('enable', True)
|
||||
if self.enable:
|
||||
self.plugin_init()
|
||||
|
@ -32,4 +33,4 @@ class base_plugin(object):
|
|||
pass
|
||||
|
||||
def post_init(self):
|
||||
pass
|
||||
self.post_inited = True
|
||||
|
|
44
sleekxmpp/plugins/jobs.py
Normal file
44
sleekxmpp/plugins/jobs.py
Normal file
|
@ -0,0 +1,44 @@
|
|||
from . import base
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
|
||||
class jobs(base.base_plugin):
|
||||
def plugin_init(self):
|
||||
self.xep = 'pubsubjob'
|
||||
self.description = "Job distribution over Pubsub"
|
||||
|
||||
def post_init(self):
|
||||
pass
|
||||
#TODO add event
|
||||
|
||||
def createJobNode(self, host, jid, node, config=None):
|
||||
pass
|
||||
|
||||
def createJob(self, host, node, jobid=None, payload=None):
|
||||
return self.xmpp.plugin['xep_0060'].setItem(host, node, ((jobid, payload),))
|
||||
|
||||
def claimJob(self, host, node, jobid, ifrom=None):
|
||||
return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}claimed'))
|
||||
|
||||
def unclaimJob(self, jobid):
|
||||
return self._setState(host, node, jobid, ET.Element('{http://andyet.net/protocol/pubsubjob}unclaimed'))
|
||||
|
||||
def finishJob(self, host, node, jobid, payload=None):
|
||||
finished = ET.Element('{http://andyet.net/protocol/pubsubjob}finished')
|
||||
if payload is not None:
|
||||
finished.append(payload)
|
||||
return self._setState(host, node, jobid, finished)
|
||||
|
||||
def _setState(self, host, node, jobid, state, ifrom=None):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['to'] = host
|
||||
if ifrom: iq['from'] = ifrom
|
||||
iq['type'] = 'set'
|
||||
iq['psstate']['node'] = node
|
||||
iq['psstate']['item'] = jobid
|
||||
iq['psstate']['payload'] = state
|
||||
result = iq.send()
|
||||
if result is None or result['type'] != 'result':
|
||||
return False
|
||||
return True
|
||||
|
|
@ -10,6 +10,39 @@ def stanzaPlugin(stanza, plugin):
|
|||
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
|
||||
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
|
||||
|
||||
class PubsubState(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/psstate'
|
||||
name = 'state'
|
||||
plugin_attrib = 'psstate'
|
||||
interfaces = set(('node', 'item', 'payload'))
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
def setPayload(self, value):
|
||||
self.xml.append(value)
|
||||
|
||||
def getPayload(self):
|
||||
childs = self.xml.getchildren()
|
||||
if len(childs) > 0:
|
||||
return childs[0]
|
||||
|
||||
def delPayload(self):
|
||||
for child in self.xml.getchildren():
|
||||
self.xml.remove(child)
|
||||
|
||||
stanzaPlugin(Iq, PubsubState)
|
||||
|
||||
class PubsubStateEvent(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/psstate#event'
|
||||
name = 'event'
|
||||
plugin_attrib = 'psstate_event'
|
||||
intefaces = set(tuple())
|
||||
plugin_attrib_map = {}
|
||||
plugin_tag_map = {}
|
||||
|
||||
stanzaPlugin(Message, PubsubStateEvent)
|
||||
stanzaPlugin(PubsubStateEvent, PubsubState)
|
||||
|
||||
class Pubsub(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/pubsub'
|
||||
name = 'pubsub'
|
||||
|
@ -281,7 +314,7 @@ class DefaultConfig(ElementBase):
|
|||
|
||||
def getType(self):
|
||||
t = self._getAttr('type')
|
||||
if not t: t == 'leaf'
|
||||
if not t: t = 'leaf'
|
||||
return t
|
||||
|
||||
stanzaPlugin(PubsubOwner, DefaultConfig)
|
||||
|
@ -321,18 +354,6 @@ class Options(ElementBase):
|
|||
stanzaPlugin(Pubsub, Options)
|
||||
stanzaPlugin(Subscribe, Options)
|
||||
|
||||
#iq = Iq()
|
||||
#iq['pubsub']['defaultconfig']
|
||||
#print(iq)
|
||||
|
||||
#from xml.etree import cElementTree as ET
|
||||
#iq = Iq()
|
||||
#item = Item()
|
||||
#item['payload'] = ET.Element("{http://netflint.net/p/crap}stupidshit")
|
||||
#item['id'] = 'aa11bbcc'
|
||||
#iq['pubsub']['items'].append(item)
|
||||
#print(iq)
|
||||
|
||||
class OwnerAffiliations(Affiliations):
|
||||
namespace = 'http://jabber.org/protocol/pubsub#owner'
|
||||
interfaces = set(('node'))
|
||||
|
|
|
@ -31,7 +31,8 @@ class xep_0004(base.base_plugin):
|
|||
self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform)
|
||||
|
||||
def post_init(self):
|
||||
self.xmpp['xep_0030'].add_feature('jabber:x:data')
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
|
||||
|
||||
def handler_message_xform(self, xml):
|
||||
object = self.handle_form(xml)
|
||||
|
@ -187,7 +188,6 @@ class Form(FieldContainer):
|
|||
|
||||
#def getXML(self, tostring = False):
|
||||
def getXML(self, ftype=None):
|
||||
logging.debug("creating form as %s" % ftype)
|
||||
if ftype:
|
||||
self.type = ftype
|
||||
form = ET.Element('{jabber:x:data}x')
|
||||
|
|
|
@ -185,8 +185,9 @@ class xep_0009(base.base_plugin):
|
|||
self.activeCalls = []
|
||||
|
||||
def post_init(self):
|
||||
self.xmpp['xep_0030'].add_feature('jabber:iq:rpc')
|
||||
self.xmpp['xep_0030'].add_identity('automatition','rpc')
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:rpc')
|
||||
self.xmpp.plugin['xep_0030'].add_identity('automatition','rpc')
|
||||
|
||||
def register_call(self, method, name=None):
|
||||
#@returns an string that can be used in acl commands.
|
||||
|
|
|
@ -1,25 +1,184 @@
|
|||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2007 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
SleekXMPP is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation; either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
SleekXMPP is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with SleekXMPP; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
See the file license.txt for copying permissio
|
||||
"""
|
||||
from . import base
|
||||
|
||||
import logging
|
||||
from xml.etree import cElementTree as ET
|
||||
from . import base
|
||||
from .. xmlstream.handler.callback import Callback
|
||||
from .. xmlstream.matcher.xpath import MatchXPath
|
||||
from .. xmlstream.stanzabase import ElementBase, ET, JID
|
||||
from .. stanza.iq import Iq
|
||||
|
||||
class DiscoInfo(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/disco#info'
|
||||
name = 'query'
|
||||
plugin_attrib = 'disco_info'
|
||||
interfaces = set(('node', 'features', 'identities'))
|
||||
|
||||
def getFeatures(self):
|
||||
features = []
|
||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
||||
for feature in featuresXML:
|
||||
features.append(feature.attrib['var'])
|
||||
return features
|
||||
|
||||
def setFeatures(self, features):
|
||||
self.delFeatures()
|
||||
for name in features:
|
||||
self.addFeature(name)
|
||||
|
||||
def delFeatures(self):
|
||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
||||
for feature in featuresXML:
|
||||
self.xml.remove(feature)
|
||||
|
||||
def addFeature(self, feature):
|
||||
featureXML = ET.Element('{%s}feature' % self.namespace,
|
||||
{'var': feature})
|
||||
self.xml.append(featureXML)
|
||||
|
||||
def delFeature(self, feature):
|
||||
featuresXML = self.xml.findall('{%s}feature' % self.namespace)
|
||||
for featureXML in featuresXML:
|
||||
if featureXML.attrib['var'] == feature:
|
||||
self.xml.remove(featureXML)
|
||||
|
||||
def getIdentities(self):
|
||||
ids = []
|
||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
||||
for idXML in idsXML:
|
||||
idData = (idXML.attrib['category'],
|
||||
idXML.attrib['type'],
|
||||
idXML.attrib.get('name', ''))
|
||||
ids.append(idData)
|
||||
return ids
|
||||
|
||||
def setIdentities(self, ids):
|
||||
self.delIdentities()
|
||||
for idData in ids:
|
||||
self.addIdentity(*idData)
|
||||
|
||||
def delIdentities(self):
|
||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
||||
for idXML in idsXML:
|
||||
self.xml.remove(idXML)
|
||||
|
||||
def addIdentity(self, category, id_type, name=''):
|
||||
idXML = ET.Element('{%s}identity' % self.namespace,
|
||||
{'category': category,
|
||||
'type': id_type,
|
||||
'name': name})
|
||||
self.xml.append(idXML)
|
||||
|
||||
def delIdentity(self, category, id_type, name=''):
|
||||
idsXML = self.xml.findall('{%s}identity' % self.namespace)
|
||||
for idXML in idsXML:
|
||||
idData = (idXML.attrib['category'],
|
||||
idXML.attrib['type'])
|
||||
delId = (category, id_type)
|
||||
if idData == delId:
|
||||
self.xml.remove(idXML)
|
||||
|
||||
|
||||
class DiscoItems(ElementBase):
|
||||
namespace = 'http://jabber.org/protocol/disco#items'
|
||||
name = 'query'
|
||||
plugin_attrib = 'disco_items'
|
||||
interfaces = set(('node', 'items'))
|
||||
|
||||
def getItems(self):
|
||||
items = []
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for item in itemsXML:
|
||||
itemData = (item.attrib['jid'],
|
||||
item.attrib.get('node'),
|
||||
item.attrib.get('name'))
|
||||
items.append(itemData)
|
||||
return items
|
||||
|
||||
def setItems(self, items):
|
||||
self.delItems()
|
||||
for item in items:
|
||||
self.addItem(*item)
|
||||
|
||||
def delItems(self):
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for item in itemsXML:
|
||||
self.xml.remove(item)
|
||||
|
||||
def addItem(self, jid, node='', name=''):
|
||||
itemXML = ET.Element('{%s}item' % self.namespace, {'jid': jid})
|
||||
if name:
|
||||
itemXML.attrib['name'] = name
|
||||
if node:
|
||||
itemXML.attrib['node'] = node
|
||||
self.xml.append(itemXML)
|
||||
|
||||
def delItem(self, jid, node=''):
|
||||
itemsXML = self.xml.findall('{%s}item' % self.namespace)
|
||||
for itemXML in itemsXML:
|
||||
itemData = (itemXML.attrib['jid'],
|
||||
itemXML.attrib.get('node', ''))
|
||||
itemDel = (jid, node)
|
||||
if itemData == itemDel:
|
||||
self.xml.remove(itemXML)
|
||||
|
||||
|
||||
class DiscoNode(object):
|
||||
"""
|
||||
Collection object for grouping info and item information
|
||||
into nodes.
|
||||
"""
|
||||
def __init__(self, name):
|
||||
self.name = name
|
||||
self.info = DiscoInfo()
|
||||
self.items = DiscoItems()
|
||||
|
||||
# This is a bit like poor man's inheritance, but
|
||||
# to simplify adding information to the node we
|
||||
# map node functions to either the info or items
|
||||
# stanza objects.
|
||||
#
|
||||
# We don't want to make DiscoNode inherit from
|
||||
# DiscoInfo and DiscoItems because DiscoNode is
|
||||
# not an actual stanza, and doing so would create
|
||||
# confusion and potential bugs.
|
||||
|
||||
self._map(self.items, 'items', ['get', 'set', 'del'])
|
||||
self._map(self.items, 'item', ['add', 'del'])
|
||||
self._map(self.info, 'identities', ['get', 'set', 'del'])
|
||||
self._map(self.info, 'identity', ['add', 'del'])
|
||||
self._map(self.info, 'features', ['get', 'set', 'del'])
|
||||
self._map(self.info, 'feature', ['add', 'del'])
|
||||
|
||||
def isEmpty(self):
|
||||
"""
|
||||
Test if the node contains any information. Useful for
|
||||
determining if a node can be deleted.
|
||||
"""
|
||||
ids = self.getIdentities()
|
||||
features = self.getFeatures()
|
||||
items = self.getItems()
|
||||
|
||||
if not ids and not features and not items:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _map(self, obj, interface, access):
|
||||
"""
|
||||
Map functions of the form obj.accessInterface
|
||||
to self.accessInterface for each given access type.
|
||||
"""
|
||||
interface = interface.title()
|
||||
for access_type in access:
|
||||
method = access_type + interface
|
||||
if hasattr(obj, method):
|
||||
setattr(self, method, getattr(obj, method))
|
||||
|
||||
|
||||
class xep_0030(base.base_plugin):
|
||||
"""
|
||||
|
@ -29,85 +188,137 @@ class xep_0030(base.base_plugin):
|
|||
def plugin_init(self):
|
||||
self.xep = '0030'
|
||||
self.description = 'Service Discovery'
|
||||
self.features = {'main': ['http://jabber.org/protocol/disco#info', 'http://jabber.org/protocol/disco#items']}
|
||||
self.identities = {'main': [{'category': 'client', 'type': 'pc', 'name': 'SleekXMPP'}]}
|
||||
self.items = {'main': []}
|
||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='http://jabber.org/protocol/disco#info' /></iq>" % self.xmpp.default_ns, self.info_handler)
|
||||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='http://jabber.org/protocol/disco#items' /></iq>" % self.xmpp.default_ns, self.item_handler)
|
||||
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Disco Items',
|
||||
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
|
||||
DiscoItems.namespace)),
|
||||
self.handle_item_query))
|
||||
|
||||
self.xmpp.registerHandler(
|
||||
Callback('Disco Info',
|
||||
MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns,
|
||||
DiscoInfo.namespace)),
|
||||
self.handle_info_query))
|
||||
|
||||
self.xmpp.stanzaPlugin(Iq, DiscoInfo)
|
||||
self.xmpp.stanzaPlugin(Iq, DiscoItems)
|
||||
|
||||
self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items)
|
||||
self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info)
|
||||
|
||||
self.nodes = {'main': DiscoNode('main')}
|
||||
|
||||
def add_node(self, node):
|
||||
if node not in self.nodes:
|
||||
self.nodes[node] = DiscoNode(node)
|
||||
|
||||
def del_node(self, node):
|
||||
if node in self.nodes:
|
||||
del self.nodes[node]
|
||||
|
||||
def handle_item_query(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
logging.debug("Items requested by %s" % iq['from'])
|
||||
self.xmpp.event('disco_items_request', iq)
|
||||
elif iq['type'] == 'result':
|
||||
logging.debug("Items result from %s" % iq['from'])
|
||||
self.xmpp.event('disco_items', iq)
|
||||
|
||||
def handle_info_query(self, iq):
|
||||
if iq['type'] == 'get':
|
||||
logging.debug("Info requested by %s" % iq['from'])
|
||||
self.xmpp.event('disco_info_request', iq)
|
||||
elif iq['type'] == 'result':
|
||||
logging.debug("Info result from %s" % iq['from'])
|
||||
self.xmpp.event('disco_info', iq)
|
||||
|
||||
def handle_disco_info(self, iq, forwarded=False):
|
||||
"""
|
||||
A default handler for disco#info requests. If another
|
||||
handler is registered, this one will defer and not run.
|
||||
"""
|
||||
handlers = self.xmpp.event_handlers['disco_info_request']
|
||||
if not forwarded and len(handlers) > 1:
|
||||
return
|
||||
|
||||
node_name = iq['disco_info']['node']
|
||||
if not node_name:
|
||||
node_name = 'main'
|
||||
|
||||
logging.debug("Using default handler for disco#info on node '%s'." % node_name)
|
||||
|
||||
if node_name in self.nodes:
|
||||
node = self.nodes[node_name]
|
||||
iq.reply().setPayload(node.info.xml).send()
|
||||
else:
|
||||
logging.debug("Node %s requested, but does not exist." % node_name)
|
||||
iq.reply().error().setPayload(iq['disco_info'].xml)
|
||||
iq['error']['code'] = '404'
|
||||
iq['error']['type'] = 'cancel'
|
||||
iq['error']['condition'] = 'item-not-found'
|
||||
iq.send()
|
||||
|
||||
def handle_disco_items(self, iq, forwarded=False):
|
||||
"""
|
||||
A default handler for disco#items requests. 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.
|
||||
"""
|
||||
handlers = self.xmpp.event_handlers['disco_items_request']
|
||||
if not forwarded and len(handlers) > 1:
|
||||
return
|
||||
|
||||
node_name = iq['disco_items']['node']
|
||||
if not node_name:
|
||||
node_name = 'main'
|
||||
|
||||
logging.debug("Using default handler for disco#items on node '%s'." % node_name)
|
||||
|
||||
if node_name in self.nodes:
|
||||
node = self.nodes[node_name]
|
||||
iq.reply().setPayload(node.items.xml).send()
|
||||
else:
|
||||
logging.debug("Node %s requested, but does not exist." % node_name)
|
||||
iq.reply().error().setPayload(iq['disco_items'].xml)
|
||||
iq['error']['code'] = '404'
|
||||
iq['error']['type'] = 'cancel'
|
||||
iq['error']['condition'] = 'item-not-found'
|
||||
iq.send()
|
||||
|
||||
# Older interface methods for backwards compatibility
|
||||
|
||||
def getInfo(self, jid, node=''):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['to'] = jid
|
||||
iq['from'] = self.xmpp.fulljid
|
||||
iq['disco_info']['node'] = node
|
||||
iq.send()
|
||||
|
||||
def getItems(self, jid, node=''):
|
||||
iq = self.xmpp.Iq()
|
||||
iq['type'] = 'get'
|
||||
iq['to'] = jid
|
||||
iq['from'] = self.xmpp.fulljid
|
||||
iq['disco_items']['node'] = node
|
||||
iq.send()
|
||||
|
||||
def add_feature(self, feature, node='main'):
|
||||
if not node in self.features:
|
||||
self.features[node] = []
|
||||
self.features[node].append(feature)
|
||||
self.add_node(node)
|
||||
self.nodes[node].addFeature(feature)
|
||||
|
||||
def add_identity(self, category=None, itype=None, name=None, node='main'):
|
||||
if not node in self.identities:
|
||||
self.identities[node] = []
|
||||
self.identities[node].append({'category': category, 'type': itype, 'name': name})
|
||||
def add_identity(self, category='', itype='', name='', node='main'):
|
||||
self.add_node(node)
|
||||
self.nodes[node].addIdentity(category=category,
|
||||
id_type=itype,
|
||||
name=name)
|
||||
|
||||
def add_item(self, jid=None, name=None, node='main', subnode=''):
|
||||
if not node in self.items:
|
||||
self.items[node] = []
|
||||
self.items[node].append({'jid': jid, 'name': name, 'node': subnode})
|
||||
|
||||
def info_handler(self, xml):
|
||||
logging.debug("Info request from %s" % xml.get('from', ''))
|
||||
iq = self.xmpp.makeIqResult(xml.get('id', self.xmpp.getNewId()))
|
||||
iq.attrib['from'] = xml.get('to')
|
||||
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||
query = xml.find('{http://jabber.org/protocol/disco#info}query')
|
||||
node = query.get('node', 'main')
|
||||
for identity in self.identities.get(node, []):
|
||||
idxml = ET.Element('identity')
|
||||
for attrib in identity:
|
||||
if identity[attrib]:
|
||||
idxml.attrib[attrib] = identity[attrib]
|
||||
query.append(idxml)
|
||||
for feature in self.features.get(node, []):
|
||||
featxml = ET.Element('feature')
|
||||
featxml.attrib['var'] = feature
|
||||
query.append(featxml)
|
||||
iq.append(query)
|
||||
#print ET.tostring(iq)
|
||||
self.xmpp.send(iq)
|
||||
|
||||
def item_handler(self, xml):
|
||||
logging.debug("Item request from %s" % xml.get('from', ''))
|
||||
iq = self.xmpp.makeIqResult(xml.get('id', self.xmpp.getNewId()))
|
||||
iq.attrib['from'] = xml.get('to')
|
||||
iq.attrib['to'] = xml.get('from', self.xmpp.server)
|
||||
query = self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#items').find('{http://jabber.org/protocol/disco#items}query')
|
||||
node = xml.find('{http://jabber.org/protocol/disco#items}query').get('node', 'main')
|
||||
for item in self.items.get(node, []):
|
||||
itemxml = ET.Element('item')
|
||||
itemxml.attrib = item
|
||||
if itemxml.attrib['jid'] is None:
|
||||
itemxml.attrib['jid'] = xml.get('to')
|
||||
query.append(itemxml)
|
||||
self.xmpp.send(iq)
|
||||
|
||||
def getItems(self, jid, node=None):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['to'] = jid
|
||||
self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#items')
|
||||
if node:
|
||||
iq.find('{http://jabber.org/protocol/disco#items}query').attrib['node'] = node
|
||||
return iq.send()
|
||||
|
||||
def getInfo(self, jid, node=None):
|
||||
iq = self.xmpp.makeIqGet()
|
||||
iq.attrib['from'] = self.xmpp.fulljid
|
||||
iq.attrib['to'] = jid
|
||||
self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#info')
|
||||
if node:
|
||||
iq.find('{http://jabber.org/protocol/disco#info}query').attrib['node'] = node
|
||||
return iq.send()
|
||||
|
||||
def parseInfo(self, xml):
|
||||
result = {'identity': {}, 'feature': []}
|
||||
for identity in xml.findall('{http://jabber.org/protocol/disco#info}query/{{http://jabber.org/protocol/disco#info}identity'):
|
||||
result['identity'][identity['name']] = identity.attrib
|
||||
for feature in xml.findall('{http://jabber.org/protocol/disco#info}query/{{http://jabber.org/protocol/disco#info}feature'):
|
||||
result['feature'].append(feature.get('var', '__unknown__'))
|
||||
return result
|
||||
def add_item(self, jid=None, name='', node='main', subnode=''):
|
||||
self.add_node(node)
|
||||
self.add_node(subnode)
|
||||
if jid is None:
|
||||
jid = self.xmpp.fulljid
|
||||
self.nodes[node].addItem(jid=jid, name=name, node=subnode)
|
||||
|
|
|
@ -42,6 +42,7 @@ class xep_0050(base.base_plugin):
|
|||
self.sd = self.xmpp.plugin['xep_0030']
|
||||
|
||||
def post_init(self):
|
||||
base.base_plugin.post_init(self)
|
||||
self.sd.add_feature('http://jabber.org/protocol/commands')
|
||||
|
||||
def addCommand(self, node, name, form, pointer=None, multi=False):
|
||||
|
|
|
@ -14,12 +14,14 @@ class xep_0060(base.base_plugin):
|
|||
self.xep = '0060'
|
||||
self.description = 'Publish-Subscribe'
|
||||
|
||||
def create_node(self, jid, node, config=None, collection=False):
|
||||
def create_node(self, jid, node, config=None, collection=False, ntype=None):
|
||||
pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub')
|
||||
create = ET.Element('create')
|
||||
create.set('node', node)
|
||||
pubsub.append(create)
|
||||
configure = ET.Element('configure')
|
||||
if collection:
|
||||
ntype = 'collection'
|
||||
#if config is None:
|
||||
# submitform = self.xmpp.plugin['xep_0004'].makeForm('submit')
|
||||
#else:
|
||||
|
@ -29,11 +31,11 @@ class xep_0060(base.base_plugin):
|
|||
submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config')
|
||||
else:
|
||||
submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config')
|
||||
if collection:
|
||||
if ntype:
|
||||
if 'pubsub#node_type' in submitform.field:
|
||||
submitform.field['pubsub#node_type'].setValue('collection')
|
||||
submitform.field['pubsub#node_type'].setValue(ntype)
|
||||
else:
|
||||
submitform.addField('pubsub#node_type', value='collection')
|
||||
submitform.addField('pubsub#node_type', value=ntype)
|
||||
else:
|
||||
if 'pubsub#node_type' in submitform.field:
|
||||
submitform.field['pubsub#node_type'].setValue('leaf')
|
||||
|
|
|
@ -33,7 +33,8 @@ class xep_0092(base.base_plugin):
|
|||
self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version)
|
||||
|
||||
def post_init(self):
|
||||
self.xmpp['xep_0030'].add_feature('jabber:iq:version')
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version')
|
||||
|
||||
def report_version(self, xml):
|
||||
iq = self.xmpp.makeIqResult(xml.get('id', 'unknown'))
|
||||
|
|
|
@ -35,7 +35,8 @@ class xep_0199(base.base_plugin):
|
|||
#self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True)
|
||||
|
||||
def post_init(self):
|
||||
self.xmpp['xep_0030'].add_feature('http://www.xmpp.org/extensions/xep-0199.html#ns')
|
||||
base.base_plugin.post_init(self)
|
||||
self.xmpp.plugin['xep_0030'].add_feature('http://www.xmpp.org/extensions/xep-0199.html#ns')
|
||||
|
||||
def handler_pingserver(self, xml):
|
||||
if not self.running:
|
||||
|
|
|
@ -11,8 +11,8 @@ class Error(ElementBase):
|
|||
namespace = 'jabber:client'
|
||||
name = 'error'
|
||||
plugin_attrib = 'error'
|
||||
conditions = set(('bad-request', 'conflict', 'feature-not-implemented', 'forbidden', 'gone', 'item-not-found', 'jid-malformed', 'not-acceptable', 'not-allowed', 'not-authorized', 'payment-required', 'recipient-unavailable', 'redirect', 'registration-required', 'remote-server-not-found', 'remote-server-timeout', 'service-unavailable', 'subscription-required', 'undefined-condition', 'unexpected-request'))
|
||||
interfaces = set(('condition', 'text', 'type'))
|
||||
conditions = set(('bad-request', 'conflict', 'feature-not-implemented', 'forbidden', 'gone', 'internal-server-error', 'item-not-found', 'jid-malformed', 'not-acceptable', 'not-allowed', 'not-authorized', 'payment-required', 'recipient-unavailable', 'redirect', 'registration-required', 'remote-server-not-found', 'remote-server-timeout', 'resource-constraint', 'service-unavailable', 'subscription-required', 'undefined-condition', 'unexpected-request'))
|
||||
interfaces = set(('code', 'condition', 'text', 'type'))
|
||||
types = set(('cancel', 'continue', 'modify', 'auth', 'wait'))
|
||||
sub_interfaces = set(('text',))
|
||||
condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
|
||||
|
|
|
@ -37,6 +37,7 @@ class Iq(RootStanza):
|
|||
def setPayload(self, value):
|
||||
self.clear()
|
||||
StanzaBase.setPayload(self, value)
|
||||
return self
|
||||
|
||||
def setQuery(self, value):
|
||||
query = self.xml.find("{%s}query" % value)
|
||||
|
|
|
@ -18,7 +18,7 @@ class BaseHandler(object):
|
|||
def match(self, xml):
|
||||
return self._matcher.match(xml)
|
||||
|
||||
def prerun(self, payload):
|
||||
def prerun(self, payload): # what's the point of this if the payload is called again in run??
|
||||
self._payload = payload
|
||||
|
||||
def run(self, payload):
|
||||
|
|
|
@ -17,13 +17,15 @@ class Callback(base.BaseHandler):
|
|||
self._once = once
|
||||
self._instream = instream
|
||||
|
||||
def prerun(self, payload):
|
||||
def prerun(self, payload): # prerun actually calls run?!? WTF! Then it gets run AGAIN!
|
||||
base.BaseHandler.prerun(self, payload)
|
||||
if self._instream:
|
||||
logging.debug('callback "%s" prerun', self.name)
|
||||
self.run(payload, True)
|
||||
|
||||
def run(self, payload, instream=False):
|
||||
if not self._instream or instream:
|
||||
logging.debug('callback "%s" run', self.name)
|
||||
base.BaseHandler.run(self, payload)
|
||||
#if self._thread:
|
||||
# x = threading.Thread(name="Callback_%s" % self.name, target=self._pointer, args=(payload,))
|
||||
|
|
87
sleekxmpp/xmlstream/scheduler.py
Normal file
87
sleekxmpp/xmlstream/scheduler.py
Normal file
|
@ -0,0 +1,87 @@
|
|||
try:
|
||||
import queue
|
||||
except ImportError:
|
||||
import Queue as queue
|
||||
import time
|
||||
import threading
|
||||
import logging
|
||||
|
||||
class Task(object):
|
||||
"""Task object for the Scheduler class"""
|
||||
def __init__(self, name, seconds, callback, args=None, kwargs=None, repeat=False, qpointer=None):
|
||||
self.name = name
|
||||
self.seconds = seconds
|
||||
self.callback = callback
|
||||
self.args = args or tuple()
|
||||
self.kwargs = kwargs or {}
|
||||
self.repeat = repeat
|
||||
self.next = time.time() + self.seconds
|
||||
self.qpointer = qpointer
|
||||
|
||||
def run(self):
|
||||
if self.qpointer is not None:
|
||||
self.qpointer.put(('schedule', self.callback, self.args))
|
||||
else:
|
||||
self.callback(*self.args, **self.kwargs)
|
||||
self.reset()
|
||||
return self.repeat
|
||||
|
||||
def reset(self):
|
||||
self.next = time.time() + self.seconds
|
||||
|
||||
class Scheduler(object):
|
||||
"""Threaded scheduler that allows for updates mid-execution unlike http://docs.python.org/library/sched.html#module-sched"""
|
||||
def __init__(self, parentqueue=None):
|
||||
self.addq = queue.Queue()
|
||||
self.schedule = []
|
||||
self.thread = None
|
||||
self.run = False
|
||||
self.parentqueue = parentqueue
|
||||
|
||||
def process(self, threaded=True):
|
||||
if threaded:
|
||||
self.thread = threading.Thread(name='shedulerprocess', target=self._process)
|
||||
self.thread.start()
|
||||
else:
|
||||
self._process()
|
||||
|
||||
def _process(self):
|
||||
self.run = True
|
||||
while self.run:
|
||||
try:
|
||||
wait = 1
|
||||
updated = False
|
||||
if self.schedule:
|
||||
wait = self.schedule[0].next - time.time()
|
||||
try:
|
||||
if wait <= 0.0:
|
||||
newtask = self.addq.get(False)
|
||||
else:
|
||||
newtask = self.addq.get(True, wait)
|
||||
except queue.Empty:
|
||||
cleanup = []
|
||||
for task in self.schedule:
|
||||
if time.time() >= task.next:
|
||||
updated = True
|
||||
if not task.run():
|
||||
cleanup.append(task)
|
||||
else:
|
||||
break
|
||||
for task in cleanup:
|
||||
x = self.schedule.pop(self.schedule.index(task))
|
||||
else:
|
||||
updated = True
|
||||
self.schedule.append(newtask)
|
||||
finally:
|
||||
if updated: self.schedule = sorted(self.schedule, key=lambda task: task.next)
|
||||
except KeyboardInterrupt:
|
||||
self.run = False
|
||||
logging.debug("Qutting Scheduler thread")
|
||||
if self.parentqueue is not None:
|
||||
self.parentqueue.put(('quit', None, None))
|
||||
|
||||
def add(self, name, seconds, callback, args=None, kwargs=None, repeat=False, qpointer=None):
|
||||
self.addq.put(Task(name, seconds, callback, args, kwargs, repeat, qpointer))
|
||||
|
||||
def quit(self):
|
||||
self.run = False
|
|
@ -78,6 +78,9 @@ class ElementBase(tostring.ToString):
|
|||
def __iter__(self):
|
||||
self.idx = 0
|
||||
return self
|
||||
|
||||
def __bool__(self):
|
||||
return True
|
||||
|
||||
def __next__(self):
|
||||
self.idx += 1
|
||||
|
@ -319,6 +322,8 @@ class StanzaBase(ElementBase):
|
|||
|
||||
def __init__(self, stream=None, xml=None, stype=None, sto=None, sfrom=None, sid=None):
|
||||
self.stream = stream
|
||||
if stream is not None:
|
||||
self.namespace = stream.default_ns
|
||||
ElementBase.__init__(self, xml)
|
||||
if stype is not None:
|
||||
self['type'] = stype
|
||||
|
@ -326,13 +331,11 @@ class StanzaBase(ElementBase):
|
|||
self['to'] = sto
|
||||
if sfrom is not None:
|
||||
self['from'] = sfrom
|
||||
if stream is not None:
|
||||
self.namespace = stream.default_ns
|
||||
self.tag = "{%s}%s" % (self.namespace, self.name)
|
||||
|
||||
def setType(self, value):
|
||||
if value in self.types:
|
||||
self.xml.attrib['type'] = value
|
||||
self.xml.attrib['type'] = value
|
||||
return self
|
||||
|
||||
def getPayload(self):
|
||||
|
@ -340,15 +343,18 @@ class StanzaBase(ElementBase):
|
|||
|
||||
def setPayload(self, value):
|
||||
self.xml.append(value)
|
||||
return self
|
||||
|
||||
def delPayload(self):
|
||||
self.clear()
|
||||
return self
|
||||
|
||||
def clear(self):
|
||||
for child in self.xml.getchildren():
|
||||
self.xml.remove(child)
|
||||
for plugin in list(self.plugins.keys()):
|
||||
del self.plugins[plugin]
|
||||
return self
|
||||
|
||||
def reply(self):
|
||||
self['from'], self['to'] = self['to'], self['from']
|
||||
|
@ -357,6 +363,7 @@ class StanzaBase(ElementBase):
|
|||
|
||||
def error(self):
|
||||
self['type'] = 'error'
|
||||
return self
|
||||
|
||||
def getTo(self):
|
||||
return JID(self._getAttr('to'))
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
"""
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
SleekXMPP: The Sleek XMPP Library
|
||||
Copyright (C) 2010 Nathanael C. Fritz
|
||||
This file is part of SleekXMPP.
|
||||
|
||||
See the file license.txt for copying permission.
|
||||
See the file license.txt for copying permission.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement, unicode_literals
|
||||
|
@ -22,6 +22,7 @@ import time
|
|||
import traceback
|
||||
import types
|
||||
import xml.sax.saxutils
|
||||
from . import scheduler
|
||||
|
||||
HANDLER_THREADS = 1
|
||||
|
||||
|
@ -53,7 +54,7 @@ class XMLStream(object):
|
|||
self.ssl_support = ssl_support
|
||||
self.escape_quotes = escape_quotes
|
||||
self.state = statemachine.StateMachine()
|
||||
self.state.addStates({'connected':False, 'is client':False, 'ssl':False, 'tls':False, 'reconnect':True, 'processing':False, 'disconnecting':False}) #set initial states
|
||||
self.state.addStates({'connected':False, 'is client':False, 'ssl':False, 'tls':False, 'reconnect':True, 'processing':False}) #set initial states
|
||||
|
||||
self.setSocket(socket)
|
||||
self.address = (host, int(port))
|
||||
|
@ -69,12 +70,14 @@ class XMLStream(object):
|
|||
self.filesocket = None
|
||||
self.use_ssl = False
|
||||
self.use_tls = False
|
||||
self.ca_certs=None
|
||||
|
||||
self.stream_header = "<stream>"
|
||||
self.stream_footer = "</stream>"
|
||||
|
||||
self.eventqueue = queue.Queue()
|
||||
self.sendqueue = queue.Queue()
|
||||
self.scheduler = scheduler.Scheduler(self.eventqueue)
|
||||
|
||||
self.namespace_map = {}
|
||||
|
||||
|
@ -98,30 +101,33 @@ class XMLStream(object):
|
|||
def connectTCP(self, host='', port=0, use_ssl=None, use_tls=None, reattempt=True):
|
||||
"Connect and create socket"
|
||||
while reattempt and not self.state['connected']:
|
||||
if host and port:
|
||||
self.address = (host, int(port))
|
||||
if use_ssl is not None:
|
||||
self.use_ssl = use_ssl
|
||||
if use_tls is not None:
|
||||
self.use_tls = use_tls
|
||||
self.state.set('is client', True)
|
||||
if sys.version_info < (3, 0):
|
||||
self.socket = filesocket.Socket26(socket.AF_INET, socket.SOCK_STREAM)
|
||||
else:
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(None)
|
||||
if self.use_ssl and self.ssl_support:
|
||||
logging.debug("Socket Wrapped for SSL")
|
||||
self.socket = ssl.wrap_socket(self.socket)
|
||||
logging.debug('connecting....')
|
||||
try:
|
||||
if host and port:
|
||||
self.address = (host, int(port))
|
||||
if use_ssl is not None:
|
||||
self.use_ssl = use_ssl
|
||||
if use_tls is not None:
|
||||
self.use_tls = use_tls
|
||||
if sys.version_info < (3, 0):
|
||||
self.socket = filesocket.Socket26(socket.AF_INET, socket.SOCK_STREAM)
|
||||
else:
|
||||
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.socket.settimeout(None) #10)
|
||||
if self.use_ssl and self.ssl_support:
|
||||
logging.debug("Socket Wrapped for SSL")
|
||||
self.socket = ssl.wrap_socket(self.socket,ca_certs=self.ca_certs)
|
||||
except:
|
||||
logging.exception("Connection error")
|
||||
try:
|
||||
self.socket.connect(self.address)
|
||||
#self.filesocket = self.socket.makefile('rb', 0)
|
||||
self.filesocket = self.socket.makefile('rb', 0)
|
||||
self.state.set('connected', True)
|
||||
logging.debug('connect complete.')
|
||||
return True
|
||||
except socket.error as serr:
|
||||
logging.error("Could not connect. Socket Error #%s: %s" % (serr.errno, serr.strerror))
|
||||
time.sleep(1)
|
||||
time.sleep(1) # TODO proper quiesce if connection attempt fails
|
||||
|
||||
def connectUnix(self, filepath):
|
||||
"Connect to Unix file and create socket"
|
||||
|
@ -130,14 +136,19 @@ class XMLStream(object):
|
|||
"Handshakes for TLS"
|
||||
if self.ssl_support:
|
||||
logging.info("Negotiating TLS")
|
||||
self.realsocket = self.socket
|
||||
self.socket = ssl.wrap_socket(self.socket, ssl_version=ssl.PROTOCOL_TLSv1, do_handshake_on_connect=False)
|
||||
# self.realsocket = self.socket # NOT USED
|
||||
self.socket = ssl.wrap_socket(self.socket,
|
||||
ssl_version=ssl.PROTOCOL_TLSv1,
|
||||
do_handshake_on_connect=False,
|
||||
ca_certs=self.ca_certs)
|
||||
self.socket.do_handshake()
|
||||
if sys.version_info < (3,0):
|
||||
from . filesocket import filesocket
|
||||
self.filesocket = filesocket(self.socket)
|
||||
else:
|
||||
self.filesocket = self.socket.makefile('rb', 0)
|
||||
|
||||
logging.debug("TLS negotitation successful")
|
||||
return True
|
||||
else:
|
||||
logging.warning("Tried to enable TLS, but ssl module not found.")
|
||||
|
@ -145,69 +156,78 @@ class XMLStream(object):
|
|||
raise RestartStream()
|
||||
|
||||
def process(self, threaded=True):
|
||||
self.scheduler.process(threaded=True)
|
||||
self.run = True
|
||||
for t in range(0, HANDLER_THREADS):
|
||||
self.__thread['eventhandle%s' % t] = threading.Thread(name='eventhandle%s' % t, target=self._eventRunner)
|
||||
self.__thread['eventhandle%s' % t].start()
|
||||
self.__thread['sendthread'] = threading.Thread(name='sendthread', target=self._sendThread)
|
||||
self.__thread['sendthread'].start()
|
||||
th = threading.Thread(name='eventhandle%s' % t, target=self._eventRunner)
|
||||
th.setDaemon(True)
|
||||
self.__thread['eventhandle%s' % t] = th
|
||||
th.start()
|
||||
th = threading.Thread(name='sendthread', target=self._sendThread)
|
||||
th.setDaemon(True)
|
||||
self.__thread['sendthread'] = th
|
||||
th.start()
|
||||
if threaded:
|
||||
self.__thread['process'] = threading.Thread(name='process', target=self._process)
|
||||
self.__thread['process'].start()
|
||||
th = threading.Thread(name='process', target=self._process)
|
||||
th.setDaemon(True)
|
||||
self.__thread['process'] = th
|
||||
th.start()
|
||||
else:
|
||||
self._process()
|
||||
|
||||
def schedule(self, seconds, handler, args=None):
|
||||
threading.Timer(seconds, handler, args).start()
|
||||
def schedule(self, name, seconds, callback, args=None, kwargs=None, repeat=False):
|
||||
self.scheduler.add(name, seconds, callback, args, kwargs, repeat, qpointer=self.eventqueue)
|
||||
|
||||
def _process(self):
|
||||
"Start processing the socket."
|
||||
firstrun = True
|
||||
while self.run and (firstrun or self.state['reconnect']):
|
||||
logging.debug('Process thread starting...')
|
||||
while self.run:
|
||||
self.state.set('processing', True)
|
||||
firstrun = False
|
||||
try:
|
||||
if self.state['is client']:
|
||||
self.sendRaw(self.stream_header)
|
||||
while self.run and self.__readXML():
|
||||
if self.state['is client']:
|
||||
self.sendRaw(self.stream_header)
|
||||
self.sendRaw(self.stream_header)
|
||||
while self.run and self.__readXML(): pass
|
||||
except socket.timeout:
|
||||
logging.debug('socket rcv timeout')
|
||||
pass
|
||||
except CloseStream:
|
||||
# TODO warn that the listener thread is exiting!!!
|
||||
pass
|
||||
except RestartStream:
|
||||
logging.debug("Restarting stream...")
|
||||
continue # DON'T re-initialize the stream -- this exception is sent
|
||||
# specifically when we've initialized TLS and need to re-send the <stream> header.
|
||||
except KeyboardInterrupt:
|
||||
logging.debug("Keyboard Escape Detected")
|
||||
self.state.set('processing', False)
|
||||
self.state.set('reconnect', False)
|
||||
self.disconnect()
|
||||
self.run = False
|
||||
# TODO this is probably not necessary...
|
||||
self.eventqueue.put(('quit', None, None))
|
||||
return
|
||||
except CloseStream:
|
||||
return
|
||||
except SystemExit:
|
||||
# TODO shouldn't this be the same as KeyboardInterrupt????
|
||||
self.eventqueue.put(('quit', None, None))
|
||||
return
|
||||
except socket.error:
|
||||
if not self.state.reconnect:
|
||||
return
|
||||
else:
|
||||
self.state.set('processing', False)
|
||||
traceback.print_exc()
|
||||
self.disconnect(reconnect=True)
|
||||
except:
|
||||
logging.exception('Unexpected error in RCV thread')
|
||||
if not self.state.reconnect:
|
||||
return
|
||||
else:
|
||||
logging.debug('reconnecting...')
|
||||
self.state.set('processing', False)
|
||||
traceback.print_exc()
|
||||
self.disconnect(reconnect=True)
|
||||
if self.state['reconnect']:
|
||||
self.state.set('connected', False)
|
||||
self.state.set('processing', False)
|
||||
self.reconnect()
|
||||
else:
|
||||
self.eventqueue.put(('quit', None, None))
|
||||
#self.__thread['readXML'] = threading.Thread(name='readXML', target=self.__readXML)
|
||||
#self.__thread['readXML'].start()
|
||||
#self.__thread['spawnEvents'] = threading.Thread(name='spawnEvents', target=self.__spawnEvents)
|
||||
#self.__thread['spawnEvents'].start()
|
||||
# TODO the individual exception handlers above already handle reconnect!
|
||||
# Why are we attempting to do it again down here???
|
||||
# if self.state['reconnect']:
|
||||
# self.state.set('connected', False)
|
||||
self.state.set('processing', False)
|
||||
# self.reconnect()
|
||||
# else:
|
||||
# TODO I think this is getting queued, and when the eventRunner comes back online after
|
||||
# reconnect, it immediately processes a 'quit' event and exits again, meanwhile the
|
||||
# rest of the client is just starting to connect and process the incoming event stream!!!
|
||||
# self.eventqueue.put(('quit', None, None))
|
||||
logging.debug('Quitting Process thread')
|
||||
|
||||
def __readXML(self):
|
||||
"Parses the incoming stream, adding to xmlin queue as it goes"
|
||||
|
@ -220,38 +240,50 @@ class XMLStream(object):
|
|||
if edepth == 0: # and xmlobj.tag.split('}', 1)[-1] == self.basetag:
|
||||
if event == b'start':
|
||||
root = xmlobj
|
||||
logging.debug('handling start stream')
|
||||
self.start_stream_handler(root)
|
||||
if event == b'end':
|
||||
edepth += -1
|
||||
if edepth == 0 and event == b'end':
|
||||
self.disconnect(reconnect=self.state['reconnect'])
|
||||
# what is this case exactly? Premature EOF?
|
||||
#self.disconnect(reconnect=self.state['reconnect'])
|
||||
logging.debug("Ending readXML loop")
|
||||
return False
|
||||
elif edepth == 1:
|
||||
#self.xmlin.put(xmlobj)
|
||||
try:
|
||||
self.__spawnEvent(xmlobj)
|
||||
except RestartStream:
|
||||
return True
|
||||
except CloseStream:
|
||||
return False
|
||||
if root:
|
||||
root.clear()
|
||||
self.__spawnEvent(xmlobj)
|
||||
if root: root.clear()
|
||||
if event == b'start':
|
||||
edepth += 1
|
||||
logging.debug("Exiting readXML loop")
|
||||
return False
|
||||
|
||||
def _sendThread(self):
|
||||
logging.debug('send thread starting...')
|
||||
while self.run:
|
||||
data = self.sendqueue.get(True)
|
||||
logging.debug("SEND: %s" % data)
|
||||
if not self.state['connected']:
|
||||
logging.warning("Not connected yet...")
|
||||
time.sleep(1)
|
||||
data = None
|
||||
try:
|
||||
self.socket.send(data.encode('utf-8'))
|
||||
#self.socket.send(bytes(data, "utf-8"))
|
||||
#except socket.error,(errno, strerror):
|
||||
data = self.sendqueue.get(True,10)
|
||||
logging.debug("SEND: %s" % data)
|
||||
self.socket.sendall(data.encode('utf-8'))
|
||||
except queue.Empty:
|
||||
logging.debug('nothing on send queue')
|
||||
except socket.timeout:
|
||||
# this is to prevent hanging
|
||||
logging.debug('timeout sending packet data')
|
||||
except:
|
||||
logging.warning("Failed to send %s" % data)
|
||||
self.state.set('connected', False)
|
||||
logging.exception("Socket error in SEND thread")
|
||||
# TODO it's somewhat unsafe for the sender thread to assume it can just
|
||||
# re-intitialize the connection, since the receiver thread could be doing
|
||||
# the same thing concurrently. Oops! The safer option would be to throw
|
||||
# some sort of event that could be handled by a common thread or the reader
|
||||
# thread to perform reconnect and then re-initialize the handler threads as well.
|
||||
if self.state.reconnect:
|
||||
logging.error("Disconnected. Socket Error.")
|
||||
logging.debug('Reconnecting...')
|
||||
traceback.print_exc()
|
||||
self.disconnect(reconnect=True)
|
||||
|
||||
|
@ -261,41 +293,40 @@ class XMLStream(object):
|
|||
|
||||
def disconnect(self, reconnect=False):
|
||||
self.state.set('reconnect', reconnect)
|
||||
if self.state['disconnecting']:
|
||||
if not self.state['connected']:
|
||||
logging.warning("Already disconnected.")
|
||||
return
|
||||
if not self.state['reconnect']:
|
||||
logging.debug("Disconnecting...")
|
||||
self.state.set('disconnecting', True)
|
||||
self.run = False
|
||||
if self.state['connected']:
|
||||
self.sendRaw(self.stream_footer)
|
||||
time.sleep(1)
|
||||
#send end of stream
|
||||
#wait for end of stream back
|
||||
logging.debug("Disconnecting...")
|
||||
self.sendRaw(self.stream_footer)
|
||||
time.sleep(5)
|
||||
#send end of stream
|
||||
#wait for end of stream back
|
||||
self.run = False
|
||||
self.scheduler.run = False
|
||||
try:
|
||||
self.state.set('connected',False)
|
||||
# self.socket.shutdown(socket.SHUT_RDWR)
|
||||
self.socket.close()
|
||||
except socket.error as (errno,strerror):
|
||||
logging.exception("Error while disconnecting. Socket Error #%s: %s" % (errno, strerror))
|
||||
try:
|
||||
self.filesocket.close()
|
||||
self.socket.shutdown(socket.SHUT_RDWR)
|
||||
except socket.error as serr:
|
||||
#logging.warning("Error while disconnecting. Socket Error #%s: %s" % (errno, strerror))
|
||||
#thread.exit_thread()
|
||||
pass
|
||||
if self.state['processing']:
|
||||
#raise CloseStream
|
||||
pass
|
||||
except socket.error as (errno,strerror):
|
||||
logging.exception("Error closing filesocket.")
|
||||
|
||||
def reconnect(self):
|
||||
self.state.set('tls',False)
|
||||
self.state.set('ssl',False)
|
||||
time.sleep(1)
|
||||
self.connect()
|
||||
|
||||
|
||||
def incoming_filter(self, xmlobj):
|
||||
return xmlobj
|
||||
|
||||
|
||||
def __spawnEvent(self, xmlobj):
|
||||
"watching xmlOut and processes handlers"
|
||||
#convert XML into Stanza
|
||||
# TODO surround this log statement with an if, it's expensive
|
||||
logging.debug("RECV: %s" % cElementTree.tostring(xmlobj))
|
||||
xmlobj = self.incoming_filter(xmlobj)
|
||||
stanza = None
|
||||
|
@ -307,17 +338,21 @@ class XMLStream(object):
|
|||
if stanza is None:
|
||||
stanza = StanzaBase(self, xmlobj)
|
||||
unhandled = True
|
||||
# TODO inefficient linear search; performance might be improved by hashtable lookup
|
||||
for handler in self.__handlers:
|
||||
if handler.match(stanza):
|
||||
logging.debug('matched stanza to handler %s', handler.name)
|
||||
handler.prerun(stanza)
|
||||
self.eventqueue.put(('stanza', handler, stanza))
|
||||
if handler.checkDelete(): self.__handlers.pop(self.__handlers.index(handler))
|
||||
if handler.checkDelete():
|
||||
logging.debug('deleting callback %s', handler.name)
|
||||
self.__handlers.pop(self.__handlers.index(handler))
|
||||
unhandled = False
|
||||
if unhandled:
|
||||
stanza.unhandled()
|
||||
#loop through handlers and test match
|
||||
#spawn threads as necessary, call handlers, sending Stanza
|
||||
|
||||
|
||||
def _eventRunner(self):
|
||||
logging.debug("Loading event runner")
|
||||
while self.run:
|
||||
|
@ -329,26 +364,27 @@ class XMLStream(object):
|
|||
etype = event[0]
|
||||
handler = event[1]
|
||||
args = event[2:]
|
||||
#etype, handler, *args = event #python 3.x way
|
||||
#etype, handler, *args = event #python 3.x way
|
||||
if etype == 'stanza':
|
||||
try:
|
||||
handler.run(args[0])
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logging.exception("Exception in event handler")
|
||||
args[0].exception(e)
|
||||
elif etype == 'sched':
|
||||
try:
|
||||
#handler(*args[0])
|
||||
handler.run(*args)
|
||||
except:
|
||||
logging.error(traceback.format_exc())
|
||||
elif etype == 'quit':
|
||||
logging.debug("Quitting eventRunner thread")
|
||||
return False
|
||||
|
||||
|
||||
def registerHandler(self, handler, before=None, after=None):
|
||||
"Add handler with matcher class and parameters."
|
||||
self.__handlers.append(handler)
|
||||
|
||||
|
||||
def removeHandler(self, name):
|
||||
"Removes the handler."
|
||||
idx = 0
|
||||
|
@ -434,4 +470,4 @@ class XMLStream(object):
|
|||
|
||||
def start_stream_handler(self, xml):
|
||||
"""Meant to be overridden"""
|
||||
pass
|
||||
logging.warn("No start stream handler has been implemented.")
|
||||
|
|
155
tests/test_disco.py
Normal file
155
tests/test_disco.py
Normal file
|
@ -0,0 +1,155 @@
|
|||
import unittest
|
||||
from xml.etree import cElementTree as ET
|
||||
from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
|
||||
from . import xmlcompare
|
||||
|
||||
import sleekxmpp.plugins.xep_0030 as sd
|
||||
|
||||
def stanzaPlugin(stanza, plugin):
|
||||
stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
|
||||
stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
|
||||
|
||||
class testdisco(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.sd = sd
|
||||
stanzaPlugin(self.sd.Iq, self.sd.DiscoInfo)
|
||||
stanzaPlugin(self.sd.Iq, self.sd.DiscoItems)
|
||||
|
||||
def try3Methods(self, xmlstring, iq):
|
||||
iq2 = self.sd.Iq(None, self.sd.ET.fromstring(xmlstring))
|
||||
values = iq2.getValues()
|
||||
iq3 = self.sd.Iq()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), str(iq)+"3 methods for creating stanza don't match")
|
||||
|
||||
def testCreateInfoQueryNoNode(self):
|
||||
"""Testing disco#info query with no node."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = ''
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" /></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testCreateInfoQueryWithNode(self):
|
||||
"""Testing disco#info query with a node."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = 'foo'
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo" /></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testCreateInfoQueryNoNode(self):
|
||||
"""Testing disco#items query with no node."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_items']['node'] = ''
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" /></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testCreateItemsQueryWithNode(self):
|
||||
"""Testing disco#items query with a node."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_items']['node'] = 'foo'
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo" /></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testInfoIdentities(self):
|
||||
"""Testing adding identities to disco#info."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = 'foo'
|
||||
iq['disco_info'].addIdentity('conference', 'text', 'Chatroom')
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><identity category="conference" type="text" name="Chatroom" /></query></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testInfoFeatures(self):
|
||||
"""Testing adding features to disco#info."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_info']['node'] = 'foo'
|
||||
iq['disco_info'].addFeature('foo')
|
||||
iq['disco_info'].addFeature('bar')
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><feature var="foo" /><feature var="bar" /></query></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testItems(self):
|
||||
"""Testing adding features to disco#info."""
|
||||
iq = self.sd.Iq()
|
||||
iq['id'] = "0"
|
||||
iq['disco_items']['node'] = 'foo'
|
||||
iq['disco_items'].addItem('user@localhost')
|
||||
iq['disco_items'].addItem('user@localhost', 'foo')
|
||||
iq['disco_items'].addItem('user@localhost', 'bar', 'Testing')
|
||||
xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo"><item jid="user@localhost" /><item node="foo" jid="user@localhost" /><item node="bar" jid="user@localhost" name="Testing" /></query></iq>"""
|
||||
self.try3Methods(xmlstring, iq)
|
||||
|
||||
def testAddRemoveIdentities(self):
|
||||
"""Test adding and removing identities to disco#info stanza"""
|
||||
ids = [('automation', 'commands', 'AdHoc'),
|
||||
('conference', 'text', 'ChatRoom')]
|
||||
|
||||
info = self.sd.DiscoInfo()
|
||||
info.addIdentity(*ids[0])
|
||||
self.failUnless(info.getIdentities() == [ids[0]])
|
||||
|
||||
info.delIdentity('automation', 'commands')
|
||||
self.failUnless(info.getIdentities() == [])
|
||||
|
||||
info.setIdentities(ids)
|
||||
self.failUnless(info.getIdentities() == ids)
|
||||
|
||||
info.delIdentity('automation', 'commands')
|
||||
self.failUnless(info.getIdentities() == [ids[1]])
|
||||
|
||||
info.delIdentities()
|
||||
self.failUnless(info.getIdentities() == [])
|
||||
|
||||
def testAddRemoveFeatures(self):
|
||||
"""Test adding and removing features to disco#info stanza"""
|
||||
features = ['foo', 'bar', 'baz']
|
||||
|
||||
info = self.sd.DiscoInfo()
|
||||
info.addFeature(features[0])
|
||||
self.failUnless(info.getFeatures() == [features[0]])
|
||||
|
||||
info.delFeature('foo')
|
||||
self.failUnless(info.getFeatures() == [])
|
||||
|
||||
info.setFeatures(features)
|
||||
self.failUnless(info.getFeatures() == features)
|
||||
|
||||
info.delFeature('bar')
|
||||
self.failUnless(info.getFeatures() == ['foo', 'baz'])
|
||||
|
||||
info.delFeatures()
|
||||
self.failUnless(info.getFeatures() == [])
|
||||
|
||||
def testAddRemoveItems(self):
|
||||
"""Test adding and removing items to disco#items stanza"""
|
||||
items = [('user@localhost', None, None),
|
||||
('user@localhost', 'foo', None),
|
||||
('user@localhost', 'bar', 'Test')]
|
||||
|
||||
info = self.sd.DiscoItems()
|
||||
self.failUnless(True, ""+str(items[0]))
|
||||
|
||||
info.addItem(*(items[0]))
|
||||
self.failUnless(info.getItems() == [items[0]], info.getItems())
|
||||
|
||||
info.delItem('user@localhost')
|
||||
self.failUnless(info.getItems() == [])
|
||||
|
||||
info.setItems(items)
|
||||
self.failUnless(info.getItems() == items)
|
||||
|
||||
info.delItem('user@localhost', 'foo')
|
||||
self.failUnless(info.getItems() == [items[0], items[2]])
|
||||
|
||||
info.delItems()
|
||||
self.failUnless(info.getItems() == [])
|
||||
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(testdisco)
|
35
tests/test_events.py
Normal file
35
tests/test_events.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
import unittest
|
||||
|
||||
class testevents(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
import sleekxmpp.stanza.presence as p
|
||||
self.p = p
|
||||
|
||||
def testEventHappening(self):
|
||||
"Test handler working"
|
||||
import sleekxmpp
|
||||
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
|
||||
happened = []
|
||||
def handletestevent(event):
|
||||
happened.append(True)
|
||||
c.add_event_handler("test_event", handletestevent)
|
||||
c.event("test_event", {})
|
||||
c.event("test_event", {})
|
||||
self.failUnless(happened == [True, True], "event did not get triggered twice")
|
||||
|
||||
def testDelEvent(self):
|
||||
"Test handler working, then deleted and not triggered"
|
||||
import sleekxmpp
|
||||
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
|
||||
happened = []
|
||||
def handletestevent(event):
|
||||
happened.append(True)
|
||||
c.add_event_handler("test_event", handletestevent)
|
||||
c.event("test_event", {})
|
||||
c.del_event_handler("test_event", handletestevent)
|
||||
c.event("test_event", {}) # should not trigger because it was deleted
|
||||
self.failUnless(happened == [True], "event did not get triggered the correct number of times")
|
||||
|
||||
|
||||
suite = unittest.TestLoader().loadTestsFromTestCase(testevents)
|
|
@ -97,6 +97,21 @@ class testpubsubstanzas(unittest.TestCase):
|
|||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testState(self):
|
||||
"Testing iq/psstate stanzas"
|
||||
from sleekxmpp.plugins import xep_0004
|
||||
iq = self.ps.Iq()
|
||||
iq['psstate']['node']= 'mynode'
|
||||
iq['psstate']['item']= 'myitem'
|
||||
pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed')
|
||||
iq['psstate']['payload'] = pl
|
||||
xmlstring = """<iq id="0"><state xmlns="http://jabber.org/protocol/psstate" node="mynode" item="myitem"><claimed xmlns="http://andyet.net/protocol/pubsubqueue" /></state></iq>"""
|
||||
iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
|
||||
iq3 = self.ps.Iq()
|
||||
values = iq2.getValues()
|
||||
iq3.setValues(values)
|
||||
self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
|
||||
|
||||
def testDefault(self):
|
||||
"Testing iq/pubsub_owner/default stanzas"
|
||||
from sleekxmpp.plugins import xep_0004
|
||||
|
|
Loading…
Reference in a new issue