pax_global_header00006660000000000000000000000064115777534040014527gustar00rootroot0000000000000052 comment=d8d9e8df16c07bd13bbac72e4445a2930407b244 fritzy-SleekXMPP-5c4ee57/000077500000000000000000000000001157775340400152475ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/.gitignore000066400000000000000000000000151157775340400172330ustar00rootroot00000000000000*.pyc build/ fritzy-SleekXMPP-5c4ee57/INSTALL000066400000000000000000000003171157775340400163010ustar00rootroot00000000000000Pre-requisites: - Python 3.1 or 2.6 Install: > python3 setup.py install Root install: > sudo python3 setup.py install To test: > cd examples > python echo_client.py -v -j [USER@example.com] -p [PASSWORD] fritzy-SleekXMPP-5c4ee57/LICENSE000066400000000000000000000020461157775340400162560ustar00rootroot00000000000000Copyright (c) 2010 Nathanael C. Fritz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. fritzy-SleekXMPP-5c4ee57/README000066400000000000000000000040711157775340400161310ustar00rootroot00000000000000SleekXMPP 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 Requirements: We try to keep requirements to a minimum, but we suggest that you install http://dnspython.org although it isn't strictly required. If you do not install this library, you may need to specify the server/port for services that use SRV records (like GTalk). "sudo pip install dnspython" on a *nix system with pip installed. SleekXMPP has several design goals/philosophies: - Low number of dependencies. - Every XEP as a plugin. - Rewarding to work with. The goals for 1.0 include (and we're getting close): - Nearly Full test coverage of stanzas. - Wide range of functional tests. - Stanza objects for all interaction with the stream - Documentation on using and extending SleekXMPP. - Complete documentation on all implemented stanza objects - Documentation on all examples used in XMPP: The Definitive Guide 1.1 will include: - More functional and unit tests - PEP-8 compliance - XEP-225 support Since 0.2, here's the Changelog: - MANY bugfixes - Re-implementation of handlers/threading to greatly simplify and remove bugs (no more spawning threads in handlers) - Stanza objects for jabber:client and all implemented XEPs - Raising XMPPError for jabber:client and extended errors in handlers - Robust error handling and better insurance of iq responses - Stanza objects have made life a lot easier! - Massive audit/cleanup. Credits ---------------- Main Author: Nathan Fritz fritz@netflint.net 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. fritzy-SleekXMPP-5c4ee57/conn_tests/000077500000000000000000000000001157775340400174265ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/conn_tests/test_pubsubjobs.py000066400000000000000000000154171157775340400232250ustar00rootroot00000000000000import 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() fritzy-SleekXMPP-5c4ee57/conn_tests/test_pubsubserver.py000066400000000000000000000230341157775340400235700ustar00rootroot00000000000000import 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.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode2', self.statev['defaultconfig'])) 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),)) msg = w.wait(5) # got to get a result in 5 seconds self.failUnless(msg != False, "Account #2 did not get message event") self.failUnless(result) #need to add check for update def test008updateitem(self): """Updating item""" item = ET.Element('{http://netflint.net/protocol/test}test', {'someattr': 'hi there'}) w = Waiter('wait publish', StanzaPath('message/pubsub_event/items')) self.xmpp2.registerHandler(w) result = self.xmpp1['xep_0060'].setItem(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") self.failUnless(result) #need to add check for update def test009deleteitem(self): """Deleting item""" w = Waiter('wait retract', StanzaPath('message/pubsub_event/items@node=testnode2')) self.xmpp2.registerHandler(w) result = self.xmpp1['xep_0060'].deleteItem(self.pshost, "testnode2", "test1") self.failUnless(result, "Got error when deleting item.") msg = w.wait(1) self.failUnless(msg != False, "Did not get retract notice.") def test010unsubscribenode(self): "Unsubscribing Account #2" self.failUnless(self.xmpp2['xep_0060'].unsubscribe(self.pshost, "testnode2"), "Got error response when unsubscribing.") def test011createcollectionnode(self): "Create a collection node w/ Account #2" self.failUnless(self.xmpp2['xep_0060'].create_node(self.pshost, "testnode3", self.statev['defaultconfig'], True), "Could not create collection node") def test012subscribecollection(self): "Subscribe Account #1 to collection" self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode3")) def test013assignnodetocollection(self): "Assign node to collection" self.failUnless(self.xmpp2['xep_0060'].addNodeToCollection(self.pshost, 'testnode2', 'testnode3')) def test014publishcollection(self): """Publishing item to collection child""" item = ET.Element('{http://netflint.net/protocol/test}test') w = Waiter('wait publish2', StanzaPath('message/pubsub_event/items@node=testnode2')) self.xmpp1.registerHandler(w) result = self.xmpp2['xep_0060'].setItem(self.pshost, "testnode2", (('test2', item),)) msg = w.wait(5) # got to get a result in 5 seconds self.failUnless(msg != False, "Account #1 did not get message event: perhaps node was advertised incorrectly?") self.failUnless(result) # def test016speedtest(self): # "Uncached speed test" # import time # start = time.time() # for y in range(0, 50000, 1000): # start2 = time.time() # for x in range(y, y+1000): # self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode4", subscribee="testuser%s@whatever" % x)) # print time.time() - start2 # seconds = time.time() - start # print "--", seconds # print "---------" # time.sleep(15) # self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode4'), "Could not delete non-cached test node") # def test015speedtest(self): # "cached speed test" # result = self.xmpp1['xep_0060'].getNodeConfig(self.pshost) # self.statev['defaultconfig'] = result # self.statev['defaultconfig'].field['pubsub#node_type'].setValue("leaf") # self.statev['defaultconfig'].field['sleek#saveonchange'].setValue(True) # self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode4', self.statev['defaultconfig'])) # self.statev['defaultconfig'].field['sleek#saveonchange'].setValue(False) # self.failUnless(self.xmpp1['xep_0060'].create_node(self.pshost, 'testnode5', self.statev['defaultconfig'])) # start = time.time() # for y in range(0, 50000, 1000): # start2 = time.time() # for x in range(y, y+1000): # self.failUnless(self.xmpp1['xep_0060'].subscribe(self.pshost, "testnode5", subscribee="testuser%s@whatever" % x)) # print time.time() - start2 # seconds = time.time() - start # print "--", seconds def test900cleanup(self): "Cleaning up" self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode2'), "Could not delete test node.") self.failUnless(self.xmpp1['xep_0060'].deleteNode(self.pshost, 'testnode3'), "Could not delete collection node") 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') xmpp2.registerPlugin('xep_0004') xmpp2.registerPlugin('xep_0030') xmpp2.registerPlugin('xep_0060') xmpp2.registerPlugin('xep_0199') 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() fritzy-SleekXMPP-5c4ee57/conn_tests/testconfig.ini000066400000000000000000000002271157775340400222750ustar00rootroot00000000000000[settings] enabled=true pubsub=pubsub.recon [account1] jid=fritzy@recon pass=testing123 server= [account2] jid=fritzy2@recon pass=testing123 server= fritzy-SleekXMPP-5c4ee57/conn_tests/testpubsub.py000077500000000000000000000313741157775340400222130ustar00rootroot00000000000000""" 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 logging import sleekxmpp from optparse import OptionParser from xml.etree import cElementTree as ET import os import time import sys import Queue import thread class testps(sleekxmpp.ClientXMPP): def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], nodenum=0, pshost=None): sleekxmpp.ClientXMPP.__init__(self, jid, password, ssl, plugin_config, plugin_whitelist) self.registerPlugin('xep_0004') self.registerPlugin('xep_0030') self.registerPlugin('xep_0060') self.registerPlugin('xep_0092') self.add_handler("", self.pubsubEventHandler, name='Pubsub Event', threaded=True) self.add_event_handler("session_start", self.start, threaded=True) self.add_handler("", self.handleError, name='Iq Error') self.events = Queue.Queue() self.default_config = None self.ps = self.plugin['xep_0060'] self.node = "pstestnode_%s" self.pshost = pshost if pshost is None: self.pshost = self.boundjid.host self.nodenum = int(nodenum) self.leafnode = self.nodenum + 1 self.collectnode = self.nodenum + 2 self.lasterror = '' self.sprintchars = 0 self.defaultconfig = None self.tests = ['test_defaultConfig', 'test_createDefaultNode', 'test_getNodes', 'test_deleteNode', 'test_createWithConfig', 'test_reconfigureNode', 'test_subscribeToNode', 'test_addItem', 'test_updateItem', 'test_deleteItem', 'test_unsubscribeNode', 'test_createCollection', 'test_subscribeCollection', 'test_addNodeCollection', 'test_deleteNodeCollection', 'test_addCollectionNode', 'test_deleteCollectionNode', 'test_unsubscribeNodeCollection', 'test_deleteCollection'] self.passed = 0 self.width = 120 def start(self, event): #TODO: make this configurable self.getRoster() self.sendPresence(ppriority=20) thread.start_new(self.test_all, tuple()) def sprint(self, msg, end=False, color=False): length = len(msg) if color: if color == "red": color = "1;31" elif color == "green": color = "0;32" msg = "%s%s%s" % ("\033[%sm" % color, msg, "\033[0m") if not end: sys.stdout.write(msg) self.sprintchars += length else: self.sprint("%s%s" % ("." * (self.width - self.sprintchars - length), msg)) print('') self.sprintchars = 0 sys.stdout.flush() def pubsubEventHandler(self, xml): for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}items/{http://jabber.org/protocol/pubsub#event}item'): self.events.put(item.get('id', '__unknown__')) for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}items/{http://jabber.org/protocol/pubsub#event}retract'): self.events.put(item.get('id', '__unknown__')) for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}collection/{http://jabber.org/protocol/pubsub#event}disassociate'): self.events.put(item.get('node', '__unknown__')) for item in xml.findall('{http://jabber.org/protocol/pubsub#event}event/{http://jabber.org/protocol/pubsub#event}collection/{http://jabber.org/protocol/pubsub#event}associate'): self.events.put(item.get('node', '__unknown__')) def handleError(self, xml): error = xml.find('{jabber:client}error') self.lasterror = error.getchildren()[0].tag.split('}')[-1] def test_all(self): print("Running Publish-Subscribe Tests") version = self.plugin['xep_0092'].getVersion(self.pshost) if version: print("%s %s on %s" % (version.get('name', 'Unknown Server'), version.get('version', 'v?'), version.get('os', 'Unknown OS'))) print("=" * self.width) for test in self.tests: testfunc = getattr(self, test) self.sprint("%s" % testfunc.__doc__) if testfunc(): self.sprint("Passed", True, "green") self.passed += 1 else: if not self.lasterror: self.lasterror = 'No response' self.sprint("Failed (%s)" % self.lasterror, True, "red") self.lasterror = '' print("=" * self.width) self.sprint("Cleaning up...") #self.ps.deleteNode(self.pshost, self.node % self.nodenum) self.ps.deleteNode(self.pshost, self.node % self.leafnode) #self.ps.deleteNode(self.pshost, self.node % self.collectnode) self.sprint("Done", True, "green") self.disconnect() self.sprint("%s" % self.passed, False, "green") self.sprint("/%s Passed -- " % len(self.tests)) if len(self.tests) - self.passed: self.sprint("%s" % (len(self.tests) - self.passed), False, "red") else: self.sprint("%s" % (len(self.tests) - self.passed), False, "green") self.sprint(" Failed Tests") print #print "%s/%s Passed -- %s Failed Tests" % (self.passed, len(self.tests), len(self.tests) - self.passed) def test_defaultConfig(self): "Retreiving default configuration" result = self.ps.getNodeConfig(self.pshost) if result is False or result is None: return False else: self.defaultconfig = result try: self.defaultconfig.field['pubsub#access_model'].setValue('open') except KeyError: pass try: self.defaultconfig.field['pubsub#notify_retract'].setValue(True) except KeyError: pass return True def test_createDefaultNode(self): "Creating default node" return self.ps.create_node(self.pshost, self.node % self.nodenum) def test_getNodes(self): "Getting list of nodes" self.ps.getNodes(self.pshost) self.ps.getItems(self.pshost, 'blog') return True def test_deleteNode(self): "Deleting node" return self.ps.deleteNode(self.pshost, self.node % self.nodenum) def test_createWithConfig(self): "Creating node with config" if self.defaultconfig is None: self.lasterror = "No Avail Config" return False return self.ps.create_node(self.pshost, self.node % self.leafnode, self.defaultconfig) def test_reconfigureNode(self): "Retrieving node config and reconfiguring" nconfig = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode) if nconfig == False: return False return self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, nconfig) def test_subscribeToNode(self): "Subscribing to node" return self.ps.subscribe(self.pshost, self.node % self.leafnode) def test_addItem(self): "Adding item, waiting for notification" item = ET.Element('test') result = self.ps.setItem(self.pshost, self.node % self.leafnode, (('test_node1', item),)) if result == False: return False try: event = self.events.get(True, 10) except Queue.Empty: return False if event == 'test_node1': return True return False def test_updateItem(self): "Updating item, waiting for notification" item = ET.Element('test') item.attrib['crap'] = 'yup, right here' result = self.ps.setItem(self.pshost, self.node % self.leafnode, (('test_node1', item),)) if result == False: return False try: event = self.events.get(True, 10) except Queue.Empty: return False if event == 'test_node1': return True return False def test_deleteItem(self): "Deleting item, waiting for notification" result = self.ps.deleteItem(self.pshost, self.node % self.leafnode, 'test_node1') if result == False: return False try: event = self.events.get(True, 10) except Queue.Empty: self.lasterror = "No Notification" return False if event == 'test_node1': return True return False def test_unsubscribeNode(self): "Unsubscribing from node" return self.ps.unsubscribe(self.pshost, self.node % self.leafnode) def test_createCollection(self): "Creating collection node" return self.ps.create_node(self.pshost, self.node % self.collectnode, self.defaultconfig, True) def test_subscribeCollection(self): "Subscribing to collection node" return self.ps.subscribe(self.pshost, self.node % self.collectnode) def test_addNodeCollection(self): "Assigning node to collection, waiting for notification" config = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode) if not config or config is None: self.lasterror = "Config Error" return False try: config.field['pubsub#collection'].setValue(self.node % self.collectnode) except KeyError: self.sprint("...Missing Field...", False, "red") config.addField('pubsub#collection', value=self.node % self.collectnode) if not self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, config): return False try: event = self.events.get(True, 10) except Queue.Empty: self.lasterror = "No Notification" return False if event == self.node % self.leafnode: return True return False def test_deleteNodeCollection(self): "Removing node assignment to collection, waiting for notification" config = self.ps.getNodeConfig(self.pshost, self.node % self.leafnode) if not config or config is None: self.lasterror = "Config Error" return False try: config.field['pubsub#collection'].delValue(self.node % self.collectnode) except KeyError: self.sprint("...Missing Field...", False, "red") config.addField('pubsub#collection', value='') if not self.ps.setNodeConfig(self.pshost, self.node % self.leafnode, config): return False try: event = self.events.get(True, 10) except Queue.Empty: self.lasterror = "No Notification" return False if event == self.node % self.leafnode: return True return False def test_addCollectionNode(self): "Assigning node from collection, waiting for notification" config = self.ps.getNodeConfig(self.pshost, self.node % self.collectnode) if not config or config is None: self.lasterror = "Config Error" return False try: config.field['pubsub#children'].setValue(self.node % self.leafnode) except KeyError: self.sprint("...Missing Field...", False, "red") config.addField('pubsub#children', value=self.node % self.leafnode) if not self.ps.setNodeConfig(self.pshost, self.node % self.collectnode, config): return False try: event = self.events.get(True, 10) except Queue.Empty: self.lasterror = "No Notification" return False if event == self.node % self.leafnode: return True return False def test_deleteCollectionNode(self): "Removing node from collection, waiting for notification" config = self.ps.getNodeConfig(self.pshost, self.node % self.collectnode) if not config or config is None: self.lasterror = "Config Error" return False try: config.field['pubsub#children'].delValue(self.node % self.leafnode) except KeyError: self.sprint("...Missing Field...", False, "red") config.addField('pubsub#children', value='') if not self.ps.setNodeConfig(self.pshost, self.node % self.collectnode, config): return False try: event = self.events.get(True, 10) except Queue.Empty: self.lasterror = "No Notification" return False if event == self.node % self.leafnode: return True return False def test_unsubscribeNodeCollection(self): "Unsubscribing from collection" return self.ps.unsubscribe(self.pshost, self.node % self.collectnode) def test_deleteCollection(self): "Deleting collection" return self.ps.deleteNode(self.pshost, self.node % self.collectnode) 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 = ET.parse(os.path.expanduser(opts.configfile)).find('auth') #init logging.info("Logging in as %s" % config.attrib['jid']) plugin_config = {} plugin_config['xep_0092'] = {'name': 'SleekXMPP Example', 'version': '0.1-dev'} plugin_config['xep_0199'] = {'keepalive': True, 'timeout': 30, 'frequency': 300} con = testps(config.attrib['jid'], config.attrib['pass'], plugin_config=plugin_config, plugin_whitelist=[], nodenum=opts.nodenum, pshost=opts.pubsub) if not config.get('server', None): # we don't know the server, but the lib can probably figure it out con.connect() else: con.connect((config.attrib['server'], 5222)) con.process(threaded=False) print("") fritzy-SleekXMPP-5c4ee57/examples/000077500000000000000000000000001157775340400170655ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/examples/adhoc_provider.py000077500000000000000000000161031157775340400224330ustar00rootroot00000000000000#!/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 import getpass 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 CommandBot(sleekxmpp.ClientXMPP): """ A simple SleekXMPP bot that provides a basic adhoc command. """ def __init__(self, jid, password): sleekxmpp.ClientXMPP.__init__(self, jid, password) # 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.send_presence() self.get_roster() # We add the command after session_start has fired # to ensure that the correct full JID is used. # If using a component, may also pass jid keyword parameter. self['xep_0050'].add_command(node='greeting', name='Greeting', handler=self._handle_command) def _handle_command(self, iq, session): """ Respond to the intial request for a command. Arguments: iq -- The iq stanza containing the command request. session -- A dictionary of data relevant to the command session. Additional, custom data may be saved here to persist across handler callbacks. """ form = self['xep_0004'].makeForm('form', 'Greeting') form.addField(var='greeting', ftype='text-single', label='Your greeting') session['payload'] = form session['next'] = self._handle_command_complete session['has_next'] = False # Other useful session values: # session['to'] -- The JID that received the # command request. # session['from'] -- The JID that sent the # command request. # session['has_next'] = True -- There are more steps to complete # session['allow_complete'] = True -- Allow user to finish immediately # and possibly skip steps # session['cancel'] = handler -- Assign a handler for if the user # cancels the command. # session['notes'] = [ -- Add informative notes about the # ('info', 'Info message'), command's results. # ('warning', 'Warning message'), # ('error', 'Error message')] return session def _handle_command_complete(self, payload, session): """ Process a command result from the user. Arguments: payload -- Either a single item, such as a form, or a list of items or forms if more than one form was provided to the user. The payload may be any stanza, such as jabber:x:oob for out of band data, or jabber:x:data for typical data forms. session -- A dictionary of data relevant to the command session. Additional, custom data may be saved here to persist across handler callbacks. """ # In this case (as is typical), the payload is a form form = payload greeting = form['values']['greeting'] self.send_message(mto=session['from'], mbody="%s, World!" % greeting) # Having no return statement is the same as unsetting the 'payload' # and 'next' session values and returning the session. # Unless it is the final step, always return the session dictionary. session['payload'] = None session['next'] = None return session 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) # 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 opts.jid is None: opts.jid = raw_input("Username: ") if opts.password is None: opts.password = getpass.getpass("Password: ") # Setup the CommandBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = CommandBot(opts.jid, opts.password) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0050') # Adhoc Commands # 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.") fritzy-SleekXMPP-5c4ee57/examples/adhoc_user.py000077500000000000000000000160511157775340400215610ustar00rootroot00000000000000#!/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 import getpass 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 CommandUserBot(sleekxmpp.ClientXMPP): """ A simple SleekXMPP bot that uses the adhoc command provided by the adhoc_provider.py example. """ def __init__(self, jid, password, other, greeting): sleekxmpp.ClientXMPP.__init__(self, jid, password) self.command_provider = other self.greeting = greeting # 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) self.add_event_handler("message", self.message) 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.send_presence() self.get_roster() # We first create a session dictionary containing: # 'next' -- the handler to execute on a successful response # 'error' -- the handler to execute if an error occurs # The session may also contain custom data. session = {'greeting': self.greeting, 'next': self._command_start, 'error': self._command_error} self['xep_0050'].start_command(jid=self.command_provider, node='greeting', session=session) def message(self, msg): """ Process incoming message stanzas. Arguments: msg -- The received message stanza. """ logging.info(msg['body']) def _command_start(self, iq, session): """ Process the initial command result. Arguments: iq -- The iq stanza containing the command result. session -- A dictionary of data relevant to the command session. Additional, custom data may be saved here to persist across handler callbacks. """ # The greeting command provides a form with a single field: # # # form = self['xep_0004'].makeForm(ftype='submit') form.addField(var='greeting', value=session['greeting']) session['payload'] = form # We don't need to process the next result. session['next'] = None # Other options include using: # continue_command() -- Continue to the next step in the workflow # cancel_command() -- Stop command execution. self['xep_0050'].complete_command(session) def _command_error(self, iq, session): """ Process an error that occurs during command execution. Arguments: iq -- The iq stanza containing the error. session -- A dictionary of data relevant to the command session. Additional, custom data may be saved here to persist across handler callbacks. """ logging.error("COMMAND: %s %s" % (iq['error']['condition'], iq['error']['text'])) # Terminate the command's execution and clear its session. # The session will automatically be cleared if no error # handler is provided. self['xep_0050'].terminate_command(session) 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) # 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") optp.add_option("-o", "--other", dest="other", help="JID providing commands") optp.add_option("-g", "--greeting", dest="greeting", help="Greeting") opts, args = optp.parse_args() # Setup logging. logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') if opts.jid is None: opts.jid = raw_input("Username: ") if opts.password is None: opts.password = getpass.getpass("Password: ") if opts.other is None: opts.other = raw_input("JID Providing Commands: ") if opts.greeting is None: opts.other = raw_input("Greeting: ") # Setup the CommandBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = CommandUserBot(opts.jid, opts.password, opts.other, opts.greeting) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0050') # Adhoc Commands # 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.") fritzy-SleekXMPP-5c4ee57/examples/config.xml000066400000000000000000000003761157775340400210620ustar00rootroot00000000000000 component.localhost ssshh localhost 8888 fritzy-SleekXMPP-5c4ee57/examples/config_component.py000077500000000000000000000146321157775340400227770ustar00rootroot00000000000000#!/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 from sleekxmpp.componentxmpp import ComponentXMPP from sleekxmpp.stanza.roster import Roster from sleekxmpp.xmlstream import ElementBase from sleekxmpp.xmlstream.stanzabase import ET, registerStanzaPlugin # 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 Config(ElementBase): """ In order to make loading and manipulating an XML config file easier, we will create a custom stanza object for our config XML file contents. See the documentation on stanza objects for more information on how to create and use stanza objects and stanza plugins. We will reuse the IQ roster query stanza to store roster information since it already exists. Example config XML: component.localhost ssshh localhost 8888 """ name = "config" namespace = "sleekxmpp:config" interfaces = set(('jid', 'secret', 'server', 'port')) sub_interfaces = interfaces registerStanzaPlugin(Config, Roster) class ConfigComponent(ComponentXMPP): """ A simple SleekXMPP component that uses an external XML file to store its configuration data. To make testing that the component works, it will also echo messages sent to it. """ def __init__(self, config): """ Create a ConfigComponent. Arguments: config -- The XML contents of the config file. config_file -- The XML config file object itself. """ ComponentXMPP.__init__(self, config['jid'], config['secret'], config['server'], config['port']) # Store the roster information. self.roster = config['roster']['items'] # The session_start event will be triggered when # the component 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 # broadcast any needed initial presence stanzas. self.add_event_handler("session_start", self.start) # The message event is triggered whenever a message # stanza is received. Be aware that that includes # MUC messages and error messages. self.add_event_handler("message", self.message) def start(self, event): """ Process the session_start event. The typical action for the session_start event in a component is to broadcast presence stanzas to all subscribers to the component. Note that the component does not have a roster provided by the XMPP server. In this case, we have possibly saved a roster in the component's configuration file. Since the component may use any number of JIDs, you should also include the JID that is sending the presence. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ for jid in self.roster: if self.roster[jid]['subscription'] != 'none': self.sendPresence(pfrom=self.jid, pto=jid) def message(self, msg): """ Process incoming message stanzas. Be aware that this also includes MUC messages and error messages. It is usually a good idea to check the messages's type before processing or sending replies. Since a component may send messages from any number of JIDs, it is best to always include a from JID. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ # The reply method will use the messages 'to' JID as the # outgoing reply's 'from' JID. msg.reply("Thanks for sending\n%(body)s" % msg).send() 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) # Component name and secret options. optp.add_option("-c", "--config", help="path to config file", dest="config", default="config.xml") opts, args = optp.parse_args() # Setup logging. logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') # Load configuration data. config_file = open(opts.config, 'r+') config_data = "\n".join([line for line in config_file]) config = Config(xml=ET.fromstring(config_data)) config_file.close() # Setup the ConfigComponent and register plugins. Note that while plugins # may have interdependencies, the order in which you register them does # not matter. xmpp = ConfigComponent(config) xmpp.registerPlugin('xep_0030') # Service Discovery xmpp.registerPlugin('xep_0004') # Data Forms xmpp.registerPlugin('xep_0060') # PubSub xmpp.registerPlugin('xep_0199') # XMPP Ping # Connect to the XMPP server and start processing XMPP stanzas. if xmpp.connect(): xmpp.process(threaded=False) print("Done") else: print("Unable to connect.") fritzy-SleekXMPP-5c4ee57/examples/disco_browser.py000077500000000000000000000151721157775340400223140ustar00rootroot00000000000000#!/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 time import logging import getpass 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 Disco(sleekxmpp.ClientXMPP): """ A demonstration for using basic service discovery. Send a disco#info and disco#items request to a JID/node combination, and print out the results. May also request only particular info categories such as just features, or just items. """ def __init__(self, jid, password, target_jid, target_node='', get=''): sleekxmpp.ClientXMPP.__init__(self, jid, password) # Using service discovery requires the XEP-0030 plugin. self.register_plugin('xep_0030') self.get = get self.target_jid = target_jid self.target_node = target_node # Values to control which disco entities are reported self.info_types = ['', 'all', 'info', 'identities', 'features'] self.identity_types = ['', 'all', 'info', 'identities'] self.feature_types = ['', 'all', 'info', 'features'] self.items_types = ['', 'all', 'items'] # 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. In this case, we send disco#info and disco#items stanzas to the requested JID and print the results. Arguments: event -- An empty dictionary. The session_start event does not provide any additional data. """ self.get_roster() self.send_presence() if self.get in self.info_types: # By using block=True, the result stanza will be # returned. Execution will block until the reply is # received. Non-blocking options would be to listen # for the disco_info event, or passing a handler # function using the callback parameter. info = self['xep_0030'].get_info(jid=self.target_jid, node=self.target_node, block=True) if self.get in self.items_types: # The same applies from above. Listen for the # disco_items event or pass a callback function # if you need to process a non-blocking request. items = self['xep_0030'].get_items(jid=self.target_jid, node=self.target_node, block=True) else: logging.error("Invalid disco request type.") self.disconnect() return header = 'XMPP Service Discovery: %s' % self.target_jid print(header) print('-' * len(header)) if self.target_node != '': print('Node: %s' % self.target_node) print('-' * len(header)) if self.get in self.identity_types: print('Identities:') for identity in info['disco_info']['identities']: print(' - %s' % str(identity)) if self.get in self.feature_types: print('Features:') for feature in info['disco_info']['features']: print(' - %s' % feature) if self.get in self.items_types: print('Items:') for item in items['disco_items']['items']: print(' - %s' % str(item)) self.disconnect() if __name__ == '__main__': # Setup the command line arguments. optp = OptionParser() optp.version = '%%prog 0.1' optp.usage = "Usage: %%prog [options] %s []" % \ 'all|info|items|identities|features' optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.ERROR) optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.ERROR) optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.ERROR) # 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 len(args) < 2: optp.print_help() exit() if len(args) == 2: args = (args[0], args[1], '') if opts.jid is None: opts.jid = raw_input("Username: ") if opts.password is None: opts.password = getpass.getpass("Password: ") # Setup the Disco browser. xmpp = Disco(opts.jid, opts.password, args[1], args[2], args[0]) # 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) else: print("Unable to connect.") fritzy-SleekXMPP-5c4ee57/examples/echo_client.py000077500000000000000000000113001157775340400217110ustar00rootroot00000000000000#!/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 import getpass 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 EchoBot(sleekxmpp.ClientXMPP): """ A simple SleekXMPP bot that will echo messages it receives, along with a short thank you message. """ def __init__(self, jid, password): sleekxmpp.ClientXMPP.__init__(self, jid, password) # 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) # The message event is triggered whenever a message # stanza is received. Be aware that that includes # MUC messages and error messages. self.add_event_handler("message", self.message) 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.send_presence() self.get_roster() def message(self, msg): """ Process incoming message stanzas. Be aware that this also includes MUC messages and error messages. It is usually a good idea to check the messages's type before processing or sending replies. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ msg.reply("Thanks for sending\n%(body)s" % msg).send() 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) # 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 opts.jid is None: opts.jid = raw_input("Username: ") if opts.password is None: opts.password = getpass.getpass("Password: ") # Setup the EchoBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = EchoBot(opts.jid, opts.password) 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.") fritzy-SleekXMPP-5c4ee57/examples/muc.py000077500000000000000000000155471157775340400202420ustar00rootroot00000000000000#!/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 MUCBot(sleekxmpp.ClientXMPP): """ A simple SleekXMPP bot that will greets those who enter the room, and acknowledge any messages that mentions the bot's nickname. """ def __init__(self, jid, password, room, nick): sleekxmpp.ClientXMPP.__init__(self, jid, password) self.room = room self.nick = nick # 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) # The groupchat_message event is triggered whenever a message # stanza is received from any chat room. If you also also # register a handler for the 'message' event, MUC messages # will be processed by both handlers. self.add_event_handler("groupchat_message", self.muc_message) # The groupchat_presence event is triggered whenever a # presence stanza is received from any chat room, including # any presences you send yourself. To limit event handling # to a single room, use the events muc::room@server::presence, # muc::room@server::got_online, or muc::room@server::got_offline. self.add_event_handler("muc::%s::got_online" % self.room, self.muc_online) 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.getRoster() self.sendPresence() self.plugin['xep_0045'].joinMUC(self.room, self.nick, # If a room password is needed, use: # password=the_room_password, wait=True) def muc_message(self, msg): """ Process incoming message stanzas from any chat room. Be aware that if you also have any handlers for the 'message' event, message stanzas may be processed by both handlers, so check the 'type' attribute when using a 'message' event handler. Whenever the bot's nickname is mentioned, respond to the message. IMPORTANT: Always check that a message is not from yourself, otherwise you will create an infinite loop responding to your own messages. This handler will reply to messages that mention the bot's nickname. Arguments: msg -- The received message stanza. See the documentation for stanza objects and the Message stanza to see how it may be used. """ if msg['mucnick'] != self.nick and self.nick in msg['body']: self.send_message(mto=msg['from'].bare, mbody="I heard that, %s." % msg['mucnick'], mtype='groupchat') def muc_online(self, presence): """ Process a presence stanza from a chat room. In this case, presences from users that have just come online are handled by sending a welcome message that includes the user's nickname and role in the room. Arguments: presence -- The received presence stanza. See the documentation for the Presence stanza to see how else it may be used. """ if presence['muc']['nick'] != self.nick: self.send_message(mto=presence['from'].bare, mbody="Hello, %s %s" % (presence['muc']['role'], presence['muc']['nick']), mtype='groupchat') 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) # 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") optp.add_option("-r", "--room", dest="room", help="MUC room to join") optp.add_option("-n", "--nick", dest="nick", help="MUC nickname") opts, args = optp.parse_args() # Setup logging. logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') if None in [opts.jid, opts.password, opts.room, opts.nick]: optp.print_help() sys.exit(1) # Setup the MUCBot and register plugins. Note that while plugins may # have interdependencies, the order in which you register them does # not matter. xmpp = MUCBot(opts.jid, opts.password, opts.room, opts.nick) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0045') # Multi-User Chat xmpp.register_plugin('xep_0199') # XMPP Ping # 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.") fritzy-SleekXMPP-5c4ee57/examples/ping.py000077500000000000000000000112001157775340400203710ustar00rootroot00000000000000#!/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 import getpass 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.send_presence() self.get_roster() 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 opts.jid is None: opts.jid = raw_input("Username: ") if opts.password is None: opts.password = getpass.getpass("Password: ") # 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.") fritzy-SleekXMPP-5c4ee57/examples/rpc_async.py000066400000000000000000000014721157775340400214240ustar00rootroot00000000000000""" 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() fritzy-SleekXMPP-5c4ee57/examples/rpc_client_side.py000066400000000000000000000021551157775340400225700ustar00rootroot00000000000000""" 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() fritzy-SleekXMPP-5c4ee57/examples/rpc_server_side.py000066400000000000000000000021711157775340400226160ustar00rootroot00000000000000""" 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() fritzy-SleekXMPP-5c4ee57/ez_setup.py000066400000000000000000000212061157775340400174600ustar00rootroot00000000000000#!python """Bootstrap setuptools installation If you want to use setuptools in your package's setup.py, just include this file in the same directory with it, and add this to the top of your setup.py:: from ez_setup import use_setuptools use_setuptools() If you want to require a specific version of setuptools, set a download mirror, or use an alternate download directory, you can do so by supplying the appropriate options to ``use_setuptools()``. This file can also be run as a script to install or upgrade setuptools. """ import sys DEFAULT_VERSION = "0.6c7" DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] md5_data = { 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', } import sys, os def _validate_md5(egg_name, data): if egg_name in md5_data: from md5 import md5 digest = md5(data).hexdigest() if digest != md5_data[egg_name]: print >>sys.stderr, ( "md5 validation of %s failed! (Possible download problem?)" % egg_name ) sys.exit(2) return data def use_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, min_version=None, download_delay=15 ): """Automatically find/download setuptools and make it available on sys.path `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where setuptools will be downloaded, if it is not already available. If `download_delay` is specified, it should be the number of seconds that will be paused before initiating a download, should one be required. If an older version of setuptools is installed, this routine will print a message to ``sys.stderr`` and raise SystemExit in an attempt to abort the calling script. """ try: import setuptools if setuptools.__version__ == '0.0.1': print >>sys.stderr, ( "You have an obsolete version of setuptools installed. Please\n" "remove it from your system entirely before rerunning this script." ) sys.exit(2) except ImportError: egg = download_setuptools(version, download_base, to_dir, download_delay) sys.path.insert(0, egg) import setuptools; setuptools.bootstrap_install_from = egg import pkg_resources try: if not min_version: min_version = version pkg_resources.require("setuptools>="+min_version) except pkg_resources.VersionConflict, e: # XXX could we install in a subprocess here? print >>sys.stderr, ( "The required version of setuptools (>=%s) is not available, and\n" "can't be installed while this script is running. Please install\n" " a more recent version first.\n\n(Currently using %r)" ) % (min_version, e.args[0]) sys.exit(2) def download_setuptools( version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, delay = 15 ): """Download setuptools from a specified location and return its filename `version` should be a valid setuptools version number that is available as an egg for download under the `download_base` URL (which should end with a '/'). `to_dir` is the directory where the egg will be downloaded. `delay` is the number of seconds to pause before an actual download attempt. """ import urllib2, shutil egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) url = download_base + egg_name saveto = os.path.join(to_dir, egg_name) src = dst = None if not os.path.exists(saveto): # Avoid repeated downloads try: from distutils import log if delay: log.warn(""" --------------------------------------------------------------------------- This script requires setuptools version %s to run (even to display help). I will attempt to download it for you (from %s), but you may need to enable firewall access for this script first. I will start the download in %d seconds. (Note: if this machine does not have network access, please obtain the file %s and place it in this directory before rerunning this script.) ---------------------------------------------------------------------------""", version, download_base, delay, url ); from time import sleep; sleep(delay) log.warn("Downloading %s", url) src = urllib2.urlopen(url) # Read/write all in one block, so we don't create a corrupt file # if the download is interrupted. data = _validate_md5(egg_name, src.read()) dst = open(saveto,"wb"); dst.write(data) finally: if src: src.close() if dst: dst.close() return os.path.realpath(saveto) def main(argv, version=DEFAULT_VERSION): """Install or upgrade setuptools and EasyInstall""" try: import setuptools except ImportError: egg = None try: egg = download_setuptools(version, delay=0) sys.path.insert(0,egg) from setuptools.command.easy_install import main return main(list(argv)+[egg]) # we're done here finally: if egg and os.path.exists(egg): os.unlink(egg) else: if setuptools.__version__ == '0.0.1': # tell the user to uninstall obsolete version use_setuptools(version) req = "setuptools>="+version import pkg_resources try: pkg_resources.require(req) except pkg_resources.VersionConflict: try: from setuptools.command.easy_install import main except ImportError: from easy_install import main main(list(argv)+[download_setuptools(delay=0)]) sys.exit(0) # try to force an exit else: if argv: from setuptools.command.easy_install import main main(argv) else: print "Setuptools version",version,"or greater has been installed." print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' def update_md5(filenames): """Update our built-in md5 registry""" import re from md5 import md5 for name in filenames: base = os.path.basename(name) f = open(name,'rb') md5_data[base] = md5(f.read()).hexdigest() f.close() data = [" %r: %r,\n" % it for it in md5_data.items()] data.sort() repl = "".join(data) import inspect srcfile = inspect.getsourcefile(sys.modules[__name__]) f = open(srcfile, 'rb'); src = f.read(); f.close() match = re.search("\nmd5_data = {\n([^}]+)}", src) if not match: print >>sys.stderr, "Internal error!" sys.exit(2) src = src[:match.start(1)] + repl + src[match.end(1):] f = open(srcfile,'w') f.write(src) f.close() if __name__=='__main__': if len(sys.argv)>2 and sys.argv[1]=='--md5update': update_md5(sys.argv[2:]) else: main(sys.argv[1:]) fritzy-SleekXMPP-5c4ee57/setup.py000066400000000000000000000053171157775340400167670ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (C) 2007-2008 Nathanael C. Fritz # All Rights Reserved # # This software is licensed as described in the README file, # which you should have received as part of this distribution. # # from ez_setup import use_setuptools from distutils.core import setup import sys import sleekxmpp # if 'cygwin' in sys.platform.lower(): # min_version = '0.6c6' # else: # min_version = '0.6a9' # # try: # use_setuptools(min_version=min_version) # except TypeError: # # locally installed ez_setup won't have min_version # use_setuptools() # # from setuptools import setup, find_packages, Extension, Feature VERSION = sleekxmpp.__version__ DESCRIPTION = 'SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc).' LONG_DESCRIPTION = """ SleekXMPP is an elegant Python library for XMPP (aka Jabber, Google Talk, etc). """ CLASSIFIERS = [ 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT', 'Programming Language :: Python', 'Topic :: Software Development :: Libraries :: Python Modules', ] packages = [ 'sleekxmpp', 'sleekxmpp/stanza', 'sleekxmpp/test', 'sleekxmpp/xmlstream', 'sleekxmpp/xmlstream/matcher', '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_0050', 'sleekxmpp/plugins/xep_0059', 'sleekxmpp/plugins/xep_0085', 'sleekxmpp/plugins/xep_0086', 'sleekxmpp/plugins/xep_0092', 'sleekxmpp/plugins/xep_0128', 'sleekxmpp/plugins/xep_0199', ] if sys.version_info < (3, 0): py_modules = ['sleekxmpp.xmlstream.tostring.tostring26'] else: py_modules = ['sleekxmpp.xmlstream.tostring.tostring'] setup( name = "sleekxmpp", version = VERSION, description = DESCRIPTION, long_description = LONG_DESCRIPTION, author = 'Nathanael Fritz', author_email = 'fritzy [at] netflint.net', url = 'http://code.google.com/p/sleekxmpp', license = 'MIT', platforms = [ 'any' ], packages = packages, py_modules = py_modules, requires = [ 'tlslite', 'pythondns' ], ) fritzy-SleekXMPP-5c4ee57/sleekxmpp/000077500000000000000000000000001157775340400172575ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/__init__.py000066400000000000000000000011631157775340400213710ustar00rootroot00000000000000""" 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.basexmpp import BaseXMPP from sleekxmpp.clientxmpp import ClientXMPP from sleekxmpp.componentxmpp import ComponentXMPP from sleekxmpp.stanza import Message, Presence, Iq from sleekxmpp.xmlstream.handler import * from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET __version__ = '1.0beta5' __version_info__ = (1, 0, 0, 'beta5', 0) fritzy-SleekXMPP-5c4ee57/sleekxmpp/basexmpp.py000066400000000000000000000616561157775340400214660ustar00rootroot00000000000000""" 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 __future__ import with_statement, unicode_literals import sys import copy import logging import sleekxmpp from sleekxmpp import plugins from sleekxmpp.stanza import Message, Presence, Iq, Error, StreamError from sleekxmpp.stanza.roster import Roster from sleekxmpp.stanza.nick import Nick from sleekxmpp.stanza.htmlim import HTMLIM from sleekxmpp.xmlstream import XMLStream, JID, tostring from sleekxmpp.xmlstream import ET, register_stanza_plugin from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.handler import * log = logging.getLogger(__name__) # In order to make sure that Unicode is handled properly # in Python 2.x, reset the default encoding. if sys.version_info < (3, 0): reload(sys) sys.setdefaultencoding('utf8') class BaseXMPP(XMLStream): """ The BaseXMPP class adapts the generic XMLStream class for use with XMPP. It also provides a plugin mechanism to easily extend and add support for new XMPP features. Attributes: auto_authorize -- Manage automatically accepting roster subscriptions. auto_subscribe -- Manage automatically requesting mutual subscriptions. is_component -- Indicates if this stream is for an XMPP component. jid -- The XMPP JID for this stream. plugin -- A dictionary of loaded plugins. plugin_config -- A dictionary of plugin configurations. plugin_whitelist -- A list of approved plugins. sentpresence -- Indicates if an initial presence has been sent. roster -- A dictionary containing subscribed JIDs and their presence statuses. Methods: Iq -- Factory for creating an Iq stanzas. Message -- Factory for creating Message stanzas. Presence -- Factory for creating Presence stanzas. get -- Return a plugin given its name. make_iq -- Create and initialize an Iq stanza. make_iq_error -- Create an Iq stanza of type 'error'. make_iq_get -- Create an Iq stanza of type 'get'. make_iq_query -- Create an Iq stanza with a given query. make_iq_result -- Create an Iq stanza of type 'result'. make_iq_set -- Create an Iq stanza of type 'set'. make_message -- Create and initialize a Message stanza. make_presence -- Create and initialize a Presence stanza. make_query_roster -- Create a roster query. process -- Overrides XMLStream.process. register_plugin -- Load and configure a plugin. register_plugins -- Load and configure multiple plugins. send_message -- Create and send a Message stanza. send_presence -- Create and send a Presence stanza. send_presence_subscribe -- Send a subscription request. """ def __init__(self, default_ns='jabber:client'): """ Adapt an XML stream for use with XMPP. Arguments: default_ns -- Ensure that the correct default XML namespace is used during initialization. """ XMLStream.__init__(self) # To comply with PEP8, method names now use underscores. # Deprecated method names are re-mapped for backwards compatibility. self.default_ns = default_ns self.stream_ns = 'http://etherx.jabber.org/streams' self.boundjid = JID("") self.plugin = {} self.plugin_config = {} self.plugin_whitelist = [] self.roster = {} self.is_component = False self.auto_authorize = True self.auto_subscribe = True self.sentpresence = False self.register_handler( Callback('IM', MatchXPath('{%s}message/{%s}body' % (self.default_ns, self.default_ns)), self._handle_message)) self.register_handler( Callback('Presence', MatchXPath("{%s}presence" % self.default_ns), self._handle_presence)) self.register_handler( Callback('Stream Error', MatchXPath("{%s}error" % self.stream_ns), self._handle_stream_error)) self.add_event_handler('presence_subscribe', self._handle_subscribe) self.add_event_handler('disconnected', self._handle_disconnected) # Set up the XML stream with XMPP's root stanzas. self.register_stanza(Message) self.register_stanza(Iq) self.register_stanza(Presence) self.register_stanza(StreamError) # Initialize a few default stanza plugins. register_stanza_plugin(Iq, Roster) register_stanza_plugin(Message, Nick) register_stanza_plugin(Message, HTMLIM) def process(self, *args, **kwargs): """ Ensure that plugin inter-dependencies are handled before starting event processing. Overrides XMLStream.process. """ for name in self.plugin: if not self.plugin[name].post_inited: self.plugin[name].post_init() return XMLStream.process(self, *args, **kwargs) def register_plugin(self, plugin, pconfig={}, module=None): """ Register and configure a plugin for use in this stream. Arguments: plugin -- The name of the plugin class. Plugin names must be unique. pconfig -- A dictionary of configuration data for the plugin. Defaults to an empty dictionary. module -- Optional refence to the module containing the plugin class if using custom plugins. """ try: # Import the given module that contains the plugin. if not module: module = sleekxmpp.plugins module = __import__("%s.%s" % (module.__name__, plugin), globals(), locals(), [plugin]) if isinstance(module, str): # We probably want to load a module from outside # the sleekxmpp package, so leave out the globals(). module = __import__(module, fromlist=[plugin]) # Load the plugin class from the module. self.plugin[plugin] = getattr(module, plugin)(self, pconfig) # Let XEP implementing plugins have some extra logging info. xep = '' if hasattr(self.plugin[plugin], 'xep'): xep = "(XEP-%s) " % self.plugin[plugin].xep desc = (xep, self.plugin[plugin].description) log.debug("Loaded Plugin %s%s" % desc) except: log.exception("Unable to load plugin: %s", plugin) def register_plugins(self): """ Register and initialize all built-in plugins. Optionally, the list of plugins loaded may be limited to those contained in self.plugin_whitelist. Plugin configurations stored in self.plugin_config will be used. """ if self.plugin_whitelist: plugin_list = self.plugin_whitelist else: plugin_list = plugins.__all__ for plugin in plugin_list: if plugin in plugins.__all__: self.register_plugin(plugin, self.plugin_config.get(plugin, {})) else: raise NameError("Plugin %s not in plugins.__all__." % plugin) # Resolve plugin inter-dependencies. for plugin in self.plugin: self.plugin[plugin].post_init() def __getitem__(self, key): """ Return a plugin given its name, if it has been registered. """ if key in self.plugin: return self.plugin[key] else: log.warning("""Plugin "%s" is not loaded.""" % key) return False def get(self, key, default): """ Return a plugin given its name, if it has been registered. """ return self.plugin.get(key, default) def Message(self, *args, **kwargs): """Create a Message stanza associated with this stream.""" return Message(self, *args, **kwargs) def Iq(self, *args, **kwargs): """Create an Iq stanza associated with this stream.""" return Iq(self, *args, **kwargs) def Presence(self, *args, **kwargs): """Create a Presence stanza associated with this stream.""" return Presence(self, *args, **kwargs) def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None): """ Create a new Iq stanza with a given Id and from JID. Arguments: id -- An ideally unique ID value for this stanza thread. Defaults to 0. ifrom -- The from JID to use for this stanza. ito -- The destination JID for this stanza. itype -- The Iq's type, one of: get, set, result, or error. iquery -- Optional namespace for adding a query element. """ iq = self.Iq() iq['id'] = str(id) iq['to'] = ito iq['from'] = ifrom iq['type'] = itype iq['query'] = iquery return iq def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None): """ Create an Iq stanza of type 'get'. Optionally, a query element may be added. Arguments: queryxmlns -- The namespace of the query to use. ito -- The destination JID for this stanza. ifrom -- The from JID to use for this stanza. iq -- Optionally use an existing stanza instead of generating a new one. """ if not iq: iq = self.Iq() iq['type'] = 'get' iq['query'] = queryxmlns if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None): """ Create an Iq stanza of type 'result' with the given ID value. Arguments: id -- An ideally unique ID value. May use self.new_id(). ito -- The destination JID for this stanza. ifrom -- The from JID to use for this stanza. iq -- Optionally use an existing stanza instead of generating a new one. """ if not iq: iq = self.Iq() if id is None: id = self.new_id() iq['id'] = id iq['type'] = 'result' if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None): """ Create an Iq stanza of type 'set'. Optionally, a substanza may be given to use as the stanza's payload. Arguments: sub -- A stanza or XML object to use as the Iq's payload. ito -- The destination JID for this stanza. ifrom -- The from JID to use for this stanza. iq -- Optionally use an existing stanza instead of generating a new one. """ if not iq: iq = self.Iq() iq['type'] = 'set' if sub != None: iq.append(sub) if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_iq_error(self, id, type='cancel', condition='feature-not-implemented', text=None, ito=None, ifrom=None, iq=None): """ Create an Iq stanza of type 'error'. Arguments: id -- An ideally unique ID value. May use self.new_id(). type -- The type of the error, such as 'cancel' or 'modify'. Defaults to 'cancel'. condition -- The error condition. Defaults to 'feature-not-implemented'. text -- A message describing the cause of the error. ito -- The destination JID for this stanza. ifrom -- The from JID to use for this stanza. iq -- Optionally use an existing stanza instead of generating a new one. """ if not iq: iq = self.Iq() iq['id'] = id iq['error']['type'] = type iq['error']['condition'] = condition iq['error']['text'] = text if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None): """ Create or modify an Iq stanza to use the given query namespace. Arguments: iq -- Optional Iq stanza to modify. A new stanza is created otherwise. xmlns -- The query's namespace. ito -- The destination JID for this stanza. ifrom -- The from JID to use for this stanza. """ if not iq: iq = self.Iq() iq['query'] = xmlns if ito: iq['to'] = ito if ifrom: iq['from'] = ifrom return iq def make_query_roster(self, iq=None): """ Create a roster query element. Arguments: iq -- Optional Iq stanza to modify. A new stanza is created otherwise. """ if iq: iq['query'] = 'jabber:iq:roster' return ET.Element("{jabber:iq:roster}query") def make_message(self, mto, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): """ Create and initialize a new Message stanza. Arguments: mto -- The recipient of the message. mbody -- The main contents of the message. msubject -- Optional subject for the message. mtype -- The message's type, such as 'chat' or 'groupchat'. mhtml -- Optional HTML body content. mfrom -- The sender of the message. If sending from a client, be aware that some servers require that the full JID of the sender be used. mnick -- Optional nickname of the sender. """ message = self.Message(sto=mto, stype=mtype, sfrom=mfrom) message['body'] = mbody message['subject'] = msubject if mnick is not None: message['nick'] = mnick if mhtml is not None: message['html']['body'] = mhtml return message def make_presence(self, pshow=None, pstatus=None, ppriority=None, pto=None, ptype=None, pfrom=None): """ Create and initialize a new Presence stanza. Arguments: pshow -- The presence's show value. pstatus -- The presence's status message. ppriority -- This connections' priority. pto -- The recipient of a directed presence. ptype -- The type of presence, such as 'subscribe'. pfrom -- The sender of the presence. """ presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto) if pshow is not None: presence['type'] = pshow if pfrom is None: presence['from'] = self.boundjid.full presence['priority'] = ppriority presence['status'] = pstatus return presence def send_message(self, mto, mbody, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): """ Create, initialize, and send a Message stanza. """ self.makeMessage(mto, mbody, msubject, mtype, mhtml, mfrom, mnick).send() def send_presence(self, pshow=None, pstatus=None, ppriority=None, pto=None, pfrom=None, ptype=None): """ Create, initialize, and send a Presence stanza. Arguments: pshow -- The presence's show value. pstatus -- The presence's status message. ppriority -- This connections' priority. pto -- The recipient of a directed presence. ptype -- The type of presence, such as 'subscribe'. pfrom -- The sender of the presence. """ self.makePresence(pshow, pstatus, ppriority, pto, ptype=ptype, pfrom=pfrom).send() # Unexpected errors may occur if if not self.sentpresence: self.event('sent_presence') self.sentpresence = True def send_presence_subscription(self, pto, pfrom=None, ptype='subscribe', pnick=None): """ Create, initialize, and send a Presence stanza of type 'subscribe'. Arguments: pto -- The recipient of a directed presence. pfrom -- The sender of the presence. ptype -- The type of presence. Defaults to 'subscribe'. pnick -- Nickname of the presence's sender. """ presence = self.makePresence(ptype=ptype, pfrom=pfrom, pto=self.getjidbare(pto)) if pnick: nick = ET.Element('{http://jabber.org/protocol/nick}nick') nick.text = pnick presence.append(nick) presence.send() @property def jid(self): """ Attribute accessor for bare jid """ log.warning("jid property deprecated. Use boundjid.bare") return self.boundjid.bare @jid.setter def jid(self, value): log.warning("jid property deprecated. Use boundjid.bare") self.boundjid.bare = value @property def fulljid(self): """ Attribute accessor for full jid """ log.warning("fulljid property deprecated. Use boundjid.full") return self.boundjid.full @fulljid.setter def fulljid(self, value): log.warning("fulljid property deprecated. Use boundjid.full") self.boundjid.full = value @property def resource(self): """ Attribute accessor for jid resource """ log.warning("resource property deprecated. Use boundjid.resource") return self.boundjid.resource @resource.setter def resource(self, value): log.warning("fulljid property deprecated. Use boundjid.full") self.boundjid.resource = value @property def username(self): """ Attribute accessor for jid usernode """ log.warning("username property deprecated. Use boundjid.user") return self.boundjid.user @username.setter def username(self, value): log.warning("username property deprecated. Use boundjid.user") self.boundjid.user = value @property def server(self): """ Attribute accessor for jid host """ log.warning("server property deprecated. Use boundjid.host") return self.boundjid.server @server.setter def server(self, value): log.warning("server property deprecated. Use boundjid.host") self.boundjid.server = value def set_jid(self, jid): """Rip a JID apart and claim it as our own.""" log.debug("setting jid to %s" % jid) self.boundjid.full = jid def getjidresource(self, fulljid): if '/' in fulljid: return fulljid.split('/', 1)[-1] else: return '' def getjidbare(self, fulljid): return fulljid.split('/', 1)[0] def _handle_disconnected(self, event): """When disconnected, reset the roster""" self.roster = {} def _handle_stream_error(self, error): self.event('stream_error', error) def _handle_message(self, msg): """Process incoming message stanzas.""" self.event('message', msg) def _handle_presence(self, presence): """ Process incoming presence stanzas. Update the roster with presence information. """ self.event("presence_%s" % presence['type'], presence) # Check for changes in subscription state. if presence['type'] in ('subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'): self.event('changed_subscription', presence) return elif not presence['type'] in ('available', 'unavailable') and \ not presence['type'] in presence.showtypes: return # Strip the information from the stanza. jid = presence['from'].bare resource = presence['from'].resource show = presence['type'] status = presence['status'] priority = presence['priority'] was_offline = False got_online = False old_roster = self.roster.get(jid, {}).get(resource, {}) # Create a new roster entry if needed. if not jid in self.roster: self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': False} # Alias to simplify some references. connections = self.roster[jid].get('presence', {}) # Determine if the user has just come online. if not resource in connections: if show == 'available' or show in presence.showtypes: got_online = True was_offline = True connections[resource] = {} if connections[resource].get('show', 'unavailable') == 'unavailable': was_offline = True # Update the roster's state for this JID's resource. connections[resource] = {'show': show, 'status': status, 'priority': priority} name = self.roster[jid].get('name', '') # Remove unneeded state information after a resource # disconnects. Determine if this was the last connection # for the JID. if show == 'unavailable': log.debug("%s %s got offline" % (jid, resource)) del connections[resource] if not connections and \ not self.roster[jid].get('in_roster', False): del self.roster[jid] if not was_offline: self.event("got_offline", presence) else: return False name = '(%s) ' % name if name else '' # Presence state has changed. self.event("changed_status", presence) if got_online: self.event("got_online", presence) log.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, show, status)) def _handle_subscribe(self, presence): """ Automatically managage subscription requests. Subscription behavior is controlled by the settings self.auto_authorize and self.auto_subscribe. auto_auth auto_sub Result: True True Create bi-directional subsriptions. True False Create only directed subscriptions. False * Decline all subscriptions. None * Disable automatic handling and use a custom handler. """ presence.reply() presence['to'] = presence['to'].bare # We are using trinary logic, so conditions have to be # more explicit than usual. if self.auto_authorize == True: presence['type'] = 'subscribed' presence.send() if self.auto_subscribe: presence['type'] = 'subscribe' presence.send() elif self.auto_authorize == False: presence['type'] = 'unsubscribed' presence.send() # 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/clientxmpp.py000066400000000000000000000433621157775340400220240ustar00rootroot00000000000000""" 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 __future__ import absolute_import, unicode_literals import logging import base64 import sys import hashlib import random import threading from sleekxmpp import plugins from sleekxmpp import stanza from sleekxmpp.basexmpp import BaseXMPP from sleekxmpp.stanza import Message, Presence, Iq from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream import StanzaBase, ET from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.handler import * # Flag indicating if DNS SRV records are available for use. SRV_SUPPORT = True try: import dns.resolver except: SRV_SUPPORT = False log = logging.getLogger(__name__) class ClientXMPP(BaseXMPP): """ SleekXMPP's client class. Use only for good, not for evil. Attributes: Methods: connect -- Overrides XMLStream.connect. del_roster_item -- Delete a roster item. get_roster -- Retrieve the roster from the server. register_feature -- Register a stream feature. update_roster -- Update a roster item. """ def __init__(self, jid, password, ssl=False, plugin_config={}, plugin_whitelist=[], escape_quotes=True): """ Create a new SleekXMPP client. Arguments: jid -- The JID of the XMPP user account. password -- The password for the XMPP user account. ssl -- Deprecated. plugin_config -- A dictionary of plugin configurations. plugin_whitelist -- A list of approved plugins that will be loaded when calling register_plugins. escape_quotes -- Deprecated. """ BaseXMPP.__init__(self, 'jabber:client') self.set_jid(jid) self.password = password self.escape_quotes = escape_quotes self.plugin_config = plugin_config self.plugin_whitelist = plugin_whitelist self.srv_support = SRV_SUPPORT self.stream_header = "" % ( self.boundjid.host, "xmlns:stream='%s'" % self.stream_ns, "xmlns='%s'" % self.default_ns) self.stream_footer = "" self.features = [] self.registered_features = [] #TODO: Use stream state here self.authenticated = False self.sessionstarted = False self.bound = False self.bindfail = False self.add_event_handler('connected', self.handle_connected) self.register_handler( Callback('Stream Features', MatchXPath('{%s}features' % self.stream_ns), self._handle_stream_features)) self.register_handler( Callback('Roster Update', MatchXPath('{%s}iq/{%s}query' % ( self.default_ns, 'jabber:iq:roster')), self._handle_roster)) self.register_feature( "", self._handle_starttls, True) self.register_feature( "", self._handle_sasl_auth, True) self.register_feature( "", self._handle_bind_resource) self.register_feature( "", self._handle_start_session) def handle_connected(self, event=None): #TODO: Use stream state here self.authenticated = False self.sessionstarted = False self.bound = False self.bindfail = False self.schedule("session timeout checker", 15, self._session_timeout_check) def _session_timeout_check(self): if not self.session_started_event.isSet(): log.debug("Session start has taken more than 15 seconds") self.disconnect(reconnect=self.auto_reconnect) def connect(self, address=tuple(), reattempt=True, use_tls=True): """ Connect to the XMPP server. When no address is given, a SRV lookup for the server will be attempted. If that fails, the server user in the JID will be used. Arguments: address -- A tuple containing the server's host and port. reattempt -- If True, reattempt the connection if an error occurs. Defaults to True. use_tls -- Indicates if TLS should be used for the connection. Defaults to True. """ self.session_started_event.clear() if not address or len(address) < 2: if not self.srv_support: log.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.") else: log.debug("Since no address is supplied," + \ "attempting SRV lookup.") try: xmpp_srv = "_xmpp-client._tcp.%s" % self.boundjid.host answers = dns.resolver.query(xmpp_srv, dns.rdatatype.SRV) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): log.debug("No appropriate SRV record found." + \ " Using JID server name.") except (dns.exception.Timeout,): log.debug("DNS resolution timed out.") else: # Pick a random server, weighted by priority. addresses = {} intmax = 0 for answer in answers: intmax += answer.priority addresses[intmax] = (answer.target.to_text()[:-1], answer.port) #python3 returns a generator for dictionary keys priorities = [x for x in addresses.keys()] priorities.sort() picked = random.randint(0, intmax) for priority in priorities: if picked <= priority: address = addresses[priority] break if not address: # If all else fails, use the server from the JID. address = (self.boundjid.host, 5222) return XMLStream.connect(self, address[0], address[1], use_tls=use_tls, reattempt=reattempt) def register_feature(self, mask, pointer, breaker=False): """ Register a stream feature. Arguments: mask -- An XML string matching the feature's element. pointer -- The function to execute if the feature is received. breaker -- Indicates if feature processing should halt with this feature. Defaults to False. """ self.registered_features.append((MatchXMLMask(mask), pointer, breaker)) def update_roster(self, jid, name=None, subscription=None, groups=[], block=True, timeout=None, callback=None): """ Add or change a roster item. Arguments: jid -- The JID of the entry to modify. name -- The user's nickname for this JID. subscription -- The subscription status. May be one of 'to', 'from', 'both', or 'none'. If set to 'remove', the entry will be deleted. groups -- The roster groups that contain this item. block -- Specify if the roster request will block until a response is received, or a timeout occurs. Defaults to True. timeout -- The length of time (in seconds) to wait for a response before continuing if blocking is used. Defaults to self.response_timeout. callback -- Optional reference to a stream handler function. Will be executed when the roster is received. Implies block=False. """ iq = self.Iq() iq['type'] = 'set' iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}} response = iq.send(block, timeout, callback) if response in [False, None] or not isinstance(response, Iq): return response return response['type'] == 'result' def del_roster_item(self, jid): """ Remove an item from the roster by setting its subscription status to 'remove'. Arguments: jid -- The JID of the item to remove. """ return self.update_roster(jid, subscription='remove') def get_roster(self, block=True, timeout=None, callback=None): """ Request the roster from the server. Arguments: block -- Specify if the roster request will block until a response is received, or a timeout occurs. Defaults to True. timeout -- The length of time (in seconds) to wait for a response before continuing if blocking is used. Defaults to self.response_timeout. callback -- Optional reference to a stream handler function. Will be executed when the roster is received. Implies block=False. """ iq = self.Iq() iq['type'] = 'get' iq.enable('roster') response = iq.send(block, timeout, callback) if response == False: self.event('roster_timeout') if response in [False, None] or not isinstance(response, Iq): return response else: return self._handle_roster(response, request=True) def _handle_stream_features(self, features): """ Process the received stream features. Arguments: features -- The features stanza. """ # Record all of the features. self.features = [] for sub in features.xml: self.features.append(sub.tag) # Process the features. for sub in features.xml: for feature in self.registered_features: mask, handler, halt = feature if mask.match(sub): if handler(sub) and halt: # Don't continue if the feature was # marked as a breaker. return True def _handle_starttls(self, xml): """ Handle notification that the server supports TLS. Arguments: xml -- The STARTLS proceed element. """ if not self.use_tls: return False elif not self.authenticated and self.ssl_support: tls_ns = 'urn:ietf:params:xml:ns:xmpp-tls' self.add_handler("" % tls_ns, self._handle_tls_start, name='TLS Proceed', instream=True) self.send_xml(xml, now=True) return True else: log.warning("The module tlslite is required to log in" +\ " to some servers, and has not been found.") return False def _handle_tls_start(self, xml): """ Handle encrypting the stream using TLS. Restarts the stream. """ log.debug("Starting TLS") if self.start_tls(): raise RestartStream() def _handle_sasl_auth(self, xml): """ Handle authenticating using SASL. Arguments: xml -- The SASL mechanisms stanza. """ if self.use_tls and \ '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features: return False log.debug("Starting SASL Auth") sasl_ns = 'urn:ietf:params:xml:ns:xmpp-sasl' self.add_handler("" % sasl_ns, self._handle_auth_success, name='SASL Sucess', instream=True) self.add_handler("" % sasl_ns, self._handle_auth_fail, name='SASL Failure', instream=True) sasl_mechs = xml.findall('{%s}mechanism' % sasl_ns) if sasl_mechs: for sasl_mech in sasl_mechs: self.features.append("sasl:%s" % sasl_mech.text) if 'sasl:PLAIN' in self.features and self.boundjid.user: if sys.version_info < (3, 0): user = bytes(self.boundjid.user) password = bytes(self.password) else: user = bytes(self.boundjid.user, 'utf-8') password = bytes(self.password, 'utf-8') auth = base64.b64encode(b'\x00' + user + \ b'\x00' + password).decode('utf-8') self.send("%s" % ( sasl_ns, auth), now=True) elif 'sasl:ANONYMOUS' in self.features and not self.boundjid.user: self.send("" % ( sasl_ns, 'ANONYMOUS'), now=True) else: log.error("No appropriate login method.") self.disconnect() return True def _handle_auth_success(self, xml): """ SASL authentication succeeded. Restart the stream. Arguments: xml -- The SASL authentication success element. """ self.authenticated = True self.features = [] raise RestartStream() def _handle_auth_fail(self, xml): """ SASL authentication failed. Disconnect and shutdown. Arguments: xml -- The SASL authentication failure element. """ log.info("Authentication failed.") self.event("failed_auth", direct=True) self.disconnect() def _handle_bind_resource(self, xml): """ Handle requesting a specific resource. Arguments: xml -- The bind feature element. """ log.debug("Requesting resource: %s" % self.boundjid.resource) xml.clear() iq = self.Iq(stype='set') if self.boundjid.resource: res = ET.Element('resource') res.text = self.boundjid.resource xml.append(res) iq.append(xml) response = iq.send(now=True) bind_ns = 'urn:ietf:params:xml:ns:xmpp-bind' self.set_jid(response.xml.find('{%s}bind/{%s}jid' % (bind_ns, bind_ns)).text) self.bound = True log.info("Node set to: %s" % self.boundjid.full) session_ns = 'urn:ietf:params:xml:ns:xmpp-session' if "{%s}session" % session_ns not in self.features or self.bindfail: log.debug("Established Session") self.sessionstarted = True self.session_started_event.set() self.event("session_start") def _handle_start_session(self, xml): """ Handle the start of the session. Arguments: xml -- The session feature element. """ if self.authenticated and self.bound: iq = self.makeIqSet(xml) response = iq.send(now=True) log.debug("Established Session") self.sessionstarted = True self.session_started_event.set() self.event("session_start") else: # Bind probably hasn't happened yet. self.bindfail = True def _handle_roster(self, iq, request=False): """ Update the roster after receiving a roster stanza. Arguments: iq -- The roster stanza. request -- Indicates if this stanza is a response to a request for the roster. """ if iq['type'] == 'set' or (iq['type'] == 'result' and request): for jid in iq['roster']['items']: if not jid in self.roster: self.roster[jid] = {'groups': [], 'name': '', 'subscription': 'none', 'presence': {}, 'in_roster': True} self.roster[jid].update(iq['roster']['items'][jid]) self.event('roster_received', iq) self.event("roster_update", iq) if iq['type'] == 'set': iq.reply() iq.enable('roster') iq.send() return True # 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/componentxmpp.py000066400000000000000000000112611157775340400225410ustar00rootroot00000000000000""" 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 __future__ import absolute_import import logging import base64 import sys import hashlib from sleekxmpp import plugins from sleekxmpp import stanza from sleekxmpp.basexmpp import BaseXMPP from sleekxmpp.xmlstream import XMLStream, RestartStream from sleekxmpp.xmlstream import StanzaBase, ET from sleekxmpp.xmlstream.matcher import * from sleekxmpp.xmlstream.handler import * log = logging.getLogger(__name__) class ComponentXMPP(BaseXMPP): """ SleekXMPP's basic XMPP server component. Use only for good, not for evil. Methods: connect -- Overrides XMLStream.connect. incoming_filter -- Overrides XMLStream.incoming_filter. start_stream_handler -- Overrides XMLStream.start_stream_handler. """ def __init__(self, jid, secret, host, port, plugin_config={}, plugin_whitelist=[], use_jc_ns=False): """ Arguments: jid -- The JID of the component. secret -- The secret or password for the component. host -- The server accepting the component. port -- The port used to connect to the server. plugin_config -- A dictionary of plugin configurations. plugin_whitelist -- A list of desired plugins to load when using register_plugins. use_js_ns -- Indicates if the 'jabber:client' namespace should be used instead of the standard 'jabber:component:accept' namespace. Defaults to False. """ if use_jc_ns: default_ns = 'jabber:client' else: default_ns = 'jabber:component:accept' BaseXMPP.__init__(self, default_ns) self.auto_authorize = None self.stream_header = "" % ( 'xmlns="jabber:component:accept"', 'xmlns:stream="%s"' % self.stream_ns, jid) self.stream_footer = "" self.server_host = host self.server_port = port self.set_jid(jid) self.secret = secret self.plugin_config = plugin_config self.plugin_whitelist = plugin_whitelist self.is_component = True self.register_handler( Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handle_handshake)) def connect(self): """ Connect to the server. Overrides XMLStream.connect. """ log.debug("Connecting to %s:%s" % (self.server_host, self.server_port)) return XMLStream.connect(self, self.server_host, self.server_port) def incoming_filter(self, xml): """ Pre-process incoming XML stanzas by converting any 'jabber:client' namespaced elements to the component's default namespace. Overrides XMLStream.incoming_filter. Arguments: xml -- The XML stanza to pre-process. """ if xml.tag.startswith('{jabber:client}'): xml.tag = xml.tag.replace('jabber:client', self.default_ns) # The incoming_filter call is only made on top level stanza # elements. So we manually continue filtering on sub-elements. for sub in xml: self.incoming_filter(sub) return xml def start_stream_handler(self, xml): """ Once the streams are established, attempt to handshake with the server to be accepted as a component. Overrides XMLStream.start_stream_handler. Arguments: xml -- The incoming stream's root element. """ # Construct a hash of the stream ID and the component secret. sid = xml.get('id', '') pre_hash = '%s%s' % (sid, self.secret) if sys.version_info >= (3, 0): # Handle Unicode byte encoding in Python 3. pre_hash = bytes(pre_hash, 'utf-8') handshake = ET.Element('{jabber:component:accept}handshake') handshake.text = hashlib.sha1(pre_hash).hexdigest().lower() self.send_xml(handshake, now=True) def _handle_handshake(self, xml): """ The handshake has been accepted. Arguments: xml -- The reply handshake stanza. """ self.session_started_event.set() self.event("session_start") fritzy-SleekXMPP-5c4ee57/sleekxmpp/exceptions.py000066400000000000000000000040401157775340400220100ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. """ class XMPPError(Exception): """ A generic exception that may be raised while processing an XMPP stanza to indicate that an error response stanza should be sent. The exception method for stanza objects extending RootStanza will create an error stanza and initialize any additional substanzas using the extension information included in the exception. Meant for use in SleekXMPP plugins and applications using SleekXMPP. """ def __init__(self, condition='undefined-condition', text=None, etype=None, extension=None, extension_ns=None, extension_args=None, clear=True): """ Create a new XMPPError exception. Extension information can be included to add additional XML elements to the generated error stanza. Arguments: condition -- The XMPP defined error condition. text -- Human readable text describing the error. etype -- The XMPP error type, such as cancel or modify. extension -- Tag name of the extension's XML content. extension_ns -- XML namespace of the extensions' XML content. 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 = {} 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/000077500000000000000000000000001157775340400207405ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/__init__.py000066400000000000000000000006071157775340400230540ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. """ __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify'] fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/base.py000066400000000000000000000066011157775340400222270ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. """ class base_plugin(object): """ The base_plugin class serves as a base for user created plugins that provide support for existing or experimental XEPS. Each plugin has a dictionary for configuration options, as well as a name and description. The lifecycle of a plugin is: 1. The plugin is instantiated during registration. 2. Once the XML stream begins processing, the method plugin_init() is called (if the plugin is configured as enabled with {'enable': True}). 3. After all plugins have been initialized, the method post_init() is called. Recommended event handlers: session_start -- Plugins which require the use of the current bound JID SHOULD wait for the session_start event to perform any initialization (or resetting). This is a transitive recommendation, plugins that use other plugins which use the bound JID should also wait for session_start before making such calls. session_end -- If the plugin keeps any per-session state, such as joined MUC rooms, such state SHOULD be cleared when the session_end event is raised. Attributes: xep -- The XEP number the plugin implements, if any. description -- A short description of the plugin, typically the long name of the implemented XEP. xmpp -- The main SleekXMPP instance. config -- A dictionary of custom configuration values. The value 'enable' is special and controls whether or not the plugin is initialized after registration. post_initted -- Executed after all plugins have been initialized to handle any cross-plugin interactions, such as registering service discovery items. enable -- Indicates that the plugin is enabled for use and will be initialized after registration. Methods: plugin_init -- Initialize the plugin state. post_init -- Handle any cross-plugin concerns. """ def __init__(self, xmpp, config=None): """ Instantiate a new plugin and store the given configuration. Arguments: xmpp -- The main SleekXMPP instance. config -- A dictionary of configuration values. """ if config is None: config = {} self.xep = 'base' 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() def plugin_init(self): """ Initialize plugin state, such as registering any stream or event handlers, or new stanza types. """ pass def post_init(self): """ Perform any cross-plugin interactions, such as registering service discovery identities or items. """ self.post_inited = True fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/gmail_notify.py000066400000000000000000000111041157775340400237700ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging from . import base from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.iq import Iq log = logging.getLogger(__name__) class GmailQuery(ElementBase): namespace = 'google:mail:notify' name = 'query' plugin_attrib = 'gmail' interfaces = set(('newer-than-time', 'newer-than-tid', 'q', 'search')) def getSearch(self): return self['q'] def setSearch(self, search): self['q'] = search def delSearch(self): del self['q'] class MailBox(ElementBase): namespace = 'google:mail:notify' name = 'mailbox' plugin_attrib = 'mailbox' interfaces = set(('result-time', 'total-matched', 'total-estimate', 'url', 'threads', 'matched', 'estimate')) def getThreads(self): threads = [] for threadXML in self.xml.findall('{%s}%s' % (MailThread.namespace, MailThread.name)): threads.append(MailThread(xml=threadXML, parent=None)) return threads def getMatched(self): return self['total-matched'] def getEstimate(self): return self['total-estimate'] == '1' class MailThread(ElementBase): namespace = 'google:mail:notify' name = 'mail-thread-info' plugin_attrib = 'thread' interfaces = set(('tid', 'participation', 'messages', 'date', 'senders', 'url', 'labels', 'subject', 'snippet')) sub_interfaces = set(('labels', 'subject', 'snippet')) def getSenders(self): senders = [] sendersXML = self.xml.find('{%s}senders' % self.namespace) if sendersXML is not None: for senderXML in sendersXML.findall('{%s}sender' % self.namespace): senders.append(MailSender(xml=senderXML, parent=None)) return senders class MailSender(ElementBase): namespace = 'google:mail:notify' name = 'sender' plugin_attrib = 'sender' interfaces = set(('address', 'name', 'originator', 'unread')) def getOriginator(self): return self.xml.attrib.get('originator', '0') == '1' def getUnread(self): return self.xml.attrib.get('unread', '0') == '1' class NewMail(ElementBase): namespace = 'google:mail:notify' name = 'new-mail' plugin_attrib = 'new-mail' class gmail_notify(base.base_plugin): """ Google Talk: Gmail Notifications """ def plugin_init(self): self.description = 'Google Talk: Gmail Notifications' self.xmpp.registerHandler( Callback('Gmail Result', MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns, MailBox.namespace, MailBox.name)), self.handle_gmail)) self.xmpp.registerHandler( Callback('Gmail New Mail', MatchXPath('{%s}iq/{%s}%s' % (self.xmpp.default_ns, NewMail.namespace, NewMail.name)), self.handle_new_mail)) registerStanzaPlugin(Iq, GmailQuery) registerStanzaPlugin(Iq, MailBox) registerStanzaPlugin(Iq, NewMail) self.last_result_time = None def handle_gmail(self, iq): mailbox = iq['mailbox'] approx = ' approximately' if mailbox['estimated'] else '' log.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched'])) self.last_result_time = mailbox['result-time'] self.xmpp.event('gmail_messages', iq) def handle_new_mail(self, iq): log.info("Gmail: New emails received!") self.xmpp.event('gmail_notify') self.checkEmail() def getEmail(self, query=None): return self.search(query) def checkEmail(self): return self.search(newer=self.last_result_time) def search(self, query=None, newer=None): if query is None: log.info("Gmail: Checking for new emails") else: log.info('Gmail: Searching for emails matching: "%s"' % query) iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = self.xmpp.boundjid.bare iq['gmail']['q'] = query iq['gmail']['newer-than-time'] = newer return iq.send() fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/jobs.py000066400000000000000000000027151157775340400222540ustar00rootroot00000000000000from . import base import logging from xml.etree import cElementTree as ET log = logging.getLogger(__name__) 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, host, node, 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 type(result) == bool or result['type'] != 'result': log.error("Unable to change %s:%s to %s" % (node, jobid, state)) return False return True fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/old_0004.py000066400000000000000000000275541157775340400225500ustar00rootroot00000000000000""" 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 . import base import logging from xml.etree import cElementTree as ET import copy import logging #TODO support item groups and results log = logging.getLogger(__name__) class old_0004(base.base_plugin): def plugin_init(self): self.xep = '0004' self.description = '*Deprecated Data Forms' self.xmpp.add_handler("", self.handler_message_xform, name='Old Message Form') def post_init(self): base.base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') log.warning("This implementation of XEP-0004 is deprecated.") def handler_message_xform(self, xml): object = self.handle_form(xml) self.xmpp.event("message_form", object) def handler_presence_xform(self, xml): object = self.handle_form(xml) self.xmpp.event("presence_form", object) def handle_form(self, xml): xmlform = xml.find('{jabber:x:data}x') object = self.buildForm(xmlform) self.xmpp.event("message_xform", object) return object def buildForm(self, xml): form = Form(ftype=xml.attrib['type']) form.fromXML(xml) return form def makeForm(self, ftype='form', title='', instructions=''): return Form(self.xmpp, ftype, title, instructions) class FieldContainer(object): def __init__(self, stanza = 'form'): self.fields = [] self.field = {} self.stanza = stanza def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None): self.field[var] = FormField(var, ftype, label, desc, required, value) self.fields.append(self.field[var]) return self.field[var] def buildField(self, xml): self.field[xml.get('var', '__unnamed__')] = FormField(xml.get('var', '__unnamed__'), xml.get('type', 'text-single')) self.fields.append(self.field[xml.get('var', '__unnamed__')]) self.field[xml.get('var', '__unnamed__')].buildField(xml) def buildContainer(self, xml): self.stanza = xml.tag for field in xml.findall('{jabber:x:data}field'): self.buildField(field) def getXML(self, ftype): container = ET.Element(self.stanza) for field in self.fields: container.append(field.getXML(ftype)) return container class Form(FieldContainer): types = ('form', 'submit', 'cancel', 'result') def __init__(self, xmpp=None, ftype='form', title='', instructions=''): if not ftype in self.types: raise ValueError("Invalid Form Type") FieldContainer.__init__(self) self.xmpp = xmpp self.type = ftype self.title = title self.instructions = instructions self.reported = [] self.items = [] def merge(self, form2): form1 = Form(ftype=self.type) form1.fromXML(self.getXML(self.type)) for field in form2.fields: if not field.var in form1.field: form1.addField(field.var, field.type, field.label, field.desc, field.required, field.value) else: form1.field[field.var].value = field.value for option, label in field.options: if (option, label) not in form1.field[field.var].options: form1.fields[field.var].addOption(option, label) return form1 def copy(self): newform = Form(ftype=self.type) newform.fromXML(self.getXML(self.type)) return newform def update(self, form): values = form.getValues() for var in values: if var in self.fields: self.fields[var].setValue(self.fields[var]) def getValues(self): result = {} for field in self.fields: value = field.value if len(value) == 1: value = value[0] result[field.var] = value return result def setValues(self, values={}): for field in values: if field in self.field: if isinstance(values[field], list) or isinstance(values[field], tuple): for value in values[field]: self.field[field].setValue(value) else: self.field[field].setValue(values[field]) def fromXML(self, xml): self.buildForm(xml) def addItem(self): newitem = FieldContainer('item') self.items.append(newitem) return newitem def buildItem(self, xml): newitem = self.addItem() newitem.buildContainer(xml) def addReported(self): reported = FieldContainer('reported') self.reported.append(reported) return reported def buildReported(self, xml): reported = self.addReported() reported.buildContainer(xml) def setTitle(self, title): self.title = title def setInstructions(self, instructions): self.instructions = instructions def setType(self, ftype): self.type = ftype def getXMLMessage(self, to): msg = self.xmpp.makeMessage(to) msg.append(self.getXML()) return msg def buildForm(self, xml): self.type = xml.get('type', 'form') if xml.find('{jabber:x:data}title') is not None: self.setTitle(xml.find('{jabber:x:data}title').text) if xml.find('{jabber:x:data}instructions') is not None: self.setInstructions(xml.find('{jabber:x:data}instructions').text) for field in xml.findall('{jabber:x:data}field'): self.buildField(field) for reported in xml.findall('{jabber:x:data}reported'): self.buildReported(reported) for item in xml.findall('{jabber:x:data}item'): self.buildItem(item) #def getXML(self, tostring = False): def getXML(self, ftype=None): if ftype: self.type = ftype form = ET.Element('{jabber:x:data}x') form.attrib['type'] = self.type if self.title and self.type in ('form', 'result'): title = ET.Element('{jabber:x:data}title') title.text = self.title form.append(title) if self.instructions and self.type == 'form': instructions = ET.Element('{jabber:x:data}instructions') instructions.text = self.instructions form.append(instructions) for field in self.fields: form.append(field.getXML(self.type)) for reported in self.reported: form.append(reported.getXML('{jabber:x:data}reported')) for item in self.items: form.append(item.getXML(self.type)) #if tostring: # form = self.xmpp.tostring(form) return form def getXHTML(self): form = ET.Element('{http://www.w3.org/1999/xhtml}form') if self.title: title = ET.Element('h2') title.text = self.title form.append(title) if self.instructions: instructions = ET.Element('p') instructions.text = self.instructions form.append(instructions) for field in self.fields: form.append(field.getXHTML()) for field in self.reported: form.append(field.getXHTML()) for field in self.items: form.append(field.getXHTML()) return form def makeSubmit(self): self.setType('submit') class FormField(object): types = ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single') listtypes = ('jid-multi', 'jid-single', 'list-multi', 'list-single') lbtypes = ('fixed', 'text-multi') def __init__(self, var, ftype='text-single', label='', desc='', required=False, value=None): if not ftype in self.types: raise ValueError("Invalid Field Type") self.type = ftype self.var = var self.label = label self.desc = desc self.options = [] self.required = False self.value = [] if self.type in self.listtypes: self.islist = True else: self.islist = False if self.type in self.lbtypes: self.islinebreak = True else: self.islinebreak = False if value: self.setValue(value) def addOption(self, value, label): if self.islist: self.options.append((value, label)) else: raise ValueError("Cannot add options to non-list type field.") def setTrue(self): if self.type == 'boolean': self.value = [True] def setFalse(self): if self.type == 'boolean': self.value = [False] def require(self): self.required = True def setDescription(self, desc): self.desc = desc def setValue(self, value): if self.type == 'boolean': if value in ('1', 1, True, 'true', 'True', 'yes'): value = True else: value = False if self.islinebreak and value is not None: self.value += value.split('\n') else: if len(self.value) and (not self.islist or self.type == 'list-single'): self.value = [value] else: self.value.append(value) def delValue(self, value): if type(self.value) == type([]): try: idx = self.value.index(value) if idx != -1: self.value.pop(idx) except ValueError: pass else: self.value = '' def setAnswer(self, value): self.setValue(value) def buildField(self, xml): self.type = xml.get('type', 'text-single') self.label = xml.get('label', '') for option in xml.findall('{jabber:x:data}option'): self.addOption(option.find('{jabber:x:data}value').text, option.get('label', '')) for value in xml.findall('{jabber:x:data}value'): self.setValue(value.text) if xml.find('{jabber:x:data}required') is not None: self.require() if xml.find('{jabber:x:data}desc') is not None: self.setDescription(xml.find('{jabber:x:data}desc').text) def getXML(self, ftype): field = ET.Element('{jabber:x:data}field') if ftype != 'result': field.attrib['type'] = self.type if self.type != 'fixed': if self.var: field.attrib['var'] = self.var if self.label: field.attrib['label'] = self.label if ftype == 'form': for option in self.options: optionxml = ET.Element('{jabber:x:data}option') optionxml.attrib['label'] = option[1] optionval = ET.Element('{jabber:x:data}value') optionval.text = option[0] optionxml.append(optionval) field.append(optionxml) if self.required: required = ET.Element('{jabber:x:data}required') field.append(required) if self.desc: desc = ET.Element('{jabber:x:data}desc') desc.text = self.desc field.append(desc) for value in self.value: valuexml = ET.Element('{jabber:x:data}value') if value is True or value is False: if value: valuexml.text = '1' else: valuexml.text = '0' else: valuexml.text = value field.append(valuexml) return field def getXHTML(self): field = ET.Element('div', {'class': 'xmpp-xforms-%s' % self.type}) if self.label: label = ET.Element('p') label.text = "%s: " % self.label else: label = ET.Element('p') label.text = "%s: " % self.var field.append(label) if self.type == 'boolean': formf = ET.Element('input', {'type': 'checkbox', 'name': self.var}) if len(self.value) and self.value[0] in (True, 'true', '1'): formf.attrib['checked'] = 'checked' elif self.type == 'fixed': formf = ET.Element('p') try: formf.text = ', '.join(self.value) except: pass field.append(formf) formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) try: formf.text = ', '.join(self.value) except: pass elif self.type == 'hidden': formf = ET.Element('input', {'type': 'hidden', 'name': self.var}) try: formf.text = ', '.join(self.value) except: pass elif self.type in ('jid-multi', 'list-multi'): formf = ET.Element('select', {'name': self.var}) for option in self.options: optf = ET.Element('option', {'value': option[0], 'multiple': 'multiple'}) optf.text = option[1] if option[1] in self.value: optf.attrib['selected'] = 'selected' formf.append(option) elif self.type in ('jid-single', 'text-single'): formf = ET.Element('input', {'type': 'text', 'name': self.var}) try: formf.attrib['value'] = ', '.join(self.value) except: pass elif self.type == 'list-single': formf = ET.Element('select', {'name': self.var}) for option in self.options: optf = ET.Element('option', {'value': option[0]}) optf.text = option[1] if not optf.text: optf.text = option[0] if option[1] in self.value: optf.attrib['selected'] = 'selected' formf.append(optf) elif self.type == 'text-multi': formf = ET.Element('textarea', {'name': self.var}) try: formf.text = ', '.join(self.value) except: pass if not formf.text: formf.text = ' ' elif self.type == 'text-private': formf = ET.Element('input', {'type': 'password', 'name': self.var}) try: formf.attrib['value'] = ', '.join(self.value) except: pass label.append(formf) return field fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/old_0009.py000066400000000000000000000172411157775340400225450ustar00rootroot00000000000000""" XEP-0009 XMPP Remote Procedure Calls """ from __future__ import with_statement from . import base import logging from xml.etree import cElementTree as ET import copy import time import base64 def py2xml(*args): params = ET.Element("params") for x in args: param = ET.Element("param") param.append(_py2xml(x)) params.append(param) #... return params def _py2xml(*args): for x in args: val = ET.Element("value") if type(x) is int: i4 = ET.Element("i4") i4.text = str(x) val.append(i4) if 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) is list: 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): vals = [] for param in params.findall('param'): vals.append(_xml2py(param.find('value'))) return vals def _xml2py(value): if value.find('i4') is not None: return int(value.find('i4').text) if value.find('int') is not None: return int(value.find('int').text) if value.find('boolean') is not None: return bool(value.find('boolean').text) if value.find('string') is not None: return value.find('string').text if value.find('double') is not None: return float(value.find('double').text) if value.find('Base64') is not None: return rpcbase64(value.find('Base64').text) if value.find('dateTime.iso8601') is not None: return rpctime(value.find('dateTime.iso8601')) if value.find('struct') is not None: struct = {} for member in value.find('struct').findall('member'): struct[member.find('name').text] = _xml2py(member.find('value')) return struct if value.find('array') is not None: array = [] for val in value.find('array').find('data').findall('value'): 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(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() class JabberRPCEntry(object): def __init__(self,call): self.call = call self.result = None self.error = None self.allow = {} #{'':['',...],...} self.deny = {} def check_acl(self, jid, resource): #Check for deny if jid in self.deny.keys(): if self.deny[jid] == None or resource in self.deny[jid]: return False #Check for allow if allow == None: return True if jid in self.allow.keys(): if self.allow[jid] == None or resource in self.allow[jid]: return True return False def acl_allow(self, jid, resource): if jid == None: self.allow = None elif resource == None: self.allow[jid] = None elif jid in self.allow.keys(): self.allow[jid].append(resource) else: self.allow[jid] = [resource] def acl_deny(self, jid, resource): if jid == None: self.deny = None elif resource == None: self.deny[jid] = None elif jid in self.deny.keys(): self.deny[jid].append(resource) else: self.deny[jid] = [resource] def call_method(self, args): ret = self.call(*args) class xep_0009(base.base_plugin): def plugin_init(self): self.xep = '0009' self.description = 'Jabber-RPC' self.xmpp.add_handler("", self._callMethod, name='Jabber RPC Call') self.xmpp.add_handler("", self._callResult, name='Jabber RPC Result') self.xmpp.add_handler("", self._callError, name='Jabber RPC Error') self.entries = {} 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('automatition','rpc') def register_call(self, method, name=None): #@returns an string that can be used in acl commands. with self.lock: if name is None: self.entries[method.__name__] = JabberRPCEntry(method) return method.__name__ else: self.entries[name] = JabberRPCEntry(method) return name def acl_allow(self, entry, jid=None, resource=None): #allow the method entry to be called by the given jid and resource. #if jid is None it will allow any jid/resource. #if resource is None it will allow any resource belonging to the jid. with self.lock: if self.entries[entry]: self.entries[entry].acl_allow(jid,resource) else: raise ValueError() def acl_deny(self, entry, jid=None, resource=None): #Note: by default all requests are denied unless allowed with acl_allow. #If you deny an entry it will not be allowed regardless of acl_allow with self.lock: if self.entries[entry]: self.entries[entry].acl_deny(jid,resource) else: raise ValueError() def unregister_call(self, entry): #removes the registered call with self.lock: if self.entries[entry]: del self.entries[entry] else: raise ValueError() def makeMethodCallQuery(self,pmethod,params): query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc") methodCall = ET.Element('methodCall') methodName = ET.Element('methodName') methodName.text = pmethod methodCall.append(methodName) methodCall.append(params) query.append(methodCall) return query def makeIqMethodCall(self,pto,pmethod,params): iq = self.xmpp.makeIqSet() iq.set('to',pto) iq.append(self.makeMethodCallQuery(pmethod,params)) return iq def makeIqMethodResponse(self,pto,pid,params): iq = self.xmpp.makeIqResult(pid) iq.set('to',pto) query = self.xmpp.makeIqQuery(iq,"jabber:iq:rpc") methodResponse = ET.Element('methodResponse') methodResponse.append(params) query.append(methodResponse) return iq def makeIqMethodError(self,pto,id,pmethod,params,condition): iq = self.xmpp.makeIqError(id) iq.set('to',pto) iq.append(self.makeMethodCallQuery(pmethod,params)) iq.append(self.xmpp['xep_0086'].makeError(condition)) return iq def call_remote(self, pto, pmethod, *args): #calls a remote method. Returns the id of the Iq. pass def _callMethod(self,xml): pass def _callResult(self,xml): pass def _callError(self,xml): pass fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/old_0050.py000066400000000000000000000127511157775340400225420ustar00rootroot00000000000000""" 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 __future__ import with_statement from . import base import logging from xml.etree import cElementTree as ET import time class old_0050(base.base_plugin): """ XEP-0050 Ad-Hoc Commands """ def plugin_init(self): self.xep = '0050' self.description = 'Ad-Hoc Commands' self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None') self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute') self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True) self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel') self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete') self.commands = {} self.sessions = {} 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): self.sd.add_item(None, name, 'http://jabber.org/protocol/commands', node) self.sd.add_identity('automation', 'command-node', name, node) self.sd.add_feature('http://jabber.org/protocol/commands', node) self.sd.add_feature('jabber:x:data', node) self.commands[node] = (name, form, pointer, multi) def getNewSession(self): return str(time.time()) + '-' + self.xmpp.getNewId() def handler_command(self, xml): in_command = xml.find('{http://jabber.org/protocol/commands}command') sessionid = in_command.get('sessionid', None) node = in_command.get('node') sessionid = self.getNewSession() name, form, pointer, multi = self.commands[node] self.sessions[sessionid] = {} self.sessions[sessionid]['jid'] = xml.get('from') self.sessions[sessionid]['to'] = xml.get('to') self.sessions[sessionid]['past'] = [(form, None)] self.sessions[sessionid]['next'] = pointer npointer = pointer if multi: actions = ['next'] status = 'executing' else: if pointer is None: status = 'completed' actions = [] else: status = 'executing' actions = ['complete'] self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions)) def handler_command_complete(self, xml): in_command = xml.find('{http://jabber.org/protocol/commands}command') sessionid = in_command.get('sessionid', None) pointer = self.sessions[sessionid]['next'] results = self.xmpp.plugin['old_0004'].makeForm('result') results.fromXML(in_command.find('{jabber:x:data}x')) pointer(results,sessionid) self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=None, id=xml.attrib['id'], sessionid=sessionid, status='completed', actions=[])) del self.sessions[in_command.get('sessionid')] def handler_command_next(self, xml): in_command = xml.find('{http://jabber.org/protocol/commands}command') sessionid = in_command.get('sessionid', None) pointer = self.sessions[sessionid]['next'] results = self.xmpp.plugin['old_0004'].makeForm('result') results.fromXML(in_command.find('{jabber:x:data}x')) form, npointer, next = pointer(results,sessionid) self.sessions[sessionid]['next'] = npointer self.sessions[sessionid]['past'].append((form, pointer)) actions = [] actions.append('prev') if npointer is None: status = 'completed' else: status = 'executing' if next: actions.append('next') else: actions.append('complete') self.xmpp.send(self.makeCommand(xml.attrib['from'], in_command.attrib['node'], form=form, id=xml.attrib['id'], sessionid=sessionid, status=status, actions=actions)) def handler_command_cancel(self, xml): command = xml.find('{http://jabber.org/protocol/commands}command') try: del self.sessions[command.get('sessionid')] except: pass self.xmpp.send(self.makeCommand(xml.attrib['from'], command.attrib['node'], id=xml.attrib['id'], sessionid=command.attrib['sessionid'], status='canceled')) def makeCommand(self, to, node, id=None, form=None, sessionid=None, status='executing', actions=[]): if not id: id = self.xmpp.getNewId() iq = self.xmpp.makeIqResult(id) iq.attrib['from'] = self.xmpp.boundjid.full iq.attrib['to'] = to command = ET.Element('{http://jabber.org/protocol/commands}command') command.attrib['node'] = node command.attrib['status'] = status xmlactions = ET.Element('actions') for action in actions: xmlactions.append(ET.Element(action)) if xmlactions: command.append(xmlactions) if not sessionid: sessionid = self.getNewSession() else: iq.attrib['from'] = self.sessions[sessionid]['to'] command.attrib['sessionid'] = sessionid if form is not None: if hasattr(form,'getXML'): form = form.getXML() command.append(form) iq.append(command) return iq fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/stanza_pubsub.py000066400000000000000000000331561157775340400242020ustar00rootroot00000000000000from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.iq import Iq from .. stanza.message import Message from .. basexmpp import basexmpp from .. xmlstream.xmlstream import XMLStream import logging from . import xep_0004 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) registerStanzaPlugin(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 = {} registerStanzaPlugin(Message, PubsubStateEvent) registerStanzaPlugin(PubsubStateEvent, PubsubState) class Pubsub(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'pubsub' plugin_attrib = 'pubsub' interfaces = set(tuple()) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(Iq, Pubsub) class PubsubOwner(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'pubsub' plugin_attrib = 'pubsub_owner' interfaces = set(tuple()) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(Iq, PubsubOwner) class Affiliation(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'affiliation' plugin_attrib = name interfaces = set(('node', 'affiliation')) plugin_attrib_map = {} plugin_tag_map = {} class Affiliations(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'affiliations' plugin_attrib = 'affiliations' interfaces = set(tuple()) plugin_attrib_map = {} plugin_tag_map = {} subitem = (Affiliation,) def append(self, affiliation): if not isinstance(affiliation, Affiliation): raise TypeError self.xml.append(affiliation.xml) return self.iterables.append(affiliation) registerStanzaPlugin(Pubsub, Affiliations) class Subscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscription' plugin_attrib = name interfaces = set(('jid', 'node', 'subscription', 'subid')) plugin_attrib_map = {} plugin_tag_map = {} def setjid(self, value): self._setattr('jid', str(value)) def getjid(self): return jid(self._getattr('jid')) registerStanzaPlugin(Pubsub, Subscription) class Subscriptions(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscriptions' plugin_attrib = 'subscriptions' interfaces = set(tuple()) plugin_attrib_map = {} plugin_tag_map = {} subitem = (Subscription,) registerStanzaPlugin(Pubsub, Subscriptions) class OptionalSetting(object): interfaces = set(('required',)) def setRequired(self, value): value = bool(value) if value and not self['required']: self.xml.append(ET.Element("{%s}required" % self.namespace)) elif not value and self['required']: self.delRequired() def getRequired(self): required = self.xml.find("{%s}required" % self.namespace) if required is not None: return True else: return False def delRequired(self): required = self.xml.find("{%s}required" % self.namespace) if required is not None: self.xml.remove(required) class SubscribeOptions(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscribe-options' plugin_attrib = 'suboptions' plugin_attrib_map = {} plugin_tag_map = {} interfaces = set(('required',)) registerStanzaPlugin(Subscription, SubscribeOptions) class Item(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'item' plugin_attrib = name interfaces = set(('id', '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) class Items(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'items' plugin_attrib = 'items' interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} subitem = (Item,) registerStanzaPlugin(Pubsub, Items) class Create(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'create' plugin_attrib = name interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(Pubsub, Create) #class Default(ElementBase): # namespace = 'http://jabber.org/protocol/pubsub' # name = 'default' # plugin_attrib = name # interfaces = set(('node', 'type')) # plugin_attrib_map = {} # plugin_tag_map = {} # # def getType(self): # t = self._getAttr('type') # if not t: t == 'leaf' # return t # #registerStanzaPlugin(Pubsub, Default) class Publish(Items): namespace = 'http://jabber.org/protocol/pubsub' name = 'publish' plugin_attrib = name interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} subitem = (Item,) registerStanzaPlugin(Pubsub, Publish) class Retract(Items): namespace = 'http://jabber.org/protocol/pubsub' name = 'retract' plugin_attrib = name interfaces = set(('node', 'notify')) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(Pubsub, Retract) class Unsubscribe(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'unsubscribe' plugin_attrib = name interfaces = set(('node', 'jid')) plugin_attrib_map = {} plugin_tag_map = {} def setJid(self, value): self._setAttr('jid', str(value)) def getJid(self): return JID(self._getAttr('jid')) registerStanzaPlugin(Pubsub, Unsubscribe) class Subscribe(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscribe' plugin_attrib = name interfaces = set(('node', 'jid')) plugin_attrib_map = {} plugin_tag_map = {} def setJid(self, value): self._setAttr('jid', str(value)) def getJid(self): return JID(self._getAttr('jid')) registerStanzaPlugin(Pubsub, Subscribe) class Configure(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'configure' plugin_attrib = name interfaces = set(('node', 'type')) plugin_attrib_map = {} plugin_tag_map = {} def getType(self): t = self._getAttr('type') if not t: t == 'leaf' return t registerStanzaPlugin(Pubsub, Configure) registerStanzaPlugin(Configure, xep_0004.Form) class DefaultConfig(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'default' plugin_attrib = 'default' interfaces = set(('node', 'type', 'config')) plugin_attrib_map = {} plugin_tag_map = {} def __init__(self, *args, **kwargs): ElementBase.__init__(self, *args, **kwargs) def getType(self): t = self._getAttr('type') if not t: t = 'leaf' return t def getConfig(self): return self['form'] def setConfig(self, value): self['form'].setStanzaValues(value.getStanzaValues()) return self registerStanzaPlugin(PubsubOwner, DefaultConfig) registerStanzaPlugin(DefaultConfig, xep_0004.Form) class Options(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'options' plugin_attrib = 'options' interfaces = set(('jid', 'node', 'options')) plugin_attrib_map = {} plugin_tag_map = {} def __init__(self, *args, **kwargs): ElementBase.__init__(self, *args, **kwargs) def getOptions(self): config = self.xml.find('{jabber:x:data}x') form = xep_0004.Form() if config is not None: form.fromXML(config) return form def setOptions(self, value): self.xml.append(value.getXML()) return self def delOptions(self): config = self.xml.find('{jabber:x:data}x') self.xml.remove(config) def setJid(self, value): self._setAttr('jid', str(value)) def getJid(self): return JID(self._getAttr('jid')) registerStanzaPlugin(Pubsub, Options) registerStanzaPlugin(Subscribe, Options) class OwnerAffiliations(Affiliations): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = set(('node')) plugin_attrib_map = {} plugin_tag_map = {} def append(self, affiliation): if not isinstance(affiliation, OwnerAffiliation): raise TypeError self.xml.append(affiliation.xml) return self.affiliations.append(affiliation) registerStanzaPlugin(PubsubOwner, OwnerAffiliations) class OwnerAffiliation(Affiliation): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = set(('affiliation', 'jid')) plugin_attrib_map = {} plugin_tag_map = {} class OwnerConfigure(Configure): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = set(('node', 'config')) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(PubsubOwner, OwnerConfigure) class OwnerDefault(OwnerConfigure): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = set(('node', 'config')) plugin_attrib_map = {} plugin_tag_map = {} def getConfig(self): return self['form'] def setConfig(self, value): self['form'].setStanzaValues(value.getStanzaValues()) return self registerStanzaPlugin(PubsubOwner, OwnerDefault) registerStanzaPlugin(OwnerDefault, xep_0004.Form) class OwnerDelete(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'delete' plugin_attrib = 'delete' plugin_attrib_map = {} plugin_tag_map = {} interfaces = set(('node',)) registerStanzaPlugin(PubsubOwner, OwnerDelete) class OwnerPurge(ElementBase, OptionalSetting): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'purge' plugin_attrib = name plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(PubsubOwner, OwnerPurge) class OwnerRedirect(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'redirect' plugin_attrib = name interfaces = set(('node', 'jid')) plugin_attrib_map = {} plugin_tag_map = {} def setJid(self, value): self._setAttr('jid', str(value)) def getJid(self): return JID(self._getAttr('jid')) registerStanzaPlugin(OwnerDelete, OwnerRedirect) class OwnerSubscriptions(Subscriptions): namespace = 'http://jabber.org/protocol/pubsub#owner' interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} def append(self, subscription): if not isinstance(subscription, OwnerSubscription): raise TypeError self.xml.append(subscription.xml) return self.subscriptions.append(subscription) registerStanzaPlugin(PubsubOwner, OwnerSubscriptions) class OwnerSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#owner' name = 'subscription' plugin_attrib = name interfaces = set(('jid', 'subscription')) plugin_attrib_map = {} plugin_tag_map = {} def setJid(self, value): self._setAttr('jid', str(value)) def getJid(self): return JID(self._getAttr('from')) class Event(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'event' plugin_attrib = 'pubsub_event' interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(Message, Event) class EventItem(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'item' plugin_attrib = 'item' interfaces = set(('id', '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) class EventRetract(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'retract' plugin_attrib = 'retract' interfaces = set(('id',)) plugin_attrib_map = {} plugin_tag_map = {} class EventItems(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'items' plugin_attrib = 'items' interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} subitem = (EventItem, EventRetract) registerStanzaPlugin(Event, EventItems) class EventCollection(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'collection' plugin_attrib = name interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(Event, EventCollection) class EventAssociate(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'associate' plugin_attrib = name interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(EventCollection, EventAssociate) class EventDisassociate(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'disassociate' plugin_attrib = name interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(EventCollection, EventDisassociate) class EventConfiguration(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'configuration' plugin_attrib = name interfaces = set(('node', 'config')) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(Event, EventConfiguration) registerStanzaPlugin(EventConfiguration, xep_0004.Form) class EventPurge(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'purge' plugin_attrib = name interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} registerStanzaPlugin(Event, EventPurge) class EventSubscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub#event' name = 'subscription' plugin_attrib = name interfaces = set(('node','expiry', 'jid', 'subid', 'subscription')) plugin_attrib_map = {} plugin_tag_map = {} def setJid(self, value): self._setAttr('jid', str(value)) def getJid(self): return JID(self._getAttr('jid')) registerStanzaPlugin(Event, EventSubscription) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0004.py000066400000000000000000000252111157775340400225520ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging import copy from . import base from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.message import Message log = logging.getLogger(__name__) class Form(ElementBase): namespace = 'jabber:x:data' name = 'x' plugin_attrib = 'form' interfaces = set(('fields', 'instructions', 'items', 'reported', 'title', 'type', 'values')) sub_interfaces = set(('title',)) form_types = set(('cancel', 'form', 'result', 'submit')) def __init__(self, *args, **kwargs): title = None if 'title' in kwargs: title = kwargs['title'] del kwargs['title'] ElementBase.__init__(self, *args, **kwargs) if title is not None: self['title'] = title self.field = FieldAccessor(self) def setup(self, xml=None): if ElementBase.setup(self, xml): #if we had to generate xml self['type'] = 'form' def addField(self, var='', ftype=None, label='', desc='', required=False, value=None, options=None, **kwargs): kwtype = kwargs.get('type', None) if kwtype is None: kwtype = ftype field = FormField(parent=self) field['var'] = var field['type'] = kwtype field['label'] = label field['desc'] = desc field['required'] = required field['value'] = value if options is not None: field['options'] = options 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 def fromXML(self, xml): log.warning("Form.fromXML() is deprecated API compatibility with plugins/old_0004.py") n = Form(xml=xml) return n def addItem(self, values): itemXML = ET.Element('{%s}item' % self.namespace) self.xml.append(itemXML) reported_vars = self['reported'].keys() for var in reported_vars: fieldXML = ET.Element('{%s}field' % FormField.namespace) itemXML.append(fieldXML) field = FormField(xml=fieldXML) field['var'] = var field['value'] = values.get(var, None) def addReported(self, var, ftype=None, label='', desc='', **kwargs): kwtype = kwargs.get('type', None) if kwtype is None: kwtype = ftype reported = self.xml.find('{%s}reported' % self.namespace) if reported is None: reported = ET.Element('{%s}reported' % self.namespace) self.xml.append(reported) fieldXML = ET.Element('{%s}field' % FormField.namespace) reported.append(fieldXML) field = FormField(xml=fieldXML) field['var'] = var field['type'] = kwtype field['label'] = label field['desc'] = desc return field def cancel(self): self['type'] = 'cancel' def delFields(self): fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) for fieldXML in fieldsXML: self.xml.remove(fieldXML) def delInstructions(self): instsXML = self.xml.findall('{%s}instructions') for instXML in instsXML: self.xml.remove(instXML) def delItems(self): itemsXML = self.xml.find('{%s}item' % self.namespace) for itemXML in itemsXML: self.xml.remove(itemXML) def delReported(self): reportedXML = self.xml.find('{%s}reported' % self.namespace) if reportedXML is not None: self.xml.remove(reportedXML) def getFields(self, use_dict=False): fields = {} if use_dict else [] fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) for fieldXML in fieldsXML: field = FormField(xml=fieldXML) if use_dict: fields[field['var']] = field else: fields.append((field['var'], field)) return fields def getInstructions(self): instructions = '' instsXML = self.xml.findall('{%s}instructions' % self.namespace) return "\n".join([instXML.text for instXML in instsXML]) def getItems(self): items = [] itemsXML = self.xml.findall('{%s}item' % self.namespace) for itemXML in itemsXML: item = {} fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) for fieldXML in fieldsXML: field = FormField(xml=fieldXML) item[field['var']] = field['value'] items.append(item) return items def getReported(self): fields = {} fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, FormField.namespace)) for fieldXML in fieldsXML: field = FormField(xml=fieldXML) fields[field['var']] = field return fields def getValues(self): values = {} fields = self.getFields(use_dict=True) for var in fields: values[var] = fields[var]['value'] return values def reply(self): if self['type'] == 'form': self['type'] = 'submit' elif self['type'] == 'submit': self['type'] = 'result' def setFields(self, fields, default=None): del self['fields'] for field_data in fields: var = field_data[0] field = field_data[1] field['var'] = var self.addField(**field) def setInstructions(self, instructions): del self['instructions'] if instructions in [None, '']: return instructions = instructions.split('\n') for instruction in instructions: inst = ET.Element('{%s}instructions' % self.namespace) inst.text = instruction self.xml.append(inst) def setItems(self, items): for item in items: self.addItem(item) def setReported(self, reported, default=None): for var in reported: field = reported[var] field['var'] = var self.addReported(var, **field) def setValues(self, values): fields = self.getFields(use_dict=True) for field in values: fields[field]['value'] = values[field] def merge(self, other): new = copy.copy(self) if type(other) == dict: new.setValues(other) return new nfields = new.getFields(use_dict=True) ofields = other.getFields(use_dict=True) nfields.update(ofields) new.setFields([(x, nfields[x]) for x in nfields]) return new class FieldAccessor(object): def __init__(self, form): self.form = form def __getitem__(self, key): return self.form.getFields(use_dict=True)[key] def __contains__(self, key): return key in self.form.getFields(use_dict=True) def has_key(self, key): return key in self.form.getFields(use_dict=True) class FormField(ElementBase): namespace = 'jabber:x:data' name = 'field' plugin_attrib = 'field' interfaces = set(('answer', 'desc', 'required', 'value', 'options', 'label', 'type', 'var')) sub_interfaces = set(('desc',)) field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single')) multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi')) multi_line_types = set(('hidden', 'text-multi')) option_types = set(('list-multi', 'list-single')) true_values = set((True, '1', 'true')) def addOption(self, label='', value=''): if self['type'] in self.option_types: opt = FieldOption(parent=self) opt['label'] = label opt['value'] = value else: raise ValueError("Cannot add options to a %s field." % self['type']) def delOptions(self): optsXML = self.xml.findall('{%s}option' % self.namespace) for optXML in optsXML: self.xml.remove(optXML) def delRequired(self): reqXML = self.xml.find('{%s}required' % self.namespace) if reqXML is not None: self.xml.remove(reqXML) def delValue(self): valsXML = self.xml.findall('{%s}value' % self.namespace) for valXML in valsXML: self.xml.remove(valXML) def getAnswer(self): return self.getValue() def getOptions(self): options = [] optsXML = self.xml.findall('{%s}option' % self.namespace) for optXML in optsXML: opt = FieldOption(xml=optXML) options.append({'label': opt['label'], 'value':opt['value']}) return options def getRequired(self): reqXML = self.xml.find('{%s}required' % self.namespace) return reqXML is not None def getValue(self): valsXML = self.xml.findall('{%s}value' % self.namespace) if len(valsXML) == 0: return None elif self['type'] == 'boolean': return valsXML[0].text in self.true_values elif self['type'] in self.multi_value_types: values = [] for valXML in valsXML: if valXML.text is None: valXML.text = '' values.append(valXML.text) if self['type'] == 'text-multi': values = "\n".join(values) return values else: return valsXML[0].text def setAnswer(self, answer): self.setValue(answer) def setFalse(self): self.setValue(False) def setOptions(self, options): for value in options: if isinstance(value, dict): self.addOption(**value) else: self.addOption(value=value) def setRequired(self, required): exists = self.getRequired() if not exists and required: self.xml.append(ET.Element('{%s}required' % self.namespace)) elif exists and not required: self.delRequired() def setTrue(self): self.setValue(True) def setValue(self, value): self.delValue() valXMLName = '{%s}value' % self.namespace if self['type'] == 'boolean': if value in self.true_values: valXML = ET.Element(valXMLName) valXML.text = '1' self.xml.append(valXML) else: valXML = ET.Element(valXMLName) valXML.text = '0' self.xml.append(valXML) elif self['type'] in self.multi_value_types or self['type'] in ['', None]: if self['type'] in self.multi_line_types and isinstance(value, str): value = value.split('\n') if not isinstance(value, list): value = [value] for val in value: if self['type'] in ['', None] and val in self.true_values: val = '1' valXML = ET.Element(valXMLName) valXML.text = val self.xml.append(valXML) else: if isinstance(value, list): raise ValueError("Cannot add multiple values to a %s field." % self['type']) valXML = ET.Element(valXMLName) valXML.text = value self.xml.append(valXML) class FieldOption(ElementBase): namespace = 'jabber:x:data' name = 'option' plugin_attrib = 'option' interfaces = set(('label', 'value')) sub_interfaces = set(('value',)) class xep_0004(base.base_plugin): """ XEP-0004: Data Forms """ def plugin_init(self): self.xep = '0004' self.description = 'Data Forms' self.xmpp.registerHandler( Callback('Data Form', MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns, Form.namespace)), self.handle_form)) registerStanzaPlugin(FormField, FieldOption) registerStanzaPlugin(Form, FormField) registerStanzaPlugin(Message, Form) def makeForm(self, ftype='form', title='', instructions=''): f = Form() f['type'] = ftype f['title'] = title f['instructions'] = instructions return f def post_init(self): base.base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') def handle_form(self, message): self.xmpp.event("message_xform", message) def buildForm(self, xml): return Form(xml=xml) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0009/000077500000000000000000000000001157775340400222045ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0009/__init__.py000066400000000000000000000005751157775340400243240ustar00rootroot00000000000000""" 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0009/binding.py000066400000000000000000000122241157775340400241710ustar00rootroot00000000000000""" 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) #... 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() fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0009/remote.py000066400000000000000000000612531157775340400240600ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Dann Martens (TOMOTON). This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from binding import py2xml, xml2py, xml2fault, fault2xml from threading import RLock import abc import inspect import logging import sleekxmpp import sys import threading import traceback log = logging.getLogger(__name__) def _intercept(method, name, public): def _resolver(instance, *args, **kwargs): log.debug("Locally calling %s.%s with arguments %s." % (instance.FQN(), method.__name__, args)) try: value = method(instance, *args, **kwargs) if value == NotImplemented: raise InvocationException("Local handler does not implement %s.%s!" % (instance.FQN(), method.__name__)) return value except InvocationException: raise except Exception as e: raise InvocationException("A problem occured calling %s.%s!" % (instance.FQN(), method.__name__), e) _resolver._rpc = public _resolver._rpc_name = method.__name__ if name is None else name return _resolver def remote(function_argument, public = True): ''' Decorator for methods which are remotely callable. This decorator works in conjunction with classes which extend ABC Endpoint. Example: @remote def remote_method(arg1, arg2) Arguments: function_argument -- a stand-in for either the actual method OR a new name (string) for the method. In that case the method is considered mapped: Example: @remote("new_name") def remote_method(arg1, arg2) public -- A flag which indicates if this method should be part of the known dictionary of remote methods. Defaults to True. Example: @remote(False) def remote_method(arg1, arg2) Note: renaming and revising (public vs. private) can be combined. Example: @remote("new_name", False) def remote_method(arg1, arg2) ''' if hasattr(function_argument, '__call__'): return _intercept(function_argument, None, public) else: if not isinstance(function_argument, basestring): if not isinstance(function_argument, bool): raise Exception('Expected an RPC method name or visibility modifier!') else: def _wrap_revised(function): function = _intercept(function, None, function_argument) return function return _wrap_revised def _wrap_remapped(function): function = _intercept(function, function_argument, public) return function return _wrap_remapped class ACL: ''' An Access Control List (ACL) is a list of rules, which are evaluated in order until a match is found. The policy of the matching rule is then applied. Rules are 3-tuples, consisting of a policy enumerated type, a JID expression and a RCP resource expression. Examples: [ (ACL.ALLOW, '*', '*') ] allow everyone everything, no restrictions [ (ACL.DENY, '*', '*') ] deny everyone everything, no restrictions [ (ACL.ALLOW, 'test@xmpp.org/unit', 'test.*'), (ACL.DENY, '*', '*') ] deny everyone everything, except named JID, which is allowed access to endpoint 'test' only. The use of wildcards is allowed in expressions, as follows: '*' everyone, or everything (= all endpoints and methods) 'test@xmpp.org/*' every JID regardless of JID resource '*@xmpp.org/rpc' every JID from domain xmpp.org with JID res 'rpc' 'frank@*' every 'frank', regardless of domain or JID res 'system.*' all methods of endpoint 'system' '*.reboot' all methods reboot regardless of endpoint ''' ALLOW = True DENY = False @classmethod def check(cls, rules, jid, resource): if rules is None: return cls.DENY # No rules means no access! for rule in rules: policy = cls._check(rule, jid, resource) if policy is not None: return policy return cls.DENY # By default if not rule matches, deny access. @classmethod def _check(cls, rule, jid, resource): if cls._match(jid, rule[1]) and cls._match(resource, rule[2]): return rule[0] else: return None @classmethod def _next_token(cls, expression, index): new_index = expression.find('*', index) if new_index == 0: return '' else: if new_index == -1: return expression[index : ] else: return expression[index : new_index] @classmethod def _match(cls, value, expression): #! print "_match [VALUE] %s [EXPR] %s" % (value, expression) index = 0 position = 0 while index < len(expression): token = cls._next_token(expression, index) #! print "[TOKEN] '%s'" % token size = len(token) if size > 0: token_index = value.find(token, position) if token_index == -1: return False else: #! print "[INDEX-OF] %s" % token_index position = token_index + len(token) pass if size == 0: index += 1 else: index += size #! print "index %s position %s" % (index, position) return True ANY_ALL = [ (ACL.ALLOW, '*', '*') ] class RemoteException(Exception): ''' Base exception for RPC. This exception is raised when a problem occurs in the network layer. ''' def __init__(self, message="", cause=None): ''' Initializes a new RemoteException. Arguments: message -- The message accompanying this exception. cause -- The underlying cause of this exception. ''' self._message = message self._cause = cause pass def __str__(self): return repr(self._message) def get_message(self): return self._message def get_cause(self): return self._cause class InvocationException(RemoteException): ''' Exception raised when a problem occurs during the remote invocation of a method. ''' pass class AuthorizationException(RemoteException): ''' Exception raised when the caller is not authorized to invoke the remote method. ''' pass class TimeoutException(Exception): ''' Exception raised when the synchronous execution of a method takes longer than the given threshold because an underlying asynchronous reply did not arrive in time. ''' pass class Callback(object): ''' A base class for callback handlers. ''' __metaclass__ = abc.ABCMeta @abc.abstractproperty def set_value(self, value): return NotImplemented @abc.abstractproperty def cancel_with_error(self, exception): return NotImplemented class Future(Callback): ''' Represents the result of an asynchronous computation. ''' def __init__(self): ''' Initializes a new Future. ''' self._value = None self._exception = None self._event = threading.Event() pass def set_value(self, value): ''' Sets the value of this Future. Once the value is set, a caller blocked on get_value will be able to continue. ''' self._value = value self._event.set() def get_value(self, timeout=None): ''' Gets the value of this Future. This call will block until the result is available, or until an optional timeout expires. When this Future is cancelled with an error, Arguments: timeout -- The maximum waiting time to obtain the value. ''' self._event.wait(timeout) if self._exception: raise self._exception if not self._event.is_set(): raise TimeoutException return self._value def is_done(self): ''' Returns true if a value has been returned. ''' return self._event.is_set() def cancel_with_error(self, exception): ''' Cancels the Future because of an error. Once cancelled, a caller blocked on get_value will be able to continue. ''' self._exception = exception self._event.set() class Endpoint(object): ''' The Endpoint class is an abstract base class for all objects participating in an RPC-enabled XMPP network. A user subclassing this class is required to implement the method: FQN(self) where FQN stands for Fully Qualified Name, an unambiguous name which specifies which object an RPC call refers to. It is the first part in a RPC method name '.'. ''' __metaclass__ = abc.ABCMeta def __init__(self, session, target_jid): ''' Initialize a new Endpoint. This constructor should never be invoked by a user, instead it will be called by the factories which instantiate the RPC-enabled objects, of which only the classes are provided by the user. Arguments: session -- An RPC session instance. target_jid -- the identity of the remote XMPP entity. ''' self.session = session self.target_jid = target_jid @abc.abstractproperty def FQN(self): return NotImplemented def get_methods(self): ''' Returns a dictionary of all RPC method names provided by this class. This method returns the actual method names as found in the class definition which have been decorated with: @remote def some_rpc_method(arg1, arg2) Unless: (1) the name has been remapped, in which case the new name will be returned. @remote("new_name") def some_rpc_method(arg1, arg2) (2) the method is set to hidden @remote(False) def some_hidden_method(arg1, arg2) ''' result = dict() for function in dir(self): test_attr = getattr(self, function, None) try: if test_attr._rpc: result[test_attr._rpc_name] = test_attr except Exception: pass return result class Proxy(Endpoint): ''' Implementation of the Proxy pattern which is intended to wrap around Endpoints in order to intercept calls, marshall them and forward them to the remote object. ''' def __init__(self, endpoint, callback = None): ''' Initializes a new Proxy. Arguments: endpoint -- The endpoint which is proxified. ''' self._endpoint = endpoint self._callback = callback def __getattribute__(self, name, *args): if name in ('__dict__', '_endpoint', 'async', '_callback'): return object.__getattribute__(self, name) else: attribute = self._endpoint.__getattribute__(name) if hasattr(attribute, '__call__'): try: if attribute._rpc: def _remote_call(*args, **kwargs): log.debug("Remotely calling '%s.%s' with arguments %s." % (self._endpoint.FQN(), attribute._rpc_name, args)) return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs) return _remote_call except: pass # If the attribute doesn't exist, don't care! return attribute def async(self, callback): return Proxy(self._endpoint, callback) def get_endpoint(self): ''' Returns the proxified endpoint. ''' return self._endpoint def FQN(self): return self._endpoint.FQN() class JabberRPCEntry(object): def __init__(self, endpoint_FQN, call): self._endpoint_FQN = endpoint_FQN self._call = call def call_method(self, args): return_value = self._call(*args) if return_value is None: return return_value else: return self._return(return_value) def get_endpoint_FQN(self): return self._endpoint_FQN def _return(self, *args): return args class RemoteSession(object): ''' A context object for a Jabber-RPC session. ''' def __init__(self, client, session_close_callback): ''' Initializes a new RPC session. Arguments: client -- The SleekXMPP client associated with this session. session_close_callback -- A callback called when the session is closed. ''' self._client = client self._session_close_callback = session_close_callback self._event = threading.Event() self._entries = {} self._callbacks = {} self._acls = {} self._lock = RLock() def _wait(self): self._event.wait() def _notify(self, event): log.debug("RPC Session as %s started." % self._client.boundjid.full) self._client.sendPresence() self._event.set() pass def _register_call(self, endpoint, method, name=None): ''' Registers a method from an endpoint as remotely callable. ''' if name is None: name = method.__name__ key = "%s.%s" % (endpoint, name) log.debug("Registering call handler for %s (%s)." % (key, method)) with self._lock: if self._entries.has_key(key): raise KeyError("A handler for %s has already been regisered!" % endpoint) self._entries[key] = JabberRPCEntry(endpoint, method) return key def _register_acl(self, endpoint, acl): log.debug("Registering ACL %s for endpoint %s." % (repr(acl), endpoint)) with self._lock: self._acls[endpoint] = acl def _register_callback(self, pid, callback): with self._lock: self._callbacks[pid] = callback def forget_callback(self, callback): with self._lock: pid = self._find_key(self._callbacks, callback) if pid is not None: del self._callback[pid] else: raise ValueError("Unknown callback!") pass def _find_key(self, dict, value): """return the key of dictionary dic given the value""" search = [k for k, v in dict.iteritems() if v == value] if len(search) == 0: return None else: return search[0] def _unregister_call(self, key): #removes the registered call with self._lock: if self._entries[key]: del self._entries[key] else: raise ValueError() def new_proxy(self, target_jid, endpoint_cls): ''' Instantiates a new proxy object, which proxies to a remote endpoint. This method uses a class reference without constructor arguments to instantiate the proxy. Arguments: target_jid -- the XMPP entity ID hosting the endpoint. endpoint_cls -- The remote (duck) type. ''' try: argspec = inspect.getargspec(endpoint_cls.__init__) args = [None] * (len(argspec[0]) - 1) result = endpoint_cls(*args) Endpoint.__init__(result, self, target_jid) return Proxy(result) except: traceback.print_exc(file=sys.stdout) def new_handler(self, acl, handler_cls, *args, **kwargs): ''' Instantiates a new handler object, which is called remotely by others. The user can control the effect of the call by implementing the remote method in the local endpoint class. The returned reference can be called locally and will behave as a regular instance. Arguments: acl -- Access control list (see ACL class) handler_clss -- The local (duck) type. *args -- Constructor arguments for the local type. **kwargs -- Constructor keyworded arguments for the local type. ''' argspec = inspect.getargspec(handler_cls.__init__) base_argspec = inspect.getargspec(Endpoint.__init__) if(argspec == base_argspec): result = handler_cls(self, self._client.boundjid.full) else: result = handler_cls(*args, **kwargs) Endpoint.__init__(result, self, self._client.boundjid.full) method_dict = result.get_methods() for method_name, method in method_dict.iteritems(): #!!! self._client.plugin['xep_0009'].register_call(result.FQN(), method, method_name) self._register_call(result.FQN(), method, method_name) self._register_acl(result.FQN(), acl) return result # def is_available(self, targetCls, pto): # return self._client.is_available(pto) def _call_remote(self, pto, pmethod, callback, *arguments): iq = self._client.plugin['xep_0009'].make_iq_method_call(pto, pmethod, py2xml(*arguments)) pid = iq['id'] if callback is None: future = Future() self._register_callback(pid, future) iq.send() return future.get_value(30) else: 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) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0009/rpc.py000066400000000000000000000213621157775340400233460ustar00rootroot00000000000000""" 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0009/stanza/000077500000000000000000000000001157775340400235045ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0009/stanza/RPC.py000066400000000000000000000031471157775340400245070ustar00rootroot00000000000000""" 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) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0009/stanza/__init__.py000066400000000000000000000004371157775340400256210ustar00rootroot00000000000000""" 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0012.py000066400000000000000000000100531157775340400225470ustar00rootroot00000000000000""" 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 datetime import datetime import logging from . import base from .. stanza.iq import Iq from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin log = logging.getLogger(__name__) class LastActivity(ElementBase): name = 'query' namespace = 'jabber:iq:last' plugin_attrib = 'last_activity' interfaces = set(('seconds', 'status')) def get_seconds(self): return int(self._get_attr('seconds')) def set_seconds(self, value): self._set_attr('seconds', str(value)) def get_status(self): return self.xml.text def set_status(self, value): self.xml.text = str(value) def del_status(self): self.xml.text = '' class xep_0012(base.base_plugin): """ XEP-0012 Last Activity """ def plugin_init(self): self.description = "Last Activity" self.xep = "0012" self.xmpp.registerHandler( Callback('Last Activity', MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns, LastActivity.namespace)), self.handle_last_activity_query)) register_stanza_plugin(Iq, LastActivity) self.xmpp.add_event_handler('last_activity_request', self.handle_last_activity) def post_init(self): base.base_plugin.post_init(self) if self.xmpp.is_component: # We are a component, so we track the uptime self.xmpp.add_event_handler("session_start", self._reset_uptime) self._start_datetime = datetime.now() self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:last') def _reset_uptime(self, event): self._start_datetime = datetime.now() def handle_last_activity_query(self, iq): if iq['type'] == 'get': log.debug("Last activity requested by %s" % iq['from']) self.xmpp.event('last_activity_request', iq) elif iq['type'] == 'result': log.debug("Last activity result from %s" % iq['from']) self.xmpp.event('last_activity', iq) def handle_last_activity(self, iq): jid = iq['from'] if self.xmpp.is_component: # Send the uptime result = LastActivity() td = (datetime.now() - self._start_datetime) result['seconds'] = td.seconds + td.days * 24 * 3600 reply = iq.reply().setPayload(result.xml).send() else: barejid = JID(jid).bare if barejid in self.xmpp.roster and ( self.xmpp.roster[barejid]['subscription'] in ('from', 'both') or barejid == self.xmpp.boundjid.bare ): # We don't know how to calculate it iq.reply().error().setPayload(iq['last_activity'].xml) iq['error']['code'] = '503' iq['error']['type'] = 'cancel' iq['error']['condition'] = 'service-unavailable' iq.send() else: iq.reply().error().setPayload(iq['last_activity'].xml) iq['error']['code'] = '403' iq['error']['type'] = 'auth' iq['error']['condition'] = 'forbidden' iq.send() def get_last_activity(self, jid): """Query the LastActivity of jid and return it in seconds""" iq = self.xmpp.makeIqGet() query = LastActivity() iq.append(query.xml) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq.get('id') result = iq.send() if result and result is not None and result.get('type', 'error') != 'error': return result['last_activity']['seconds'] else: return False fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0030/000077500000000000000000000000001157775340400221765ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0030/__init__.py000066400000000000000000000006431157775340400243120ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.plugins.xep_0030 import stanza from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems from sleekxmpp.plugins.xep_0030.static import StaticDisco from sleekxmpp.plugins.xep_0030.disco import xep_0030 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0030/disco.py000066400000000000000000000577001157775340400236620ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging import sleekxmpp from sleekxmpp import Iq from sleekxmpp.exceptions import XMPPError from sleekxmpp.plugins.base import base_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco log = logging.getLogger(__name__) class xep_0030(base_plugin): """ XEP-0030: Service Discovery Service discovery in XMPP allows entities to discover information about other agents in the network, such as the feature sets supported by a client, or signposts to other, related entities. Also see . The XEP-0030 plugin works using a hierarchy of dynamic node handlers, ranging from global handlers to specific JID+node handlers. The default set of handlers operate in a static manner, storing disco information in memory. However, custom handlers may use any available backend storage mechanism desired, such as SQLite or Redis. Node handler hierarchy: JID | Node | Level --------------------- None | None | Global Given | None | All nodes for the JID None | Given | Node on self.xmpp.boundjid Given | Given | A single node Stream Handlers: Disco Info -- Any Iq stanze that includes a query with the namespace http://jabber.org/protocol/disco#info. Disco Items -- Any Iq stanze that includes a query with the namespace http://jabber.org/protocol/disco#items. Events: disco_info -- Received a disco#info Iq query result. disco_items -- Received a disco#items Iq query result. disco_info_query -- Received a disco#info Iq query request. disco_items_query -- Received a disco#items Iq query request. Attributes: stanza -- A reference to the module containing the stanza classes provided by this plugin. static -- Object containing the default set of static node handlers. default_handlers -- A dictionary mapping operations to the default global handler (by default, the static handlers). xmpp -- The main SleekXMPP object. Methods: set_node_handler -- Assign a handler to a JID/node combination. del_node_handler -- Remove a handler from a JID/node combination. get_info -- Retrieve disco#info data, locally or remote. get_items -- Retrieve disco#items data, locally or remote. set_identities -- set_features -- set_items -- del_items -- del_identity -- del_feature -- del_item -- add_identity -- add_feature -- add_item -- """ def plugin_init(self): """ Start the XEP-0030 plugin. """ self.xep = '0030' self.description = 'Service Discovery' self.stanza = sleekxmpp.plugins.xep_0030.stanza self.xmpp.register_handler( Callback('Disco Info', StanzaPath('iq/disco_info'), self._handle_disco_info)) self.xmpp.register_handler( Callback('Disco Items', StanzaPath('iq/disco_items'), self._handle_disco_items)) register_stanza_plugin(Iq, DiscoInfo) register_stanza_plugin(Iq, DiscoItems) self.static = StaticDisco(self.xmpp) self._disco_ops = ['get_info', 'set_identities', 'set_features', 'get_items', 'set_items', 'del_items', 'add_identity', 'del_identity', 'add_feature', 'del_feature', 'add_item', 'del_item', 'del_identities', 'del_features'] self.default_handlers = {} self._handlers = {} for op in self._disco_ops: self._add_disco_op(op, getattr(self.static, op)) def post_init(self): """Handle cross-plugin dependencies.""" base_plugin.post_init(self) if 'xep_0059' in self.xmpp.plugin: register_stanza_plugin(DiscoItems, self.xmpp['xep_0059'].stanza.Set) def _add_disco_op(self, op, default_handler): self.default_handlers[op] = default_handler self._handlers[op] = {'global': default_handler, 'jid': {}, 'node': {}} def set_node_handler(self, htype, jid=None, node=None, handler=None): """ Add a node handler for the given hierarchy level and handler type. Node handlers are ordered in a hierarchy where the most specific handler is executed. Thus, a fallback, global handler can be used for the majority of cases with a few node specific handler that override the global behavior. Node handler hierarchy: JID | Node | Level --------------------- None | None | Global Given | None | All nodes for the JID None | Given | Node on self.xmpp.boundjid Given | Given | A single node Handler types: get_info get_items set_identities set_features set_items del_items del_identities del_identity del_feature del_features del_item add_identity add_feature add_item Arguments: htype -- The operation provided by the handler. jid -- The JID the handler applies to. May be narrowed further if a node is given. node -- The particular node the handler is for. If no JID is given, then the self.xmpp.boundjid.full is assumed. handler -- The handler function to use. """ if htype not in self._disco_ops: return if jid is None and node is None: self._handlers[htype]['global'] = handler elif node is None: self._handlers[htype]['jid'][jid] = handler elif jid is None: if self.xmpp.is_component: jid = self.xmpp.boundjid.full else: jid = self.xmpp.boundjid.bare self._handlers[htype]['node'][(jid, node)] = handler else: self._handlers[htype]['node'][(jid, node)] = handler def del_node_handler(self, htype, jid, node): """ Remove a handler type for a JID and node combination. The next handler in the hierarchy will be used if one exists. If removing the global handler, make sure that other handlers exist to process existing nodes. Node handler hierarchy: JID | Node | Level --------------------- None | None | Global Given | None | All nodes for the JID None | Given | Node on self.xmpp.boundjid Given | Given | A single node Arguments: htype -- The type of handler to remove. jid -- The JID from which to remove the handler. node -- The node from which to remove the handler. """ self.set_node_handler(htype, jid, node, None) def restore_defaults(self, jid=None, node=None, handlers=None): """ Change all or some of a node's handlers to the default handlers. Useful for manually overriding the contents of a node that would otherwise be handled by a JID level or global level dynamic handler. The default is to use the built-in static handlers, but that may be changed by modifying self.default_handlers. Arguments: jid -- The JID owning the node to modify. node -- The node to change to using static handlers. handlers -- Optional list of handlers to change to the default version. If provided, only these handlers will be changed. Otherwise, all handlers will use the default version. """ if handlers is None: handlers = self._disco_ops for op in handlers: self.del_node_handler(op, jid, node) self.set_node_handler(op, jid, node, self.default_handlers[op]) def get_info(self, jid=None, node=None, local=False, **kwargs): """ Retrieve the disco#info results from a given JID/node combination. Info may be retrieved from both local resources and remote agents; the local parameter indicates if the information should be gathered by executing the local node handlers, or if a disco#info stanza must be generated and sent. If requesting items from a local JID/node, then only a DiscoInfo stanza will be returned. Otherwise, an Iq stanza will be returned. Arguments: jid -- Request info from this JID. node -- The particular node to query. local -- If true, then the query is for a JID/node combination handled by this Sleek instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the info. ifrom -- Specifiy the sender's JID. block -- If true, block and wait for the stanzas' reply. timeout -- The time in seconds to block while waiting for a reply. If None, then wait indefinitely. The timeout value is only used when block=True. callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. """ if local or jid is None: log.debug("Looking up local disco#info data " + \ "for %s, node %s." % (jid, node)) info = self._run_node_handler('get_info', jid, node, kwargs) return self._fix_default_info(info) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', '')) iq['to'] = jid iq['type'] = 'get' iq['disco_info']['node'] = node if node else '' return iq.send(timeout=kwargs.get('timeout', None), block=kwargs.get('block', True), callback=kwargs.get('callback', None)) def get_items(self, jid=None, node=None, local=False, **kwargs): """ Retrieve the disco#items results from a given JID/node combination. Items may be retrieved from both local resources and remote agents; the local parameter indicates if the items should be gathered by executing the local node handlers, or if a disco#items stanza must be generated and sent. If requesting items from a local JID/node, then only a DiscoItems stanza will be returned. Otherwise, an Iq stanza will be returned. Arguments: jid -- Request info from this JID. node -- The particular node to query. local -- If true, then the query is for a JID/node combination handled by this Sleek instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the items. ifrom -- Specifiy the sender's JID. block -- If true, block and wait for the stanzas' reply. timeout -- The time in seconds to block while waiting for a reply. If None, then wait indefinitely. callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. iterator -- If True, return a result set iterator using the XEP-0059 plugin, if the plugin is loaded. Otherwise the parameter is ignored. """ if local or jid is None: return self._run_node_handler('get_items', jid, node, kwargs) iq = self.xmpp.Iq() # Check dfrom parameter for backwards compatibility iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', '')) iq['to'] = jid iq['type'] = 'get' iq['disco_items']['node'] = node if node else '' if kwargs.get('iterator', False) and self.xmpp['xep_0059']: return self.xmpp['xep_0059'].iterate(iq, 'disco_items') else: return iq.send(timeout=kwargs.get('timeout', None), block=kwargs.get('block', True), callback=kwargs.get('callback', None)) def set_items(self, jid=None, node=None, **kwargs): """ Set or replace all items for the specified JID/node combination. The given items must be in a list or set where each item is a tuple of the form: (jid, node, name). Arguments: jid -- The JID to modify. node -- Optional node to modify. items -- A series of items in tuple format. """ self._run_node_handler('set_items', jid, node, kwargs) def del_items(self, jid=None, node=None, **kwargs): """ Remove all items from the given JID/node combination. Arguments: jid -- The JID to modify. node -- Optional node to modify. """ self._run_node_handler('del_items', jid, node, kwargs) def add_item(self, jid='', name='', node=None, subnode='', ijid=None): """ Add a new item element to the given JID/node combination. Each item is required to have a JID, but may also specify a node value to reference non-addressable entities. Arguments: jid -- The JID for the item. name -- Optional name for the item. node -- The node to modify. subnode -- Optional node for the item. ijid -- The JID to modify. """ if not jid: jid = self.xmpp.boundjid.full kwargs = {'ijid': jid, 'name': name, 'inode': subnode} self._run_node_handler('add_item', ijid, node, kwargs) def del_item(self, jid=None, node=None, **kwargs): """ Remove a single item from the given JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. ijid -- The item's JID. inode -- The item's node. """ self._run_node_handler('del_item', jid, node, kwargs) def add_identity(self, category='', itype='', name='', node=None, jid=None, lang=None): """ Add a new identity to the given JID/node combination. Each identity must be unique in terms of all four identity components: category, type, name, and language. Multiple, identical category/type pairs are allowed only if the xml:lang values are different. Likewise, multiple category/type/xml:lang pairs are allowed so long as the names are different. A category and type is always required. Arguments: category -- The identity's category. itype -- The identity's type. name -- Optional name for the identity. lang -- Optional two-letter language code. node -- The node to modify. jid -- The JID to modify. """ kwargs = {'category': category, 'itype': itype, 'name': name, 'lang': lang} self._run_node_handler('add_identity', jid, node, kwargs) def add_feature(self, feature, node=None, jid=None): """ Add a feature to a JID/node combination. Arguments: feature -- The namespace of the supported feature. node -- The node to modify. jid -- The JID to modify. """ kwargs = {'feature': feature} self._run_node_handler('add_feature', jid, node, kwargs) def del_identity(self, jid=None, node=None, **kwargs): """ Remove an identity from the given JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. category -- The identity's category. itype -- The identity's type value. name -- Optional, human readable name for the identity. lang -- Optional, the identity's xml:lang value. """ self._run_node_handler('del_identity', jid, node, kwargs) def del_feature(self, jid=None, node=None, **kwargs): """ Remove a feature from a given JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. feature -- The feature's namespace. """ self._run_node_handler('del_feature', jid, node, kwargs) def set_identities(self, jid=None, node=None, **kwargs): """ Add or replace all identities for the given JID/node combination. The identities must be in a set where each identity is a tuple of the form: (category, type, lang, name) Arguments: jid -- The JID to modify. node -- The node to modify. identities -- A set of identities in tuple form. lang -- Optional, xml:lang value. """ self._run_node_handler('set_identities', jid, node, kwargs) def del_identities(self, jid=None, node=None, **kwargs): """ Remove all identities for a JID/node combination. If a language is specified, only identities using that language will be removed. Arguments: jid -- The JID to modify. node -- The node to modify. lang -- Optional. If given, only remove identities using this xml:lang value. """ self._run_node_handler('del_identities', jid, node, kwargs) def set_features(self, jid=None, node=None, **kwargs): """ Add or replace the set of supported features for a JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. features -- The new set of supported features. """ self._run_node_handler('set_features', jid, node, kwargs) def del_features(self, jid=None, node=None, **kwargs): """ Remove all features from a JID/node combination. Arguments: jid -- The JID to modify. node -- The node to modify. """ self._run_node_handler('del_features', jid, node, kwargs) def _run_node_handler(self, htype, jid, node, data={}): """ Execute the most specific node handler for the given JID/node combination. Arguments: htype -- The handler type to execute. jid -- The JID requested. node -- The node requested. data -- Optional, custom data to pass to the handler. """ if jid is None: if self.xmpp.is_component: jid = self.xmpp.boundjid.full else: jid = self.xmpp.boundjid.bare if node is None: node = '' if self._handlers[htype]['node'].get((jid, node), False): return self._handlers[htype]['node'][(jid, node)](jid, node, data) elif self._handlers[htype]['jid'].get(jid, False): return self._handlers[htype]['jid'][jid](jid, node, data) elif self._handlers[htype]['global']: return self._handlers[htype]['global'](jid, node, data) else: return None def _handle_disco_info(self, iq): """ Process an incoming disco#info stanza. If it is a get request, find and return the appropriate identities and features. If it is an info result, fire the disco_info event. Arguments: iq -- The incoming disco#items stanza. """ if iq['type'] == 'get': log.debug("Received disco info query from " + \ "<%s> to <%s>." % (iq['from'], iq['to'])) if self.xmpp.is_component: jid = iq['to'].full else: jid = iq['to'].bare info = self._run_node_handler('get_info', jid, iq['disco_info']['node'], iq) iq.reply() if info: info = self._fix_default_info(info) iq.set_payload(info.xml) iq.send() elif iq['type'] == 'result': log.debug("Received disco info result from" + \ "%s to %s." % (iq['from'], iq['to'])) self.xmpp.event('disco_info', iq) def _handle_disco_items(self, iq): """ Process an incoming disco#items stanza. If it is a get request, find and return the appropriate items. If it is an items result, fire the disco_items event. Arguments: iq -- The incoming disco#items stanza. """ if iq['type'] == 'get': log.debug("Received disco items query from " + \ "<%s> to <%s>." % (iq['from'], iq['to'])) if self.xmpp.is_component: jid = iq['to'].full else: jid = iq['to'].bare items = self._run_node_handler('get_items', jid, iq['disco_items']['node']) iq.reply() if items: iq.set_payload(items.xml) iq.send() elif iq['type'] == 'result': log.debug("Received disco items result from" + \ "%s to %s." % (iq['from'], iq['to'])) self.xmpp.event('disco_items', iq) def _fix_default_info(self, info): """ Disco#info results for a JID are required to include at least one identity and feature. As a default, if no other identity is provided, SleekXMPP will use either the generic component or the bot client identity. A the standard disco#info feature will also be added if no features are provided. Arguments: info -- The disco#info quest (not the full Iq stanza) to modify. """ if not info['node']: if not info['identities']: if self.xmpp.is_component: log.debug("No identity found for this entity." + \ "Using default component identity.") info.add_identity('component', 'generic') else: log.debug("No identity found for this entity." + \ "Using default client identity.") info.add_identity('client', 'bot') if not info['features']: log.debug("No features found for this entity." + \ "Using default disco#info feature.") 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 xep_0030.make_static = xep_0030.restore_defaults fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0030/stanza/000077500000000000000000000000001157775340400234765ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0030/stanza/__init__.py000066400000000000000000000004751157775340400256150ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.plugins.xep_0030.stanza.info import DiscoInfo from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0030/stanza/info.py000066400000000000000000000232771157775340400250160ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.xmlstream import ElementBase, ET class DiscoInfo(ElementBase): """ XMPP allows for users and agents to find the identities and features supported by other entities in the XMPP network through service discovery, or "disco". In particular, the "disco#info" query type for stanzas is used to request the list of identities and features offered by a JID. An identity is a combination of a category and type, such as the 'client' category with a type of 'pc' to indicate the agent is a human operated client with a GUI, or a category of 'gateway' with a type of 'aim' to identify the agent as a gateway for the legacy AIM protocol. See for a full list of accepted category and type combinations. Features are simply a set of the namespaces that identify the supported features. For example, a client that supports service discovery will include the feature 'http://jabber.org/protocol/disco#info'. Since clients and components may operate in several roles at once, identity and feature information may be grouped into "nodes". If one were to write all of the identities and features used by a client, then node names would be like section headings. Example disco#info stanzas: Stanza Interface: node -- The name of the node to either query or return info from. identities -- A set of 4-tuples, where each tuple contains the category, type, xml:lang, and name of an identity. features -- A set of namespaces for features. Methods: add_identity -- Add a new, single identity. del_identity -- Remove a single identity. get_identities -- Return all identities in tuple form. set_identities -- Use multiple identities, each given in tuple form. del_identities -- Remove all identities. add_feature -- Add a single feature. del_feature -- Remove a single feature. get_features -- Return a list of all features. set_features -- Use a given list of features. del_features -- Remove all features. """ name = 'query' namespace = 'http://jabber.org/protocol/disco#info' plugin_attrib = 'disco_info' interfaces = set(('node', 'features', 'identities')) lang_interfaces = set(('identities',)) # Cache identities and features _identities = set() _features = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches identity and feature information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._identities = set([id[0:3] for id in self['identities']]) self._features = self['features'] def add_identity(self, category, itype, name=None, lang=None): """ Add a new identity element. Each identity must be unique in terms of all four identity components. Multiple, identical category/type pairs are allowed only if the xml:lang values are different. Likewise, multiple category/type/xml:lang pairs are allowed so long as the names are different. In any case, a category and type are required. Arguments: category -- The general category to which the agent belongs. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional standard xml:lang value. """ identity = (category, itype, lang) if identity not in self._identities: self._identities.add(identity) id_xml = ET.Element('{%s}identity' % self.namespace) id_xml.attrib['category'] = category id_xml.attrib['type'] = itype if lang: id_xml.attrib['{%s}lang' % self.xml_ns] = lang if name: id_xml.attrib['name'] = name self.xml.append(id_xml) return True return False def del_identity(self, category, itype, name=None, lang=None): """ Remove a given identity. Arguments: category -- The general category to which the agent belonged. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional, standard xml:lang value. """ identity = (category, itype, lang) if identity in self._identities: self._identities.remove(identity) for id_xml in self.findall('{%s}identity' % self.namespace): id = (id_xml.attrib['category'], id_xml.attrib['type'], id_xml.attrib.get('{%s}lang' % self.xml_ns, None)) if id == identity: self.xml.remove(id_xml) return True return False def get_identities(self, lang=None): """ Return a set of all identities in tuple form as so: (category, type, lang, name) If a language was specified, only return identities using that language. Arguments: lang -- Optional, standard xml:lang value. """ identities = set() for id_xml in self.findall('{%s}identity' % self.namespace): xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None) if lang is None or xml_lang == lang: identities.add(( id_xml.attrib['category'], id_xml.attrib['type'], id_xml.attrib.get('{%s}lang' % self.xml_ns, None), id_xml.attrib.get('name', None))) return identities def set_identities(self, identities, lang=None): """ Add or replace all identities. The identities must be a in set where each identity is a tuple of the form: (category, type, lang, name) If a language is specifified, any identities using that language will be removed to be replaced with the given identities. NOTE: An identity's language will not be changed regardless of the value of lang. Arguments: identities -- A set of identities in tuple form. lang -- Optional, standard xml:lang value. """ self.del_identities(lang) for identity in identities: category, itype, lang, name = identity self.add_identity(category, itype, name, lang) def del_identities(self, lang=None): """ Remove all identities. If a language was specified, only remove identities using that language. Arguments: lang -- Optional, standard xml:lang value. """ for id_xml in self.findall('{%s}identity' % self.namespace): if lang is None: self.xml.remove(id_xml) elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang: self._identities.remove(( id_xml.attrib['category'], id_xml.attrib['type'], id_xml.attrib.get('{%s}lang' % self.xml_ns, None))) self.xml.remove(id_xml) def add_feature(self, feature): """ Add a single, new feature. Arguments: feature -- The namespace of the supported feature. """ if feature not in self._features: self._features.add(feature) feature_xml = ET.Element('{%s}feature' % self.namespace) feature_xml.attrib['var'] = feature self.xml.append(feature_xml) return True return False def del_feature(self, feature): """ Remove a single feature. Arguments: feature -- The namespace of the removed feature. """ if feature in self._features: self._features.remove(feature) for feature_xml in self.findall('{%s}feature' % self.namespace): if feature_xml.attrib['var'] == feature: self.xml.remove(feature_xml) return True return False def get_features(self): """Return the set of all supported features.""" features = set() for feature_xml in self.findall('{%s}feature' % self.namespace): features.add(feature_xml.attrib['var']) return features def set_features(self, features): """ Add or replace the set of supported features. Arguments: features -- The new set of supported features. """ self.del_features() for feature in features: self.add_feature(feature) def del_features(self): """Remove all features.""" self._features = set() for feature_xml in self.findall('{%s}feature' % self.namespace): self.xml.remove(feature_xml) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0030/stanza/items.py000066400000000000000000000101651157775340400251740ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.xmlstream import ElementBase, ET class DiscoItems(ElementBase): """ Example disco#items stanzas: Stanza Interface: node -- The name of the node to either query or return info from. items -- A list of 3-tuples, where each tuple contains the JID, node, and name of an item. Methods: add_item -- Add a single new item. del_item -- Remove a single item. get_items -- Return all items. set_items -- Set or replace all items. del_items -- Remove all items. """ name = 'query' namespace = 'http://jabber.org/protocol/disco#items' plugin_attrib = 'disco_items' interfaces = set(('node', 'items')) # Cache items _items = set() def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup Caches item information. Arguments: xml -- Use an existing XML object for the stanza's values. """ ElementBase.setup(self, xml) self._items = set([item[0:2] for item in self['items']]) def add_item(self, jid, node=None, name=None): """ Add a new item element. Each item is required to have a JID, but may also specify a node value to reference non-addressable entitities. Arguments: jid -- The JID for the item. node -- Optional additional information to reference non-addressable items. name -- Optional human readable name for the item. """ if (jid, node) not in self._items: self._items.add((jid, node)) item_xml = ET.Element('{%s}item' % self.namespace) item_xml.attrib['jid'] = jid if name: item_xml.attrib['name'] = name if node: item_xml.attrib['node'] = node self.xml.append(item_xml) return True return False def del_item(self, jid, node=None): """ Remove a single item. Arguments: jid -- JID of the item to remove. node -- Optional extra identifying information. """ if (jid, node) in self._items: for item_xml in self.findall('{%s}item' % self.namespace): item = (item_xml.attrib['jid'], item_xml.attrib.get('node', None)) if item == (jid, node): self.xml.remove(item_xml) return True return False def get_items(self): """Return all items.""" items = set() for item_xml in self.findall('{%s}item' % self.namespace): item = (item_xml.attrib['jid'], item_xml.attrib.get('node'), item_xml.attrib.get('name')) items.add(item) return items def set_items(self, items): """ Set or replace all items. The given items must be in a list or set where each item is a tuple of the form: (jid, node, name) Arguments: items -- A series of items in tuple format. """ self.del_items() for item in items: jid, node, name = item self.add_item(jid, node, name) def del_items(self): """Remove all items.""" self._items = set() for item_xml in self.findall('{%s}item' % self.namespace): self.xml.remove(item_xml) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0030/static.py000066400000000000000000000214511157775340400240420ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging import sleekxmpp from sleekxmpp import Iq from sleekxmpp.exceptions import XMPPError from sleekxmpp.plugins.base import base_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems log = logging.getLogger(__name__) class StaticDisco(object): """ While components will likely require fully dynamic handling of service discovery information, most clients and simple bots only need to manage a few disco nodes that will remain mostly static. StaticDisco provides a set of node handlers that will store static sets of disco info and items in memory. Attributes: nodes -- A dictionary mapping (JID, node) tuples to a dict containing a disco#info and a disco#items stanza. xmpp -- The main SleekXMPP object. """ def __init__(self, xmpp): """ Create a static disco interface. Sets of disco#info and disco#items are maintained for every given JID and node combination. These stanzas are used to store disco information in memory without any additional processing. Arguments: xmpp -- The main SleekXMPP object. """ self.nodes = {} self.xmpp = xmpp def add_node(self, jid=None, node=None): """ Create a new set of stanzas for the provided JID and node combination. Arguments: jid -- The JID that will own the new stanzas. node -- The node that will own the new stanzas. """ if jid is None: jid = self.xmpp.boundjid.full if node is None: node = '' if (jid, node) not in self.nodes: self.nodes[(jid, node)] = {'info': DiscoInfo(), 'items': DiscoItems()} self.nodes[(jid, node)]['info']['node'] = node self.nodes[(jid, node)]['items']['node'] = node # ================================================================= # Node Handlers # # Each handler accepts three arguments: jid, node, and data. # The jid and node parameters together determine the set of # info and items stanzas that will be retrieved or added. # The data parameter is a dictionary with additional paramters # that will be passed to other calls. def get_info(self, jid, node, data): """ Return the stored info data for the requested JID/node combination. The data parameter is not used. """ if (jid, node) not in self.nodes: if not node: return DiscoInfo() else: raise XMPPError(condition='item-not-found') else: return self.nodes[(jid, node)]['info'] def del_info(self, jid, node, data): """ Reset the info stanza for a given JID/node combination. The data parameter is not used. """ if (jid, node) in self.nodes: self.nodes[(jid, node)]['info'] = DiscoInfo() def get_items(self, jid, node, data): """ Return the stored items data for the requested JID/node combination. The data parameter is not used. """ if (jid, node) not in self.nodes: if not node: return DiscoInfo() else: raise XMPPError(condition='item-not-found') else: return self.nodes[(jid, node)]['items'] def set_items(self, jid, node, data): """ Replace the stored items data for a JID/node combination. The data parameter may provided: items -- A set of items in tuple format. """ items = data.get('items', set()) self.add_node(jid, node) self.nodes[(jid, node)]['items']['items'] = items def del_items(self, jid, node, data): """ Reset the items stanza for a given JID/node combination. The data parameter is not used. """ if (jid, node) in self.nodes: self.nodes[(jid, node)]['items'] = DiscoItems() def add_identity(self, jid, node, data): """ Add a new identity to te JID/node combination. The data parameter may provide: category -- The general category to which the agent belongs. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional standard xml:lang value. """ self.add_node(jid, node) self.nodes[(jid, node)]['info'].add_identity( data.get('category', ''), data.get('itype', ''), data.get('name', None), data.get('lang', None)) def set_identities(self, jid, node, data): """ Add or replace all identities for a JID/node combination. The data parameter should include: identities -- A list of identities in tuple form: (category, type, name, lang) """ identities = data.get('identities', set()) self.add_node(jid, node) self.nodes[(jid, node)]['info']['identities'] = identities def del_identity(self, jid, node, data): """ Remove an identity from a JID/node combination. The data parameter may provide: category -- The general category to which the agent belonged. itype -- A more specific designation with the category. name -- Optional human readable name for this identity. lang -- Optional, standard xml:lang value. """ if (jid, node) not in self.nodes: return self.nodes[(jid, node)]['info'].del_identity( data.get('category', ''), data.get('itype', ''), data.get('name', None), data.get('lang', None)) def del_identities(self, jid, node, data): """ Remove all identities from a JID/node combination. The data parameter is not used. """ if (jid, node) not in self.nodes: return del self.nodes[(jid, node)]['info']['identities'] def add_feature(self, jid, node, data): """ Add a feature to a JID/node combination. The data parameter should include: feature -- The namespace of the supported feature. """ self.add_node(jid, node) self.nodes[(jid, node)]['info'].add_feature(data.get('feature', '')) def set_features(self, jid, node, data): """ Add or replace all features for a JID/node combination. The data parameter should include: features -- The new set of supported features. """ features = data.get('features', set()) self.add_node(jid, node) self.nodes[(jid, node)]['info']['features'] = features def del_feature(self, jid, node, data): """ Remove a feature from a JID/node combination. The data parameter should include: feature -- The namespace of the removed feature. """ if (jid, node) not in self.nodes: return self.nodes[(jid, node)]['info'].del_feature(data.get('feature', '')) def del_features(self, jid, node, data): """ Remove all features from a JID/node combination. The data parameter is not used. """ if (jid, node) not in self.nodes: return del self.nodes[(jid, node)]['info']['features'] def add_item(self, jid, node, data): """ Add an item to a JID/node combination. The data parameter may include: ijid -- The JID for the item. inode -- Optional additional information to reference non-addressable items. name -- Optional human readable name for the item. """ self.add_node(jid, node) self.nodes[(jid, node)]['items'].add_item( data.get('ijid', ''), node=data.get('inode', ''), name=data.get('name', '')) def del_item(self, jid, node, data): """ Remove an item from a JID/node combination. The data parameter may include: ijid -- JID of the item to remove. inode -- Optional extra identifying information. """ if (jid, node) in self.nodes: self.nodes[(jid, node)]['items'].del_item( data.get('ijid', ''), node=data.get('inode', None)) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0033.py000066400000000000000000000102011157775340400225450ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging from . import base from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID from .. stanza.message import Message class Addresses(ElementBase): namespace = 'http://jabber.org/protocol/address' name = 'addresses' plugin_attrib = 'addresses' interfaces = set(('addresses', 'bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) def addAddress(self, atype='to', jid='', node='', uri='', desc='', delivered=False): address = Address(parent=self) address['type'] = atype address['jid'] = jid address['node'] = node address['uri'] = uri address['desc'] = desc address['delivered'] = delivered return address def getAddresses(self, atype=None): addresses = [] for addrXML in self.xml.findall('{%s}address' % Address.namespace): # ElementTree 1.2.6 does not support [@attr='value'] in findall if atype is None or addrXML.attrib.get('type') == atype: addresses.append(Address(xml=addrXML, parent=None)) return addresses def setAddresses(self, addresses, set_type=None): self.delAddresses(set_type) for addr in addresses: addr = dict(addr) # Remap 'type' to 'atype' to match the add method if set_type is not None: addr['type'] = set_type curr_type = addr.get('type', None) if curr_type is not None: del addr['type'] addr['atype'] = curr_type self.addAddress(**addr) def delAddresses(self, atype=None): if atype is None: return for addrXML in self.xml.findall('{%s}address' % Address.namespace): # ElementTree 1.2.6 does not support [@attr='value'] in findall if addrXML.attrib.get('type') == atype: self.xml.remove(addrXML) # -------------------------------------------------------------- def delBcc(self): self.delAddresses('bcc') def delCc(self): self.delAddresses('cc') def delNoreply(self): self.delAddresses('noreply') def delReplyroom(self): self.delAddresses('replyroom') def delReplyto(self): self.delAddresses('replyto') def delTo(self): self.delAddresses('to') # -------------------------------------------------------------- def getBcc(self): return self.getAddresses('bcc') def getCc(self): return self.getAddresses('cc') def getNoreply(self): return self.getAddresses('noreply') def getReplyroom(self): return self.getAddresses('replyroom') def getReplyto(self): return self.getAddresses('replyto') def getTo(self): return self.getAddresses('to') # -------------------------------------------------------------- def setBcc(self, addresses): self.setAddresses(addresses, 'bcc') def setCc(self, addresses): self.setAddresses(addresses, 'cc') def setNoreply(self, addresses): self.setAddresses(addresses, 'noreply') def setReplyroom(self, addresses): self.setAddresses(addresses, 'replyroom') def setReplyto(self, addresses): self.setAddresses(addresses, 'replyto') def setTo(self, addresses): self.setAddresses(addresses, 'to') class Address(ElementBase): namespace = 'http://jabber.org/protocol/address' name = 'address' plugin_attrib = 'address' interfaces = set(('delivered', 'desc', 'jid', 'node', 'type', 'uri')) address_types = set(('bcc', 'cc', 'noreply', 'replyroom', 'replyto', 'to')) def getDelivered(self): return self.xml.attrib.get('delivered', False) def setDelivered(self, delivered): if delivered: self.xml.attrib['delivered'] = "true" else: del self['delivered'] def setUri(self, uri): if uri: del self['jid'] del self['node'] self.xml.attrib['uri'] = uri elif 'uri' in self.xml.attrib: del self.xml.attrib['uri'] class xep_0033(base.base_plugin): """ XEP-0033: Extended Stanza Addressing """ def plugin_init(self): self.xep = '0033' self.description = 'Extended Stanza Addressing' registerStanzaPlugin(Message, Addresses) def post_init(self): base.base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature(Addresses.namespace) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0045.py000066400000000000000000000312061157775340400225600ustar00rootroot00000000000000""" 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 __future__ import with_statement from . import base import logging from xml.etree import cElementTree as ET from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID from .. stanza.presence import Presence from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath from .. xmlstream.matcher.xmlmask import MatchXMLMask log = logging.getLogger(__name__) class MUCPresence(ElementBase): name = 'x' namespace = 'http://jabber.org/protocol/muc#user' plugin_attrib = 'muc' interfaces = set(('affiliation', 'role', 'jid', 'nick', 'room')) affiliations = set(('', )) roles = set(('', )) def getXMLItem(self): item = self.xml.find('{http://jabber.org/protocol/muc#user}item') if item is None: item = ET.Element('{http://jabber.org/protocol/muc#user}item') self.xml.append(item) return item def getAffiliation(self): #TODO if no affilation, set it to the default and return default item = self.getXMLItem() return item.get('affiliation', '') def setAffiliation(self, value): item = self.getXMLItem() #TODO check for valid affiliation item.attrib['affiliation'] = value return self def delAffiliation(self): item = self.getXMLItem() #TODO set default affiliation if 'affiliation' in item.attrib: del item.attrib['affiliation'] return self def getJid(self): item = self.getXMLItem() return JID(item.get('jid', '')) def setJid(self, value): item = self.getXMLItem() if not isinstance(value, str): value = str(value) item.attrib['jid'] = value return self def delJid(self): item = self.getXMLItem() if 'jid' in item.attrib: del item.attrib['jid'] return self def getRole(self): item = self.getXMLItem() #TODO get default role, set default role if none return item.get('role', '') def setRole(self, value): item = self.getXMLItem() #TODO check for valid role item.attrib['role'] = value return self def delRole(self): item = self.getXMLItem() #TODO set default role if 'role' in item.attrib: del item.attrib['role'] return self def getNick(self): return self.parent()['from'].resource def getRoom(self): return self.parent()['from'].bare def setNick(self, value): log.warning("Cannot set nick through mucpresence plugin.") return self def setRoom(self, value): log.warning("Cannot set room through mucpresence plugin.") return self def delNick(self): log.warning("Cannot delete nick through mucpresence plugin.") return self def delRoom(self): log.warning("Cannot delete room through mucpresence plugin.") return self class xep_0045(base.base_plugin): """ Implements XEP-0045 Multi User Chat """ def plugin_init(self): self.rooms = {} self.ourNicks = {} self.xep = '0045' self.description = 'Multi User Chat' # load MUC support in presence stanzas registerStanzaPlugin(Presence, MUCPresence) self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_presence)) self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_message)) self.xmpp.registerHandler(Callback('MUCSubject', MatchXMLMask("" % self.xmpp.default_ns), self.handle_groupchat_subject)) self.xmpp.registerHandler(Callback('MUCInvite', MatchXPath("{%s}message/{http://jabber.org/protocol/muc#user}x/invite" % self.xmpp.default_ns), self.handle_groupchat_invite)) def handle_groupchat_invite(self, inv): """ Handle an invite into a muc. """ logging.debug("MUC invite to %s from %s: %s" % (inv['from'], inv["from"], inv)) if inv['from'] not in self.rooms.keys(): self.xmpp.event("groupchat_invite", inv) def handle_groupchat_presence(self, pr): """ Handle a presence in a muc. """ got_offline = False got_online = False if pr['muc']['room'] not in self.rooms.keys(): return entry = pr['muc'].getStanzaValues() entry['show'] = pr['show'] entry['status'] = pr['status'] if pr['type'] == 'unavailable': if entry['nick'] in self.rooms[entry['room']]: del self.rooms[entry['room']][entry['nick']] got_offline = True else: if entry['nick'] not in self.rooms[entry['room']]: got_online = True self.rooms[entry['room']][entry['nick']] = entry log.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry)) self.xmpp.event("groupchat_presence", pr) self.xmpp.event("muc::%s::presence" % entry['room'], pr) if got_offline: self.xmpp.event("muc::%s::got_offline" % entry['room'], pr) if got_online: self.xmpp.event("muc::%s::got_online" % entry['room'], pr) def handle_groupchat_message(self, msg): """ Handle a message event in a muc. """ self.xmpp.event('groupchat_message', msg) self.xmpp.event("muc::%s::message" % msg['from'].bare, msg) def handle_groupchat_subject(self, msg): """ Handle a message coming from a muc indicating a change of subject (or announcing it when joining the room) """ self.xmpp.event('groupchat_subject', msg) def jidInRoom(self, room, jid): for nick in self.rooms[room]: entry = self.rooms[room][nick] if entry is not None and entry['jid'].full == jid: return True return False def getNick(self, room, jid): for nick in self.rooms[room]: entry = self.rooms[room][nick] if entry is not None and entry['jid'].full == jid: return nick def getRoomForm(self, room, ifrom=None): iq = self.xmpp.makeIqGet() iq['to'] = room if ifrom is not None: iq['from'] = ifrom query = ET.Element('{http://jabber.org/protocol/muc#owner}query') iq.append(query) result = iq.send() if result['type'] == 'error': return False xform = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') if xform is None: return False form = self.xmpp.plugin['old_0004'].buildForm(xform) return form def configureRoom(self, room, form=None, ifrom=None): if form is None: form = self.getRoomForm(room, ifrom=ifrom) #form = self.xmpp.plugin['old_0004'].makeForm(ftype='submit') #form.addField('FORM_TYPE', value='http://jabber.org/protocol/muc#roomconfig') iq = self.xmpp.makeIqSet() iq['to'] = room if ifrom is not None: iq['from'] = ifrom query = ET.Element('{http://jabber.org/protocol/muc#owner}query') form = form.getXML('submit') query.append(form) iq.append(query) result = iq.send() if result['type'] == 'error': return False return True def joinMUC(self, room, nick, maxhistory="0", password='', wait=False, pstatus=None, pshow=None): """ Join the specified room, requesting 'maxhistory' lines of history. """ stanza = self.xmpp.makePresence(pto="%s/%s" % (room, nick), pstatus=pstatus, pshow=pshow) x = ET.Element('{http://jabber.org/protocol/muc}x') if password: passelement = ET.Element('password') passelement.text = password x.append(passelement) if maxhistory: history = ET.Element('history') if maxhistory == "0": history.attrib['maxchars'] = maxhistory else: history.attrib['maxstanzas'] = maxhistory x.append(history) stanza.append(x) if not wait: self.xmpp.send(stanza) else: #wait for our own room presence back expect = ET.Element("{%s}presence" % self.xmpp.default_ns, {'from':"%s/%s" % (room, nick)}) self.xmpp.send(stanza, expect) self.rooms[room] = {} self.ourNicks[room] = nick def destroy(self, room, reason='', altroom = '', ifrom=None): iq = self.xmpp.makeIqSet() if ifrom is not None: iq['from'] = ifrom iq['to'] = room query = ET.Element('{http://jabber.org/protocol/muc#owner}query') destroy = ET.Element('destroy') if altroom: destroy.attrib['jid'] = altroom xreason = ET.Element('reason') xreason.text = reason destroy.append(xreason) query.append(destroy) iq.append(query) r = iq.send() if r is False or r['type'] == 'error': return False return True def setAffiliation(self, room, jid=None, nick=None, affiliation='member'): """ Change room affiliation.""" if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): raise TypeError query = ET.Element('{http://jabber.org/protocol/muc#admin}query') if nick is not None: item = ET.Element('item', {'affiliation':affiliation, 'nick':nick}) else: item = ET.Element('item', {'affiliation':affiliation, 'jid':jid}) query.append(item) iq = self.xmpp.makeIqSet(query) iq['to'] = room result = iq.send() if result is False or result['type'] != 'result': raise ValueError return True def invite(self, room, jid, reason='', mfrom=''): """ Invite a jid to a room.""" msg = self.xmpp.makeMessage(room) msg['from'] = mfrom x = ET.Element('{http://jabber.org/protocol/muc#user}x') invite = ET.Element('{http://jabber.org/protocol/muc#user}invite', {'to': jid}) if reason: rxml = ET.Element('reason') rxml.text = reason invite.append(rxml) x.append(invite) msg.append(x) self.xmpp.send(msg) def leaveMUC(self, room, nick, msg=''): """ Leave the specified room. """ if msg: self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick), pstatus=msg) else: self.xmpp.sendPresence(pshow='unavailable', pto="%s/%s" % (room, nick)) del self.rooms[room] def getRoomConfig(self, room, ifrom=''): iq = self.xmpp.makeIqGet('http://jabber.org/protocol/muc#owner') iq['to'] = room iq['from'] = ifrom result = iq.send() if result is None or result['type'] != 'result': raise ValueError form = result.xml.find('{http://jabber.org/protocol/muc#owner}query/{jabber:x:data}x') if form is None: raise ValueError return self.xmpp.plugin['xep_0004'].buildForm(form) def cancelConfig(self, room): query = ET.Element('{http://jabber.org/protocol/muc#owner}query') x = ET.Element('{jabber:x:data}x', type='cancel') query.append(x) iq = self.xmpp.makeIqSet(query) iq['to'] = room iq.send() def setRoomConfig(self, room, config, ifrom=''): query = ET.Element('{http://jabber.org/protocol/muc#owner}query') x = config.getXML('submit') query.append(x) iq = self.xmpp.makeIqSet(query) iq['to'] = room iq['from'] = ifrom iq.send() def getJoinedRooms(self): return self.rooms.keys() def getOurJidInRoom(self, roomJid): """ Return the jid we're using in a room. """ return "%s/%s" % (roomJid, self.ourNicks[roomJid]) def getJidProperty(self, room, nick, jidProperty): """ Get the property of a nick in a room, such as its 'jid' or 'affiliation' If not found, return None. """ if room in self.rooms and nick in self.rooms[room] and jidProperty in self.rooms[room][nick]: return self.rooms[room][nick][jidProperty] else: return None def getRoster(self, room): """ Get the list of nicks in a room. """ if room not in self.rooms.keys(): return None return self.rooms[room].keys() fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0050/000077500000000000000000000000001157775340400222005ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0050/__init__.py000066400000000000000000000004551157775340400243150ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.plugins.xep_0050.stanza import Command from sleekxmpp.plugins.xep_0050.adhoc import xep_0050 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0050/adhoc.py000066400000000000000000000527061157775340400236420ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging import time from sleekxmpp import Iq from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream import register_stanza_plugin, JID from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.xep_0050 import stanza from sleekxmpp.plugins.xep_0050 import Command log = logging.getLogger(__name__) class xep_0050(base_plugin): """ XEP-0050: Ad-Hoc Commands XMPP's Adhoc Commands provides a generic workflow mechanism for interacting with applications. The result is similar to menu selections and multi-step dialogs in normal desktop applications. Clients do not need to know in advance what commands are provided by any particular application or agent. While adhoc commands provide similar functionality to Jabber-RPC, adhoc commands are used primarily for human interaction. Also see Configuration Values: threaded -- Indicates if command events should be threaded. Defaults to True. Events: command_execute -- Received a command with action="execute" command_next -- Received a command with action="next" command_complete -- Received a command with action="complete" command_cancel -- Received a command with action="cancel" Attributes: threaded -- Indicates if command events should be threaded. Defaults to True. commands -- A dictionary mapping JID/node pairs to command names and handlers. sessions -- A dictionary or equivalent backend mapping session IDs to dictionaries containing data relevant to a command's session. Methods: plugin_init -- Overrides base_plugin.plugin_init post_init -- Overrides base_plugin.post_init new_session -- Return a new session ID. prep_handlers -- Placeholder. May call with a list of handlers to prepare them for use with the session storage backend, if needed. set_backend -- Replace the default session storage with some external storage mechanism, such as a database. The provided backend wrapper must be able to act using the same syntax as a dictionary. add_command -- Add a command for use by external entitites. get_commands -- Retrieve a list of commands provided by a remote agent. send_command -- Send a command request to a remote agent. start_command -- Command user API: initiate a command session continue_command -- Command user API: proceed to the next step cancel_command -- Command user API: cancel a command complete_command -- Command user API: finish a command terminate_command -- Command user API: delete a command's session """ def plugin_init(self): """Start the XEP-0050 plugin.""" self.xep = '0050' self.description = 'Ad-Hoc Commands' self.stanza = stanza self.threaded = self.config.get('threaded', True) self.commands = {} self.sessions = self.config.get('session_db', {}) self.xmpp.register_handler( Callback("Ad-Hoc Execute", StanzaPath('iq@type=set/command'), self._handle_command)) self.xmpp.register_handler( Callback("Ad-Hoc Result", StanzaPath('iq@type=result/command'), self._handle_command_result)) self.xmpp.register_handler( Callback("Ad-Hoc Error", StanzaPath('iq@type=error/command'), self._handle_command_result)) register_stanza_plugin(Iq, stanza.Command) self.xmpp.add_event_handler('command_execute', self._handle_command_start, threaded=self.threaded) self.xmpp.add_event_handler('command_next', self._handle_command_next, threaded=self.threaded) self.xmpp.add_event_handler('command_cancel', self._handle_command_cancel, threaded=self.threaded) self.xmpp.add_event_handler('command_complete', self._handle_command_complete, threaded=self.threaded) def post_init(self): """Handle cross-plugin interactions.""" base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Command.namespace) def set_backend(self, db): """ Replace the default session storage dictionary with a generic, external data storage mechanism. The replacement backend must be able to interact through the same syntax and interfaces as a normal dictionary. Arguments: db -- The new session storage mechanism. """ self.sessions = db def prep_handlers(self, handlers, **kwargs): """ Prepare a list of functions for use by the backend service. Intended to be replaced by the backend service as needed. Arguments: handlers -- A list of function pointers **kwargs -- Any additional parameters required by the backend. """ pass # ================================================================= # Server side (command provider) API def add_command(self, jid=None, node=None, name='', handler=None): """ Make a new command available to external entities. Access control may be implemented in the provided handler. Command workflow is done across a sequence of command handlers. The first handler is given the intial Iq stanza of the request in order to support access control. Subsequent handlers are given only the payload items of the command. All handlers will receive the command's session data. Arguments: jid -- The JID that will expose the command. node -- The node associated with the command. name -- A human readable name for the command. handler -- A function that will generate the response to the initial command request, as well as enforcing any access control policies. """ if jid is None: jid = self.xmpp.boundjid elif not isinstance(jid, JID): jid = JID(jid) item_jid = jid.full # Client disco uses only the bare JID if self.xmpp.is_component: jid = jid.full else: jid = jid.bare self.xmpp['xep_0030'].add_identity(category='automation', itype='command-list', name='Ad-Hoc commands', node=Command.namespace, jid=jid) self.xmpp['xep_0030'].add_item(jid=item_jid, name=name, node=Command.namespace, subnode=node, ijid=jid) self.xmpp['xep_0030'].add_identity(category='automation', itype='command-node', name=name, node=node, jid=jid) self.xmpp['xep_0030'].add_feature(Command.namespace, None, jid) self.commands[(item_jid, node)] = (name, handler) def new_session(self): """Return a new session ID.""" return str(time.time()) + '-' + self.xmpp.new_id() def _handle_command(self, iq): """Raise command events based on the command action.""" self.xmpp.event('command_%s' % iq['command']['action'], iq) def _handle_command_start(self, iq): """ Process an initial request to execute a command. Arguments: iq -- The command execution request. """ sessionid = self.new_session() node = iq['command']['node'] key = (iq['to'].full, node) name, handler = self.commands.get(key, ('Not found', None)) if not handler: log.debug('Command not found: %s, %s' % (key, self.commands)) initial_session = {'id': sessionid, 'from': iq['from'], 'to': iq['to'], 'node': node, 'payload': None, 'interfaces': '', 'payload_classes': None, 'notes': None, 'has_next': False, 'allow_complete': False, 'allow_prev': False, 'past': [], 'next': None, 'prev': None, 'cancel': None} session = handler(iq, initial_session) self._process_command_response(iq, session) def _handle_command_next(self, iq): """ Process a request for the next step in the workflow for a command with multiple steps. Arguments: iq -- The command continuation request. """ sessionid = iq['command']['sessionid'] session = self.sessions[sessionid] handler = session['next'] interfaces = session['interfaces'] results = [] for stanza in iq['command']['substanzas']: if stanza.plugin_attrib in interfaces: results.append(stanza) if len(results) == 1: results = results[0] session = handler(results, session) self._process_command_response(iq, session) def _process_command_response(self, iq, session): """ Generate a command reply stanza based on the provided session data. Arguments: iq -- The command request stanza. session -- A dictionary of relevant session data. """ sessionid = session['id'] payload = session['payload'] if not isinstance(payload, list): payload = [payload] session['interfaces'] = [item.plugin_attrib for item in payload] session['payload_classes'] = [item.__class__ for item in payload] self.sessions[sessionid] = session for item in payload: register_stanza_plugin(Command, item.__class__, iterable=True) iq.reply() iq['command']['node'] = session['node'] iq['command']['sessionid'] = session['id'] if session['next'] is None: iq['command']['actions'] = [] iq['command']['status'] = 'completed' elif session['has_next']: actions = ['next'] if session['allow_complete']: actions.append('complete') if session['allow_prev']: actions.append('prev') iq['command']['actions'] = actions iq['command']['status'] = 'executing' else: iq['command']['actions'] = ['complete'] iq['command']['status'] = 'executing' iq['command']['notes'] = session['notes'] for item in payload: iq['command'].append(item) iq.send() def _handle_command_cancel(self, iq): """ Process a request to cancel a command's execution. Arguments: iq -- The command cancellation request. """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] session = self.sessions[sessionid] handler = session['cancel'] if handler: handler(iq, session) try: del self.sessions[sessionid] except: pass iq.reply() iq['command']['node'] = node iq['command']['sessionid'] = sessionid iq['command']['status'] = 'canceled' iq['command']['notes'] = session['notes'] iq.send() def _handle_command_complete(self, iq): """ Process a request to finish the execution of command and terminate the workflow. All data related to the command session will be removed. Arguments: iq -- The command completion request. """ node = iq['command']['node'] sessionid = iq['command']['sessionid'] session = self.sessions[sessionid] handler = session['next'] interfaces = session['interfaces'] results = [] for stanza in iq['command']['substanzas']: if stanza.plugin_attrib in interfaces: results.append(stanza) if len(results) == 1: results = results[0] if handler: handler(results, session) iq.reply() iq['command']['node'] = node iq['command']['sessionid'] = sessionid iq['command']['actions'] = [] iq['command']['status'] = 'completed' iq['command']['notes'] = session['notes'] iq.send() del self.sessions[sessionid] # ================================================================= # Client side (command user) API def get_commands(self, jid, **kwargs): """ Return a list of commands provided by a given JID. Arguments: jid -- The JID to query for commands. local -- If true, then the query is for a JID/node combination handled by this Sleek instance and no stanzas need to be sent. Otherwise, a disco stanza must be sent to the remove JID to retrieve the items. ifrom -- Specifiy the sender's JID. block -- If true, block and wait for the stanzas' reply. timeout -- The time in seconds to block while waiting for a reply. If None, then wait indefinitely. callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. iterator -- If True, return a result set iterator using the XEP-0059 plugin, if the plugin is loaded. Otherwise the parameter is ignored. """ return self.xmpp['xep_0030'].get_items(jid=jid, node=Command.namespace, **kwargs) def send_command(self, jid, node, ifrom=None, action='execute', payload=None, sessionid=None, **kwargs): """ Create and send a command stanza, without using the provided workflow management APIs. Arguments: jid -- The JID to send the command request or result. node -- The node for the command. ifrom -- Specify the sender's JID. action -- May be one of: execute, cancel, complete, or cancel. payload -- Either a list of payload items, or a single payload item such as a data form. sessionid -- The current session's ID value. block -- Specify if the send call will block until a response is received, or a timeout occurs. Defaults to True. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. """ iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid if ifrom: iq['from'] = ifrom iq['command']['node'] = node iq['command']['action'] = action if sessionid is not None: iq['command']['sessionid'] = sessionid if payload is not None: if not isinstance(payload, list): payload = [payload] for item in payload: iq['command'].append(item) return iq.send(**kwargs) def start_command(self, jid, node, session, ifrom=None): """ Initiate executing a command provided by a remote agent. The workflow provided is always non-blocking. The provided session dictionary should contain: next -- A handler for processing the command result. error -- A handler for processing any error stanzas generated by the request. Arguments: jid -- The JID to send the command request. node -- The node for the desired command. session -- A dictionary of relevant session data. ifrom -- Optionally specify the sender's JID. """ session['jid'] = jid session['node'] = node session['timestamp'] = time.time() session['payload'] = None iq = self.xmpp.Iq() iq['type'] = 'set' iq['to'] = jid if ifrom: iq['from'] = ifrom session['from'] = ifrom iq['command']['node'] = node iq['command']['action'] = 'execute' sessionid = 'client:pending_' + iq['id'] session['id'] = sessionid self.sessions[sessionid] = session iq.send(block=False) def continue_command(self, session): """ Execute the next action of the command. Arguments: session -- All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] self.sessions[sessionid] = session self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), action='next', payload=session.get('payload', None), sessionid=session['id']) def cancel_command(self, session): """ Cancel the execution of a command. Arguments: session -- All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] self.sessions[sessionid] = session self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), action='cancel', payload=session.get('payload', None), sessionid=session['id']) def complete_command(self, session): """ Finish the execution of a command workflow. Arguments: session -- All stored data relevant to the current command session. """ sessionid = 'client:' + session['id'] self.sessions[sessionid] = session self.send_command(session['jid'], session['node'], ifrom=session.get('from', None), action='complete', payload=session.get('payload', None), sessionid=session['id']) def terminate_command(self, session): """ Delete a command's session after a command has completed or an error has occured. Arguments: session -- All stored data relevant to the current command session. """ try: del self.sessions[session['id']] except: pass def _handle_command_result(self, iq): """ Process the results of a command request. Will execute the 'next' handler stored in the session data, or the 'error' handler depending on the Iq's type. Arguments: iq -- The command response. """ sessionid = 'client:' + iq['command']['sessionid'] pending = False if sessionid not in self.sessions: pending = True pendingid = 'client:pending_' + iq['id'] if pendingid not in self.sessions: return sessionid = pendingid session = self.sessions[sessionid] sessionid = 'client:' + iq['command']['sessionid'] session['id'] = iq['command']['sessionid'] self.sessions[sessionid] = session if pending: del self.sessions[pendingid] handler_type = 'next' if iq['type'] == 'error': handler_type = 'error' handler = session.get(handler_type, None) if handler: handler(iq, session) elif iq['type'] == 'error': self.terminate_command(session) if iq['command']['status'] == 'completed': self.terminate_command(session) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0050/stanza.py000066400000000000000000000141311157775340400240520ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.xmlstream import ElementBase, ET class Command(ElementBase): """ XMPP's Adhoc Commands provides a generic workflow mechanism for interacting with applications. The result is similar to menu selections and multi-step dialogs in normal desktop applications. Clients do not need to know in advance what commands are provided by any particular application or agent. While adhoc commands provide similar functionality to Jabber-RPC, adhoc commands are used primarily for human interaction. Also see Example command stanzas: Information! Stanza Interface: action -- The action to perform. actions -- The set of allowable next actions. node -- The node associated with the command. notes -- A list of tuples for informative notes. sessionid -- A unique identifier for a command session. status -- May be one of: canceled, completed, or executing. Attributes: actions -- A set of allowed action values. statuses -- A set of allowed status values. next_actions -- A set of allowed next action names. Methods: get_action -- Return the requested action. get_actions -- Return the allowable next actions. set_actions -- Set the allowable next actions. del_actions -- Remove the current set of next actions. get_notes -- Return a list of informative note data. set_notes -- Set informative notes. del_notes -- Remove any note data. add_note -- Add a single note. """ name = 'command' namespace = 'http://jabber.org/protocol/commands' plugin_attrib = 'command' interfaces = set(('action', 'sessionid', 'node', 'status', 'actions', 'notes')) actions = set(('cancel', 'complete', 'execute', 'next', 'prev')) statuses = set(('canceled', 'completed', 'executing')) next_actions = set(('prev', 'next', 'complete')) def get_action(self): """ Return the value of the action attribute. If the Iq stanza's type is "set" then use a default value of "execute". """ if self.parent()['type'] == 'set': return self._get_attr('action', default='execute') return self._get_attr('action') def set_actions(self, values): """ Assign the set of allowable next actions. Arguments: values -- A list containing any combination of: 'prev', 'next', and 'complete' """ self.del_actions() if values: self._set_sub_text('{%s}actions' % self.namespace, '', True) actions = self.find('{%s}actions' % self.namespace) for val in values: if val in self.next_actions: action = ET.Element('{%s}%s' % (self.namespace, val)) actions.append(action) def get_actions(self): """ Return the set of allowable next actions. """ actions = [] actions_xml = self.find('{%s}actions' % self.namespace) if actions_xml is not None: for action in self.next_actions: action_xml = actions_xml.find('{%s}%s' % (self.namespace, action)) if action_xml is not None: actions.append(action) return actions def del_actions(self): """ Remove all allowable next actions. """ self._del_sub('{%s}actions' % self.namespace) def get_notes(self): """ Return a list of note information. Example: [('info', 'Some informative data'), ('warning', 'Use caution'), ('error', 'The command ran, but had errors')] """ notes = [] notes_xml = self.findall('{%s}note' % self.namespace) for note in notes_xml: notes.append((note.attrib.get('type', 'info'), note.text)) return notes def set_notes(self, notes): """ Add multiple notes to the command result. Each note is a tuple, with the first item being one of: 'info', 'warning', or 'error', and the second item being any human readable message. Example: [('info', 'Some informative data'), ('warning', 'Use caution'), ('error', 'The command ran, but had errors')] Arguments: notes -- A list of tuples of note information. """ self.del_notes() for note in notes: self.add_note(note[1], note[0]) def del_notes(self): """ Remove all notes associated with the command result. """ notes_xml = self.findall('{%s}note' % self.namespace) for note in notes_xml: self.xml.remove(note) def add_note(self, msg='', ntype='info'): """ Add a single note annotation to the command. Arguments: msg -- A human readable message. ntype -- One of: 'info', 'warning', 'error' """ xml = ET.Element('{%s}note' % self.namespace) xml.attrib['type'] = ntype xml.text = msg self.xml.append(xml) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0059/000077500000000000000000000000001157775340400222115ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0059/__init__.py000066400000000000000000000004761157775340400243310ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.plugins.xep_0059.stanza import Set from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, xep_0059 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0059/rsm.py000066400000000000000000000067661157775340400234030ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging import sleekxmpp from sleekxmpp import Iq from sleekxmpp.plugins.base import base_plugin from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.xep_0059 import Set log = logging.getLogger(__name__) class ResultIterator(): """ An iterator for Result Set Managment """ def __init__(self, query, interface, amount=10, start=None, reverse=False): """ Arguments: query -- The template query interface -- The substanza of the query, for example disco_items amount -- The max amounts of items to request per iteration start -- From which item id to start reverse -- If True, page backwards through the results Example: q = Iq() q['to'] = 'pubsub.example.com' q['disco_items']['node'] = 'blog' for i in ResultIterator(q, 'disco_items', '10'): print i['disco_items']['items'] """ self.query = query self.amount = amount self.start = start self.interface = interface self.reverse = reverse def __iter__(self): return self def __next__(self): return self.next() def next(self): """ Return the next page of results from a query. Note: If using backwards paging, then the next page of results will be the items before the current page of items. """ self.query[self.interface]['rsm']['before'] = self.reverse self.query['id'] = self.query.stream.new_id() self.query[self.interface]['rsm']['max'] = str(self.amount) if self.start and self.reverse: self.query[self.interface]['rsm']['before'] = self.start elif self.start: self.query[self.interface]['rsm']['after'] = self.start r = self.query.send(block=True) if not r or not r[self.interface]['rsm']['first'] and \ not r[self.interface]['rsm']['last']: raise StopIteration if self.reverse: self.start = r[self.interface]['rsm']['first'] else: self.start = r[self.interface]['rsm']['last'] return r class xep_0059(base_plugin): """ XEP-0050: Result Set Management """ def plugin_init(self): """ Start the XEP-0059 plugin. """ self.xep = '0059' self.description = 'Result Set Management' self.stanza = sleekxmpp.plugins.xep_0059.stanza def post_init(self): """Handle inter-plugin dependencies.""" base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Set.namespace) def iterate(self, stanza, interface): """ Create a new result set iterator for a given stanza query. Arguments: stanza -- A stanza object to serve as a template for queries made each iteration. For example, a basic disco#items query. interface -- The name of the substanza to which the result set management stanza should be appended. For example, for disco#items queries the interface 'disco_items' should be used. """ return ResultIterator(stanza, interface) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0059/stanza.py000066400000000000000000000074041157775340400240700ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.xmlstream import ElementBase, ET from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems class Set(ElementBase): """ XEP-0059 (Result Set Managment) can be used to manage the results of queries. For example, limiting the number of items per response or starting at certain positions. Example set stanzas: 2 conference.example.com pubsub.example.com Stanza Interface: first_index -- The index attribute of after -- The id defining from which item to start before -- The id defining from which item to start when browsing backwards max -- Max amount per response first -- Id for the first item in the response last -- Id for the last item in the response index -- Used to set an index to start from count -- The number of remote items available Methods: set_first_index -- Sets the index attribute for and creates the element if it doesn't exist get_first_index -- Returns the value of the index attribute for del_first_index -- Removes the index attribute for but keeps the element set_before -- Sets the value of , if the value is True then the element will be created without a value get_before -- Returns the value of , if it is empty it will return True """ namespace = 'http://jabber.org/protocol/rsm' name = 'set' plugin_attrib = 'rsm' sub_interfaces = set(('first', 'after', 'before', 'count', 'index', 'last', 'max')) interfaces = set(('first_index', 'first', 'after', 'before', 'count', 'index', 'last', 'max')) def set_first_index(self, val): fi = self.find("{%s}first" % (self.namespace)) if fi is not None: if val: fi.attrib['index'] = val else: del fi.attrib['index'] elif val: fi = ET.Element("{%s}first" % (self.namespace)) fi.attrib['index'] = val self.xml.append(fi) def get_first_index(self): fi = self.find("{%s}first" % (self.namespace)) if fi is not None: return fi.attrib.get('index', '') def del_first_index(self): fi = self.xml.find("{%s}first" % (self.namespace)) if fi is not None: del fi.attrib['index'] def set_before(self, val): b = self.xml.find("{%s}before" % (self.namespace)) if b is None and val == True: self._set_sub_text('{%s}before' % self.namespace, '', True) else: self._set_sub_text('{%s}before' % self.namespace, val) def get_before(self): b = self.xml.find("{%s}before" % (self.namespace)) if b is not None and not b.text: return True elif b is not None: return b.text else: return None fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0060.py000066400000000000000000000246261157775340400225650ustar00rootroot00000000000000from __future__ import with_statement from . import base import logging #from xml.etree import cElementTree as ET from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET from . import stanza_pubsub from . xep_0004 import Form log = logging.getLogger(__name__) class xep_0060(base.base_plugin): """ XEP-0060 Publish Subscribe """ def plugin_init(self): self.xep = '0060' self.description = 'Publish-Subscribe' 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: if config is not None: submitform = config if 'FORM_TYPE' in submitform.field: 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 ntype: if 'pubsub#node_type' in submitform.field: submitform.field['pubsub#node_type'].setValue(ntype) else: submitform.addField('pubsub#node_type', value=ntype) else: if 'pubsub#node_type' in submitform.field: submitform.field['pubsub#node_type'].setValue('leaf') else: submitform.addField('pubsub#node_type', value='leaf') submitform['type'] = 'submit' configure.append(submitform.xml) pubsub.append(configure) iq = self.xmpp.makeIqSet(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] result = iq.send() if result is False or result is None or result['type'] == 'error': return False return True def subscribe(self, jid, node, bare=True, subscribee=None): pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') subscribe = ET.Element('subscribe') subscribe.attrib['node'] = node if subscribee is None: if bare: subscribe.attrib['jid'] = self.xmpp.boundjid.bare else: subscribe.attrib['jid'] = self.xmpp.boundjid.full else: subscribe.attrib['jid'] = subscribee pubsub.append(subscribe) iq = self.xmpp.makeIqSet(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] result = iq.send() if result is False or result is None or result['type'] == 'error': return False return True def unsubscribe(self, jid, node, bare=True, subscribee=None): pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') unsubscribe = ET.Element('unsubscribe') unsubscribe.attrib['node'] = node if subscribee is None: if bare: unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare else: unsubscribe.attrib['jid'] = self.xmpp.boundjid.full else: unsubscribe.attrib['jid'] = subscribee pubsub.append(unsubscribe) iq = self.xmpp.makeIqSet(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] result = iq.send() if result is False or result is None or result['type'] == 'error': return False return True def getNodeConfig(self, jid, node=None): # if no node, then grab default pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') if node is not None: configure = ET.Element('configure') configure.attrib['node'] = node else: configure = ET.Element('default') pubsub.append(configure) #TODO: Add configure support. iq = self.xmpp.makeIqGet() iq.append(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] #self.xmpp.add_handler("" % id, self.handlerCreateNodeResponse) result = iq.send() if result is None or result == False or result['type'] == 'error': log.warning("got error instead of config") return False if node is not None: form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x') else: form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x') if not form or form is None: log.error("No form found.") return False return Form(xml=form) def getNodeSubscriptions(self, jid, node): pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') subscriptions = ET.Element('subscriptions') subscriptions.attrib['node'] = node pubsub.append(subscriptions) iq = self.xmpp.makeIqGet() iq.append(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] result = iq.send() if result is None or result == False or result['type'] == 'error': log.warning("got error instead of config") return False else: results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription') if results is None: return False subs = {} for sub in results: subs[sub.get('jid')] = sub.get('subscription') return subs def getNodeAffiliations(self, jid, node): pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') affiliations = ET.Element('affiliations') affiliations.attrib['node'] = node pubsub.append(affiliations) iq = self.xmpp.makeIqGet() iq.append(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] result = iq.send() if result is None or result == False or result['type'] == 'error': log.warning("got error instead of config") return False else: results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation') if results is None: return False subs = {} for sub in results: subs[sub.get('jid')] = sub.get('affiliation') return subs def deleteNode(self, jid, node): pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') iq = self.xmpp.makeIqSet() delete = ET.Element('delete') delete.attrib['node'] = node pubsub.append(delete) iq.append(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full result = iq.send() if result is not None and result is not False and result['type'] != 'error': return True else: return False def setNodeConfig(self, jid, node, config): pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') configure = ET.Element('configure') configure.attrib['node'] = node config = config.getXML('submit') configure.append(config) pubsub.append(configure) iq = self.xmpp.makeIqSet(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] result = iq.send() if result is None or result['type'] == 'error': return False return True def setItem(self, jid, node, items=[]): pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') publish = ET.Element('publish') publish.attrib['node'] = node for pub_item in items: id, payload = pub_item item = ET.Element('item') if id is not None: item.attrib['id'] = id item.append(payload) publish.append(item) pubsub.append(publish) iq = self.xmpp.makeIqSet(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] result = iq.send() if result is None or result is False or result['type'] == 'error': return False return True def addItem(self, jid, node, items=[]): return self.setItem(jid, node, items) def deleteItem(self, jid, node, item): pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') retract = ET.Element('retract') retract.attrib['node'] = node itemn = ET.Element('item') itemn.attrib['id'] = item retract.append(itemn) pubsub.append(retract) iq = self.xmpp.makeIqSet(pubsub) iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] result = iq.send() if result is None or result is False or result['type'] == 'error': return False return True def getNodes(self, jid): response = self.xmpp.plugin['xep_0030'].getItems(jid) items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') nodes = {} if items is not None and items is not False: for item in items: nodes[item.get('node')] = item.get('name') return nodes def getItems(self, jid, node): response = self.xmpp.plugin['xep_0030'].getItems(jid, node) items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') nodeitems = [] if items is not None and items is not False: for item in items: nodeitems.append(item.get('node')) return nodeitems def addNodeToCollection(self, jid, child, parent=''): config = self.getNodeConfig(jid, child) if not config or config is None: self.lasterror = "Config Error" return False try: config.field['pubsub#collection'].setValue(parent) except KeyError: log.warning("pubsub#collection doesn't exist in config, trying to add it") config.addField('pubsub#collection', value=parent) if not self.setNodeConfig(jid, child, config): return False return True def modifyAffiliation(self, ps_jid, node, user_jid, affiliation): if affiliation not in ('owner', 'publisher', 'member', 'none', 'outcast'): raise TypeError pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') affs = ET.Element('affiliations') affs.attrib['node'] = node aff = ET.Element('affiliation') aff.attrib['jid'] = user_jid aff.attrib['affiliation'] = affiliation affs.append(aff) pubsub.append(affs) iq = self.xmpp.makeIqSet(pubsub) iq.attrib['to'] = ps_jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq['id'] result = iq.send() if result is None or result is False or result['type'] == 'error': return False return True def addNodeToCollection(self, jid, child, parent=''): config = self.getNodeConfig(jid, child) if not config or config is None: self.lasterror = "Config Error" return False try: config.field['pubsub#collection'].setValue(parent) except KeyError: log.warning("pubsub#collection doesn't exist in config, trying to add it") config.addField('pubsub#collection', value=parent) if not self.setNodeConfig(jid, child, config): return False return True def removeNodeFromCollection(self, jid, child): self.addNodeToCollection(jid, child, '') fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0078.py000066400000000000000000000043521157775340400225700ustar00rootroot00000000000000""" 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 __future__ import with_statement from xml.etree import cElementTree as ET import logging import hashlib from . import base log = logging.getLogger(__name__) class xep_0078(base.base_plugin): """ XEP-0078 NON-SASL Authentication """ def plugin_init(self): self.description = "Non-SASL Authentication (broken)" self.xep = "0078" self.xmpp.add_event_handler("session_start", self.check_stream) #disabling until I fix conflict with PLAIN #self.xmpp.registerFeature("", self.auth) self.streamid = '' def check_stream(self, xml): self.streamid = xml.attrib['id'] if xml.get('version', '0') != '1.0': self.auth() def auth(self, xml=None): log.debug("Starting jabber:iq:auth Authentication") auth_request = self.xmpp.makeIqGet() auth_request_query = ET.Element('{jabber:iq:auth}query') auth_request.attrib['to'] = self.xmpp.boundjid.host username = ET.Element('username') username.text = self.xmpp.username auth_request_query.append(username) auth_request.append(auth_request_query) result = auth_request.send() rquery = result.find('{jabber:iq:auth}query') attempt = self.xmpp.makeIqSet() query = ET.Element('{jabber:iq:auth}query') resource = ET.Element('resource') resource.text = self.xmpp.resource query.append(username) query.append(resource) if rquery.find('{jabber:iq:auth}digest') is None: log.warning("Authenticating via jabber:iq:auth Plain.") password = ET.Element('password') password.text = self.xmpp.password query.append(password) else: log.debug("Authenticating via jabber:iq:auth Digest") digest = ET.Element('digest') digest.text = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest() query.append(digest) attempt.append(query) result = attempt.send() if result.attrib['type'] == 'result': with self.xmpp.lock: self.xmpp.authenticated = True self.xmpp.sessionstarted = True self.xmpp.event("session_start") else: log.info("Authentication failed") self.xmpp.disconnect() self.xmpp.event("failed_auth") fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0085/000077500000000000000000000000001157775340400222105ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0085/__init__.py000066400000000000000000000004631157775340400243240ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permissio """ from sleekxmpp.plugins.xep_0085.stanza import ChatState from sleekxmpp.plugins.xep_0085.chat_states import xep_0085 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0085/chat_states.py000066400000000000000000000026221157775340400250660ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permissio """ import logging import sleekxmpp from sleekxmpp.stanza import Message from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.xep_0085 import stanza, ChatState log = logging.getLogger(__name__) class xep_0085(base_plugin): """ XEP-0085 Chat State Notifications """ def plugin_init(self): self.xep = '0085' self.description = 'Chat State Notifications' self.stanza = stanza for state in ChatState.states: self.xmpp.register_handler( Callback('Chat State: %s' % state, StanzaPath('message@chat_state=%s' % state), self._handle_chat_state)) register_stanza_plugin(Message, ChatState) def post_init(self): base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature(ChatState.namespace) def _handle_chat_state(self, msg): state = msg['chat_state'] log.debug("Chat State: %s, %s" % (state, msg['from'].jid)) self.xmpp.event('chatstate_%s' % state, msg) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0085/stanza.py000066400000000000000000000036231157775340400240660ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permissio """ import sleekxmpp from sleekxmpp.xmlstream import ElementBase, ET class ChatState(ElementBase): """ Example chat state stanzas: Stanza Interfaces: chat_state Attributes: states Methods: get_chat_state set_chat_state del_chat_state """ name = '' namespace = 'http://jabber.org/protocol/chatstates' plugin_attrib = 'chat_state' interfaces = set(('chat_state',)) is_extension = True states = set(('active', 'composing', 'gone', 'inactive', 'paused')) def setup(self, xml=None): self.xml = ET.Element('') return True def get_chat_state(self): parent = self.parent() for state in self.states: state_xml = parent.find('{%s}%s' % (self.namespace, state)) if state_xml is not None: self.xml = state_xml return state return '' def set_chat_state(self, state): self.del_chat_state() parent = self.parent() if state in self.states: self.xml = ET.Element('{%s}%s' % (self.namespace, state)) parent.append(self.xml) elif state not in [None, '']: raise ValueError('Invalid chat state') def del_chat_state(self): parent = self.parent() for state in self.states: state_xml = parent.find('{%s}%s' % (self.namespace, state)) if state_xml is not None: self.xml = ET.Element('') parent.xml.remove(state_xml) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0086/000077500000000000000000000000001157775340400222115ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0086/__init__.py000066400000000000000000000004701157775340400243230ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.plugins.xep_0086.stanza import LegacyError from sleekxmpp.plugins.xep_0086.legacy_error import xep_0086 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0086/legacy_error.py000066400000000000000000000026661157775340400252520ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.stanza import Error from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.xep_0086 import stanza, LegacyError class xep_0086(base_plugin): """ XEP-0086: Error Condition Mappings Older XMPP implementations used code based error messages, similar to HTTP response codes. Since then, error condition elements have been introduced. XEP-0086 provides a mapping between the new condition elements and a combination of error types and the older response codes. Also see . Configuration Values: override -- Indicates if applying legacy error codes should be done automatically. Defaults to True. If False, then inserting legacy error codes can be done using: iq['error']['legacy']['condition'] = ... """ def plugin_init(self): self.xep = '0086' self.description = 'Error Condition Mappings' self.stanza = stanza register_stanza_plugin(Error, LegacyError, overrides=self.config.get('override', True)) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0086/stanza.py000066400000000000000000000063321157775340400240670ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.stanza import Error from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin class LegacyError(ElementBase): """ Older XMPP implementations used code based error messages, similar to HTTP response codes. Since then, error condition elements have been introduced. XEP-0086 provides a mapping between the new condition elements and a combination of error types and the older response codes. Also see . Example legacy error stanzas: Attributes: error_map -- A map of error conditions to error types and code values. Methods: setup -- Overrides ElementBase.setup set_condition -- Remap the type and code interfaces when a condition is set. """ name = 'legacy' namespace = Error.namespace plugin_attrib = name interfaces = set(('condition',)) overrides = ['set_condition'] error_map = {'bad-request': ('modify','400'), 'conflict': ('cancel','409'), 'feature-not-implemented': ('cancel','501'), 'forbidden': ('auth','403'), 'gone': ('modify','302'), 'internal-server-error': ('wait','500'), 'item-not-found': ('cancel','404'), 'jid-malformed': ('modify','400'), 'not-acceptable': ('modify','406'), 'not-allowed': ('cancel','405'), 'not-authorized': ('auth','401'), 'payment-required': ('auth','402'), 'recipient-unavailable': ('wait','404'), 'redirect': ('modify','302'), 'registration-required': ('auth','407'), 'remote-server-not-found': ('cancel','404'), 'remote-server-timeout': ('wait','504'), 'resource-constraint': ('wait','500'), 'service-unavailable': ('cancel','503'), 'subscription-required': ('auth','407'), 'undefined-condition': (None,'500'), 'unexpected-request': ('wait','400')} def setup(self, xml): """Don't create XML for the plugin.""" self.xml = ET.Element('') def set_condition(self, value): """ Set the error type and code based on the given error condition value. Arguments: value -- The new error condition. """ self.parent().set_condition(value) error_data = self.error_map.get(value, None) if error_data is not None: if error_data[0] is not None: self.parent()['type'] = error_data[0] self.parent()['code'] = error_data[1] fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0092/000077500000000000000000000000001157775340400222065ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0092/__init__.py000066400000000000000000000005351157775340400243220ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.plugins.xep_0092 import stanza from sleekxmpp.plugins.xep_0092.stanza import Version from sleekxmpp.plugins.xep_0092.version import xep_0092 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0092/stanza.py000066400000000000000000000021451157775340400240620ustar00rootroot00000000000000""" 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.xmlstream import ElementBase, ET class Version(ElementBase): """ XMPP allows for an agent to advertise the name and version of the underlying software libraries, as well as the operating system that the agent is running on. Example version stanzas: SleekXMPP 1.0 Linux Stanza Interface: name -- The human readable name of the software. version -- The specific version of the software. os -- The name of the operating system running the program. """ name = 'query' namespace = 'jabber:iq:version' plugin_attrib = 'software_version' interfaces = set(('name', 'version', 'os')) sub_interfaces = interfaces fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0092/version.py000066400000000000000000000044751157775340400242570ustar00rootroot00000000000000""" 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 logging import sleekxmpp from sleekxmpp import Iq from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.xep_0092 import Version log = logging.getLogger(__name__) class xep_0092(base_plugin): """ XEP-0092: Software Version """ def plugin_init(self): """ Start the XEP-0092 plugin. """ self.xep = "0092" self.description = "Software Version" self.stanza = sleekxmpp.plugins.xep_0092.stanza self.name = self.config.get('name', 'SleekXMPP') self.version = self.config.get('version', '0.1-dev') self.os = self.config.get('os', '') self.getVersion = self.get_version self.xmpp.register_handler( Callback('Software Version', StanzaPath('iq@type=get/software_version'), self._handle_version)) register_stanza_plugin(Iq, Version) def post_init(self): """ Handle cross-plugin dependencies. """ base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version') def _handle_version(self, iq): """ Respond to a software version query. Arguments: iq -- The Iq stanza containing the software version query. """ iq.reply() iq['software_version']['name'] = self.name iq['software_version']['version'] = self.version iq['software_version']['os'] = self.os iq.send() def get_version(self, jid, ifrom=None): """ Retrieve the software version of a remote agent. Arguments: jid -- The JID of the entity to query. """ iq = self.xmpp.Iq() iq['to'] = jid if ifrom: iq['from'] = ifrom iq['type'] = 'get' iq['query'] = Version.namespace result = iq.send() if result and result['type'] != 'error': return result['software_version'].values return False fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0128/000077500000000000000000000000001157775340400222065ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0128/__init__.py000066400000000000000000000005021157775340400243140ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.plugins.xep_0128.static import StaticExtendedDisco from sleekxmpp.plugins.xep_0128.extended_disco import xep_0128 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0128/extended_disco.py000066400000000000000000000064501157775340400255460ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging import sleekxmpp from sleekxmpp import Iq from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.xep_0004 import Form from sleekxmpp.plugins.xep_0030 import DiscoInfo from sleekxmpp.plugins.xep_0128 import StaticExtendedDisco class xep_0128(base_plugin): """ XEP-0128: Service Discovery Extensions Allow the use of data forms to add additional identity information to disco#info results. Also see . Attributes: disco -- A reference to the XEP-0030 plugin. static -- Object containing the default set of static node handlers. xmpp -- The main SleekXMPP object. Methods: set_extended_info -- Set extensions to a disco#info result. add_extended_info -- Add an extension to a disco#info result. del_extended_info -- Remove all extensions from a disco#info result. """ def plugin_init(self): """Start the XEP-0128 plugin.""" self.xep = '0128' self.description = 'Service Discovery Extensions' self._disco_ops = ['set_extended_info', 'add_extended_info', 'del_extended_info'] register_stanza_plugin(DiscoInfo, Form, iterable=True) def post_init(self): """Handle cross-plugin dependencies.""" base_plugin.post_init(self) self.disco = self.xmpp['xep_0030'] self.static = StaticExtendedDisco(self.disco.static) self.disco.set_extended_info = self.set_extended_info self.disco.add_extended_info = self.add_extended_info self.disco.del_extended_info = self.del_extended_info for op in self._disco_ops: self.disco._add_disco_op(op, getattr(self.static, op)) def set_extended_info(self, jid=None, node=None, **kwargs): """ Set additional, extended identity information to a node. Replaces any existing extended information. Arguments: jid -- The JID to modify. node -- The node to modify. data -- Either a form, or a list of forms to use as extended information, replacing any existing extensions. """ self.disco._run_node_handler('set_extended_info', jid, node, kwargs) def add_extended_info(self, jid=None, node=None, **kwargs): """ Add additional, extended identity information to a node. Arguments: jid -- The JID to modify. node -- The node to modify. data -- Either a form, or a list of forms to add as extended information. """ self.disco._run_node_handler('add_extended_info', jid, node, kwargs) def del_extended_info(self, jid=None, node=None, **kwargs): """ Remove all extended identity information to a node. Arguments: jid -- The JID to modify. node -- The node to modify. """ self.disco._run_node_handler('del_extended_info', jid, node, kwargs) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0128/static.py000066400000000000000000000036361157775340400240570ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging import sleekxmpp from sleekxmpp.plugins.xep_0030 import StaticDisco log = logging.getLogger(__name__) class StaticExtendedDisco(object): """ Extend the default StaticDisco implementation to provide support for extended identity information. """ def __init__(self, static): """ Augment the default XEP-0030 static handler object. Arguments: static -- The default static XEP-0030 handler object. """ self.static = static def set_extended_info(self, jid, node, data): """ Replace the extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ self.del_extended_info(jid, node, data) self.add_extended_info(jid, node, data) def add_extended_info(self, jid, node, data): """ Add additional extended identity data for a JID/node combination. The data parameter may provide: data -- Either a single data form, or a list of data forms. """ self.static.add_node(jid, node) forms = data.get('data', []) if not isinstance(forms, list): forms = [forms] for form in forms: self.static.nodes[(jid, node)]['info'].append(form) def del_extended_info(self, jid, node, data): """ Replace the extended identity data for a JID/node combination. The data parameter is not used. """ if (jid, node) not in self.static.nodes: return info = self.static.nodes[(jid, node)]['info'] for form in info['substanza']: info.xml.remove(form.xml) fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0199/000077500000000000000000000000001157775340400222165ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0199/__init__.py000066400000000000000000000004271157775340400243320ustar00rootroot00000000000000""" 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0199/ping.py000066400000000000000000000122521157775340400235270ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import time import logging import sleekxmpp from sleekxmpp import Iq from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.xep_0199 import stanza, Ping log = logging.getLogger(__name__) class xep_0199(base_plugin): """ XEP-0199: XMPP Ping Given that XMPP is based on TCP connections, it is possible for the underlying connection to be terminated without the application's awareness. Ping stanzas provide an alternative to whitespace based keepalive methods for detecting lost connections. Also see . Attributes: keepalive -- If True, periodically send ping requests to the server. If a ping is not answered, the connection will be reset. frequency -- Time in seconds between keepalive pings. Defaults to 300 seconds. timeout -- Time in seconds to wait for a ping response. Defaults to 30 seconds. Methods: send_ping -- Send a ping to a given JID, returning the round trip time. """ def plugin_init(self): """ Start the XEP-0199 plugin. """ self.description = 'XMPP Ping' self.xep = '0199' self.stanza = stanza self.keepalive = self.config.get('keepalive', False) 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 resp is None or resp is False: 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 xep_0199.sendPing = xep_0199.send_ping fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0199/stanza.py000066400000000000000000000014571157775340400240770ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import sleekxmpp from sleekxmpp.xmlstream import ElementBase class Ping(ElementBase): """ Given that XMPP is based on TCP connections, it is possible for the underlying connection to be terminated without the application's awareness. Ping stanzas provide an alternative to whitespace based keepalive methods for detecting lost connections. Example ping stanza: Stanza Interface: None Methods: None """ name = 'ping' namespace = 'urn:xmpp:ping' plugin_attrib = 'ping' interfaces = set() fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0202.py000066400000000000000000000101651157775340400225540ustar00rootroot00000000000000""" 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 datetime import datetime, tzinfo import logging import time from . import base from .. stanza.iq import Iq from .. xmlstream.handler.callback import Callback from .. xmlstream.matcher.xpath import MatchXPath from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin log = logging.getLogger(__name__) class EntityTime(ElementBase): name = 'time' namespace = 'urn:xmpp:time' plugin_attrib = 'entity_time' interfaces = set(('tzo', 'utc')) sub_interfaces = set(('tzo', 'utc')) #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): 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 sign = ('+' if seconds >= 0 else '-') minutes = abs(seconds // 60) tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60) elif not isinstance(tzo, str): raise TypeError('The time should be a string or a datetime.tzinfo object.') self._set_sub_text('tzo', tzo) def get_utc(self): # 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-%dT%H:%M:%S.%fZ') else: return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ') def set_utc(self, tim=None): if isinstance(tim, datetime): if tim.utcoffset(): tim = tim - tim.utcoffset() tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ') elif isinstance(tim, time.struct_time): tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim) elif not isinstance(tim, str): raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.') self._set_sub_text('utc', tim) class xep_0202(base.base_plugin): """ XEP-0202 Entity Time """ def plugin_init(self): self.description = "Entity Time" self.xep = "0202" self.xmpp.registerHandler( Callback('Time Request', MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns, EntityTime.namespace)), self.handle_entity_time_query)) register_stanza_plugin(Iq, EntityTime) self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time) def post_init(self): base.base_plugin.post_init(self) self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time') def handle_entity_time_query(self, iq): if iq['type'] == 'get': log.debug("Entity time requested by %s" % iq['from']) self.xmpp.event('entity_time_request', iq) elif iq['type'] == 'result': log.debug("Entity time result from %s" % iq['from']) self.xmpp.event('entity_time', iq) def handle_entity_time(self, iq): iq = iq.reply() iq.enable('entity_time') tzo = time.strftime('%z') # %z is not on all ANSI C libraries tzo = tzo[:3] + ':' + tzo[3:] iq['entity_time']['tzo'] = tzo iq['entity_time']['utc'] = datetime.utcnow() iq.send() def get_entity_time(self, jid): iq = self.xmpp.makeIqGet() iq.enable('entity_time') iq.attrib['to'] = jid iq.attrib['from'] = self.xmpp.boundjid.full id = iq.get('id') result = iq.send() if result and result is not None and result.get('type', 'error') != 'error': return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']} else: return False fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0249/000077500000000000000000000000001157775340400222125ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0249/__init__.py000066400000000000000000000004421157775340400243230ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Dalek This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.plugins.xep_0249.stanza import Invite from sleekxmpp.plugins.xep_0249.invite import xep_0249 fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0249/invite.py000066400000000000000000000046011157775340400240630ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Dalek This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import logging import sleekxmpp from sleekxmpp import Message from sleekxmpp.plugins.base import base_plugin from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from sleekxmpp.plugins.xep_0249 import Invite log = logging.getLogger(__name__) class xep_0249(base_plugin): """ XEP-0249: Direct MUC Invitations """ def plugin_init(self): self.xep = "0249" self.description = "Direct MUC Invitations" self.stanza = sleekxmpp.plugins.xep_0249.stanza self.xmpp.register_handler( Callback('Direct MUC Invitations', StanzaPath('message/groupchat_invite'), self._handle_invite)) register_stanza_plugin(Message, Invite) def post_init(self): base_plugin.post_init(self) self.xmpp['xep_0030'].add_feature(Invite.namespace) def _handle_invite(self, msg): """ Raise an event for all invitations received. """ log.debug("Received direct muc invitation from %s to room %s", msg['from'], msg['groupchat_invite']['jid']) self.xmpp.event('groupchat_direct_invite', msg) def send_invitation(self, jid, roomjid, password=None, reason=None, ifrom=None): """ Send a direct MUC invitation to an XMPP entity. Arguments: jid -- The JID of the entity that will receive the invitation roomjid -- the address of the groupchat room to be joined password -- a password needed for entry into a password-protected room (OPTIONAL). reason -- a human-readable purpose for the invitation (OPTIONAL). """ msg = self.xmpp.Message() msg['to'] = jid if ifrom is not None: msg['from'] = ifrom msg['groupchat_invite']['jid'] = roomjid if password is not None: msg['groupchat_invite']['password'] = password if reason is not None: msg['groupchat_invite']['reason'] = reason return msg.send() fritzy-SleekXMPP-5c4ee57/sleekxmpp/plugins/xep_0249/stanza.py000066400000000000000000000021721157775340400240660ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2011 Nathanael C. Fritz, Dalek This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.xmlstream import ElementBase class Invite(ElementBase): """ XMPP allows for an agent in an MUC room to directly invite another user to join the chat room (as opposed to a mediated invitation done through the server). Example invite stanza: Stanza Interface: jid -- The JID of the groupchat room password -- The password used to gain entry in the room (optional) reason -- The reason for the invitation (optional) """ name = "x" namespace = "jabber:x:conference" plugin_attrib = "groupchat_invite" interfaces = ("jid", "password", "reason") fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/000077500000000000000000000000001157775340400205575ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/__init__.py000066400000000000000000000006171157775340400226740ustar00rootroot00000000000000""" 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.stanza.error import Error from sleekxmpp.stanza.stream_error import StreamError from sleekxmpp.stanza.iq import Iq from sleekxmpp.stanza.message import Message from sleekxmpp.stanza.presence import Presence fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/atom.py000066400000000000000000000011461157775340400220730ustar00rootroot00000000000000""" 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.xmlstream import ElementBase class AtomEntry(ElementBase): """ A simple Atom feed entry. Stanza Interface: title -- The title of the Atom feed entry. summary -- The summary of the Atom feed entry. """ namespace = 'http://www.w3.org/2005/Atom' name = 'entry' plugin_attrib = 'entry' interfaces = set(('title', 'summary')) sub_interfaces = set(('title', 'summary')) fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/error.py000066400000000000000000000121361157775340400222650ustar00rootroot00000000000000""" 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.xmlstream import ElementBase, ET, register_stanza_plugin class Error(ElementBase): """ XMPP stanzas of type 'error' should include an stanza that describes the nature of the error and how it should be handled. Use the 'XEP-0086: Error Condition Mappings' plugin to include error codes used in older XMPP versions. Example error stanza: The item was not found. Stanza Interface: code -- The error code used in older XMPP versions. condition -- The name of the condition element. text -- Human readable description of the error. type -- Error type indicating how the error should be handled. Attributes: conditions -- The set of allowable error condition elements. condition_ns -- The namespace for the condition element. types -- A set of values indicating how the error should be treated. Methods: setup -- Overrides ElementBase.setup. get_condition -- Retrieve the name of the condition element. set_condition -- Add a condition element. del_condition -- Remove the condition element. get_text -- Retrieve the contents of the element. set_text -- Set the contents of the element. del_text -- Remove the element. """ namespace = 'jabber:client' name = 'error' plugin_attrib = 'error' interfaces = set(('code', 'condition', 'text', 'type')) sub_interfaces = set(('text',)) 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')) condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas' types = set(('cancel', 'continue', 'modify', 'auth', 'wait')) def setup(self, xml=None): """ Populate the stanza object using an optional XML object. Overrides ElementBase.setup. Sets a default error type and condition, and changes the parent stanza's type to 'error'. Arguments: xml -- Use an existing XML object for the stanza's values. """ if ElementBase.setup(self, xml): #If we had to generate XML then set default values. self['type'] = 'cancel' self['condition'] = 'feature-not-implemented' if self.parent is not None: self.parent()['type'] = 'error' def get_condition(self): """Return the condition element's name.""" for child in self.xml.getchildren(): if "{%s}" % self.condition_ns in child.tag: return child.tag.split('}', 1)[-1] return '' def set_condition(self, value): """ Set the tag name of the condition element. Arguments: value -- The tag name of the condition element. """ if value in self.conditions: del self['condition'] self.xml.append(ET.Element("{%s}%s" % (self.condition_ns, value))) return self def del_condition(self): """Remove the condition element.""" for child in self.xml.getchildren(): if "{%s}" % self.condition_ns in child.tag: tag = child.tag.split('}', 1)[-1] if tag in self.conditions: self.xml.remove(child) return self def get_text(self): """Retrieve the contents of the element.""" return self._get_sub_text('{%s}text' % self.condition_ns) def set_text(self, value): """ Set the contents of the element. Arguments: value -- The new contents for the element. """ self._set_sub_text('{%s}text' % self.condition_ns, text=value) return self def del_text(self): """Remove the element.""" self._del_sub('{%s}text' % self.condition_ns) return self # To comply with PEP8, method names now use underscores. # Deprecated method names are re-mapped for backwards compatibility. Error.getCondition = Error.get_condition Error.setCondition = Error.set_condition Error.delCondition = Error.del_condition Error.getText = Error.get_text Error.setText = Error.set_text Error.delText = Error.del_text fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/htmlim.py000066400000000000000000000051661157775340400224330ustar00rootroot00000000000000""" 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.stanza import Message from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin class HTMLIM(ElementBase): """ XEP-0071: XHTML-IM defines a method for embedding XHTML content within a stanza so that lightweight markup can be used to format the message contents and to create links. Only a subset of XHTML is recommended for use with XHTML-IM. See the full spec at 'http://xmpp.org/extensions/xep-0071.html' for more information. Example stanza: Non-html message content.

HTML!

Stanza Interface: body -- The contents of the HTML body tag. Methods: setup -- Overrides ElementBase.setup. get_body -- Return the HTML body contents. set_body -- Set the HTML body contents. del_body -- Remove the HTML body contents. """ namespace = 'http://jabber.org/protocol/xhtml-im' name = 'html' interfaces = set(('body',)) plugin_attrib = name def set_body(self, html): """ Set the contents of the HTML body. Arguments: html -- Either a string or XML object. If the top level element is not with a namespace of 'http://www.w3.org/1999/xhtml', it will be wrapped. """ if isinstance(html, str): html = ET.XML(html) if html.tag != '{http://www.w3.org/1999/xhtml}body': body = ET.Element('{http://www.w3.org/1999/xhtml}body') body.append(html) self.xml.append(body) else: self.xml.append(html) def get_body(self): """Return the contents of the HTML body.""" html = self.xml.find('{http://www.w3.org/1999/xhtml}body') if html is None: return '' return html def del_body(self): """Remove the HTML body contents.""" if self.parent is not None: self.parent().xml.remove(self.xml) 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/iq.py000066400000000000000000000202631157775340400215450ustar00rootroot00000000000000""" 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.stanza import Error from sleekxmpp.stanza.rootstanza import RootStanza from sleekxmpp.xmlstream import StanzaBase, ET from sleekxmpp.xmlstream.handler import Waiter, Callback from sleekxmpp.xmlstream.matcher import MatcherId class Iq(RootStanza): """ XMPP stanzas, or info/query stanzas, are XMPP's method of requesting and modifying information, similar to HTTP's GET and POST methods. Each stanza must have an 'id' value which associates the stanza with the response stanza. XMPP entities must always be given a response stanza with a type of 'result' after sending a stanza of type 'get' or 'set'. Most uses cases for stanzas will involve adding a element whose namespace indicates the type of information desired. However, some custom XMPP applications use stanzas as a carrier stanza for an application-specific protocol instead. Example Stanzas: Friends Stanza Interface: query -- The namespace of the element if one exists. Attributes: types -- May be one of: get, set, result, or error. Methods: __init__ -- Overrides StanzaBase.__init__. unhandled -- Send error if there are no handlers. set_payload -- Overrides StanzaBase.set_payload. set_query -- Add or modify a element. get_query -- Return the namespace of the element. del_query -- Remove the element. reply -- Overrides StanzaBase.reply send -- Overrides StanzaBase.send """ namespace = 'jabber:client' name = 'iq' interfaces = set(('type', 'to', 'from', 'id', 'query')) types = set(('get', 'result', 'set', 'error')) plugin_attrib = name def __init__(self, *args, **kwargs): """ Initialize a new stanza with an 'id' value. Overrides StanzaBase.__init__. """ StanzaBase.__init__(self, *args, **kwargs) if self['id'] == '': if self.stream is not None: self['id'] = self.stream.new_id() else: self['id'] = '0' def unhandled(self): """ Send a feature-not-implemented error if the stanza is not handled. Overrides StanzaBase.unhandled. """ if self['type'] in ('get', 'set'): self.reply() self['error']['condition'] = 'feature-not-implemented' self['error']['text'] = 'No handlers registered for this request.' self.send() def set_payload(self, value): """ Set the XML contents of the stanza. Arguments: value -- An XML object to use as the stanza's contents """ self.clear() StanzaBase.set_payload(self, value) return self def set_query(self, value): """ Add or modify a element. Query elements are differentiated by their namespace. Arguments: value -- The namespace of the element. """ query = self.xml.find("{%s}query" % value) if query is None and value: self.clear() query = ET.Element("{%s}query" % value) self.xml.append(query) return self def get_query(self): """Return the namespace of the element.""" for child in self.xml.getchildren(): if child.tag.endswith('query'): ns = child.tag.split('}')[0] if '{' in ns: ns = ns[1:] return ns return '' def del_query(self): """Remove the element.""" for child in self.xml.getchildren(): if child.tag.endswith('query'): self.xml.remove(child) return self def reply(self, clear=True): """ Send a reply stanza. Overrides StanzaBase.reply 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, clear) return self def send(self, block=True, timeout=None, callback=None, now=False): """ Send an stanza over the XML stream. The send call can optionally block until a response is received or a timeout occurs. Be aware that using blocking in non-threaded event handlers can drastically impact performance. Otherwise, a callback handler can be provided that will be executed when the Iq stanza's result reply is received. Be aware though that that the callback handler will not be executed in its own thread. Using both block and callback is not recommended, and only the callback argument will be used in that case. Overrides StanzaBase.send Arguments: block -- Specify if the send call will block until a response is received, or a timeout occurs. Defaults to True. timeout -- The length of time (in seconds) to wait for a response before exiting the send call if blocking is used. Defaults to sleekxmpp.xmlstream.RESPONSE_TIMEOUT callback -- Optional reference to a stream handler function. Will be executed when a reply stanza is received. now -- Indicates if the send queue should be skipped and send the stanza immediately. Used during stream initialization. Defaults to False. """ if timeout is None: timeout = self.stream.response_timeout if callback is not None and self['type'] in ('get', 'set'): handler_name = 'IqCallback_%s' % self['id'] handler = Callback(handler_name, MatcherId(self['id']), callback, once=True) self.stream.register_handler(handler) StanzaBase.send(self, now=now) 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) StanzaBase.send(self, now=now) return waitfor.wait(timeout) else: return StanzaBase.send(self, now=now) def _set_stanza_values(self, values): """ Set multiple stanza interface values using a dictionary. Stanza plugin values may be set usind nested dictionaries. If the interface 'query' is given, then it will be set last to avoid duplication of the element. Overrides ElementBase._set_stanza_values. Arguments: values -- A dictionary mapping stanza interface with values. Plugin interfaces may accept a nested dictionary that will be used recursively. """ query = values.get('query', '') if query: del values['query'] StanzaBase._set_stanza_values(self, values) self['query'] = query 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/message.py000066400000000000000000000113121157775340400225530ustar00rootroot00000000000000""" 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.stanza import Error from sleekxmpp.stanza.rootstanza import RootStanza from sleekxmpp.xmlstream import StanzaBase, ET class Message(RootStanza): """ XMPP's stanzas are a "push" mechanism to send information to other XMPP entities without requiring a response. Chat clients will typically use stanzas that have a type of either "chat" or "groupchat". When handling a message event, be sure to check if the message is an error response. Example stanzas: Hi! Hi everyone! Stanza Interface: body -- The main contents of the message. subject -- An optional description of the message's contents. mucroom -- (Read-only) The name of the MUC room that sent the message. mucnick -- (Read-only) The MUC nickname of message's sender. Attributes: types -- May be one of: normal, chat, headline, groupchat, or error. Methods: setup -- Overrides StanzaBase.setup. chat -- Set the message type to 'chat'. normal -- Set the message type to 'normal'. reply -- Overrides StanzaBase.reply get_type -- Overrides StanzaBase interface get_mucroom -- Return the name of the MUC room of the message. set_mucroom -- Dummy method to prevent assignment. del_mucroom -- Dummy method to prevent deletion. get_mucnick -- Return the MUC nickname of the message's sender. set_mucnick -- Dummy method to prevent assignment. del_mucnick -- Dummy method to prevent deletion. """ namespace = 'jabber:client' name = 'message' interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject', 'mucroom', 'mucnick')) sub_interfaces = set(('body', 'subject')) plugin_attrib = name types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat')) def get_type(self): """ Return the message type. Overrides default stanza interface behavior. Returns 'normal' if no type attribute is present. """ return self._get_attr('type', 'normal') def chat(self): """Set the message type to 'chat'.""" self['type'] = 'chat' return self def normal(self): """Set the message type to 'chat'.""" self['type'] = 'normal' return self def reply(self, body=None, clear=True): """ Create a message reply. Overrides StanzaBase.reply. Sets proper 'to' attribute if the message is from a MUC, and adds a message body if one is given. Arguments: body -- Optional text content for the message. clear -- Indicates if existing content should be removed before replying. Defaults to True. """ StanzaBase.reply(self, clear) if self['type'] == 'groupchat': self['to'] = self['to'].bare del self['id'] if body is not None: self['body'] = body return self def get_mucroom(self): """ Return the name of the MUC room where the message originated. Read-only stanza interface. """ if self['type'] == 'groupchat': return self['from'].bare else: return '' def get_mucnick(self): """ Return the nickname of the MUC user that sent the message. Read-only stanza interface. """ if self['type'] == 'groupchat': return self['from'].resource else: return '' def set_mucroom(self, value): """Dummy method to prevent modification.""" pass def del_mucroom(self): """Dummy method to prevent deletion.""" pass def set_mucnick(self, value): """Dummy method to prevent modification.""" pass 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/nick.py000066400000000000000000000045531157775340400220640ustar00rootroot00000000000000""" 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.stanza import Message, Presence from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin class Nick(ElementBase): """ XEP-0172: User Nickname allows the addition of a element in several stanza types, including and stanzas. The nickname contained in a should be the global, friendly or informal name chosen by the owner of a bare JID. The element may be included when establishing communications with new entities, such as normal XMPP users or MUC services. The nickname contained in a element will not necessarily be the same as the nickname used in a MUC. Example stanzas: The User ... The User Stanza Interface: nick -- A global, friendly or informal name chosen by a user. Methods: setup -- Overrides ElementBase.setup. get_nick -- Return the nickname in the element. set_nick -- Add a element with the given nickname. del_nick -- Remove the element. """ namespace = 'http://jabber.org/protocol/nick' name = 'nick' plugin_attrib = name interfaces = set(('nick',)) def set_nick(self, nick): """ Add a element with the given nickname. Arguments: nick -- A human readable, informal name. """ self.xml.text = nick def get_nick(self): """Return the nickname in the element.""" return self.xml.text def del_nick(self): """Remove the element.""" if self.parent is not None: self.parent().xml.remove(self.xml) 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/presence.py000066400000000000000000000134421157775340400227410ustar00rootroot00000000000000""" 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.stanza import Error from sleekxmpp.stanza.rootstanza import RootStanza from sleekxmpp.xmlstream import StanzaBase, ET class Presence(RootStanza): """ XMPP's stanza allows entities to know the status of other clients and components. Since it is currently the only multi-cast stanza in XMPP, many extensions add more information to stanzas to broadcast to every entry in the roster, such as capabilities, music choices, or locations (XEP-0115: Entity Capabilities and XEP-0163: Personal Eventing Protocol). Since stanzas are broadcast when an XMPP entity changes its status, the bulk of the traffic in an XMPP network will be from stanzas. Therefore, do not include more information than necessary in a status message or within a stanza in order to help keep the network running smoothly. Example stanzas: away Getting lunch. 5 Stanza Interface: priority -- A value used by servers to determine message routing. show -- The type of status, such as away or available for chat. status -- Custom, human readable status message. Attributes: types -- One of: available, unavailable, error, probe, subscribe, subscribed, unsubscribe, and unsubscribed. showtypes -- One of: away, chat, dnd, and xa. Methods: setup -- Overrides StanzaBase.setup reply -- Overrides StanzaBase.reply set_show -- Set the value of the element. get_type -- Get the value of the type attribute or element. set_type -- Set the value of the type attribute or element. get_priority -- Get the value of the element. set_priority -- Set the value of the element. """ namespace = 'jabber:client' name = 'presence' interfaces = set(('type', 'to', 'from', 'id', 'show', 'status', 'priority')) sub_interfaces = set(('show', 'status', 'priority')) plugin_attrib = name types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed')) showtypes = set(('dnd', 'chat', 'xa', 'away')) def exception(self, e): """ Override exception passback for presence. """ pass def set_show(self, show): """ Set the value of the element. Arguments: show -- Must be one of: away, chat, dnd, or xa. """ if show is None: self._del_sub('show') elif show in self.showtypes: self._set_sub_text('show', text=show) return self def get_type(self): """ Return the value of the stanza's type attribute, or the value of the element. """ out = self._get_attr('type') if not out: out = self['show'] if not out or out is None: out = 'available' return out def set_type(self, value): """ Set the type attribute's value, and the element if applicable. Arguments: value -- Must be in either self.types or self.showtypes. """ if value in self.types: self['show'] = None if value == 'available': value = '' self._set_attr('type', value) elif value in self.showtypes: self['show'] = value return self def del_type(self): """ Remove both the type attribute and the element. """ self._del_attr('type') self._del_sub('show') def set_priority(self, value): """ Set the entity's priority value. Some server use priority to determine message routing behavior. Bot clients should typically use a priority of 0 if the same JID is used elsewhere by a human-interacting client. Arguments: value -- An integer value greater than or equal to 0. """ self._set_sub_text('priority', text=str(value)) def get_priority(self): """ Return the value of the element as an integer. """ p = self._get_sub_text('priority') if not p: p = 0 try: return int(p) except ValueError: # The priority is not a number: we consider it 0 as a default return 0 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, 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/rootstanza.py000066400000000000000000000042251157775340400233400ustar00rootroot00000000000000""" 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 logging import traceback import sys from sleekxmpp.exceptions import XMPPError from sleekxmpp.stanza import Error from sleekxmpp.xmlstream import ET, StanzaBase, register_stanza_plugin log = logging.getLogger(__name__) class RootStanza(StanzaBase): """ A top-level XMPP stanza in an XMLStream. The RootStanza class provides a more XMPP specific exception handler than provided by the generic StanzaBase class. Methods: exception -- Overrides StanzaBase.exception """ def exception(self, e): """ Create and send an error reply. Typically called when an event handler raises an exception. The error's type and text content are based on the exception object's type and content. Overrides StanzaBase.exception. Arguments: e -- Exception object """ if isinstance(e, XMPPError): self.reply(clear=e.clear) # We raised this deliberately self['error']['condition'] = e.condition self['error']['text'] = e.text if e.extension is not None: # Extended error tag extxml = ET.Element("{%s}%s" % (e.extension_ns, e.extension), e.extension_args) self['error'].append(extxml) 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." self.send() # log the error log.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) # Finally raise the exception, so it can be handled (or not) # at a higher level by using sys.excepthook. raise e register_stanza_plugin(RootStanza, Error) fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/roster.py000066400000000000000000000075111157775340400224530ustar00rootroot00000000000000""" 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.stanza import Iq from sleekxmpp.xmlstream import JID from sleekxmpp.xmlstream import ET, ElementBase, register_stanza_plugin class Roster(ElementBase): """ Example roster stanzas: Friends Stanza Inteface: items -- A dictionary of roster entries contained in the stanza. Methods: get_items -- Return a dictionary of roster entries. set_items -- Add elements. del_items -- Remove all elements. """ namespace = 'jabber:iq:roster' name = 'query' plugin_attrib = 'roster' interfaces = set(('items',)) def set_items(self, items): """ Set the roster entries in the stanza. Uses a dictionary using JIDs as keys, where each entry is itself a dictionary that contains: name -- An alias or nickname for the JID. subscription -- The subscription type. Can be one of 'to', 'from', 'both', 'none', or 'remove'. groups -- A list of group names to which the JID has been assigned. Arguments: items -- A dictionary of roster entries. """ self.del_items() for jid in items: ijid = str(jid) item = ET.Element('{jabber:iq:roster}item', {'jid': ijid}) if 'subscription' in items[jid]: item.attrib['subscription'] = items[jid]['subscription'] if 'name' in items[jid]: name = items[jid]['name'] if name is not None: item.attrib['name'] = name if 'groups' in items[jid]: for group in items[jid]['groups']: groupxml = ET.Element('{jabber:iq:roster}group') groupxml.text = group item.append(groupxml) self.xml.append(item) return self def get_items(self): """ Return a dictionary of roster entries. Each item is keyed using its JID, and contains: name -- An assigned alias or nickname for the JID. subscription -- The subscription type. Can be one of 'to', 'from', 'both', 'none', or 'remove'. groups -- A list of group names to which the JID has been assigned. """ items = {} itemsxml = self.xml.findall('{jabber:iq:roster}item') if itemsxml is not None: for itemxml in itemsxml: item = {} item['name'] = itemxml.get('name', '') item['subscription'] = itemxml.get('subscription', '') item['groups'] = [] groupsxml = itemxml.findall('{jabber:iq:roster}group') if groupsxml is not None: for groupxml in groupsxml: item['groups'].append(groupxml.text) items[itemxml.get('jid')] = item return items def del_items(self): """ Remove all elements from the roster stanza. """ for child in self.xml.getchildren(): self.xml.remove(child) 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/stanza/stream_error.py000066400000000000000000000051741157775340400236440ustar00rootroot00000000000000""" 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.stanza.error import Error from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET from sleekxmpp.xmlstream import register_stanza_plugin class StreamError(Error, StanzaBase): """ XMPP stanzas of type 'error' should include an stanza that describes the nature of the error and how it should be handled. Use the 'XEP-0086: Error Condition Mappings' plugin to include error codes used in older XMPP versions. The stream:error stanza is used to provide more information for error that occur with the underlying XML stream itself, and not a particular stanza. Note: The StreamError stanza is mostly the same as the normal Error stanza, but with different namespaces and condition names. Example error stanza: XML was not well-formed. Stanza Interface: condition -- The name of the condition element. text -- Human readable description of the error. Attributes: conditions -- The set of allowable error condition elements. condition_ns -- The namespace for the condition element. Methods: setup -- Overrides ElementBase.setup. get_condition -- Retrieve the name of the condition element. set_condition -- Add a condition element. del_condition -- Remove the condition element. get_text -- Retrieve the contents of the element. set_text -- Set the contents of the element. del_text -- Remove the element. """ namespace = 'http://etherx.jabber.org/streams' interfaces = set(('condition', 'text')) conditions = set(( 'bad-format', 'bad-namespace-prefix', 'conflict', 'connection-timeout', 'host-gone', 'host-unknown', 'improper-addressing', 'internal-server-error', 'invalid-from', 'invalid-namespace', 'invalid-xml', 'not-authorized', 'not-well-formed', 'policy-violation', 'remote-connection-failed', 'reset', 'resource-constraint', 'restricted-xml', 'see-other-host', 'system-shutdown', 'undefined-condition', 'unsupported-encoding', 'unsupported-feature', 'unsupported-stanza-type', 'unsupported-version')) condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams' fritzy-SleekXMPP-5c4ee57/sleekxmpp/test/000077500000000000000000000000001157775340400202365ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/test/__init__.py000066400000000000000000000005161157775340400223510ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ from sleekxmpp.test.mocksocket import TestSocket from sleekxmpp.test.livesocket import TestLiveSocket from sleekxmpp.test.sleektest import * fritzy-SleekXMPP-5c4ee57/sleekxmpp/test/livesocket.py000066400000000000000000000112671157775340400227670ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import socket import threading try: import queue except ImportError: import Queue as queue class TestLiveSocket(object): """ A live test socket that reads and writes to queues in addition to an actual networking socket. Methods: next_sent -- Return the next sent stanza. next_recv -- Return the next received stanza. recv_data -- Dummy method to have same interface as TestSocket. recv -- Read the next stanza from the socket. send -- Write a stanza to the socket. makefile -- Dummy call, returns self. read -- Read the next stanza from the socket. """ def __init__(self, *args, **kwargs): """ Create a new, live test socket. Arguments: Same as arguments for socket.socket """ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.recv_buffer = [] self.recv_queue = queue.Queue() self.send_queue = queue.Queue() self.send_queue_lock = threading.Lock() self.recv_queue_lock = threading.Lock() self.is_live = True def __getattr__(self, name): """ Return attribute values of internal, live socket. Arguments: name -- Name of the attribute requested. """ return getattr(self.socket, name) # ------------------------------------------------------------------ # Testing Interface def disconnect_errror(self): """ Used to simulate a socket disconnection error. Not used by live sockets. """ try: self.socket.shutdown() self.socket.close() except: pass def next_sent(self, timeout=None): """ Get the next stanza that has been sent. Arguments: timeout -- Optional timeout for waiting for a new value. """ args = {'block': False} if timeout is not None: args = {'block': True, 'timeout': timeout} try: return self.send_queue.get(**args) except: return None def next_recv(self, timeout=None): """ Get the next stanza that has been received. Arguments: timeout -- Optional timeout for waiting for a new value. """ args = {'block': False} if timeout is not None: args = {'block': True, 'timeout': timeout} try: if self.recv_buffer: return self.recv_buffer.pop(0) else: return self.recv_queue.get(**args) except: return None def recv_data(self, data): """ Add data to a receive buffer for cases when more than a single stanza was received. """ self.recv_buffer.append(data) # ------------------------------------------------------------------ # Socket Interface def recv(self, *args, **kwargs): """ Read data from the socket. Store a copy in the receive queue. Arguments: Placeholders. Same as for socket.recv. """ data = self.socket.recv(*args, **kwargs) with self.recv_queue_lock: self.recv_queue.put(data) return data def send(self, data): """ Send data on the socket. Store a copy in the send queue. Arguments: data -- String value to write. """ with self.send_queue_lock: self.send_queue.put(data) self.socket.send(data) # ------------------------------------------------------------------ # File Socket def makefile(self, *args, **kwargs): """ File socket version to use with ElementTree. Arguments: Placeholders, same as socket.makefile() """ return self def read(self, *args, **kwargs): """ Implement the file socket read interface. Arguments: Placeholders, same as socket.recv() """ return self.recv(*args, **kwargs) def clear(self): """ Empty the send queue, typically done once the session has started to remove the feature negotiation and log in stanzas. """ with self.send_queue_lock: for i in range(0, self.send_queue.qsize()): self.send_queue.get(block=False) with self.recv_queue_lock: for i in range(0, self.recv_queue.qsize()): self.recv_queue.get(block=False) fritzy-SleekXMPP-5c4ee57/sleekxmpp/test/mocksocket.py000066400000000000000000000102451157775340400227540ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import socket try: import queue except ImportError: import Queue as queue class TestSocket(object): """ A dummy socket that reads and writes to queues instead of an actual networking socket. Methods: next_sent -- Return the next sent stanza. recv_data -- Make a stanza available to read next. recv -- Read the next stanza from the socket. send -- Write a stanza to the socket. makefile -- Dummy call, returns self. read -- Read the next stanza from the socket. """ def __init__(self, *args, **kwargs): """ Create a new test socket. Arguments: Same as arguments for socket.socket """ self.socket = socket.socket(*args, **kwargs) self.recv_queue = queue.Queue() self.send_queue = queue.Queue() self.is_live = False self.disconnected = False def __getattr__(self, name): """ Return attribute values of internal, dummy socket. Some attributes and methods are disabled to prevent the socket from connecting to the network. Arguments: name -- Name of the attribute requested. """ def dummy(*args): """Method to do nothing and prevent actual socket connections.""" return None overrides = {'connect': dummy, 'close': dummy, 'shutdown': dummy} return overrides.get(name, getattr(self.socket, name)) # ------------------------------------------------------------------ # Testing Interface def next_sent(self, timeout=None): """ Get the next stanza that has been 'sent'. Arguments: timeout -- Optional timeout for waiting for a new value. """ args = {'block': False} if timeout is not None: args = {'block': True, 'timeout': timeout} try: return self.send_queue.get(**args) except: return None def recv_data(self, data): """ Add data to the receiving queue. Arguments: data -- String data to 'write' to the socket to be received by the XMPP client. """ self.recv_queue.put(data) def disconnect_error(self): """ Simulate a disconnect error by raising a socket.error exception for any current or further socket operations. """ self.disconnected = True # ------------------------------------------------------------------ # Socket Interface def recv(self, *args, **kwargs): """ Read a value from the received queue. Arguments: Placeholders. Same as for socket.Socket.recv. """ if self.disconnected: raise socket.error return self.read(block=True) def send(self, data): """ Send data by placing it in the send queue. Arguments: data -- String value to write. """ if self.disconnected: raise socket.error self.send_queue.put(data) # ------------------------------------------------------------------ # File Socket def makefile(self, *args, **kwargs): """ File socket version to use with ElementTree. Arguments: Placeholders, same as socket.Socket.makefile() """ return self def read(self, block=True, timeout=None, **kwargs): """ Implement the file socket interface. Arguments: block -- Indicate if the read should block until a value is ready. timeout -- Time in seconds a block should last before returning None. """ if self.disconnected: raise socket.error if timeout is not None: block = True try: return self.recv_queue.get(block, timeout) except: return None fritzy-SleekXMPP-5c4ee57/sleekxmpp/test/sleektest.py000066400000000000000000000720221157775340400226160ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permission. """ import unittest try: import Queue as queue except: import queue import sleekxmpp from sleekxmpp import ClientXMPP, ComponentXMPP from sleekxmpp.stanza import Message, Iq, Presence from sleekxmpp.test import TestSocket, TestLiveSocket from sleekxmpp.xmlstream import ET, register_stanza_plugin from sleekxmpp.xmlstream import ElementBase, StanzaBase from sleekxmpp.xmlstream.tostring import tostring from sleekxmpp.xmlstream.matcher import StanzaPath, MatcherId from sleekxmpp.xmlstream.matcher import MatchXMLMask, MatchXPath class SleekTest(unittest.TestCase): """ A SleekXMPP specific TestCase class that provides methods for comparing message, iq, and presence stanzas. Methods: Message -- Create a Message stanza object. Iq -- Create an Iq stanza object. Presence -- Create a Presence stanza object. check_jid -- Check a JID and its component parts. check -- Compare a stanza against an XML string. stream_start -- Initialize a dummy XMPP client. stream_close -- Disconnect the XMPP client. make_header -- Create a stream header. send_header -- Check that the given header has been sent. send_feature -- Send a raw XML element. send -- Check that the XMPP client sent the given generic stanza. recv -- Queue data for XMPP client to receive, or verify the data that was received from a live connection. recv_header -- Check that a given stream header was received. recv_feature -- Check that a given, raw XML element was recveived. fix_namespaces -- Add top-level namespace to an XML object. compare -- Compare XML objects against each other. """ def __init__(self, *args, **kwargs): unittest.TestCase.__init__(self, *args, **kwargs) self.xmpp = None def runTest(self): pass def parse_xml(self, xml_string): try: xml = ET.fromstring(xml_string) return xml except SyntaxError as e: if 'unbound' in e.msg: known_prefixes = { 'stream': 'http://etherx.jabber.org/streams'} prefix = xml_string.split('<')[1].split(':')[0] if prefix in known_prefixes: xml_string = '%s' % ( prefix, known_prefixes[prefix], xml_string) xml = self.parse_xml(xml_string) xml = xml.getchildren()[0] return xml else: self.fail("XML data was mal-formed:\n%s" % xml_string) # ------------------------------------------------------------------ # Shortcut methods for creating stanza objects def Message(self, *args, **kwargs): """ Create a Message stanza. Uses same arguments as StanzaBase.__init__ Arguments: xml -- An XML object to use for the Message's values. """ return Message(self.xmpp, *args, **kwargs) def Iq(self, *args, **kwargs): """ Create an Iq stanza. Uses same arguments as StanzaBase.__init__ Arguments: xml -- An XML object to use for the Iq's values. """ return Iq(self.xmpp, *args, **kwargs) def Presence(self, *args, **kwargs): """ Create a Presence stanza. Uses same arguments as StanzaBase.__init__ Arguments: xml -- An XML object to use for the Iq's values. """ return Presence(self.xmpp, *args, **kwargs) def check_jid(self, jid, user=None, domain=None, resource=None, bare=None, full=None, string=None): """ Verify the components of a JID. Arguments: jid -- The JID object to test. user -- Optional. The user name portion of the JID. domain -- Optional. The domain name portion of the JID. resource -- Optional. The resource portion of the JID. bare -- Optional. The bare JID. full -- Optional. The full JID. string -- Optional. The string version of the JID. """ if user is not None: self.assertEqual(jid.user, user, "User does not match: %s" % jid.user) if domain is not None: self.assertEqual(jid.domain, domain, "Domain does not match: %s" % jid.domain) if resource is not None: self.assertEqual(jid.resource, resource, "Resource does not match: %s" % jid.resource) if bare is not None: self.assertEqual(jid.bare, bare, "Bare JID does not match: %s" % jid.bare) if full is not None: self.assertEqual(jid.full, full, "Full JID does not match: %s" % jid.full) if string is not None: self.assertEqual(str(jid), string, "String does not match: %s" % str(jid)) # ------------------------------------------------------------------ # Methods for comparing stanza objects to XML strings def check(self, stanza, criteria, method='exact', defaults=None, use_values=True): """ Create and compare several stanza objects to a correct XML string. If use_values is False, tests using stanza.values will not be used. Some stanzas provide default values for some interfaces, but these defaults can be problematic for testing since they can easily be forgotten when supplying the XML string. A list of interfaces that use defaults may be provided and the generated stanzas will use the default values for those interfaces if needed. However, correcting the supplied XML is not possible for interfaces that add or remove XML elements. Only interfaces that map to XML attributes may be set using the defaults parameter. The supplied XML must take into account any extra elements that are included by default. Arguments: stanza -- The stanza object to test. criteria -- An expression the stanza must match against. method -- The type of matching to use; one of: 'exact', 'mask', 'id', 'xpath', and 'stanzapath'. Defaults to the value of self.match_method. defaults -- A list of stanza interfaces that have default values. These interfaces will be set to their defaults for the given and generated stanzas to prevent unexpected test failures. use_values -- Indicates if testing using stanza.values should be used. Defaults to True. """ if method is None and hasattr(self, 'match_method'): method = getattr(self, 'match_method') if method != 'exact': matchers = {'stanzapath': StanzaPath, 'xpath': MatchXPath, 'mask': MatchXMLMask, 'id': MatcherId} Matcher = matchers.get(method, None) if Matcher is None: raise ValueError("Unknown matching method.") test = Matcher(criteria) self.failUnless(test.match(stanza), "Stanza did not match using %s method:\n" % method + \ "Criteria:\n%s\n" % str(criteria) + \ "Stanza:\n%s" % str(stanza)) else: stanza_class = stanza.__class__ if not isinstance(criteria, ElementBase): xml = self.parse_xml(criteria) else: xml = criteria.xml # Ensure that top level namespaces are used, even if they # were not provided. self.fix_namespaces(stanza.xml, 'jabber:client') self.fix_namespaces(xml, 'jabber:client') stanza2 = stanza_class(xml=xml) if use_values: # Using stanza.values will add XML for any interface that # has a default value. We need to set those defaults on # the existing stanzas and XML so that they will compare # correctly. default_stanza = stanza_class() if defaults is None: known_defaults = { Message: ['type'], Presence: ['priority'] } defaults = known_defaults.get(stanza_class, []) for interface in defaults: stanza[interface] = stanza[interface] stanza2[interface] = stanza2[interface] # Can really only automatically add defaults for top # level attribute values. Anything else must be accounted # for in the provided XML string. if interface not in xml.attrib: if interface in default_stanza.xml.attrib: value = default_stanza.xml.attrib[interface] xml.attrib[interface] = value values = stanza2.values stanza3 = stanza_class() stanza3.values = values debug = "Three methods for creating stanzas do not match.\n" debug += "Given XML:\n%s\n" % tostring(xml) debug += "Given stanza:\n%s\n" % tostring(stanza.xml) debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml) debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml) result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml) else: debug = "Two methods for creating stanzas do not match.\n" debug += "Given XML:\n%s\n" % tostring(xml) debug += "Given stanza:\n%s\n" % tostring(stanza.xml) debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml) result = self.compare(xml, stanza.xml, stanza2.xml) self.failUnless(result, debug) # ------------------------------------------------------------------ # Methods for simulating stanza streams. def stream_disconnect(self): """ Simulate a stream disconnection. """ if self.xmpp: self.xmpp.socket.disconnect_error() def stream_start(self, mode='client', skip=True, header=None, socket='mock', jid='tester@localhost', password='test', server='localhost', port=5222, plugins=None): """ Initialize an XMPP client or component using a dummy XML stream. Arguments: mode -- Either 'client' or 'component'. Defaults to 'client'. skip -- Indicates if the first item in the sent queue (the stream header) should be removed. Tests that wish to test initializing the stream should set this to False. Otherwise, the default of True should be used. socket -- Either 'mock' or 'live' to indicate if the socket should be a dummy, mock socket or a live, functioning socket. Defaults to 'mock'. jid -- The JID to use for the connection. Defaults to 'tester@localhost'. password -- The password to use for the connection. Defaults to 'test'. server -- The name of the XMPP server. Defaults to 'localhost'. port -- The port to use when connecting to the server. Defaults to 5222. plugins -- List of plugins to register. By default, all plugins are loaded. """ if mode == 'client': self.xmpp = ClientXMPP(jid, password) elif mode == 'component': self.xmpp = ComponentXMPP(jid, password, server, port) else: raise ValueError("Unknown XMPP connection mode.") # We will use this to wait for the session_start event # for live connections. skip_queue = queue.Queue() if socket == 'mock': self.xmpp.set_socket(TestSocket()) # Simulate connecting for mock sockets. self.xmpp.auto_reconnect = False self.xmpp.is_client = True self.xmpp.state._set_state('connected') # Must have the stream header ready for xmpp.process() to work. if not header: header = self.xmpp.stream_header self.xmpp.socket.recv_data(header) elif socket == 'live': self.xmpp.socket_class = TestLiveSocket def wait_for_session(x): self.xmpp.socket.clear() skip_queue.put('started') self.xmpp.add_event_handler('session_start', wait_for_session) self.xmpp.connect() else: raise ValueError("Unknown socket type.") if plugins is None: self.xmpp.register_plugins() else: for plugin in plugins: self.xmpp.register_plugin(plugin) self.xmpp.process(threaded=True) if skip: if socket != 'live': # Mark send queue as usable self.xmpp.session_started_event.set() # Clear startup stanzas self.xmpp.socket.next_sent(timeout=1) if mode == 'component': self.xmpp.socket.next_sent(timeout=1) else: skip_queue.get(block=True, timeout=10) def make_header(self, sto='', sfrom='', sid='', stream_ns="http://etherx.jabber.org/streams", default_ns="jabber:client", version="1.0", xml_header=True): """ Create a stream header to be received by the test XMPP agent. The header must be saved and passed to stream_start. Arguments: sto -- The recipient of the stream header. sfrom -- The agent sending the stream header. sid -- The stream's id. stream_ns -- The namespace of the stream's root element. default_ns -- The default stanza namespace. version -- The stream version. xml_header -- Indicates if the XML version header should be appended before the stream header. """ header = '' parts = [] if xml_header: header = '' + header if sto: parts.append('to="%s"' % sto) if sfrom: parts.append('from="%s"' % sfrom) if sid: parts.append('id="%s"' % sid) parts.append('version="%s"' % version) parts.append('xmlns:stream="%s"' % stream_ns) parts.append('xmlns="%s"' % default_ns) return header % ' '.join(parts) def recv(self, data, defaults=[], method='exact', use_values=True, timeout=1): """ Pass data to the dummy XMPP client as if it came from an XMPP server. If using a live connection, verify what the server has sent. Arguments: data -- If a dummy socket is being used, the XML that is to be received next. Otherwise it is the criteria used to match against live data that is received. defaults -- A list of stanza interfaces with default values that may interfere with comparisons. method -- Select the type of comparison to use for verifying the received stanza. Options are 'exact', 'id', 'stanzapath', 'xpath', and 'mask'. Defaults to the value of self.match_method. use_values -- Indicates if stanza comparisons should test using stanza.values. Defaults to True. timeout -- Time to wait in seconds for data to be received by a live connection. """ if self.xmpp.socket.is_live: # we are working with a live connection, so we should # verify what has been received instead of simulating # receiving data. recv_data = self.xmpp.socket.next_recv(timeout) if recv_data is None: self.fail("No stanza was received.") xml = self.parse_xml(recv_data) self.fix_namespaces(xml, 'jabber:client') stanza = self.xmpp._build_stanza(xml, 'jabber:client') self.check(stanza, data, method=method, defaults=defaults, use_values=use_values) else: # place the data in the dummy socket receiving queue. data = str(data) self.xmpp.socket.recv_data(data) def recv_header(self, sto='', sfrom='', sid='', stream_ns="http://etherx.jabber.org/streams", default_ns="jabber:client", version="1.0", xml_header=False, timeout=1): """ Check that a given stream header was received. Arguments: sto -- The recipient of the stream header. sfrom -- The agent sending the stream header. sid -- The stream's id. Set to None to ignore. stream_ns -- The namespace of the stream's root element. default_ns -- The default stanza namespace. version -- The stream version. xml_header -- Indicates if the XML version header should be appended before the stream header. timeout -- Length of time to wait in seconds for a response. """ header = self.make_header(sto, sfrom, sid, stream_ns=stream_ns, default_ns=default_ns, version=version, xml_header=xml_header) recv_header = self.xmpp.socket.next_recv(timeout) if recv_header is None: raise ValueError("Socket did not return data.") # Apply closing elements so that we can construct # XML objects for comparison. header2 = header + '' recv_header2 = recv_header + '' xml = self.parse_xml(header2) recv_xml = self.parse_xml(recv_header2) if sid is None: # Ignore the id sent by the server since # we can't know in advance what it will be. if 'id' in recv_xml.attrib: del recv_xml.attrib['id'] # Ignore the xml:lang attribute for now. if 'xml:lang' in recv_xml.attrib: del recv_xml.attrib['xml:lang'] xml_ns = 'http://www.w3.org/XML/1998/namespace' if '{%s}lang' % xml_ns in recv_xml.attrib: del recv_xml.attrib['{%s}lang' % xml_ns] if recv_xml.getchildren: # We received more than just the header for xml in recv_xml.getchildren(): self.xmpp.socket.recv_data(tostring(xml)) attrib = recv_xml.attrib recv_xml.clear() recv_xml.attrib = attrib self.failUnless( self.compare(xml, recv_xml), "Stream headers do not match:\nDesired:\n%s\nReceived:\n%s" % ( '%s %s' % (xml.tag, xml.attrib), '%s %s' % (recv_xml.tag, recv_xml.attrib))) def recv_feature(self, data, method='mask', use_values=True, timeout=1): """ """ if method is None and hasattr(self, 'match_method'): method = getattr(self, 'match_method') if self.xmpp.socket.is_live: # we are working with a live connection, so we should # verify what has been received instead of simulating # receiving data. recv_data = self.xmpp.socket.next_recv(timeout) xml = self.parse_xml(data) recv_xml = self.parse_xml(recv_data) if recv_data is None: self.fail("No stanza was received.") if method == 'exact': self.failUnless(self.compare(xml, recv_xml), "Features do not match.\nDesired:\n%s\nReceived:\n%s" % ( tostring(xml), tostring(recv_xml))) elif method == 'mask': matcher = MatchXMLMask(xml) self.failUnless(matcher.match(recv_xml), "Stanza did not match using %s method:\n" % method + \ "Criteria:\n%s\n" % tostring(xml) + \ "Stanza:\n%s" % tostring(recv_xml)) else: raise ValueError("Uknown matching method: %s" % method) else: # place the data in the dummy socket receiving queue. data = str(data) self.xmpp.socket.recv_data(data) def send_header(self, sto='', sfrom='', sid='', stream_ns="http://etherx.jabber.org/streams", default_ns="jabber:client", version="1.0", xml_header=False, timeout=1): """ Check that a given stream header was sent. Arguments: sto -- The recipient of the stream header. sfrom -- The agent sending the stream header. sid -- The stream's id. stream_ns -- The namespace of the stream's root element. default_ns -- The default stanza namespace. version -- The stream version. xml_header -- Indicates if the XML version header should be appended before the stream header. timeout -- Length of time to wait in seconds for a response. """ header = self.make_header(sto, sfrom, sid, stream_ns=stream_ns, default_ns=default_ns, version=version, xml_header=xml_header) sent_header = self.xmpp.socket.next_sent(timeout) if sent_header is None: raise ValueError("Socket did not return data.") # Apply closing elements so that we can construct # XML objects for comparison. header2 = header + '' sent_header2 = sent_header + b'' xml = self.parse_xml(header2) sent_xml = self.parse_xml(sent_header2) self.failUnless( self.compare(xml, sent_xml), "Stream headers do not match:\nDesired:\n%s\nSent:\n%s" % ( header, sent_header)) def send_feature(self, data, method='mask', use_values=True, timeout=1): """ """ sent_data = self.xmpp.socket.next_sent(timeout) xml = self.parse_xml(data) sent_xml = self.parse_xml(sent_data) if sent_data is None: self.fail("No stanza was sent.") if method == 'exact': self.failUnless(self.compare(xml, sent_xml), "Features do not match.\nDesired:\n%s\nReceived:\n%s" % ( tostring(xml), tostring(sent_xml))) elif method == 'mask': matcher = MatchXMLMask(xml) self.failUnless(matcher.match(sent_xml), "Stanza did not match using %s method:\n" % method + \ "Criteria:\n%s\n" % tostring(xml) + \ "Stanza:\n%s" % tostring(sent_xml)) else: raise ValueError("Uknown matching method: %s" % method) def send(self, data, defaults=None, use_values=True, timeout=.5, method='exact'): """ Check that the XMPP client sent the given stanza XML. Extracts the next sent stanza and compares it with the given XML using check. Arguments: stanza_class -- The class of the sent stanza object. data -- The XML string of the expected Message stanza, or an equivalent stanza object. use_values -- Modifies the type of tests used by check_message. defaults -- A list of stanza interfaces that have defaults values which may interfere with comparisons. timeout -- Time in seconds to wait for a stanza before failing the check. method -- Select the type of comparison to use for verifying the sent stanza. Options are 'exact', 'id', 'stanzapath', 'xpath', and 'mask'. Defaults to the value of self.match_method. """ sent = self.xmpp.socket.next_sent(timeout) if data is None and sent is None: return if data is None and sent is not None: self.fail("Stanza data was sent: %s" % sent) if sent is None: self.fail("No stanza was sent.") xml = self.parse_xml(sent) self.fix_namespaces(xml, 'jabber:client') sent = self.xmpp._build_stanza(xml, 'jabber:client') self.check(sent, data, method=method, defaults=defaults, use_values=use_values) def stream_close(self): """ Disconnect the dummy XMPP client. Can be safely called even if stream_start has not been called. Must be placed in the tearDown method of a test class to ensure that the XMPP client is disconnected after an error. """ if hasattr(self, 'xmpp') and self.xmpp is not None: self.xmpp.socket.recv_data(self.xmpp.stream_footer) self.xmpp.disconnect() # ------------------------------------------------------------------ # XML Comparison and Cleanup def fix_namespaces(self, xml, ns): """ Assign a namespace to an element and any children that don't have a namespace. Arguments: xml -- The XML object to fix. ns -- The namespace to add to the XML object. """ if xml.tag.startswith('{'): return xml.tag = '{%s}%s' % (ns, xml.tag) for child in xml.getchildren(): self.fix_namespaces(child, ns) def compare(self, xml, *other): """ Compare XML objects. Arguments: xml -- The XML object to compare against. *other -- The list of XML objects to compare. """ if not other: return False # Compare multiple objects if len(other) > 1: for xml2 in other: if not self.compare(xml, xml2): return False return True other = other[0] # Step 1: Check tags if xml.tag != other.tag: return False # Step 2: Check attributes if xml.attrib != other.attrib: return False # Step 3: Check text if xml.text is None: xml.text = "" if other.text is None: other.text = "" xml.text = xml.text.strip() other.text = other.text.strip() if xml.text != other.text: return False # Step 4: Check children count if len(xml.getchildren()) != len(other.getchildren()): return False # Step 5: Recursively check children for child in xml: child2s = other.findall("%s" % child.tag) if child2s is None: return False for child2 in child2s: if self.compare(child, child2): break else: return False # Step 6: Recursively check children the other way. for child in other: child2s = xml.findall("%s" % child.tag) if child2s is None: return False for child2 in child2s: if self.compare(child, child2): break else: return False # Everything matches return True fritzy-SleekXMPP-5c4ee57/sleekxmpp/thirdparty/000077500000000000000000000000001157775340400214515ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/thirdparty/__init__.py000066400000000000000000000001621157775340400235610ustar00rootroot00000000000000try: from collections import OrderedDict except: from sleekxmpp.thirdparty.ordereddict import OrderedDict fritzy-SleekXMPP-5c4ee57/sleekxmpp/thirdparty/ordereddict.py000066400000000000000000000101751157775340400243170ustar00rootroot00000000000000# Copyright (c) 2009 Raymond Hettinger # # Permission is hereby granted, free of charge, to any person # obtaining a copy of this software and associated documentation files # (the "Software"), to deal in the Software without restriction, # including without limitation the rights to use, copy, modify, merge, # publish, distribute, sublicense, and/or sell copies of the Software, # and to permit persons to whom the Software is furnished to do so, # subject to the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. from UserDict import DictMixin class OrderedDict(dict, DictMixin): def __init__(self, *args, **kwds): if len(args) > 1: raise TypeError('expected at most 1 arguments, got %d' % len(args)) try: self.__end except AttributeError: self.clear() self.update(*args, **kwds) def clear(self): self.__end = end = [] end += [None, end, end] # sentinel node for doubly linked list self.__map = {} # key --> [key, prev, next] dict.clear(self) def __setitem__(self, key, value): if key not in self: end = self.__end curr = end[1] curr[2] = end[1] = self.__map[key] = [key, curr, end] dict.__setitem__(self, key, value) def __delitem__(self, key): dict.__delitem__(self, key) key, prev, next = self.__map.pop(key) prev[2] = next next[1] = prev def __iter__(self): end = self.__end curr = end[2] while curr is not end: yield curr[0] curr = curr[2] def __reversed__(self): end = self.__end curr = end[1] while curr is not end: yield curr[0] curr = curr[1] def popitem(self, last=True): if not self: raise KeyError('dictionary is empty') if last: key = reversed(self).next() else: key = iter(self).next() value = self.pop(key) return key, value def __reduce__(self): items = [[k, self[k]] for k in self] tmp = self.__map, self.__end del self.__map, self.__end inst_dict = vars(self).copy() self.__map, self.__end = tmp if inst_dict: return (self.__class__, (items,), inst_dict) return self.__class__, (items,) def keys(self): return list(self) setdefault = DictMixin.setdefault update = DictMixin.update pop = DictMixin.pop values = DictMixin.values items = DictMixin.items iterkeys = DictMixin.iterkeys itervalues = DictMixin.itervalues iteritems = DictMixin.iteritems def __repr__(self): if not self: return '%s()' % (self.__class__.__name__,) return '%s(%r)' % (self.__class__.__name__, self.items()) def copy(self): return self.__class__(self) @classmethod def fromkeys(cls, iterable, value=None): d = cls() for key in iterable: d[key] = value return d def __eq__(self, other): if isinstance(other, OrderedDict): if len(self) != len(other): return False for p, q in zip(self.items(), other.items()): if p != q: return False return True return dict.__eq__(self, other) def __ne__(self, other): return not self == other fritzy-SleekXMPP-5c4ee57/sleekxmpp/thirdparty/statemachine.py000066400000000000000000000271741157775340400245030ustar00rootroot00000000000000""" 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 threading import time import logging log = logging.getLogger(__name__) class StateMachine(object): def __init__(self, states=[]): self.lock = threading.Lock() self.notifier = threading.Event() self.__states = [] self.addStates(states) self.__default_state = self.__states[0] self.__current_state = self.__default_state def addStates(self, states): self.lock.acquire() try: for state in states: if state in self.__states: raise IndexError("The state '%s' is already in the StateMachine." % state) self.__states.append(state) finally: self.lock.release() def transition(self, from_state, to_state, wait=0.0, func=None, args=[], kwargs={}): ''' Transition from the given `from_state` to the given `to_state`. This method will return `True` if the state machine is now in `to_state`. It will return `False` if a timeout occurred the transition did not occur. If `wait` is 0 (the default,) this method returns immediately if the state machine is not in `from_state`. If you want the thread to block and transition once the state machine to enters `from_state`, set `wait` to a non-negative value. Note there is no 'block indefinitely' flag since this leads to deadlock. If you want to wait indefinitely, choose a reasonable value for `wait` (e.g. 20 seconds) and do so in a while loop like so: :: while not thread_should_exit and not state_machine.transition('disconnected', 'connecting', wait=20 ): pass # timeout will occur every 20s unless transition occurs if thread_should_exit: return # perform actions here after successful transition This allows the thread to be responsive by setting `thread_should_exit=True`. The optional `func` argument allows the user to pass a callable operation which occurs within the context of the state transition (e.g. while the state machine is locked.) If `func` returns a True value, the transition will occur. If `func` returns a non- True value or if an exception is thrown, the transition will not occur. Any thrown exception is not caught by the state machine and is the caller's responsibility to handle. If `func` completes normally, this method will return the value returned by `func.` If values for `args` and `kwargs` are provided, they are expanded and passed like so: `func( *args, **kwargs )`. ''' return self.transition_any((from_state,), to_state, wait=wait, func=func, args=args, kwargs=kwargs) def transition_any(self, from_states, to_state, wait=0.0, func=None, args=[], kwargs={}): ''' Transition from any of the given `from_states` to the given `to_state`. ''' if not (isinstance(from_states,tuple) or isinstance(from_states,list)): raise ValueError("from_states should be a list or tuple") for state in from_states: if not state in self.__states: raise ValueError("StateMachine does not contain from_state %s." % state) if not to_state in self.__states: raise ValueError("StateMachine does not contain to_state %s." % to_state) start = time.time() while not self.lock.acquire(False): time.sleep(.001) if (start + wait - time.time()) <= 0.0: log.debug("Could not acquire lock") return False while not self.__current_state in from_states: # detect timeout: remainder = start + wait - time.time() if remainder > 0: self.notifier.wait(remainder) else: log.debug("State was not ready") self.lock.release() return False try: # lock is acquired; all other threads will return false or wait until notify/timeout if self.__current_state in from_states: # should always be True due to lock # Note that func might throw an exception, but that's OK, it aborts the transition return_val = func(*args,**kwargs) if func is not None else True # some 'false' value returned from func, # indicating that transition should not occur: if not return_val: return return_val log.debug(' ==== TRANSITION %s -> %s', self.__current_state, to_state) self._set_state(to_state) return return_val # some 'true' value returned by func or True if func was None else: log.error("StateMachine bug!! The lock should ensure this doesn't happen!") return False finally: self.notifier.set() # notify any waiting threads that the state has changed. self.notifier.clear() self.lock.release() def transition_ctx(self, from_state, to_state, wait=0.0): ''' Use the state machine as a context manager. The transition occurs on /exit/ from the `with` context, so long as no exception is thrown. For example: :: with state_machine.transition_ctx('one','two', wait=5) as locked: if locked: # the state machine is currently locked in state 'one', and will # transition to 'two' when the 'with' statement ends, so long as # no exception is thrown. print 'Currently locked in state one: %s' % state_machine['one'] else: # The 'wait' timed out, and no lock has been acquired print 'Timed out before entering state "one"' print 'Since no exception was thrown, we are now in state "two": %s' % state_machine['two'] The other main difference between this method and `transition()` is that the state machine is locked for the duration of the `with` statement. Normally, after a `transition()` occurs, the state machine is immediately unlocked and available to another thread to call `transition()` again. ''' if not from_state in self.__states: raise ValueError("StateMachine does not contain from_state %s." % from_state) if not to_state in self.__states: raise ValueError("StateMachine does not contain to_state %s." % to_state) return _StateCtx(self, from_state, to_state, wait) def ensure(self, state, wait=0.0, block_on_transition=False): ''' Ensure the state machine is currently in `state`, or wait until it enters `state`. ''' return self.ensure_any((state,), wait=wait, block_on_transition=block_on_transition) def ensure_any(self, states, wait=0.0, block_on_transition=False): ''' Ensure we are currently in one of the given `states` or wait until we enter one of those states. Note that due to the nature of the function, you cannot guarantee that the entirety of some operation completes while you remain in a given state. That would require acquiring and holding a lock, which would mean no other threads could do the same. (You'd essentially be serializing all of the threads that are 'ensuring' their tasks occurred in some state. ''' if not (isinstance(states,tuple) or isinstance(states,list)): raise ValueError('states arg should be a tuple or list') for state in states: if not state in self.__states: raise ValueError("StateMachine does not contain state '%s'" % state) # if we're in the middle of a transition, determine whether we should # 'fall back' to the 'current' state, or wait for the new state, in order to # avoid an operation occurring in the wrong state. # TODO another option would be an ensure_ctx that uses a semaphore to allow # threads to indicate they want to remain in a particular state. # will return immediately if no transition is in process. if block_on_transition: # we're not in the middle of a transition; don't hold the lock if self.lock.acquire(False): self.lock.release() # wait for the transition to complete else: self.notifier.wait() start = time.time() while not self.__current_state in states: # detect timeout: remainder = start + wait - time.time() if remainder > 0: self.notifier.wait(remainder) else: return False return True def reset(self): # TODO need to lock before calling this? self.transition(self.__current_state, self.__default_state) def _set_state(self, state): #unsynchronized, only call internally after lock is acquired self.__current_state = state return state def current_state(self): ''' Return the current state name. ''' return self.__current_state def __getitem__(self, state): ''' Non-blocking, non-synchronized test to determine if we are in the given state. Use `StateMachine.ensure(state)` to wait until the machine enters a certain state. ''' return self.__current_state == state def __str__(self): return "".join(("StateMachine(", ','.join(self.__states), "): ", self.__current_state)) class _StateCtx: def __init__(self, state_machine, from_state, to_state, wait): self.state_machine = state_machine self.from_state = from_state self.to_state = to_state self.wait = wait self._locked = False def __enter__(self): start = time.time() while not self.state_machine[self.from_state] or not self.state_machine.lock.acquire(False): # detect timeout: remainder = start + self.wait - time.time() if remainder > 0: self.state_machine.notifier.wait(remainder) else: log.debug('StateMachine timeout while waiting for state: %s', self.from_state) return False self._locked = True # lock has been acquired at this point self.state_machine.notifier.clear() log.debug('StateMachine entered context in state: %s', self.state_machine.current_state()) return True def __exit__(self, exc_type, exc_val, exc_tb): if exc_val is not None: log.exception("StateMachine exception in context, remaining in state: %s\n%s:%s", self.state_machine.current_state(), exc_type.__name__, exc_val) if self._locked: if exc_val is None: log.debug(' ==== TRANSITION %s -> %s', self.state_machine.current_state(), self.to_state) self.state_machine._set_state(self.to_state) self.state_machine.notifier.set() self.state_machine.lock.release() return False # re-raise any exception if __name__ == '__main__': def callback(s, s2): print((1, s.transition('on', 'off', wait=0.0, func=callback, args=[s,s2]))) print((2, s2.transition('off', 'on', func=callback, args=[s,s2]))) return True s = StateMachine(('off', 'on')) s2 = StateMachine(('off', 'on')) print((3, s.transition('off', 'on', wait=0.0, func=callback, args=[s,s2]),)) print((s.current_state(), s2.current_state())) fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/000077500000000000000000000000001157775340400212735ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/__init__.py000066400000000000000000000013541157775340400234070ustar00rootroot00000000000000""" 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.xmlstream.jid import JID from sleekxmpp.xmlstream.scheduler import Scheduler from sleekxmpp.xmlstream.stanzabase import StanzaBase, ElementBase, ET from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin from sleekxmpp.xmlstream.tostring import tostring from sleekxmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT from sleekxmpp.xmlstream.xmlstream import RestartStream __all__ = ['JID', 'Scheduler', 'StanzaBase', 'ElementBase', 'ET', 'StateMachine', 'tostring', 'XMLStream', 'RESPONSE_TIMEOUT', 'RestartStream'] fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/filesocket.py000066400000000000000000000023641157775340400240020ustar00rootroot00000000000000""" 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 socket import _fileobject import socket class FileSocket(_fileobject): """ Create a file object wrapper for a socket to work around issues present in Python 2.6 when using sockets as file objects. The parser for xml.etree.cElementTree requires a file, but we will be reading from the XMPP connection socket instead. """ def read(self, size=4096): """Read data from the socket as if it were a file.""" if self._sock is None: return None data = self._sock.recv(size) if data is not None: return data class Socket26(socket._socketobject): """ A custom socket implementation that uses our own FileSocket class to work around issues in Python 2.6 when using sockets as files. """ def makefile(self, mode='r', bufsize=-1): """makefile([mode[, bufsize]]) -> file object Return a regular file object corresponding to the socket. The mode and bufsize arguments are as for the built-in open() function.""" return FileSocket(self._sock, mode, bufsize) fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/handler/000077500000000000000000000000001157775340400227105ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/handler/__init__.py000066400000000000000000000007321157775340400250230ustar00rootroot00000000000000""" 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.xmlstream.handler.callback import Callback from sleekxmpp.xmlstream.handler.waiter import Waiter from sleekxmpp.xmlstream.handler.xmlcallback import XMLCallback from sleekxmpp.xmlstream.handler.xmlwaiter import XMLWaiter __all__ = ['Callback', 'Waiter', 'XMLCallback', 'XMLWaiter'] fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/handler/base.py000066400000000000000000000054451157775340400242040ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. """ class BaseHandler(object): """ Base class for stream handlers. Stream handlers are matched with incoming stanzas so that the stanza may be processed in some way. Stanzas may be matched with multiple handlers. Handler execution may take place in two phases. The first is during the stream processing itself. The second is after stream processing and during SleekXMPP's main event loop. The prerun method is used for execution during stream processing, and the run method is used during the main event loop. Attributes: name -- The name of the handler. stream -- The stream this handler is assigned to. Methods: match -- Compare a stanza with the handler's matcher. prerun -- Handler execution during stream processing. run -- Handler execution during the main event loop. check_delete -- Indicate if the handler may be removed from use. """ def __init__(self, name, matcher, stream=None): """ Create a new stream handler. Arguments: name -- The name of the handler. matcher -- A matcher object from xmlstream.matcher that will be used to determine if a stanza should be accepted by this handler. stream -- The XMLStream instance the handler should monitor. """ self.name = name self.stream = stream self._destroy = False self._payload = None self._matcher = matcher if stream is not None: stream.registerHandler(self) def match(self, xml): """ Compare a stanza or XML object with the handler's matcher. Arguments xml -- An XML or stanza object. """ return self._matcher.match(xml) def prerun(self, payload): """ Prepare the handler for execution while the XML stream is being processed. Arguments: payload -- A stanza object. """ self._payload = payload def run(self, payload): """ Execute the handler after XML stream processing and during the main event loop. Arguments: payload -- A stanza object. """ self._payload = payload def check_delete(self): """ Check if the handler should be removed from the list of stream 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 fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/handler/callback.py000066400000000000000000000054771157775340400250330ustar00rootroot00000000000000""" 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.xmlstream.handler.base import BaseHandler class Callback(BaseHandler): """ The Callback handler will execute a callback function with matched stanzas. The handler may execute the callback either during stream processing or during the main event loop. Callback functions are all executed in the same thread, so be aware if you are executing functions that will block for extended periods of time. Typically, you should signal your own events using the SleekXMPP object's event() method to pass the stanza off to a threaded event handler for further processing. Methods: prerun -- Overrides BaseHandler.prerun run -- Overrides BaseHandler.run """ def __init__(self, name, matcher, pointer, thread=False, once=False, instream=False, stream=None): """ Create a new callback handler. Arguments: name -- The name of the handler. matcher -- A matcher object for matching stanza objects. pointer -- The function to execute during callback. thread -- DEPRECATED. Remains only for backwards compatibility. once -- Indicates if the handler should be used only once. Defaults to False. instream -- Indicates if the callback should be executed during stream processing instead of in the main event loop. stream -- The XMLStream instance this handler should monitor. """ BaseHandler.__init__(self, name, matcher, stream) self._pointer = pointer self._once = once self._instream = instream def prerun(self, payload): """ Execute the callback during stream processing, if the callback was created with instream=True. Overrides BaseHandler.prerun Arguments: payload -- The matched stanza object. """ if self._once: self._destroy = True if self._instream: self.run(payload, True) def run(self, payload, instream=False): """ Execute the callback function with the matched stanza payload. Overrides BaseHandler.run Arguments: payload -- The matched stanza object. instream -- Force the handler to execute during stream processing. Used only by prerun. Defaults to False. """ if not self._instream or instream: self._pointer(payload) if self._once: self._destroy = True del self._pointer fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/handler/waiter.py000066400000000000000000000054451157775340400245650ustar00rootroot00000000000000""" 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 logging try: import queue except ImportError: import Queue as queue from sleekxmpp.xmlstream import StanzaBase from sleekxmpp.xmlstream.handler.base import BaseHandler log = logging.getLogger(__name__) class Waiter(BaseHandler): """ The Waiter handler allows an event handler to block until a particular stanza has been received. The handler will either be given the matched stanza, or False if the waiter has timed out. Methods: check_delete -- Overrides BaseHandler.check_delete prerun -- Overrides BaseHandler.prerun run -- Overrides BaseHandler.run wait -- Wait for a stanza to arrive and return it to an event handler. """ def __init__(self, name, matcher, stream=None): """ Create a new Waiter. Arguments: name -- The name of the waiter. matcher -- A matcher object to detect the desired stanza. stream -- Optional XMLStream instance to monitor. """ BaseHandler.__init__(self, name, matcher, stream=stream) self._payload = queue.Queue() def prerun(self, payload): """ Store the matched stanza. Overrides BaseHandler.prerun Arguments: payload -- The matched stanza object. """ self._payload.put(payload) def run(self, payload): """ Do not process this handler during the main event loop. Overrides BaseHandler.run Arguments: payload -- The matched stanza object. """ pass def wait(self, timeout=None): """ Block an event handler while waiting for a stanza to arrive. Be aware that this will impact performance if called from a non-threaded event handler. Will return either the received stanza, or False if the waiter timed out. Arguments: timeout -- The number of seconds to wait for the stanza to arrive. Defaults to the global default timeout value sleekxmpp.xmlstream.RESPONSE_TIMEOUT. """ if timeout is None: timeout = self.stream.response_timeout try: stanza = self._payload.get(True, timeout) except queue.Empty: stanza = False log.warning("Timed out waiting for %s" % self.name) self.stream.removeHandler(self.name) return stanza def check_delete(self): """ Always remove waiters after use. Overrides BaseHandler.check_delete """ return True fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/handler/xmlcallback.py000066400000000000000000000017661157775340400255510ustar00rootroot00000000000000""" 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.xmlstream.handler import Callback class XMLCallback(Callback): """ The XMLCallback class is identical to the normal Callback class, except that XML contents of matched stanzas will be processed instead of the stanza objects themselves. Methods: run -- Overrides Callback.run """ def run(self, payload, instream=False): """ Execute the callback function with the matched stanza's XML contents, instead of the stanza itself. Overrides BaseHandler.run Arguments: payload -- The matched stanza object. instream -- Force the handler to execute during stream processing. Used only by prerun. Defaults to False. """ Callback.run(self, payload.xml, instream) fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/handler/xmlwaiter.py000066400000000000000000000014201157775340400252730ustar00rootroot00000000000000""" 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.xmlstream.handler import Waiter class XMLWaiter(Waiter): """ The XMLWaiter class is identical to the normal Waiter class except that it returns the XML contents of the stanza instead of the full stanza object itself. Methods: prerun -- Overrides Waiter.prerun """ def prerun(self, payload): """ Store the XML contents of the stanza to return to the waiting event handler. Overrides Waiter.prerun Arguments: payload -- The matched stanza object. """ Waiter.prerun(self, payload.xml) fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/jid.py000066400000000000000000000103531157775340400224150ustar00rootroot00000000000000""" 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 __future__ import unicode_literals class JID(object): """ A representation of a Jabber ID, or JID. Each JID may have three components: a user, a domain, and an optional resource. For example: user@domain/resource When a resource is not used, the JID is called a bare JID. The JID is a full JID otherwise. Attributes: jid -- Alias for 'full'. full -- The value of the full JID. bare -- The value of the bare JID. user -- The username portion of the JID. domain -- The domain name portion of the JID. server -- Alias for 'domain'. resource -- The resource portion of the JID. Methods: reset -- Use a new JID value. regenerate -- Recreate the JID from its components. """ def __init__(self, jid): """Initialize a new JID""" self.reset(jid) def reset(self, jid): """ Start fresh from a new JID string. Arguments: jid - The new JID value. """ if isinstance(jid, JID): jid = jid.full self._full = self._jid = jid self._domain = None self._resource = None self._user = None self._bare = None def __getattr__(self, name): """ Handle getting the JID values, using cache if available. Arguments: name -- One of: user, server, domain, resource, full, or bare. """ if name == 'resource': if self._resource is None and '/' in self._jid: self._resource = self._jid.split('/', 1)[-1] return self._resource or "" elif name == 'user': if self._user is None: if '@' in self._jid: self._user = self._jid.split('@', 1)[0] else: self._user = self._user return self._user or "" elif name in ('server', 'domain', 'host'): if self._domain is None: self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0] return self._domain or "" elif name in ('full', 'jid'): return self._jid or "" elif name == 'bare': if self._bare is None: self._bare = self._jid.split('/', 1)[0] return self._bare or "" def __setattr__(self, name, value): """ Edit a JID by updating it's individual values, resetting the generated JID in the end. Arguments: name -- The name of the JID part. One of: user, domain, server, resource, full, jid, or bare. value -- The new value for the JID part. """ if name in ('resource', 'user', 'domain'): object.__setattr__(self, "_%s" % name, value) self.regenerate() elif name in ('server', 'domain', 'host'): self.domain = value elif name in ('full', 'jid'): self.reset(value) self.regenerate() elif name == 'bare': if '@' in value: u, d = value.split('@', 1) object.__setattr__(self, "_user", u) object.__setattr__(self, "_domain", d) else: object.__setattr__(self, "_user", '') object.__setattr__(self, "_domain", value) self.regenerate() else: object.__setattr__(self, name, value) def regenerate(self): """Generate a new JID based on current values, useful after editing.""" jid = "" if self.user: jid = "%s@" % self.user jid += self.domain if self.resource: jid += "/%s" % self.resource self.reset(jid) def __str__(self): """Use the full JID as the string value.""" return self.full def __repr__(self): return self.full def __eq__(self, other): """ Two JIDs are considered equal if they have the same full JID value. """ other = JID(other) return self.full == other.full fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/matcher/000077500000000000000000000000001157775340400227165ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/matcher/__init__.py000066400000000000000000000010551157775340400250300ustar00rootroot00000000000000""" 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.xmlstream.matcher.id import MatcherId from sleekxmpp.xmlstream.matcher.many import MatchMany from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath from sleekxmpp.xmlstream.matcher.xmlmask import MatchXMLMask from sleekxmpp.xmlstream.matcher.xpath import MatchXPath __all__ = ['MatcherId', 'MatchMany', 'StanzaPath', 'MatchXMLMask', 'MatchXPath'] fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/matcher/base.py000066400000000000000000000014231157775340400242020ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. """ class MatcherBase(object): """ Base class for stanza matchers. Stanza matchers are used to pick stanzas out of the XML stream and pass them to the appropriate stream handlers. """ def __init__(self, criteria): """ Create a new stanza matcher. Arguments: criteria -- Object to compare some aspect of a stanza against. """ self._criteria = criteria def match(self, xml): """ Check if a stanza matches the stored criteria. Meant to be overridden. """ return False fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/matcher/id.py000066400000000000000000000013331157775340400236640ustar00rootroot00000000000000""" 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.xmlstream.matcher.base import MatcherBase class MatcherId(MatcherBase): """ The ID matcher selects stanzas that have the same stanza 'id' interface value as the desired ID. Methods: match -- Overrides MatcherBase.match. """ def match(self, xml): """ Compare the given stanza's 'id' attribute to the stored id value. Overrides MatcherBase.match. Arguments: xml -- The stanza to compare against. """ return xml['id'] == self._criteria fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/matcher/many.py000066400000000000000000000017471157775340400242450ustar00rootroot00000000000000""" 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.xmlstream.matcher.base import MatcherBase class MatchMany(MatcherBase): """ The MatchMany matcher may compare a stanza against multiple criteria. It is essentially an OR relation combining multiple matchers. Each of the criteria must implement a match() method. Methods: match -- Overrides MatcherBase.match. """ def match(self, xml): """ Match a stanza against multiple criteria. The match is successful if one of the criteria matches. Each of the criteria must implement a match() method. Overrides MatcherBase.match. Arguments: xml -- The stanza object to compare against. """ for m in self._criteria: if m.match(xml): return True return False fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/matcher/stanzapath.py000066400000000000000000000022521157775340400254460ustar00rootroot00000000000000""" 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.xmlstream.matcher.base import MatcherBase class StanzaPath(MatcherBase): """ The StanzaPath matcher selects stanzas that match a given "stanza path", which is similar to a normal XPath except that it uses the interfaces and plugins of the stanza instead of the actual, underlying XML. In most cases, the stanza path and XPath should be identical, but be aware that differences may occur. Methods: match -- Overrides MatcherBase.match. """ def match(self, stanza): """ Compare a stanza against a "stanza path". A stanza path is similar to an XPath expression, but uses the stanza's interfaces and plugins instead of the underlying XML. For most cases, the stanza path and XPath should be identical, but be aware that differences may occur. Overrides MatcherBase.match. Arguments: stanza -- The stanza object to compare against. """ return stanza.match(self._criteria) fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/matcher/xmlmask.py000066400000000000000000000127071157775340400247530ustar00rootroot00000000000000""" 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 logging from xml.parsers.expat import ExpatError from sleekxmpp.xmlstream.stanzabase import ET from sleekxmpp.xmlstream.matcher.base import MatcherBase # Flag indicating if the builtin XPath matcher should be used, which # uses namespaces, or a custom matcher that ignores namespaces. # Changing this will affect ALL XMLMask matchers. IGNORE_NS = False log = logging.getLogger(__name__) class MatchXMLMask(MatcherBase): """ The XMLMask matcher selects stanzas whose XML matches a given XML pattern, or mask. For example, message stanzas with body elements could be matched using the mask: Use of XMLMask is discouraged, and XPath or StanzaPath should be used instead. The use of namespaces in the mask comparison is controlled by IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching for ALL XMLMask matchers. Methods: match -- Overrides MatcherBase.match. setDefaultNS -- Set the default namespace for the mask. """ def __init__(self, criteria): """ Create a new XMLMask matcher. Arguments: criteria -- Either an XML object or XML string to use as a mask. """ MatcherBase.__init__(self, criteria) if isinstance(criteria, str): self._criteria = ET.fromstring(self._criteria) self.default_ns = 'jabber:client' def setDefaultNS(self, ns): """ Set the default namespace to use during comparisons. Arguments: ns -- The new namespace to use as the default. """ self.default_ns = ns def match(self, xml): """ Compare a stanza object or XML object against the stored XML mask. Overrides MatcherBase.match. Arguments: xml -- The stanza object or XML object to compare against. """ if hasattr(xml, 'xml'): xml = xml.xml return self._mask_cmp(xml, self._criteria, True) def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'): """ Compare an XML object against an XML mask. Arguments: source -- The XML object to compare against the mask. mask -- The XML object serving as the mask. use_ns -- Indicates if namespaces should be respected during the comparison. default_ns -- The default namespace to apply to elements that do not have a specified namespace. Defaults to "__no_ns__". """ use_ns = not IGNORE_NS if source is None: # If the element was not found. May happend during recursive calls. return False # Convert the mask to an XML object if it is a string. if not hasattr(mask, 'attrib'): try: mask = ET.fromstring(mask) except ExpatError: log.warning("Expat error: %s\nIn parsing: %s" % ('', mask)) if not use_ns: # Compare the element without using namespaces. source_tag = source.tag.split('}', 1)[-1] mask_tag = mask.tag.split('}', 1)[-1] if source_tag != mask_tag: return False else: # Compare the element using namespaces mask_ns_tag = "{%s}%s" % (self.default_ns, mask.tag) if source.tag not in [mask.tag, mask_ns_tag]: return False # If the mask includes text, compare it. if mask.text and source.text and \ source.text.strip() != mask.text.strip(): return False # Compare attributes. The stanza must include the attributes # defined by the mask, but may include others. for name, value in mask.attrib.items(): if source.attrib.get(name, "__None__") != value: return False # Recursively check subelements. matched_elements = {} for subelement in mask: if use_ns: matched = False for other in source.findall(subelement.tag): matched_elements[other] = False if self._mask_cmp(other, subelement, use_ns): if not matched_elements.get(other, False): matched_elements[other] = True matched = True if not matched: return False else: if not self._mask_cmp(self._get_child(source, subelement.tag), subelement, use_ns): return False # Everything matches. return True def _get_child(self, xml, tag): """ Return a child element given its tag, ignoring namespace values. Returns None if the child was not found. Arguments: xml -- The XML object to search for the given child tag. tag -- The name of the subelement to find. """ tag = tag.split('}')[-1] try: children = [c.tag.split('}')[-1] for c in xml.getchildren()] index = children.index(tag) except ValueError: return None return xml.getchildren()[index] fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/matcher/xpath.py000066400000000000000000000050531157775340400244170ustar00rootroot00000000000000""" 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.xmlstream.stanzabase import ET from sleekxmpp.xmlstream.matcher.base import MatcherBase # Flag indicating if the builtin XPath matcher should be used, which # uses namespaces, or a custom matcher that ignores namespaces. # Changing this will affect ALL XPath matchers. IGNORE_NS = False class MatchXPath(MatcherBase): """ The XPath matcher selects stanzas whose XML contents matches a given XPath expression. Note that using this matcher may not produce expected behavior when using attribute selectors. For Python 2.6 and 3.1, the ElementTree find method does not support the use of attribute selectors. If you need to support Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher. If the value of IGNORE_NS is set to true, then XPath expressions will be matched without using namespaces. Methods: match -- Overrides MatcherBase.match. """ def match(self, xml): """ Compare a stanza's XML contents to an XPath expression. If the value of IGNORE_NS is set to true, then XPath expressions will be matched without using namespaces. Note that in Python 2.6 and 3.1 the ElementTree find method does not support attribute selectors in the XPath expression. Arguments: xml -- The stanza object to compare against. """ if hasattr(xml, 'xml'): xml = xml.xml x = ET.Element('x') x.append(xml) if not IGNORE_NS: # Use builtin, namespace respecting, XPath matcher. if x.find(self._criteria) is not None: return True return False else: # Remove namespaces from the XPath expression. criteria = [] for ns_block in self._criteria.split('{'): criteria.extend(ns_block.split('}')[-1].split('/')) # Walk the XPath expression. xml = x for tag in criteria: if not tag: # Skip empty tag name artifacts from the cleanup phase. continue children = [c.tag.split('}')[-1] for c in xml.getchildren()] try: index = children.index(tag) except ValueError: return False xml = xml.getchildren()[index] return True fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/scheduler.py000066400000000000000000000161771157775340400236370ustar00rootroot00000000000000""" 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 threading import logging try: import queue except ImportError: import Queue as queue log = logging.getLogger(__name__) class Task(object): """ A scheduled task that will be executed by the scheduler after a given time interval has passed. Attributes: name -- The name of the task. seconds -- The number of seconds to wait before executing. callback -- The function to execute. args -- The arguments to pass to the callback. kwargs -- The keyword arguments to pass to the callback. repeat -- Indicates if the task should repeat. Defaults to False. qpointer -- A pointer to an event queue for queuing callback execution instead of executing immediately. Methods: run -- Either queue or execute the callback. reset -- Reset the task's timer. """ def __init__(self, name, seconds, callback, args=None, kwargs=None, repeat=False, qpointer=None): """ Create a new task. Arguments: name -- The name of the task. seconds -- The number of seconds to wait before executing. callback -- The function to execute. args -- The arguments to pass to the callback. kwargs -- The keyword arguments to pass to the callback. repeat -- Indicates if the task should repeat. Defaults to False. qpointer -- A pointer to an event queue for queuing callback execution instead of executing immediately. """ 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): """ Execute the task's callback. If an event queue was supplied, place the callback in the queue; otherwise, execute the callback immediately. """ 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): """ Reset the task's timer so that it will repeat. """ self.next = time.time() + self.seconds class Scheduler(object): """ A threaded scheduler that allows for updates mid-execution unlike the scheduler in the standard library. http://docs.python.org/library/sched.html#module-sched Attributes: addq -- A queue storing added tasks. schedule -- A list of tasks in order of execution times. thread -- If threaded, the thread processing the schedule. run -- Indicates if the scheduler is running. parentqueue -- A parent event queue in control of this scheduler. Methods: add -- Add a new task to the schedule. process -- Process and schedule tasks. quit -- Stop the scheduler. """ def __init__(self, parentqueue=None, parentstop=None): """ Create a new scheduler. Arguments: parentqueue -- A separate event queue controlling this scheduler. """ self.addq = queue.Queue() self.schedule = [] self.thread = None self.run = False self.parentqueue = parentqueue self.parentstop = parentstop def process(self, threaded=True): """ Begin accepting and processing scheduled tasks. Arguments: threaded -- Indicates if the scheduler should execute in its own thread. Defaults to True. """ if threaded: self.thread = threading.Thread(name='sheduler_process', target=self._process) self.thread.daemon = True self.thread.start() else: self._process() def _process(self): """Process scheduled tasks.""" self.run = True try: while self.run and (self.parentstop is None or \ not self.parentstop.isSet()): 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: if wait >= 3.0: wait = 3.0 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 if self.parentstop is not None: log.debug("stopping parent") self.parentstop.set() except SystemExit: self.run = False if self.parentstop is not None: self.parentstop.set() log.debug("Quitting 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): """ Schedule a new task. Arguments: name -- The name of the task. seconds -- The number of seconds to wait before executing. callback -- The function to execute. args -- The arguments to pass to the callback. kwargs -- The keyword arguments to pass to the callback. repeat -- Indicates if the task should repeat. Defaults to False. qpointer -- A pointer to an event queue for queuing callback execution instead of executing immediately. """ self.addq.put(Task(name, seconds, callback, args, kwargs, repeat, qpointer)) def quit(self): """Shutdown the scheduler.""" self.run = False fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/stanzabase.py000066400000000000000000001362511157775340400240100ustar00rootroot00000000000000""" 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 copy import logging import sys import weakref from xml.etree import cElementTree as ET from sleekxmpp.xmlstream import JID from sleekxmpp.xmlstream.tostring import tostring from sleekxmpp.thirdparty import OrderedDict log = logging.getLogger(__name__) # Used to check if an argument is an XML object. XML_TYPE = type(ET.Element('xml')) def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False): """ Associate a stanza object as a plugin for another stanza. Arguments: stanza -- The class of the parent stanza. plugin -- The class of the plugin stanza. iterable -- Indicates if the plugin stanza should be included in the parent stanza's iterable 'substanzas' interface results. overrides -- Indicates if the plugin should be allowed to override the interface handlers for the parent stanza. """ tag = "{%s}%s" % (plugin.namespace, plugin.name) stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin stanza.plugin_tag_map[tag] = plugin if iterable: # Prevent weird memory reference gotchas. stanza.plugin_iterables = stanza.plugin_iterables.copy() stanza.plugin_iterables.add(plugin) if overrides: # Prevent weird memory reference gotchas. stanza.plugin_overrides = stanza.plugin_overrides.copy() for interface in plugin.overrides: stanza.plugin_overrides[interface] = plugin.plugin_attrib # To maintain backwards compatibility for now, preserve the camel case name. registerStanzaPlugin = register_stanza_plugin class ElementBase(object): """ The core of SleekXMPP's stanza XML manipulation and handling is provided by ElementBase. ElementBase wraps XML cElementTree objects and enables access to the XML contents through dictionary syntax, similar in style to the Ruby XMPP library Blather's stanza implementation. Stanzas are defined by their name, namespace, and interfaces. For example, a simplistic Message stanza could be defined as: >>> class Message(ElementBase): ... name = "message" ... namespace = "jabber:client" ... interfaces = set(('to', 'from', 'type', 'body')) ... sub_interfaces = set(('body',)) The resulting Message stanza's contents may be accessed as so: >>> message['to'] = "user@example.com" >>> message['body'] = "Hi!" >>> message['body'] "Hi!" >>> del message['body'] >>> message['body'] "" The interface values map to either custom access methods, stanza XML attributes, or (if the interface is also in sub_interfaces) the text contents of a stanza's subelement. Custom access methods may be created by adding methods of the form "getInterface", "setInterface", or "delInterface", where "Interface" is the titlecase version of the interface name. Stanzas may be extended through the use of plugins. A plugin is simply a stanza that has a plugin_attrib value. For example: >>> class MessagePlugin(ElementBase): ... name = "custom_plugin" ... namespace = "custom" ... interfaces = set(('useful_thing', 'custom')) ... plugin_attrib = "custom" The plugin stanza class must be associated with its intended container stanza by using register_stanza_plugin as so: >>> register_stanza_plugin(Message, MessagePlugin) The plugin may then be accessed as if it were built-in to the parent stanza. >>> message['custom']['useful_thing'] = 'foo' If a plugin provides an interface that is the same as the plugin's plugin_attrib value, then the plugin's interface may be assigned directly from the parent stanza, as shown below, but retrieving information will require all interfaces to be used, as so: >>> message['custom'] = 'bar' # Same as using message['custom']['custom'] >>> message['custom']['custom'] # Must use all interfaces 'bar' If the plugin sets the value is_extension = True, then both setting and getting an interface value that is the same as the plugin's plugin_attrib value will work, as so: >>> message['custom'] = 'bar' # Using is_extension=True >>> message['custom'] 'bar' Class Attributes: name -- The name of the stanza's main element. namespace -- The namespace of the stanza's main element. interfaces -- A set of attribute and element names that may be accessed using dictionary syntax. sub_interfaces -- A subset of the set of interfaces which map to subelements instead of attributes. subitem -- A set of stanza classes which are allowed to be added as substanzas. Deprecated version of plugin_iterables. overrides -- A list of interfaces prepended with 'get_', 'set_', or 'del_'. If the stanza is registered as a plugin with overrides=True, then the parent's interface handlers will be overridden by the plugin's matching handler. types -- A set of generic type attribute values. tag -- The namespaced name of the stanza's root element. Example: "{foo_ns}bar" plugin_attrib -- The interface name that the stanza uses to be accessed as a plugin from another stanza. plugin_attrib_map -- A mapping of plugin attribute names with the associated plugin stanza classes. plugin_iterables -- A set of stanza classes which are allowed to be added as substanzas. plugin_overrides -- A mapping of interfaces prepended with 'get_', 'set_' or 'del_' to plugin attrib names. Allows a plugin to override the behaviour of a parent stanza's interface handlers. plugin_tag_map -- A mapping of plugin stanza tag names with the associated plugin stanza classes. is_extension -- When True, allows the stanza to provide one additional interface to the parent stanza, extending the interfaces supported by the parent. Defaults to False. xml_ns -- The XML namespace, http://www.w3.org/XML/1998/namespace, for use with xml:lang values. Instance Attributes: xml -- The stanza's XML contents. parent -- The parent stanza of this stanza. plugins -- A map of enabled plugin names with the initialized plugin stanza objects. values -- A dictionary of the stanza's interfaces and interface values, including plugins. Class Methods tag_name -- Return the namespaced version of the stanza's root element's name. Methods: setup -- Initialize the stanza's XML contents. enable -- Instantiate a stanza plugin. Alias for init_plugin. init_plugin -- Instantiate a stanza plugin. _get_stanza_values -- Return a dictionary of stanza interfaces and their values. _set_stanza_values -- Set stanza interface values given a dictionary of interfaces and values. __getitem__ -- Return the value of a stanza interface. __setitem__ -- Set the value of a stanza interface. __delitem__ -- Remove the value of a stanza interface. _set_attr -- Set an attribute value of the main stanza element. _del_attr -- Remove an attribute from the main stanza element. _get_attr -- Return an attribute's value from the main stanza element. _get_sub_text -- Return the text contents of a subelement. _set_sub_text -- Set the text contents of a subelement. _del_sub -- Remove a subelement. match -- Compare the stanza against an XPath expression. find -- Return subelement matching an XPath expression. findall -- Return subelements matching an XPath expression. get -- Return the value of a stanza interface, with an optional default value. keys -- Return the set of interface names accepted by the stanza. append -- Add XML content or a substanza to the stanza. appendxml -- Add XML content to the stanza. pop -- Remove a substanza. next -- Return the next iterable substanza. clear -- Reset the stanza's XML contents. _fix_ns -- Apply the stanza's namespace to non-namespaced elements in an XPath expression. """ name = 'stanza' plugin_attrib = 'plugin' namespace = 'jabber:client' interfaces = set(('type', 'to', 'from', 'id', 'payload')) types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) sub_interfaces = tuple() overrides = {} plugin_attrib_map = {} plugin_overrides = {} plugin_iterables = set() plugin_tag_map = {} subitem = set() is_extension = False xml_ns = 'http://www.w3.org/XML/1998/namespace' def __init__(self, xml=None, parent=None): """ Create a new stanza object. Arguments: xml -- Initialize the stanza with optional existing XML. parent -- Optional stanza object that contains this stanza. """ self.xml = xml self.plugins = OrderedDict() self.iterables = [] self._index = 0 self.tag = self.tag_name() if parent is None: self.parent = None else: self.parent = weakref.ref(parent) ElementBase.values = property(ElementBase._get_stanza_values, ElementBase._set_stanza_values) if self.subitem is not None: for sub in self.subitem: self.plugin_iterables.add(sub) if self.setup(xml): # If we generated our own XML, then everything is ready. return # Initialize values using provided XML for child in self.xml.getchildren(): if child.tag in self.plugin_tag_map: plugin = self.plugin_tag_map[child.tag] self.plugins[plugin.plugin_attrib] = plugin(child, self) for sub in self.plugin_iterables: if child.tag == "{%s}%s" % (sub.namespace, sub.name): self.iterables.append(sub(child, self)) break def setup(self, xml=None): """ Initialize the stanza's XML contents. Will return True if XML was generated according to the stanza's definition. Arguments: xml -- Optional XML object to use for the stanza's content instead of generating XML. """ if self.xml is None: self.xml = xml if self.xml is None: # Generate XML from the stanza definition for ename in self.name.split('/'): new = ET.Element("{%s}%s" % (self.namespace, ename)) if self.xml is None: self.xml = new else: last_xml.append(new) last_xml = new if self.parent is not None: self.parent().xml.append(self.xml) # We had to generate XML return True else: # We did not generate XML return False def enable(self, attrib): """ Enable and initialize a stanza plugin. Alias for init_plugin. Arguments: attrib -- The stanza interface for the plugin. """ return self.init_plugin(attrib) def init_plugin(self, attrib): """ Enable and initialize a stanza plugin. Arguments: attrib -- The stanza interface for the plugin. """ if attrib not in self.plugins: plugin_class = self.plugin_attrib_map[attrib] self.plugins[attrib] = plugin_class(parent=self) return self def _get_stanza_values(self): """ Return a dictionary of the stanza's interface values. Stanza plugin values are included as nested dictionaries. """ values = {} for interface in self.interfaces: values[interface] = self[interface] for plugin, stanza in self.plugins.items(): values[plugin] = stanza.values if self.iterables: iterables = [] for stanza in self.iterables: iterables.append(stanza.values) iterables[-1]['__childtag__'] = stanza.tag values['substanzas'] = iterables return values def _set_stanza_values(self, values): """ Set multiple stanza interface values using a dictionary. Stanza plugin values may be set using nested dictionaries. Arguments: values -- A dictionary mapping stanza interface with values. Plugin interfaces may accept a nested dictionary that will be used recursively. """ iterable_interfaces = [p.plugin_attrib for \ p in self.plugin_iterables] for interface, value in values.items(): if interface == 'substanzas': # Remove existing substanzas for stanza in self.iterables: self.xml.remove(stanza.xml) self.iterables = [] # Add new substanzas for subdict in value: if '__childtag__' in subdict: for subclass in self.plugin_iterables: child_tag = "{%s}%s" % (subclass.namespace, subclass.name) if subdict['__childtag__'] == child_tag: sub = subclass(parent=self) sub.values = subdict self.iterables.append(sub) break elif interface in self.interfaces: self[interface] = value elif interface in self.plugin_attrib_map: if interface not in iterable_interfaces: if interface not in self.plugins: self.init_plugin(interface) self.plugins[interface].values = value return self def __getitem__(self, attrib): """ Return the value of a stanza interface using dictionary-like syntax. Example: >>> msg['body'] 'Message contents' Stanza interfaces are typically mapped directly to the underlying XML object, but can be overridden by the presence of a get_attrib method (or get_foo where the interface is named foo, etc). The search order for interface value retrieval for an interface named 'foo' is: 1. The list of substanzas. 2. The result of calling the get_foo override handler. 3. The result of calling get_foo. 4. The result of calling getFoo. 5. The contents of the foo subelement, if foo is a sub interface. 6. The value of the foo attribute of the XML object. 7. The plugin named 'foo' 8. An empty string. Arguments: attrib -- The name of the requested stanza interface. """ if attrib == 'substanzas': return self.iterables elif attrib in self.interfaces: get_method = "get_%s" % attrib.lower() get_method2 = "get%s" % attrib.title() if self.plugin_overrides: plugin = self.plugin_overrides.get(get_method, None) if plugin: if plugin not in self.plugins: self.init_plugin(plugin) handler = getattr(self.plugins[plugin], get_method, None) if handler: return handler() if hasattr(self, get_method): return getattr(self, get_method)() elif hasattr(self, get_method2): return getattr(self, get_method2)() else: if attrib in self.sub_interfaces: return self._get_sub_text(attrib) else: return self._get_attr(attrib) elif attrib in self.plugin_attrib_map: if attrib not in self.plugins: self.init_plugin(attrib) if self.plugins[attrib].is_extension: return self.plugins[attrib][attrib] return self.plugins[attrib] else: return '' def __setitem__(self, attrib, value): """ Set the value of a stanza interface using dictionary-like syntax. Example: >>> msg['body'] = "Hi!" >>> msg['body'] 'Hi!' Stanza interfaces are typically mapped directly to the underlying XML object, but can be overridden by the presence of a set_attrib method (or set_foo where the interface is named foo, etc). The effect of interface value assignment for an interface named 'foo' will be one of: 1. Delete the interface's contents if the value is None. 2. Call the set_foo override handler, if it exists. 3. Call set_foo, if it exists. 4. Call setFoo, if it exists. 5. Set the text of a foo element, if foo is in sub_interfaces. 6. Set the value of a top level XML attribute name foo. 7. Attempt to pass value to a plugin named foo using the plugin's foo interface. 8. Do nothing. Arguments: attrib -- The name of the stanza interface to modify. value -- The new value of the stanza interface. """ if attrib in self.interfaces: if value is not None: set_method = "set_%s" % attrib.lower() set_method2 = "set%s" % attrib.title() if self.plugin_overrides: plugin = self.plugin_overrides.get(set_method, None) if plugin: if plugin not in self.plugins: self.init_plugin(plugin) handler = getattr(self.plugins[plugin], set_method, None) if handler: return handler(value) if hasattr(self, set_method): getattr(self, set_method)(value,) elif hasattr(self, set_method2): getattr(self, set_method2)(value,) else: if attrib in self.sub_interfaces: return self._set_sub_text(attrib, text=value) else: self._set_attr(attrib, value) else: self.__delitem__(attrib) elif attrib in self.plugin_attrib_map: if attrib not in self.plugins: self.init_plugin(attrib) self.plugins[attrib][attrib] = value return self def __delitem__(self, attrib): """ Delete the value of a stanza interface using dictionary-like syntax. Example: >>> msg['body'] = "Hi!" >>> msg['body'] 'Hi!' >>> del msg['body'] >>> msg['body'] '' Stanza interfaces are typically mapped directly to the underlyig XML object, but can be overridden by the presence of a del_attrib method (or del_foo where the interface is named foo, etc). The effect of deleting a stanza interface value named foo will be one of: 1. Call del_foo override handler, if it exists. 2. Call del_foo, if it exists. 3. Call delFoo, if it exists. 4. Delete foo element, if foo is in sub_interfaces. 5. Delete top level XML attribute named foo. 6. Remove the foo plugin, if it was loaded. 7. Do nothing. Arguments: attrib -- The name of the affected stanza interface. """ if attrib in self.interfaces: del_method = "del_%s" % attrib.lower() del_method2 = "del%s" % attrib.title() if self.plugin_overrides: plugin = self.plugin_overrides.get(del_method, None) if plugin: if plugin not in self.plugins: self.init_plugin(plugin) handler = getattr(self.plugins[plugin], del_method, None) if handler: return handler() if hasattr(self, del_method): getattr(self, del_method)() elif hasattr(self, del_method2): getattr(self, del_method2)() else: if attrib in self.sub_interfaces: return self._del_sub(attrib) else: self._del_attr(attrib) elif attrib in self.plugin_attrib_map: if attrib in self.plugins: xml = self.plugins[attrib].xml if self.plugins[attrib].is_extension: del self.plugins[attrib][attrib] del self.plugins[attrib] try: self.xml.remove(xml) except: pass return self def _set_attr(self, name, value): """ Set the value of a top level attribute of the underlying XML object. If the new value is None or an empty string, then the attribute will be removed. Arguments: name -- The name of the attribute. value -- The new value of the attribute, or None or '' to remove it. """ if value is None or value == '': self.__delitem__(name) else: self.xml.attrib[name] = value def _del_attr(self, name): """ Remove a top level attribute of the underlying XML object. Arguments: name -- The name of the attribute. """ if name in self.xml.attrib: del self.xml.attrib[name] def _get_attr(self, name, default=''): """ Return the value of a top level attribute of the underlying XML object. In case the attribute has not been set, a default value can be returned instead. An empty string is returned if no other default is supplied. Arguments: name -- The name of the attribute. default -- Optional value to return if the attribute has not been set. An empty string is returned otherwise. """ return self.xml.attrib.get(name, default) def _get_sub_text(self, name, default=''): """ Return the text contents of a sub element. In case the element does not exist, or it has no textual content, a default value can be returned instead. An empty string is returned if no other default is supplied. Arguments: name -- The name or XPath expression of the element. default -- Optional default to return if the element does not exists. An empty string is returned otherwise. """ name = self._fix_ns(name) stanza = self.xml.find(name) if stanza is None or stanza.text is None: return default else: return stanza.text def _set_sub_text(self, name, text=None, keep=False): """ Set the text contents of a sub element. In case the element does not exist, a element will be created, and its text contents will be set. If the text is set to an empty string, or None, then the element will be removed, unless keep is set to True. Arguments: name -- The name or XPath expression of the element. text -- The new textual content of the element. If the text is an empty string or None, the element will be removed unless the parameter keep is True. keep -- Indicates if the element should be kept if its text is removed. Defaults to False. """ path = self._fix_ns(name, split=True) element = self.xml.find(name) if not text and not keep: return self._del_sub(name) if element is None: # We need to add the element. If the provided name was # an XPath expression, some of the intermediate elements # may already exist. If so, we want to use those instead # of generating new elements. last_xml = self.xml walked = [] for ename in path: walked.append(ename) element = self.xml.find("/".join(walked)) if element is None: element = ET.Element(ename) last_xml.append(element) last_xml = element element = last_xml element.text = text return element def _del_sub(self, name, all=False): """ Remove sub elements that match the given name or XPath. If the element is in a path, then any parent elements that become empty after deleting the element may also be deleted if requested by setting all=True. Arguments: name -- The name or XPath expression for the element(s) to remove. all -- If True, remove all empty elements in the path to the deleted element. Defaults to False. """ path = self._fix_ns(name, split=True) original_target = path[-1] for level, _ in enumerate(path): # Generate the paths to the target elements and their parent. element_path = "/".join(path[:len(path) - level]) parent_path = "/".join(path[:len(path) - level - 1]) elements = self.xml.findall(element_path) parent = self.xml.find(parent_path) if elements: if parent is None: parent = self.xml for element in elements: if element.tag == original_target or \ not element.getchildren(): # Only delete the originally requested elements, and # any parent elements that have become empty. parent.remove(element) if not all: # If we don't want to delete elements up the tree, stop # after deleting the first level of elements. return def match(self, xpath): """ Compare a stanza object with an XPath expression. If the XPath matches the contents of the stanza object, the match is successful. The XPath expression may include checks for stanza attributes. For example: presence@show=xa@priority=2/status Would match a presence stanza whose show value is set to 'xa', has a priority value of '2', and has a status element. Arguments: xpath -- The XPath expression to check against. It may be either a string or a list of element names with attribute checks. """ if isinstance(xpath, str): xpath = self._fix_ns(xpath, split=True, propagate_ns=False) # Extract the tag name and attribute checks for the first XPath node. components = xpath[0].split('@') tag = components[0] attributes = components[1:] if tag not in (self.name, "{%s}%s" % (self.namespace, self.name)) and \ tag not in self.plugins and tag not in self.plugin_attrib: # The requested tag is not in this stanza, so no match. return False # Check the rest of the XPath against any substanzas. matched_substanzas = False for substanza in self.iterables: if xpath[1:] == []: break matched_substanzas = substanza.match(xpath[1:]) if matched_substanzas: break # Check attribute values. for attribute in attributes: name, value = attribute.split('=') if self[name] != value: return False # Check sub interfaces. if len(xpath) > 1: next_tag = xpath[1] if next_tag in self.sub_interfaces and self[next_tag]: return True # Attempt to continue matching the XPath using the stanza's plugins. if not matched_substanzas and len(xpath) > 1: # Convert {namespace}tag@attribs to just tag next_tag = xpath[1].split('@')[0].split('}')[-1] if next_tag in self.plugins: return self.plugins[next_tag].match(xpath[1:]) else: return False # Everything matched. return True def find(self, xpath): """ Find an XML object in this stanza given an XPath expression. Exposes ElementTree interface for backwards compatibility. Note that matching on attribute values is not supported in Python 2.6 or Python 3.1 Arguments: xpath -- An XPath expression matching a single desired element. """ return self.xml.find(xpath) def findall(self, xpath): """ Find multiple XML objects in this stanza given an XPath expression. Exposes ElementTree interface for backwards compatibility. Note that matching on attribute values is not supported in Python 2.6 or Python 3.1. Arguments: xpath -- An XPath expression matching multiple desired elements. """ return self.xml.findall(xpath) def get(self, key, default=None): """ Return the value of a stanza interface. If the found value is None or an empty string, return the supplied default value. Allows stanza objects to be used like dictionaries. Arguments: key -- The name of the stanza interface to check. default -- Value to return if the stanza interface has a value of None or "". Will default to returning None. """ value = self[key] if value is None or value == '': return default return value def keys(self): """ Return the names of all stanza interfaces provided by the stanza object. Allows stanza objects to be used like dictionaries. """ out = [] out += [x for x in self.interfaces] out += [x for x in self.plugins] if self.iterables: out.append('substanzas') return out def append(self, item): """ Append either an XML object or a substanza to this stanza object. If a substanza object is appended, it will be added to the list of iterable stanzas. Allows stanza objects to be used like lists. Arguments: item -- Either an XML object or a stanza object to add to this stanza's contents. """ if not isinstance(item, ElementBase): if type(item) == XML_TYPE: return self.appendxml(item) else: raise TypeError self.xml.append(item.xml) self.iterables.append(item) return self def appendxml(self, xml): """ Append an XML object to the stanza's XML. The added XML will not be included in the list of iterable substanzas. Arguments: xml -- The XML object to add to the stanza. """ self.xml.append(xml) return self def pop(self, index=0): """ Remove and return the last substanza in the list of iterable substanzas. Allows stanza objects to be used like lists. Arguments: index -- The index of the substanza to remove. """ substanza = self.iterables.pop(index) self.xml.remove(substanza.xml) return substanza def next(self): """ Return the next iterable substanza. """ return self.__next__() def clear(self): """ Remove all XML element contents and plugins. Any attribute values will be preserved. """ for child in self.xml.getchildren(): self.xml.remove(child) for plugin in list(self.plugins.keys()): del self.plugins[plugin] return self @classmethod def tag_name(cls): """ Return the namespaced name of the stanza's root element. For example, for the stanza , stanza.tag would return "{bar}foo". """ return "{%s}%s" % (cls.namespace, cls.name) @property def attrib(self): """ DEPRECATED For backwards compatibility, stanza.attrib returns the stanza itself. Older implementations of stanza objects used XML objects directly, requiring the use of .attrib to access attribute values. Use of the dictionary syntax with the stanza object itself for accessing stanza interfaces is preferred. """ return self def _fix_ns(self, xpath, split=False, propagate_ns=True): """ Apply the stanza's namespace to elements in an XPath expression. Arguments: xpath -- The XPath expression to fix with namespaces. split -- Indicates if the fixed XPath should be left as a list of element names with namespaces. Defaults to False, which returns a flat string path. propagate_ns -- Overrides propagating parent element namespaces to child elements. Useful if you wish to simply split an XPath that has non-specified namespaces, and child and parent namespaces are known not to always match. Defaults to True. """ fixed = [] # Split the XPath into a series of blocks, where a block # is started by an element with a namespace. ns_blocks = xpath.split('{') for ns_block in ns_blocks: if '}' in ns_block: # Apply the found namespace to following elements # that do not have namespaces. namespace = ns_block.split('}')[0] elements = ns_block.split('}')[1].split('/') else: # Apply the stanza's namespace to the following # elements since no namespace was provided. namespace = self.namespace elements = ns_block.split('/') for element in elements: if element: # Skip empty entry artifacts from splitting. if propagate_ns: tag = '{%s}%s' % (namespace, element) else: tag = element fixed.append(tag) if split: return fixed return '/'.join(fixed) def __eq__(self, other): """ Compare the stanza object with another to test for equality. Stanzas are equal if their interfaces return the same values, and if they are both instances of ElementBase. Arguments: other -- The stanza object to compare against. """ if not isinstance(other, ElementBase): return False # Check that this stanza is a superset of the other stanza. values = self.values for key in other.keys(): if key not in values or values[key] != other[key]: return False # Check that the other stanza is a superset of this stanza. values = other.values for key in self.keys(): if key not in values or values[key] != self[key]: return False # Both stanzas are supersets of each other, therefore they # must be equal. return True def __ne__(self, other): """ Compare the stanza object with another to test for inequality. Stanzas are not equal if their interfaces return different values, or if they are not both instances of ElementBase. Arguments: other -- The stanza object to compare against. """ return not self.__eq__(other) def __bool__(self): """ Stanza objects should be treated as True in boolean contexts. Python 3.x version. """ return True def __nonzero__(self): """ Stanza objects should be treated as True in boolean contexts. Python 2.x version. """ return True def __len__(self): """ Return the number of iterable substanzas contained in this stanza. """ return len(self.iterables) def __iter__(self): """ Return an iterator object for iterating over the stanza's substanzas. The iterator is the stanza object itself. Attempting to use two iterators on the same stanza at the same time is discouraged. """ self._index = 0 return self def __next__(self): """ Return the next iterable substanza. """ self._index += 1 if self._index > len(self.iterables): self._index = 0 raise StopIteration return self.iterables[self._index - 1] def __copy__(self): """ Return a copy of the stanza object that does not share the same underlying XML object. """ return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) def __str__(self, top_level_ns=True): """ Return a string serialization of the underlying XML object. Arguments: top_level_ns -- Display the top-most namespace. Defaults to True. """ stanza_ns = '' if top_level_ns else self.namespace return tostring(self.xml, xmlns='', stanza_ns=stanza_ns) def __repr__(self): """ Use the stanza's serialized XML as its representation. """ return self.__str__() class StanzaBase(ElementBase): """ StanzaBase provides the foundation for all other stanza objects used by SleekXMPP, and defines a basic set of interfaces common to nearly all stanzas. These interfaces are the 'id', 'type', 'to', and 'from' attributes. An additional interface, 'payload', is available to access the XML contents of the stanza. Most stanza objects will provided more specific interfaces, however. Stanza Interface: from -- A JID object representing the sender's JID. id -- An optional id value that can be used to associate stanzas with their replies. payload -- The XML contents of the stanza. to -- A JID object representing the recipient's JID. type -- The type of stanza, typically will be 'normal', 'error', 'get', or 'set', etc. Attributes: stream -- The XMLStream instance that will handle sending this stanza. Methods: set_type -- Set the type of the stanza. get_to -- Return the stanza recipients JID. set_to -- Set the stanza recipient's JID. get_from -- Return the stanza sender's JID. set_from -- Set the stanza sender's JID. get_payload -- Return the stanza's XML contents. set_payload -- Append to the stanza's XML contents. del_payload -- Remove the stanza's XML contents. reply -- Reset the stanza and modify the 'to' and 'from' attributes to prepare for sending a reply. error -- Set the stanza's type to 'error'. unhandled -- Callback for when the stanza is not handled by a stream handler. exception -- Callback for if an exception is raised while handling the stanza. send -- Send the stanza using the stanza's stream. """ name = 'stanza' namespace = 'jabber:client' interfaces = set(('type', 'to', 'from', 'id', 'payload')) types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) sub_interfaces = tuple() def __init__(self, stream=None, xml=None, stype=None, sto=None, sfrom=None, sid=None): """ Create a new stanza. Arguments: stream -- Optional XMLStream responsible for sending this stanza. xml -- Optional XML contents to initialize stanza values. stype -- Optional stanza type value. sto -- Optional string or JID object of the recipient's JID. sfrom -- Optional string or JID object of the sender's JID. sid -- Optional ID value for the stanza. """ 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 if sto is not None: self['to'] = sto if sfrom is not None: self['from'] = sfrom self.tag = "{%s}%s" % (self.namespace, self.name) def set_type(self, value): """ Set the stanza's 'type' attribute. Only type values contained in StanzaBase.types are accepted. Arguments: value -- One of the values contained in StanzaBase.types """ if value in self.types: self.xml.attrib['type'] = value return self def get_to(self): """Return the value of the stanza's 'to' attribute.""" return JID(self._get_attr('to')) def set_to(self, value): """ Set the 'to' attribute of the stanza. Arguments: value -- A string or JID object representing the recipient's JID. """ return self._set_attr('to', str(value)) def get_from(self): """Return the value of the stanza's 'from' attribute.""" return JID(self._get_attr('from')) def set_from(self, value): """ Set the 'from' attribute of the stanza. Arguments: from -- A string or JID object representing the sender's JID. """ return self._set_attr('from', str(value)) def get_payload(self): """Return a list of XML objects contained in the stanza.""" return self.xml.getchildren() def set_payload(self, value): """ Add XML content to the stanza. Arguments: value -- Either an XML or a stanza object, or a list of XML or stanza objects. """ if not isinstance(value, list): value = [value] for val in value: self.append(val) return self def del_payload(self): """Remove the XML contents of the stanza.""" self.clear() return self def reply(self, clear=True): """ 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 \ self.stream.is_component: self['from'], self['to'] = self['to'], self['from'] else: self['to'] = self['from'] del self['from'] if clear: self.clear() return self def error(self): """Set the stanza's type to 'error'.""" self['type'] = 'error' return self def unhandled(self): """ Called when no handlers have been registered to process this stanza. Meant to be overridden. """ pass def exception(self, e): """ Handle exceptions raised during stanza processing. Meant to be overridden. """ log.exception('Error handling {%s}%s stanza' % (self.namespace, self.name)) def send(self, now=False): """ Queue the stanza to be sent on the XML stream. Arguments: now -- Indicates if the queue should be skipped and the stanza sent immediately. Useful for stream initialization. Defaults to False. """ self.stream.send_raw(self.__str__(), now=now) def __copy__(self): """ Return a copy of the stanza object that does not share the same underlying XML object, but does share the same XML stream. """ return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) def __str__(self, top_level_ns=False): """ Serialize the stanza's XML to a string. Arguments: top_level_ns -- Display the top-most namespace. Defaults to False. """ stanza_ns = '' if top_level_ns else self.namespace return tostring(self.xml, xmlns='', stanza_ns=stanza_ns, stream=self.stream) # To comply with PEP8, method names now use underscores. # Deprecated method names are re-mapped for backwards compatibility. ElementBase.initPlugin = ElementBase.init_plugin ElementBase._getAttr = ElementBase._get_attr ElementBase._setAttr = ElementBase._set_attr ElementBase._delAttr = ElementBase._del_attr ElementBase._getSubText = ElementBase._get_sub_text ElementBase._setSubText = ElementBase._set_sub_text ElementBase._delSub = ElementBase._del_sub ElementBase.getStanzaValues = ElementBase._get_stanza_values ElementBase.setStanzaValues = ElementBase._set_stanza_values StanzaBase.setType = StanzaBase.set_type StanzaBase.getTo = StanzaBase.get_to StanzaBase.setTo = StanzaBase.set_to StanzaBase.getFrom = StanzaBase.get_from StanzaBase.setFrom = StanzaBase.set_from StanzaBase.getPayload = StanzaBase.get_payload StanzaBase.setPayload = StanzaBase.set_payload StanzaBase.delPayload = StanzaBase.del_payload fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/test.py000066400000000000000000000010731157775340400226250ustar00rootroot00000000000000import xmlstream import time import socket from handler.callback import Callback from matcher.xpath import MatchXPath def server(): s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) s.bind(('localhost', 5228)) s.listen(1) servers = [] while True: conn, addr = s.accept() server = xmlstream.XMLStream(conn, 'localhost', 5228) server.registerHandler(Callback('test', MatchXPath('test'), testHandler)) server.process() servers.append(server) def testHandler(xml): print("weeeeeeeee!") server() fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/test.xml000066400000000000000000000000231157775340400227670ustar00rootroot00000000000000 fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/testclient.py000066400000000000000000000003561157775340400240270ustar00rootroot00000000000000import socket import time s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(('localhost', 5228)) s.send("") #s.flush() s.send("") s.send("") s.send("") s.send("") #s.flush() s.close() fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/tostring/000077500000000000000000000000001157775340400231445ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/tostring/__init__.py000066400000000000000000000010311157775340400252500ustar00rootroot00000000000000""" 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 the correct tostring and xml_escape functions based on the Python # version in order to properly handle Unicode. if sys.version_info < (3, 0): from sleekxmpp.xmlstream.tostring.tostring26 import tostring, xml_escape else: from sleekxmpp.xmlstream.tostring.tostring import tostring, xml_escape __all__ = ['tostring', 'xml_escape'] fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/tostring/tostring.py000066400000000000000000000070261157775340400253740ustar00rootroot00000000000000""" SleekXMPP: The Sleek XMPP Library Copyright (C) 2010 Nathanael C. Fritz This file is part of SleekXMPP. See the file LICENSE for copying permission. """ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): """ Serialize an XML object to a Unicode string. If namespaces are provided using xmlns or stanza_ns, then elements that use those namespaces will not include the xmlns attribute in the output. Arguments: xml -- The XML object to serialize. If the value is None, then the XML object contained in this stanza object will be used. xmlns -- Optional namespace of an element wrapping the XML object. stanza_ns -- The namespace of the stanza object that contains the XML object. stream -- The XML stream that generated the XML object. outbuffer -- Optional buffer for storing serializations during recursive calls. """ # Add previous results to the start of the output. output = [outbuffer] # Extract the element's tag name. tag_name = xml.tag.split('}', 1)[-1] # Extract the element's namespace if it is defined. if '}' in xml.tag: tag_xmlns = xml.tag.split('}', 1)[0][1:] else: tag_xmlns = '' # Output the tag name and derived namespace of the element. namespace = '' if tag_xmlns not in ['', xmlns, stanza_ns]: namespace = ' xmlns="%s"' % tag_xmlns if stream and tag_xmlns in stream.namespace_map: mapped_namespace = stream.namespace_map[tag_xmlns] if mapped_namespace: tag_name = "%s:%s" % (mapped_namespace, tag_name) output.append("<%s" % tag_name) output.append(namespace) # Output escaped attribute values. for attrib, value in xml.attrib.items(): value = xml_escape(value) if '}' not in attrib: output.append(' %s="%s"' % (attrib, value)) else: attrib_ns = attrib.split('}')[0][1:] attrib = attrib.split('}')[1] if stream and attrib_ns in stream.namespace_map: mapped_ns = stream.namespace_map[attrib_ns] if mapped_ns: output.append(' %s:%s="%s"' % (mapped_ns, attrib, value)) if len(xml) or xml.text: # If there are additional child elements to serialize. output.append(">") if xml.text: output.append(xml_escape(xml.text)) if len(xml): for child in xml.getchildren(): output.append(tostring(child, tag_xmlns, stanza_ns, stream)) output.append("" % tag_name) elif xml.text: # If we only have text content. output.append(">%s" % (xml_escape(xml.text), tag_name)) else: # Empty element. output.append(" />") if xml.tail: # If there is additional text after the element. output.append(xml_escape(xml.tail)) return ''.join(output) def xml_escape(text): """ Convert special characters in XML to escape sequences. Arguments: text -- The XML text to convert. """ text = list(text) escapes = {'&': '&', '<': '<', '>': '>', "'": ''', '"': '"'} for i, c in enumerate(text): text[i] = escapes.get(c, c) return ''.join(text) fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/tostring/tostring26.py000066400000000000000000000073151157775340400255450ustar00rootroot00000000000000""" 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 __future__ import unicode_literals import types def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer=''): """ Serialize an XML object to a Unicode string. If namespaces are provided using xmlns or stanza_ns, then elements that use those namespaces will not include the xmlns attribute in the output. Arguments: xml -- The XML object to serialize. If the value is None, then the XML object contained in this stanza object will be used. xmlns -- Optional namespace of an element wrapping the XML object. stanza_ns -- The namespace of the stanza object that contains the XML object. stream -- The XML stream that generated the XML object. outbuffer -- Optional buffer for storing serializations during recursive calls. """ # Add previous results to the start of the output. output = [outbuffer] # Extract the element's tag name. tag_name = xml.tag.split('}', 1)[-1] # Extract the element's namespace if it is defined. if '}' in xml.tag: tag_xmlns = xml.tag.split('}', 1)[0][1:] else: tag_xmlns = u'' # Output the tag name and derived namespace of the element. namespace = u'' if tag_xmlns not in ['', xmlns, stanza_ns]: namespace = u' xmlns="%s"' % tag_xmlns if stream and tag_xmlns in stream.namespace_map: mapped_namespace = stream.namespace_map[tag_xmlns] if mapped_namespace: tag_name = u"%s:%s" % (mapped_namespace, tag_name) output.append(u"<%s" % tag_name) output.append(namespace) # Output escaped attribute values. for attrib, value in xml.attrib.items(): value = xml_escape(value) if '}' not in attrib: output.append(' %s="%s"' % (attrib, value)) else: attrib_ns = attrib.split('}')[0][1:] attrib = attrib.split('}')[1] if stream and attrib_ns in stream.namespace_map: mapped_ns = stream.namespace_map[attrib_ns] if mapped_ns: output.append(' %s:%s="%s"' % (mapped_ns, attrib, value)) if len(xml) or xml.text: # If there are additional child elements to serialize. output.append(u">") if xml.text: output.append(xml_escape(xml.text)) if len(xml): for child in xml.getchildren(): output.append(tostring(child, tag_xmlns, stanza_ns, stream)) output.append(u"" % tag_name) elif xml.text: # If we only have text content. output.append(u">%s" % (xml_escape(xml.text), tag_name)) else: # Empty element. output.append(u" />") if xml.tail: # If there is additional text after the element. output.append(xml_escape(xml.tail)) return u''.join(output) def xml_escape(text): """ Convert special characters in XML to escape sequences. Arguments: text -- The XML text to convert. """ if type(text) != types.UnicodeType: text = list(unicode(text, 'utf-8', 'ignore')) else: text = list(text) escapes = {u'&': u'&', u'<': u'<', u'>': u'>', u"'": u''', u'"': u'"'} for i, c in enumerate(text): text[i] = escapes.get(c, c) return u''.join(text) fritzy-SleekXMPP-5c4ee57/sleekxmpp/xmlstream/xmlstream.py000066400000000000000000001231041157775340400236620ustar00rootroot00000000000000""" 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 __future__ import with_statement, unicode_literals import copy import logging import signal import socket as Socket import ssl import sys import threading import time import types import random try: import queue except ImportError: import Queue as queue from sleekxmpp.thirdparty.statemachine import StateMachine from sleekxmpp.xmlstream import Scheduler, tostring from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET from sleekxmpp.xmlstream.handler import Waiter, XMLCallback from sleekxmpp.xmlstream.matcher import MatchXMLMask # In Python 2.x, file socket objects are broken. A patched socket # wrapper is provided for this case in filesocket.py. if sys.version_info < (3, 0): from sleekxmpp.xmlstream.filesocket import FileSocket, Socket26 # The time in seconds to wait before timing out waiting for response stanzas. RESPONSE_TIMEOUT = 10 # The number of threads to use to handle XML stream events. This is not the # same as the number of custom event handling threads. HANDLER_THREADS must # be at least 1. HANDLER_THREADS = 1 # Flag indicating if the SSL library is available for use. SSL_SUPPORT = True # Maximum time to delay between connection attempts is one hour. RECONNECT_MAX_DELAY = 3600 log = logging.getLogger(__name__) class RestartStream(Exception): """ Exception to restart stream processing, including resending the stream header. """ class XMLStream(object): """ An XML stream connection manager and event dispatcher. The XMLStream class abstracts away the issues of establishing a connection with a server and sending and receiving XML "stanzas". A stanza is a complete XML element that is a direct child of a root document element. Two streams are used, one for each communication direction, over the same socket. Once the connection is closed, both streams should be complete and valid XML documents. Three types of events are provided to manage the stream: Stream -- Triggered based on received stanzas, similar in concept to events in a SAX XML parser. Custom -- Triggered manually. Scheduled -- Triggered based on time delays. Typically, stanzas are first processed by a stream event handler which will then trigger custom events to continue further processing, especially since custom event handlers may run in individual threads. Attributes: address -- The hostname and port of the server. default_ns -- The default XML namespace that will be applied to all non-namespaced stanzas. event_queue -- A queue of stream, custom, and scheduled events to be processed. filesocket -- A filesocket created from the main connection socket. Required for ElementTree.iterparse. namespace_map -- Optional mapping of namespaces to namespace prefixes. scheduler -- A scheduler object for triggering events after a given period of time. send_queue -- A queue of stanzas to be sent on the stream. socket -- The connection to the server. ssl_support -- Indicates if a SSL library is available for use. ssl_version -- The version of the SSL protocol to use. Defaults to ssl.PROTOCOL_TLSv1. ca_certs -- File path to a CA certificate to verify the server's identity. state -- A state machine for managing the stream's connection state. stream_footer -- The start tag and any attributes for the stream's root element. stream_header -- The closing tag of the stream's root element. use_ssl -- Flag indicating if SSL should be used. use_tls -- Flag indicating if TLS should be used. stop -- threading Event used to stop all threads. auto_reconnect -- Flag to determine whether we auto reconnect. reconnect_max_delay -- Maximum time to delay between connection attempts. Defaults to RECONNECT_MAX_DELAY, which is one hour. Methods: add_event_handler -- Add a handler for a custom event. add_handler -- Shortcut method for registerHandler. connect -- Connect to the given server. del_event_handler -- Remove a handler for a custom event. disconnect -- Disconnect from the server and terminate processing. event -- Trigger a custom event. get_id -- Return the current stream ID. incoming_filter -- Optionally filter stanzas before processing. new_id -- Generate a new, unique ID value. process -- Read XML stanzas from the stream and apply matching stream handlers. reconnect -- Reestablish a connection to the server. register_handler -- Add a handler for a stream event. register_stanza -- Add a new stanza object type that may appear as a direct child of the stream's root. remove_handler -- Remove a stream handler. remove_stanza -- Remove a stanza object type. schedule -- Schedule an event handler to execute after a given delay. send -- Send a stanza object on the stream. send_raw -- Send a raw string on the stream. send_xml -- Send an XML string on the stream. set_socket -- Set the stream's socket and generate a new filesocket. start_stream_handler -- Perform any stream initialization such as handshakes. start_tls -- Establish a TLS connection and restart the stream. """ def __init__(self, socket=None, host='', port=0): """ Establish a new XML stream. Arguments: socket -- Use an existing socket for the stream. Defaults to None to generate a new socket. host -- The name of the target server. Defaults to the empty string. port -- The port to use for the connection. Defaults to 0. """ self.ssl_support = SSL_SUPPORT self.ssl_version = ssl.PROTOCOL_TLSv1 self.ca_certs = None self.response_timeout = RESPONSE_TIMEOUT self.reconnect_delay = None self.reconnect_max_delay = RECONNECT_MAX_DELAY self.state = StateMachine(('disconnected', 'connected')) self.state._set_state('disconnected') self.address = (host, int(port)) self.filesocket = None self.set_socket(socket) if sys.version_info < (3, 0): self.socket_class = Socket26 else: self.socket_class = Socket.socket self.use_ssl = False self.use_tls = False self.default_ns = '' self.stream_header = "" self.stream_footer = "" self.stop = threading.Event() self.stream_end_event = threading.Event() self.stream_end_event.set() self.session_started_event = threading.Event() self.event_queue = queue.Queue() self.send_queue = queue.Queue() self.__failed_send_stanza = None self.scheduler = Scheduler(self.event_queue, self.stop) self.namespace_map = {StanzaBase.xml_ns: 'xml'} self.__thread = {} self.__root_stanza = [] self.__handlers = [] self.__event_handlers = {} self.__event_handlers_lock = threading.Lock() self._id = 0 self._id_lock = threading.Lock() self.auto_reconnect = True self.is_client = False def use_signals(self, signals=None): """ Register signal handlers for SIGHUP and SIGTERM, if possible, which will raise a "killed" event when the application is terminated. If a signal handler already existed, it will be executed first, before the "killed" event is raised. Arguments: signals -- A list of signal names to be monitored. Defaults to ['SIGHUP', 'SIGTERM']. """ if signals is None: signals = ['SIGHUP', 'SIGTERM'] existing_handlers = {} for sig_name in signals: if hasattr(signal, sig_name): sig = getattr(signal, sig_name) handler = signal.getsignal(sig) if handler: existing_handlers[sig] = handler def handle_kill(signum, frame): """ Capture kill event and disconnect cleanly after first spawning the "killed" event. """ if signum in existing_handlers and \ existing_handlers[signum] != handle_kill: existing_handlers[signum](signum, frame) self.event("killed", direct=True) self.disconnect() try: for sig_name in signals: if hasattr(signal, sig_name): sig = getattr(signal, sig_name) signal.signal(sig, handle_kill) self.__signals_installed = True except: log.debug("Can not set interrupt signal handlers. " + \ "SleekXMPP is not running from a main thread.") def new_id(self): """ Generate and return a new stream ID in hexadecimal form. Many stanzas, handlers, or matchers may require unique ID values. Using this method ensures that all new ID values are unique in this stream. """ with self._id_lock: self._id += 1 return self.get_id() def get_id(self): """ Return the current unique stream ID in hexadecimal form. """ return "%X" % self._id def connect(self, host='', port=0, use_ssl=False, use_tls=True, reattempt=True): """ Create a new socket and connect to the server. Setting reattempt to True will cause connection attempts to be made every second until a successful connection is established. Arguments: host -- The name of the desired server for the connection. port -- Port to connect to on the server. use_ssl -- Flag indicating if SSL should be used. use_tls -- Flag indicating if TLS should be used. reattempt -- Flag indicating if the socket should reconnect after disconnections. """ if host and port: self.address = (host, int(port)) self.is_client = True # Respect previous SSL and TLS usage directives. if use_ssl is not None: self.use_ssl = use_ssl if use_tls is not None: self.use_tls = use_tls # Repeatedly attempt to connect until a successful connection # is established. connected = self.state.transition('disconnected', 'connected', func=self._connect) while reattempt and not connected: connected = self.state.transition('disconnected', 'connected', func=self._connect) return connected def _connect(self): self.stop.clear() self.socket = self.socket_class(Socket.AF_INET, Socket.SOCK_STREAM) self.socket.settimeout(None) if self.reconnect_delay is None: delay = 1.0 else: delay = min(self.reconnect_delay * 2, self.reconnect_max_delay) delay = random.normalvariate(delay, delay * 0.1) log.debug('Waiting %s seconds before connecting.' % delay) time.sleep(delay) if self.use_ssl and self.ssl_support: log.debug("Socket Wrapped for SSL") if self.ca_certs is None: cert_policy = ssl.CERT_NONE else: cert_policy = ssl.CERT_REQUIRED ssl_socket = ssl.wrap_socket(self.socket, ca_certs=self.ca_certs, cert_reqs=cert_policy) if hasattr(self.socket, 'socket'): # We are using a testing socket, so preserve the top # layer of wrapping. self.socket.socket = ssl_socket else: self.socket = ssl_socket try: log.debug("Connecting to %s:%s" % self.address) self.socket.connect(self.address) self.set_socket(self.socket, ignore=True) #this event is where you should set your application state self.event("connected", direct=True) self.reconnect_delay = 1.0 return True except Socket.error as serr: error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" self.event('socket_error', serr) log.error(error_msg % (self.address[0], self.address[1], serr.errno, serr.strerror)) self.reconnect_delay = delay return False def disconnect(self, reconnect=False): """ Terminate processing and close the XML streams. Optionally, the connection may be reconnected and resume processing afterwards. Arguments: reconnect -- Flag indicating if the connection and processing should be restarted. Defaults to False. """ self.state.transition('connected', 'disconnected', wait=0.0, func=self._disconnect, args=(reconnect,)) def _disconnect(self, reconnect=False): # Send the end of stream marker. self.send_raw(self.stream_footer, now=True) self.session_started_event.clear() # Wait for confirmation that the stream was # closed in the other direction. self.auto_reconnect = reconnect self.stream_end_event.wait(4) if not self.auto_reconnect: self.stop.set() try: self.socket.close() self.filesocket.close() self.socket.shutdown(Socket.SHUT_RDWR) except Socket.error as serr: self.event('socket_error', serr) finally: #clear your application state self.event('session_end', direct=True) self.event("disconnected", direct=True) return True def reconnect(self): """ Reset the stream's state and reconnect to the server. """ log.debug("reconnecting...") self.state.transition('connected', 'disconnected', wait=2.0, func=self._disconnect, args=(True,)) log.debug("connecting...") return self.state.transition('disconnected', 'connected', wait=2.0, func=self._connect) def set_socket(self, socket, ignore=False): """ Set the socket to use for the stream. The filesocket will be recreated as well. Arguments: socket -- The new socket to use. ignore -- don't set the state """ self.socket = socket if socket is not None: # ElementTree.iterparse requires a file. # 0 buffer files have to be binary. # Use the correct fileobject type based on the Python # version to work around a broken implementation in # Python 2.x. if sys.version_info < (3, 0): self.filesocket = FileSocket(self.socket) else: self.filesocket = self.socket.makefile('rb', 0) if not ignore: self.state._set_state('connected') def start_tls(self): """ Perform handshakes for TLS. If the handshake is successful, the XML stream will need to be restarted. """ if self.ssl_support: log.info("Negotiating TLS") log.info("Using SSL version: %s" % str(self.ssl_version)) if self.ca_certs is None: cert_policy = ssl.CERT_NONE else: cert_policy = ssl.CERT_REQUIRED ssl_socket = ssl.wrap_socket(self.socket, ssl_version=self.ssl_version, do_handshake_on_connect=False, ca_certs=self.ca_certs, cert_reqs=cert_policy) if hasattr(self.socket, 'socket'): # We are using a testing socket, so preserve the top # layer of wrapping. self.socket.socket = ssl_socket else: self.socket = ssl_socket self.socket.do_handshake() self.set_socket(self.socket) return True else: log.warning("Tried to enable TLS, but ssl module not found.") return False def start_stream_handler(self, xml): """ Perform any initialization actions, such as handshakes, once the stream header has been sent. Meant to be overridden. """ pass def register_stanza(self, stanza_class): """ Add a stanza object class as a known root stanza. A root stanza is one that appears as a direct child of the stream's root element. Stanzas that appear as substanzas of a root stanza do not need to be registered here. That is done using register_stanza_plugin() from sleekxmpp.xmlstream.stanzabase. Stanzas that are not registered will not be converted into stanza objects, but may still be processed using handlers and matchers. Arguments: stanza_class -- The top-level stanza object's class. """ self.__root_stanza.append(stanza_class) def remove_stanza(self, stanza_class): """ Remove a stanza from being a known root stanza. A root stanza is one that appears as a direct child of the stream's root element. Stanzas that are not registered will not be converted into stanza objects, but may still be processed using handlers and matchers. """ del self.__root_stanza[stanza_class] def add_handler(self, mask, pointer, name=None, disposable=False, threaded=False, filter=False, instream=False): """ A shortcut method for registering a handler using XML masks. Arguments: mask -- An XML snippet matching the structure of the stanzas that will be passed to this handler. pointer -- The handler function itself. name -- A unique name for the handler. A name will be generated if one is not provided. disposable -- Indicates if the handler should be discarded after one use. threaded -- Deprecated. Remains for backwards compatibility. filter -- Deprecated. Remains for backwards compatibility. instream -- Indicates if the handler should execute during stream processing and not during normal event processing. """ # To prevent circular dependencies, we must load the matcher # and handler classes here. if name is None: name = 'add_handler_%s' % self.getNewId() self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, once=disposable, instream=instream)) def register_handler(self, handler, before=None, after=None): """ Add a stream event handler that will be executed when a matching stanza is received. Arguments: handler -- The handler object to execute. """ if handler.stream is None: self.__handlers.append(handler) handler.stream = self def remove_handler(self, name): """ Remove any stream event handlers with the given name. Arguments: name -- The name of the handler. """ idx = 0 for handler in self.__handlers: if handler.name == name: self.__handlers.pop(idx) return True idx += 1 return False def add_event_handler(self, name, pointer, threaded=False, disposable=False): """ Add a custom event handler that will be executed whenever its event is manually triggered. Arguments: name -- The name of the event that will trigger this handler. pointer -- The function to execute. threaded -- If set to True, the handler will execute in its own thread. Defaults to False. disposable -- If set to True, the handler will be discarded after one use. Defaults to False. """ if not name in self.__event_handlers: self.__event_handlers[name] = [] self.__event_handlers[name].append((pointer, threaded, disposable)) def del_event_handler(self, name, pointer): """ Remove a function as a handler for an event. Arguments: name -- The name of the event. pointer -- The function to remove as a handler. """ 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_handled(self, name): """ Indicates if an event has any associated handlers. Returns the number of registered handlers. Arguments: name -- The name of the event to check. """ return len(self.__event_handlers.get(name, [])) def event(self, name, data={}, direct=False): """ Manually trigger a custom event. Arguments: name -- The name of the event to trigger. data -- Data that will be passed to each event handler. Defaults to an empty dictionary. direct -- Runs the event directly if True, skipping the event queue. All event handlers will run in the same thread. """ for handler in self.__event_handlers.get(name, []): if direct: try: handler[0](copy.copy(data)) except Exception as e: error_msg = 'Error processing event handler: %s' log.exception(error_msg % str(handler[0])) if hasattr(data, 'exception'): data.exception(e) else: self.event_queue.put(('event', handler, copy.copy(data))) if handler[2]: # If the handler is disposable, we will go ahead and # remove it now instead of waiting for it to be # processed in the queue. with self.__event_handlers_lock: try: h_index = self.__event_handlers[name].index(handler) self.__event_handlers[name].pop(h_index) except: pass def schedule(self, name, seconds, callback, args=None, kwargs=None, repeat=False): """ Schedule a callback function to execute after a given delay. Arguments: name -- A unique name for the scheduled callback. seconds -- The time in seconds to wait before executing. callback -- A pointer to the function to execute. args -- A tuple of arguments to pass to the function. kwargs -- A dictionary of keyword arguments to pass to the function. repeat -- Flag indicating if the scheduled event should be reset and repeat after executing. """ self.scheduler.add(name, seconds, callback, args, kwargs, repeat, qpointer=self.event_queue) def incoming_filter(self, xml): """ Filter incoming XML objects before they are processed. Possible uses include remapping namespaces, or correcting elements from sources with incorrect behavior. Meant to be overridden. """ return xml def send(self, data, mask=None, timeout=None, now=False): """ A wrapper for send_raw for sending stanza objects. May optionally block until an expected response is received. Arguments: data -- The stanza object to send on the stream. mask -- Deprecated. An XML snippet matching the structure of the expected response. Execution will block in this thread until the response is received or a timeout occurs. timeout -- Time in seconds to wait for a response before continuing. Defaults to RESPONSE_TIMEOUT. now -- Indicates if the send queue should be skipped, sending the stanza immediately. Useful mainly for stream initialization stanzas. Defaults to False. """ if timeout is None: timeout = self.response_timeout if hasattr(mask, 'xml'): mask = mask.xml data = str(data) if mask is not None: log.warning("Use of send mask waiters is deprecated.") wait_for = Waiter("SendWait_%s" % self.new_id(), MatchXMLMask(mask)) self.register_handler(wait_for) self.send_raw(data, now) if mask is not None: return wait_for.wait(timeout) def send_xml(self, data, mask=None, timeout=None, now=False): """ Send an XML object on the stream, and optionally wait for a response. Arguments: data -- The XML object to send on the stream. mask -- Deprecated. An XML snippet matching the structure of the expected response. Execution will block in this thread until the response is received or a timeout occurs. timeout -- Time in seconds to wait for a response before continuing. Defaults to RESPONSE_TIMEOUT. now -- Indicates if the send queue should be skipped, sending the stanza immediately. Useful mainly for stream initialization stanzas. Defaults to False. """ if timeout is None: timeout = self.response_timeout return self.send(tostring(data), mask, timeout, now) def send_raw(self, data, now=False, reconnect=None): """ Send raw data across the stream. Arguments: data -- Any string value. reconnect -- Indicates if the stream should be restarted if there is an error sending the stanza. Used mainly for testing. Defaults to self.auto_reconnect. """ if now: log.debug("SEND (IMMED): %s" % data) try: self.socket.send(data.encode('utf-8')) except Socket.error as serr: self.event('socket_error', serr) log.warning("Failed to send %s" % data) if reconnect is None: reconnect = self.auto_reconnect self.disconnect(reconnect) else: self.send_queue.put(data) return True def process(self, threaded=True): """ Initialize the XML streams and begin processing events. The number of threads used for processing stream events is determined by HANDLER_THREADS. Arguments: threaded -- If threaded=True then event dispatcher will run in a separate thread, allowing for the stream to be used in the background for another application. Defaults to True. Event handlers and the send queue will be threaded regardless of this parameter's value. """ self._thread_excepthook() self.scheduler.process(threaded=True) def start_thread(name, target): self.__thread[name] = threading.Thread(name=name, target=target) self.__thread[name].daemon = True self.__thread[name].start() for t in range(0, HANDLER_THREADS): log.debug("Starting HANDLER THREAD") start_thread('stream_event_handler_%s' % t, self._event_runner) start_thread('send_thread', self._send_thread) if threaded: # Run the XML stream in the background for another application. start_thread('process', self._process) else: self._process() def _process(self): """ Start processing the XML streams. Processing will continue after any recoverable errors if reconnections are allowed. """ firstrun = True # The body of this loop will only execute once per connection. # Additional passes will be made only if an error occurs and # reconnecting is permitted. while firstrun or (self.auto_reconnect and not self.stop.isSet()): firstrun = False try: if self.is_client: self.send_raw(self.stream_header, now=True) # The call to self.__read_xml will block and prevent # the body of the loop from running until a disconnect # occurs. After any reconnection, the stream header will # be resent and processing will resume. while not self.stop.isSet() and self.__read_xml(): # Ensure the stream header is sent for any # new connections. if self.is_client: self.send_raw(self.stream_header, now=True) except KeyboardInterrupt: log.debug("Keyboard Escape Detected in _process") self.stop.set() except SystemExit: log.debug("SystemExit in _process") self.stop.set() except Socket.error as serr: self.event('socket_error', serr) log.exception('Socket Error') except: if not self.stop.isSet(): log.exception('Connection error.') if not self.stop.isSet() and self.auto_reconnect: self.reconnect() else: self.event('killed', direct=True) self.disconnect() self.event_queue.put(('quit', None, None)) self.scheduler.run = False def __read_xml(self): """ Parse the incoming XML stream, raising stream events for each received stanza. """ depth = 0 root = None try: for (event, xml) in ET.iterparse(self.filesocket, (b'end', b'start')): if event == b'start': if depth == 0: # We have received the start of the root element. root = xml # Perform any stream initialization actions, such # as handshakes. self.stream_end_event.clear() self.start_stream_handler(root) depth += 1 if event == b'end': depth -= 1 if depth == 0: # The stream's root element has closed, # terminating the stream. log.debug("End of stream recieved") self.stream_end_event.set() return False elif depth == 1: # We only raise events for stanzas that are direct # children of the root element. try: self.__spawn_event(xml) except RestartStream: return True if root: # Keep the root element empty of children to # save on memory use. root.clear() except SyntaxError: log.error("Error reading from XML stream.") log.debug("Ending read XML loop") def _build_stanza(self, xml, default_ns=None): """ Create a stanza object from a given XML object. If a specialized stanza type is not found for the XML, then a generic StanzaBase stanza will be returned. Arguments: xml -- The XML object to convert into a stanza object. default_ns -- Optional default namespace to use instead of the stream's current default namespace. """ if default_ns is None: default_ns = self.default_ns stanza_type = StanzaBase for stanza_class in self.__root_stanza: if xml.tag == "{%s}%s" % (default_ns, stanza_class.name) or \ xml.tag == stanza_class.tag_name(): stanza_type = stanza_class break stanza = stanza_type(self, xml) return stanza def __spawn_event(self, xml): """ Analyze incoming XML stanzas and convert them into stanza objects if applicable and queue stream events to be processed by matching handlers. Arguments: xml -- The XML stanza to analyze. """ log.debug("RECV: %s" % tostring(xml, xmlns=self.default_ns, stream=self)) # Apply any preprocessing filters. xml = self.incoming_filter(xml) # Convert the raw XML object into a stanza object. If no registered # stanza type applies, a generic StanzaBase stanza will be used. stanza = self._build_stanza(xml) # Match the stanza against registered handlers. Handlers marked # to run "in stream" will be executed immediately; the rest will # be queued. unhandled = True for handler in self.__handlers: if handler.match(stanza): stanza_copy = copy.copy(stanza) handler.prerun(stanza_copy) self.event_queue.put(('stanza', handler, stanza_copy)) try: if handler.check_delete(): self.__handlers.remove(handler) except: pass # not thread safe unhandled = False # Some stanzas require responses, such as Iq queries. A default # handler will be executed immediately for this case. if unhandled: stanza.unhandled() def _threaded_event_wrapper(self, func, args): """ Capture exceptions for event handlers that run in individual threads. Arguments: func -- The event handler to execute. args -- Arguments to the event handler. """ orig = copy.copy(args[0]) try: func(*args) except Exception as e: error_msg = 'Error processing event handler: %s' log.exception(error_msg % str(func)) if hasattr(orig, 'exception'): orig.exception(e) def _event_runner(self): """ Process the event queue and execute handlers. The number of event runner threads is controlled by HANDLER_THREADS. Stream event handlers will all execute in this thread. Custom event handlers may be spawned in individual threads. """ log.debug("Loading event runner") try: while not self.stop.isSet(): try: event = self.event_queue.get(True, timeout=5) except queue.Empty: event = None if event is None: continue etype, handler = event[0:2] args = event[2:] orig = copy.copy(args[0]) if etype == 'stanza': try: handler.run(args[0]) except Exception as e: error_msg = 'Error processing stream handler: %s' log.exception(error_msg % handler.name) orig.exception(e) elif etype == 'schedule': try: log.debug('Scheduled event: %s' % args) handler(*args[0]) except: log.exception('Error processing scheduled task') elif etype == 'event': func, threaded, disposable = handler orig = copy.copy(args[0]) try: if threaded: x = threading.Thread( name="Event_%s" % str(func), target=self._threaded_event_wrapper, args=(func, args)) x.start() else: func(*args) except Exception as e: error_msg = 'Error processing event handler: %s' log.exception(error_msg % str(func)) if hasattr(orig, 'exception'): orig.exception(e) elif etype == 'quit': log.debug("Quitting event runner thread") return False except KeyboardInterrupt: log.debug("Keyboard Escape Detected in _event_runner") self.event('killed', direct=True) self.disconnect() return except SystemExit: self.disconnect() self.event_queue.put(('quit', None, None)) return def _send_thread(self): """ Extract stanzas from the send queue and send them on the stream. """ try: while not self.stop.isSet(): self.session_started_event.wait() if self.__failed_send_stanza is not None: data = self.__failed_send_stanza self.__failed_send_stanza = None else: try: data = self.send_queue.get(True, 1) except queue.Empty: continue log.debug("SEND: %s" % data) try: self.socket.send(data.encode('utf-8')) except Socket.error as serr: self.event('socket_error', serr) log.warning("Failed to send %s" % data) self.__failed_send_stanza = data self.disconnect(self.auto_reconnect) except KeyboardInterrupt: log.debug("Keyboard Escape Detected in _send_thread") self.event('killed', direct=True) self.disconnect() return except SystemExit: self.disconnect() self.event_queue.put(('quit', None, None)) return def _thread_excepthook(self): """ If a threaded event handler raises an exception, there is no way to catch it except with an excepthook. Currently, each thread has its own excepthook, but ideally we could use the main sys.excepthook. Modifies threading.Thread to use sys.excepthook when an exception 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) except (KeyboardInterrupt, SystemExit): raise except: 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 fritzy-SleekXMPP-5c4ee57/testall.py000066400000000000000000000056401157775340400172760ustar00rootroot00000000000000#!/usr/bin/env python import unittest import logging import sys import os class testoverall(unittest.TestCase): def testModules(self): """Testing all modules by compiling them""" import compileall import re if sys.version_info < (3,0): self.failUnless(compileall.compile_dir('.' + os.sep + 'sleekxmpp', rx=re.compile('/[.]svn'), quiet=True)) else: self.failUnless(compileall.compile_dir('.' + os.sep + 'sleekxmpp', rx=re.compile('/[.]svn|.*26.*'), quiet=True)) def testTabNanny(self): """Invoking the tabnanny""" import tabnanny self.failIf(tabnanny.check("." + os.sep + 'sleekxmpp')) #raise "Help!" def disabled_testMethodLength(self): """Testing for excessive method lengths""" import re dirs = os.walk(sys.path[0] + os.sep + 'sleekxmpp') offenders = [] for d in dirs: if not '.svn' in d[0]: for filename in d[2]: if filename.endswith('.py') and d[0].find("template%stemplates" % os.sep) == -1: with open("%s%s%s" % (d[0],os.sep,filename), "r") as fp: cur = None methodline = lineno = methodlen = methodindent = 0 for line in fp: indentlevel = re.compile("^[\t ]*").search(line).end() line = line.expandtabs() lineno += 1 if line.strip().startswith("def ") or line.strip().startswith("except") or (line.strip() and methodindent > indentlevel) or (line.strip() and methodindent == indentlevel): #new method found or old one ended if cur: #existing method needs final evaluation if methodlen > 50 and not cur.strip().startswith("def setupUi"): offenders.append("Method '%s' on line %s of %s/%s is longer than 50 lines (%s)" % (cur.strip(),methodline,d[0][len(rootp):],filename,methodlen)) methodlen = 0 cur = line methodindent = indentlevel methodline = lineno if line and cur and not line.strip().startswith("#") and not (cur.strip().startswith("try:") and methodindent == 0): #if we weren't all whitespace and weren't a comment methodlen += 1 self.failIf(offenders,"\n".join(offenders)) if __name__ == '__main__': logging.basicConfig(level=100) logging.disable(100) #this doesn't need to be very clean alltests = [unittest.TestLoader().loadTestsFromTestCase(testoverall)] rootp = sys.path[0] + os.sep + 'tests' dirs = os.walk(rootp) for d in dirs: if not '.svn' in d[0]: for filename in d[2]: if filename.startswith('test_') and filename.endswith('.py'): modname = ('tests' + "." + filename)[:-3].replace(os.sep,'.') __import__(modname) #sys.modules[modname].config = moduleconfig alltests.append(sys.modules[modname].suite) alltests_suite = unittest.TestSuite(alltests) result = unittest.TextTestRunner(verbosity=2).run(alltests_suite) print("""""" % (result.testsRun, len(result.errors), len(result.failures), result.wasSuccessful())) fritzy-SleekXMPP-5c4ee57/tests/000077500000000000000000000000001157775340400164115ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/tests/__init__.py000066400000000000000000000000001157775340400205100ustar00rootroot00000000000000fritzy-SleekXMPP-5c4ee57/tests/live_multiple_streams.py000066400000000000000000000034361157775340400234010ustar00rootroot00000000000000import logging from sleekxmpp.test import * class TestMultipleStreams(SleekTest): """ Test that we can test a live stanza stream. """ def setUp(self): self.client1 = SleekTest() self.client2 = SleekTest() def tearDown(self): self.client1.stream_close() self.client2.stream_close() def testMultipleStreams(self): """Test that we can interact with multiple live ClientXMPP instance.""" client1 = self.client1 client2 = self.client2 client1.stream_start(mode='client', socket='live', skip=True, jid='user@localhost/test1', password='user') client2.stream_start(mode='client', socket='live', skip=True, jid='user@localhost/test2', password='user') client1.xmpp.send_message(mto='user@localhost/test2', mbody='test') client1.send('message@body=test', method='stanzapath') client2.recv('message@body=test', method='stanzapath') suite = unittest.TestLoader().loadTestsFromTestCase(TestMultipleStreams) if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') tests = unittest.TestSuite([suite]) result = unittest.TextTestRunner(verbosity=2).run(tests) test_ns = 'http://andyet.net/protocol/tests' print("" % ( test_ns, 'ran="%s"' % result.testsRun, 'errors="%s"' % len(result.errors), 'fails="%s"' % len(result.failures), 'success="%s"' % result.wasSuccessful())) fritzy-SleekXMPP-5c4ee57/tests/live_test.py000066400000000000000000000065361157775340400207730ustar00rootroot00000000000000import logging from sleekxmpp.test import * class TestLiveStream(SleekTest): """ Test that we can test a live stanza stream. """ def tearDown(self): self.stream_close() def testClientConnection(self): """Test that we can interact with a live ClientXMPP instance.""" self.stream_start(mode='client', socket='live', skip=False, jid='user@localhost/test', password='user') # Use sid=None to ignore any id sent by the server since # we can't know it in advance. self.recv_header(sfrom='localhost', sid=None) self.send_header(sto='localhost') self.recv_feature(""" DIGEST-MD5 PLAIN """) self.send_feature(""" """) self.recv_feature(""" """) self.send_header(sto='localhost') self.recv_header(sfrom='localhost', sid=None) self.recv_feature(""" DIGEST-MD5 PLAIN """) self.send_feature(""" AHVzZXIAdXNlcg== """) self.recv_feature(""" """) self.send_header(sto='localhost') self.recv_header(sfrom='localhost', sid=None) self.recv_feature(""" """) # Should really use send, but our Iq stanza objects # can't handle bind element payloads yet. self.send_feature(""" test """) self.recv_feature(""" user@localhost/test """) self.stream_close() suite = unittest.TestLoader().loadTestsFromTestCase(TestLiveStream) if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, format='%(levelname)-8s %(message)s') tests = unittest.TestSuite([suite]) result = unittest.TextTestRunner(verbosity=2).run(tests) test_ns = 'http://andyet.net/protocol/tests' print("" % ( test_ns, 'ran="%s"' % result.testsRun, 'errors="%s"' % len(result.errors), 'fails="%s"' % len(result.failures), 'success="%s"' % result.wasSuccessful())) fritzy-SleekXMPP-5c4ee57/tests/test_events.py000066400000000000000000000040511157775340400213260ustar00rootroot00000000000000import time from sleekxmpp.test import * class TestEvents(SleekTest): def setUp(self): self.stream_start() def tearDown(self): self.stream_close() def testEventHappening(self): """Test handler working""" happened = [] def handletestevent(event): happened.append(True) self.xmpp.add_event_handler("test_event", handletestevent) self.xmpp.event("test_event") self.xmpp.event("test_event") # Give the event queue time to process. time.sleep(0.1) msg = "Event was not triggered the correct number of times: %s" self.failUnless(happened == [True, True], msg) def testDelEvent(self): """Test handler working, then deleted and not triggered""" happened = [] def handletestevent(event): happened.append(True) self.xmpp.add_event_handler("test_event", handletestevent) self.xmpp.event("test_event", {}) self.xmpp.del_event_handler("test_event", handletestevent) # Should not trigger because it was deleted self.xmpp.event("test_event", {}) # Give the event queue time to process. time.sleep(0.1) msg = "Event was not triggered the correct number of times: %s" self.failUnless(happened == [True], msg % happened) def testDisposableEvent(self): """Test disposable handler working, then not being triggered again.""" happened = [] def handletestevent(event): happened.append(True) self.xmpp.add_event_handler("test_event", handletestevent, disposable=True) self.xmpp.event("test_event", {}) # Should not trigger because it was deleted self.xmpp.event("test_event", {}) # Give the event queue time to process. time.sleep(0.1) msg = "Event was not triggered the correct number of times: %s" self.failUnless(happened == [True], msg % happened) suite = unittest.TestLoader().loadTestsFromTestCase(TestEvents) fritzy-SleekXMPP-5c4ee57/tests/test_jid.py000066400000000000000000000110061157775340400205660ustar00rootroot00000000000000from sleekxmpp.test import * from sleekxmpp.xmlstream.jid import JID class TestJIDClass(SleekTest): """Verify that the JID class can parse and manipulate JIDs.""" def testJIDFromFull(self): """Test using JID of the form 'user@server/resource/with/slashes'.""" self.check_jid(JID('user@someserver/some/resource'), 'user', 'someserver', 'some/resource', 'user@someserver', 'user@someserver/some/resource', 'user@someserver/some/resource') def testJIDchange(self): """Test changing JID of the form 'user@server/resource/with/slashes'""" j = JID('user1@someserver1/some1/resource1') j.user = 'user' j.domain = 'someserver' j.resource = 'some/resource' self.check_jid(j, 'user', 'someserver', 'some/resource', 'user@someserver', 'user@someserver/some/resource', 'user@someserver/some/resource') def testJIDaliases(self): """Test changing JID using aliases for domain.""" j = JID('user@someserver/resource') j.server = 'anotherserver' self.check_jid(j, domain='anotherserver') j.host = 'yetanother' self.check_jid(j, domain='yetanother') def testJIDSetFullWithUser(self): """Test setting the full JID with a user portion.""" j = JID('user@domain/resource') j.full = 'otheruser@otherdomain/otherresource' self.check_jid(j, 'otheruser', 'otherdomain', 'otherresource', 'otheruser@otherdomain', 'otheruser@otherdomain/otherresource', 'otheruser@otherdomain/otherresource') def testJIDFullNoUserWithResource(self): """ Test setting the full JID without a user portion and with a resource. """ j = JID('user@domain/resource') j.full = 'otherdomain/otherresource' self.check_jid(j, '', 'otherdomain', 'otherresource', 'otherdomain', 'otherdomain/otherresource', 'otherdomain/otherresource') def testJIDFullNoUserNoResource(self): """ Test setting the full JID without a user portion and without a resource. """ j = JID('user@domain/resource') j.full = 'otherdomain' self.check_jid(j, '', 'otherdomain', '', 'otherdomain', 'otherdomain', 'otherdomain') def testJIDBareUser(self): """Test setting the bare JID with a user.""" j = JID('user@domain/resource') j.bare = 'otheruser@otherdomain' self.check_jid(j, 'otheruser', 'otherdomain', 'resource', 'otheruser@otherdomain', 'otheruser@otherdomain/resource', 'otheruser@otherdomain/resource') def testJIDBareNoUser(self): """Test setting the bare JID without a user.""" j = JID('user@domain/resource') j.bare = 'otherdomain' self.check_jid(j, '', 'otherdomain', 'resource', 'otherdomain', 'otherdomain/resource', 'otherdomain/resource') def testJIDNoResource(self): """Test using JID of the form 'user@domain'.""" self.check_jid(JID('user@someserver'), 'user', 'someserver', '', 'user@someserver', 'user@someserver', 'user@someserver') def testJIDNoUser(self): """Test JID of the form 'component.domain.tld'.""" self.check_jid(JID('component.someserver'), '', 'component.someserver', '', 'component.someserver', 'component.someserver', 'component.someserver') suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_base.py000066400000000000000000000055651157775340400223270ustar00rootroot00000000000000from sleekxmpp.test import * from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase class TestStanzaBase(SleekTest): def testTo(self): """Test the 'to' interface of StanzaBase.""" stanza = StanzaBase() stanza['to'] = 'user@example.com' self.failUnless(str(stanza['to']) == 'user@example.com', "Setting and retrieving stanza 'to' attribute did not work.") def testFrom(self): """Test the 'from' interface of StanzaBase.""" stanza = StanzaBase() stanza['from'] = 'user@example.com' self.failUnless(str(stanza['from']) == 'user@example.com', "Setting and retrieving stanza 'from' attribute did not work.") def testPayload(self): """Test the 'payload' interface of StanzaBase.""" stanza = StanzaBase() self.failUnless(stanza['payload'] == [], "Empty stanza does not have an empty payload.") stanza['payload'] = ET.Element("{foo}foo") self.failUnless(len(stanza['payload']) == 1, "Stanza contents and payload do not match.") stanza['payload'] = ET.Element('{bar}bar') self.failUnless(len(stanza['payload']) == 2, "Stanza payload was not appended.") del stanza['payload'] self.failUnless(stanza['payload'] == [], "Stanza payload not cleared after deletion.") stanza['payload'] = [ET.Element('{foo}foo'), ET.Element('{bar}bar')] self.failUnless(len(stanza['payload']) == 2, "Adding multiple elements to stanza's payload did not work.") def testClear(self): """Test clearing a stanza.""" stanza = StanzaBase() stanza['to'] = 'user@example.com' stanza['payload'] = ET.Element("{foo}foo") stanza.clear() self.failUnless(stanza['payload'] == [], "Stanza payload was not cleared after calling .clear()") self.failUnless(str(stanza['to']) == "user@example.com", "Stanza attributes were not preserved after calling .clear()") def testReply(self): """Test creating a reply stanza.""" stanza = StanzaBase() stanza['to'] = "recipient@example.com" stanza['from'] = "sender@example.com" stanza['payload'] = ET.Element("{foo}foo") stanza.reply() self.failUnless(str(stanza['to'] == "sender@example.com"), "Stanza reply did not change 'to' attribute.") self.failUnless(stanza['payload'] == [], "Stanza reply did not empty stanza payload.") def testError(self): """Test marking a stanza as an error.""" stanza = StanzaBase() stanza['type'] = 'get' stanza.error() self.failUnless(stanza['type'] == 'error', "Stanza type is not 'error' after calling error()") suite = unittest.TestLoader().loadTestsFromTestCase(TestStanzaBase) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_element.py000066400000000000000000000530051157775340400230360ustar00rootroot00000000000000from sleekxmpp.test import * from sleekxmpp.xmlstream.stanzabase import ElementBase class TestElementBase(SleekTest): def testFixNs(self): """Test fixing namespaces in an XPath expression.""" e = ElementBase() ns = "http://jabber.org/protocol/disco#items" result = e._fix_ns("{%s}foo/bar/{abc}baz/{%s}more" % (ns, ns)) expected = "/".join(["{%s}foo" % ns, "{%s}bar" % ns, "{abc}baz", "{%s}more" % ns]) self.failUnless(expected == result, "Incorrect namespace fixing result: %s" % str(result)) def testExtendedName(self): """Test element names of the form tag1/tag2/tag3.""" class TestStanza(ElementBase): name = "foo/bar/baz" namespace = "test" stanza = TestStanza() self.check(stanza, """ """) def testGetStanzaValues(self): """Test getStanzaValues using plugins and substanzas.""" class TestStanzaPlugin(ElementBase): name = "foo2" namespace = "foo" interfaces = set(('bar', 'baz')) plugin_attrib = "foo2" class TestSubStanza(ElementBase): name = "subfoo" namespace = "foo" interfaces = set(('bar', 'baz')) class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) register_stanza_plugin(TestStanza, TestStanzaPlugin, iterable=True) stanza = TestStanza() stanza['bar'] = 'a' stanza['foo2']['baz'] = 'b' substanza = TestSubStanza() substanza['bar'] = 'c' stanza.append(substanza) values = stanza.getStanzaValues() expected = {'bar': 'a', 'baz': '', 'foo2': {'bar': '', 'baz': 'b'}, 'substanzas': [{'__childtag__': '{foo}subfoo', 'bar': 'c', 'baz': ''}]} self.failUnless(values == expected, "Unexpected stanza values:\n%s\n%s" % (str(expected), str(values))) def testSetStanzaValues(self): """Test using setStanzaValues with substanzas and plugins.""" class TestStanzaPlugin(ElementBase): name = "pluginfoo" namespace = "foo" interfaces = set(('bar', 'baz')) plugin_attrib = "plugin_foo" class TestStanzaPlugin2(ElementBase): name = "pluginfoo2" namespace = "foo" interfaces = set(('bar', 'baz')) plugin_attrib = "plugin_foo2" class TestSubStanza(ElementBase): name = "subfoo" namespace = "foo" interfaces = set(('bar', 'baz')) class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) register_stanza_plugin(TestStanza, TestSubStanza, iterable=True) register_stanza_plugin(TestStanza, TestStanzaPlugin) register_stanza_plugin(TestStanza, TestStanzaPlugin2) stanza = TestStanza() values = {'bar': 'a', 'baz': '', 'plugin_foo': {'bar': '', 'baz': 'b'}, 'plugin_foo2': {'bar': 'd', 'baz': 'e'}, 'substanzas': [{'__childtag__': '{foo}subfoo', 'bar': 'c', 'baz': ''}]} stanza.values = values self.check(stanza, """ """) def testGetItem(self): """Test accessing stanza interfaces.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz', 'qux')) sub_interfaces = set(('baz',)) def getQux(self): return 'qux' class TestStanzaPlugin(ElementBase): name = "foobar" namespace = "foo" plugin_attrib = "foobar" interfaces = set(('fizz',)) register_stanza_plugin(TestStanza, TestStanza, iterable=True) register_stanza_plugin(TestStanza, TestStanzaPlugin) stanza = TestStanza() substanza = TestStanza() stanza.append(substanza) stanza.setStanzaValues({'bar': 'a', 'baz': 'b', 'qux': 42, 'foobar': {'fizz': 'c'}}) # Test non-plugin interfaces expected = {'substanzas': [substanza], 'bar': 'a', 'baz': 'b', 'qux': 'qux', 'meh': ''} for interface, value in expected.items(): result = stanza[interface] self.failUnless(result == value, "Incorrect stanza interface access result: %s" % result) # Test plugin interfaces self.failUnless(isinstance(stanza['foobar'], TestStanzaPlugin), "Incorrect plugin object result.") self.failUnless(stanza['foobar']['fizz'] == 'c', "Incorrect plugin subvalue result.") def testSetItem(self): """Test assigning to stanza interfaces.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz', 'qux')) sub_interfaces = set(('baz',)) def setQux(self, value): pass class TestStanzaPlugin(ElementBase): name = "foobar" namespace = "foo" plugin_attrib = "foobar" interfaces = set(('foobar',)) register_stanza_plugin(TestStanza, TestStanzaPlugin) stanza = TestStanza() stanza['bar'] = 'attribute!' stanza['baz'] = 'element!' stanza['qux'] = 'overridden' stanza['foobar'] = 'plugin' self.check(stanza, """ element! """) def testDelItem(self): """Test deleting stanza interface values.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz', 'qux')) sub_interfaces = set(('bar',)) def delQux(self): pass class TestStanzaPlugin(ElementBase): name = "foobar" namespace = "foo" plugin_attrib = "foobar" interfaces = set(('foobar',)) register_stanza_plugin(TestStanza, TestStanzaPlugin) stanza = TestStanza() stanza['bar'] = 'a' stanza['baz'] = 'b' stanza['qux'] = 'c' stanza['foobar']['foobar'] = 'd' self.check(stanza, """ a """) del stanza['bar'] del stanza['baz'] del stanza['qux'] del stanza['foobar'] self.check(stanza, """ """) def testModifyingAttributes(self): """Test modifying top level attributes of a stanza's XML object.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) stanza = TestStanza() self.check(stanza, """ """) self.failUnless(stanza._get_attr('bar') == '', "Incorrect value returned for an unset XML attribute.") stanza._set_attr('bar', 'a') stanza._set_attr('baz', 'b') self.check(stanza, """ """) self.failUnless(stanza._get_attr('bar') == 'a', "Retrieved XML attribute value is incorrect.") stanza._set_attr('bar', None) stanza._del_attr('baz') self.check(stanza, """ """) self.failUnless(stanza._get_attr('bar', 'c') == 'c', "Incorrect default value returned for an unset XML attribute.") def testGetSubText(self): """Test retrieving the contents of a sub element.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar',)) def setBar(self, value): wrapper = ET.Element("{foo}wrapper") bar = ET.Element("{foo}bar") bar.text = value wrapper.append(bar) self.xml.append(wrapper) def getBar(self): return self._get_sub_text("wrapper/bar", default="not found") stanza = TestStanza() self.failUnless(stanza['bar'] == 'not found', "Default _get_sub_text value incorrect.") stanza['bar'] = 'found' self.check(stanza, """ found """) self.failUnless(stanza['bar'] == 'found', "_get_sub_text value incorrect: %s." % stanza['bar']) def testSubElement(self): """Test setting the contents of a sub element.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) def setBaz(self, value): self._set_sub_text("wrapper/baz", text=value) def getBaz(self): return self._get_sub_text("wrapper/baz") def setBar(self, value): self._set_sub_text("wrapper/bar", text=value) def getBar(self): return self._get_sub_text("wrapper/bar") stanza = TestStanza() stanza['bar'] = 'a' stanza['baz'] = 'b' self.check(stanza, """ a b """) stanza._set_sub_text('wrapper/bar', text='', keep=True) self.check(stanza, """ b """, use_values=False) stanza['bar'] = 'a' stanza._set_sub_text('wrapper/bar', text='') self.check(stanza, """ b """) def testDelSub(self): """Test removing sub elements.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) def setBar(self, value): self._set_sub_text("path/to/only/bar", value); def getBar(self): return self._get_sub_text("path/to/only/bar") def delBar(self): self._del_sub("path/to/only/bar") def setBaz(self, value): self._set_sub_text("path/to/just/baz", value); def getBaz(self): return self._get_sub_text("path/to/just/baz") def delBaz(self): self._del_sub("path/to/just/baz") stanza = TestStanza() stanza['bar'] = 'a' stanza['baz'] = 'b' self.check(stanza, """ a b """) del stanza['bar'] del stanza['baz'] self.check(stanza, """ """, use_values=False) stanza['bar'] = 'a' stanza['baz'] = 'b' stanza._del_sub('path/to/only/bar', all=True) self.check(stanza, """ b """) def testMatch(self): """Test matching a stanza against an XPath expression.""" class TestSubStanza(ElementBase): name = "sub" namespace = "baz" interfaces = set(('attrib',)) class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar','baz', 'qux')) sub_interfaces = set(('qux',)) def setQux(self, value): self._set_sub_text('qux', text=value) def getQux(self): return self._get_sub_text('qux') class TestStanzaPlugin(ElementBase): name = "plugin" namespace = "http://test/slash/bar" interfaces = set(('attrib',)) register_stanza_plugin(TestStanza, TestSubStanza, iterable=True) register_stanza_plugin(TestStanza, TestStanzaPlugin) stanza = TestStanza() self.failUnless(stanza.match("foo"), "Stanza did not match its own tag name.") self.failUnless(stanza.match("{foo}foo"), "Stanza did not match its own namespaced name.") stanza['bar'] = 'a' self.failUnless(stanza.match("foo@bar=a"), "Stanza did not match its own name with attribute value check.") stanza['baz'] = 'b' self.failUnless(stanza.match("foo@bar=a@baz=b"), "Stanza did not match its own name with multiple attributes.") stanza['qux'] = 'c' self.failUnless(stanza.match("foo/qux"), "Stanza did not match with subelements.") stanza['qux'] = '' self.failUnless(stanza.match("foo/qux") == False, "Stanza matched missing subinterface element.") self.failUnless(stanza.match("foo/bar") == False, "Stanza matched nonexistent element.") stanza['plugin']['attrib'] = 'c' self.failUnless(stanza.match("foo/plugin@attrib=c"), "Stanza did not match with plugin and attribute.") self.failUnless(stanza.match("foo/{http://test/slash/bar}plugin"), "Stanza did not match with namespaced plugin.") substanza = TestSubStanza() substanza['attrib'] = 'd' stanza.append(substanza) self.failUnless(stanza.match("foo/sub@attrib=d"), "Stanza did not match with substanzas and attribute.") self.failUnless(stanza.match("foo/{baz}sub"), "Stanza did not match with namespaced substanza.") def testComparisons(self): """Test comparing ElementBase objects.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) stanza1 = TestStanza() stanza1['bar'] = 'a' self.failUnless(stanza1, "Stanza object does not evaluate to True") stanza2 = TestStanza() stanza2['baz'] = 'b' self.failUnless(stanza1 != stanza2, "Different stanza objects incorrectly compared equal.") stanza1['baz'] = 'b' stanza2['bar'] = 'a' self.failUnless(stanza1 == stanza2, "Equal stanzas incorrectly compared inequal.") def testKeys(self): """Test extracting interface names from a stanza object.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) plugin_attrib = 'qux' register_stanza_plugin(TestStanza, TestStanza) stanza = TestStanza() self.failUnless(set(stanza.keys()) == set(('bar', 'baz')), "Returned set of interface keys does not match expected.") stanza.enable('qux') self.failUnless(set(stanza.keys()) == set(('bar', 'baz', 'qux')), "Incorrect set of interface and plugin keys.") def testGet(self): """Test accessing stanza interfaces using get().""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) stanza = TestStanza() stanza['bar'] = 'a' self.failUnless(stanza.get('bar') == 'a', "Incorrect value returned by stanza.get") self.failUnless(stanza.get('baz', 'b') == 'b', "Incorrect default value returned by stanza.get") def testSubStanzas(self): """Test manipulating substanzas of a stanza object.""" class TestSubStanza(ElementBase): name = "foobar" namespace = "foo" interfaces = set(('qux',)) class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) register_stanza_plugin(TestStanza, TestSubStanza, iterable=True) stanza = TestStanza() substanza1 = TestSubStanza() substanza2 = TestSubStanza() substanza1['qux'] = 'a' substanza2['qux'] = 'b' # Test appending substanzas self.failUnless(len(stanza) == 0, "Incorrect empty stanza size.") stanza.append(substanza1) self.check(stanza, """ """, use_values=False) self.failUnless(len(stanza) == 1, "Incorrect stanza size with 1 substanza.") stanza.append(substanza2) self.check(stanza, """ """, use_values=False) self.failUnless(len(stanza) == 2, "Incorrect stanza size with 2 substanzas.") # Test popping substanzas stanza.pop(0) self.check(stanza, """ """, use_values=False) # Test iterating over substanzas stanza.append(substanza1) results = [] for substanza in stanza: results.append(substanza['qux']) self.failUnless(results == ['b', 'a'], "Iteration over substanzas failed: %s." % str(results)) def testCopy(self): """Test copying stanza objects.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) stanza1 = TestStanza() stanza1['bar'] = 'a' stanza2 = stanza1.__copy__() self.failUnless(stanza1 == stanza2, "Copied stanzas are not equal to each other.") stanza1['baz'] = 'b' self.failUnless(stanza1 != stanza2, "Divergent stanza copies incorrectly compared equal.") def testExtension(self): """Testing using is_extension.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) class TestExtension(ElementBase): name = 'extended' namespace = 'foo' plugin_attrib = name interfaces = set((name,)) is_extension = True def set_extended(self, value): self.xml.text = value def get_extended(self): return self.xml.text def del_extended(self): self.parent().xml.remove(self.xml) register_stanza_plugin(TestStanza, TestExtension) stanza = TestStanza() stanza['extended'] = 'testing' self.check(stanza, """ testing """) self.failUnless(stanza['extended'] == 'testing', "Could not retrieve stanza extension value.") del stanza['extended'] self.check(stanza, """ """) def testOverrides(self): """Test using interface overrides.""" class TestStanza(ElementBase): name = "foo" namespace = "foo" interfaces = set(('bar', 'baz')) class TestOverride(ElementBase): name = 'overrider' namespace = 'foo' plugin_attrib = name interfaces = set(('bar',)) overrides = ['set_bar'] def setup(self, xml): # Don't create XML for the plugin self.xml = ET.Element('') def set_bar(self, value): if not value.startswith('override-'): self.parent()._set_attr('bar', 'override-%s' % value) else: self.parent()._set_attr('bar', value) stanza = TestStanza() stanza['bar'] = 'foo' self.check(stanza, """ """) register_stanza_plugin(TestStanza, TestOverride, overrides=True) stanza = TestStanza() stanza['bar'] = 'foo' self.check(stanza, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_error.py000066400000000000000000000047001157775340400225340ustar00rootroot00000000000000from sleekxmpp.test import * class TestErrorStanzas(SleekTest): def setUp(self): # Ensure that the XEP-0086 plugin has been loaded. self.stream_start() self.stream_close() def testSetup(self): """Test setting initial values in error stanza.""" msg = self.Message() msg.enable('error') self.check(msg, """ """) def testCondition(self): """Test modifying the error condition.""" msg = self.Message() msg['error']['condition'] = 'item-not-found' self.check(msg, """ """) self.failUnless(msg['error']['condition'] == 'item-not-found', "Error condition doesn't match.") msg['error']['condition'] = 'resource-constraint' self.check(msg, """ """) def testDelCondition(self): """Test that deleting error conditions doesn't remove extra elements.""" msg = self.Message() msg['error']['text'] = 'Error!' msg['error']['condition'] = 'internal-server-error' del msg['error']['condition'] self.check(msg, """ Error! """, use_values=False) def testDelText(self): """Test deleting the text of an error.""" msg = self.Message() msg['error']['test'] = 'Error!' msg['error']['condition'] = 'internal-server-error' del msg['error']['text'] self.check(msg, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorStanzas) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_gmail.py000066400000000000000000000101501157775340400224700ustar00rootroot00000000000000from sleekxmpp.test import * import sleekxmpp.plugins.gmail_notify as gmail class TestGmail(SleekTest): def setUp(self): register_stanza_plugin(Iq, gmail.GmailQuery) register_stanza_plugin(Iq, gmail.MailBox) register_stanza_plugin(Iq, gmail.NewMail) def testCreateQuery(self): """Testing querying Gmail for emails.""" iq = self.Iq() iq['type'] = 'get' iq['gmail']['search'] = 'is:starred' iq['gmail']['newer-than-time'] = '1140638252542' iq['gmail']['newer-than-tid'] = '11134623426430234' self.check(iq, """ """, use_values=False) def testMailBox(self): """Testing reading from Gmail mailbox result""" # Use the example from Google's documentation at # http://code.google.com/apis/talk/jep_extensions/gmail.html#notifications xml = ET.fromstring(""" act1scene3 Put thy rapier up. Ay, ay, a scratch, a scratch; marry, 'tis enough. """) iq = self.Iq(xml=xml) mailbox = iq['mailbox'] self.failUnless(mailbox['result-time'] == '1118012394209', "result-time doesn't match") self.failUnless(mailbox['url'] == 'http://mail.google.com/mail', "url doesn't match") self.failUnless(mailbox['matched'] == '95', "total-matched incorrect") self.failUnless(mailbox['estimate'] == False, "total-estimate incorrect") self.failUnless(len(mailbox['threads']) == 1, "could not extract message threads") thread = mailbox['threads'][0] self.failUnless(thread['tid'] == '1172320964060972012', "thread tid doesn't match") self.failUnless(thread['participation'] == '1', "thread participation incorrect") self.failUnless(thread['messages'] == '28', "thread message count incorrect") self.failUnless(thread['date'] == '1118012394209', "thread date doesn't match") self.failUnless(thread['url'] == 'http://mail.google.com/mail?view=cv', "thread url doesn't match") self.failUnless(thread['labels'] == 'act1scene3', "thread labels incorrect") self.failUnless(thread['subject'] == 'Put thy rapier up.', "thread subject doesn't match") self.failUnless(thread['snippet'] == "Ay, ay, a scratch, a scratch; marry, 'tis enough.", "snippet doesn't match") self.failUnless(len(thread['senders']) == 3, "could not extract senders") sender1 = thread['senders'][0] self.failUnless(sender1['name'] == 'Me', "sender name doesn't match") self.failUnless(sender1['address'] == 'romeo@gmail.com', "sender address doesn't match") self.failUnless(sender1['originator'] == True, "sender originator incorrect") self.failUnless(sender1['unread'] == False, "sender unread incorrectly True") sender2 = thread['senders'][2] self.failUnless(sender2['unread'] == True, "sender unread incorrectly False") suite = unittest.TestLoader().loadTestsFromTestCase(TestGmail) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_iq.py000066400000000000000000000045061157775340400220200ustar00rootroot00000000000000from sleekxmpp.test import * from sleekxmpp.xmlstream.stanzabase import ET class TestIqStanzas(SleekTest): def tearDown(self): """Shutdown the XML stream after testing.""" self.stream_close() def testSetup(self): """Test initializing default Iq values.""" iq = self.Iq() self.check(iq, """ """) def testPayload(self): """Test setting Iq stanza payload.""" iq = self.Iq() iq.setPayload(ET.Element('{test}tester')) self.check(iq, """ """, use_values=False) def testUnhandled(self): """Test behavior for Iq.unhandled.""" self.stream_start() self.recv(""" """) iq = self.Iq() iq['id'] = 'test' iq['error']['condition'] = 'feature-not-implemented' iq['error']['text'] = 'No handlers registered for this request.' self.send(iq, """ No handlers registered for this request. """) def testQuery(self): """Test modifying query element of Iq stanzas.""" iq = self.Iq() iq['query'] = 'query_ns' self.check(iq, """ """) iq['query'] = 'query_ns2' self.check(iq, """ """) self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match") del iq['query'] self.check(iq, """ """) def testReply(self): """Test setting proper result type in Iq replies.""" iq = self.Iq() iq['to'] = 'user@localhost' iq['type'] = 'get' iq.reply() self.check(iq, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestIqStanzas) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_message.py000066400000000000000000000037161157775340400230350ustar00rootroot00000000000000from sleekxmpp.test import * from sleekxmpp.stanza.message import Message from sleekxmpp.stanza.htmlim import HTMLIM class TestMessageStanzas(SleekTest): def setUp(self): register_stanza_plugin(Message, HTMLIM) def testGroupchatReplyRegression(self): "Regression groupchat reply should be to barejid" msg = self.Message() msg['to'] = 'me@myserver.tld' msg['from'] = 'room@someservice.someserver.tld/somenick' msg['type'] = 'groupchat' msg['body'] = "this is a message" msg.reply() self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld') def testAttribProperty(self): "Test attrib property returning self" msg = self.Message() msg.attrib.attrib.attrib['to'] = 'usr@server.tld' self.failUnless(str(msg['to']) == 'usr@server.tld') def testHTMLPlugin(self): "Test message/html/body stanza" msg = self.Message() msg['to'] = "fritzy@netflint.net/sleekxmpp" msg['body'] = "this is the plaintext message" msg['type'] = 'chat' p = ET.Element('{http://www.w3.org/1999/xhtml}p') p.text = "This is the htmlim message" msg['html']['body'] = p self.check(msg, """ this is the plaintext message

This is the htmlim message

""") def testNickPlugin(self): "Test message/nick/nick stanza." msg = self.Message() msg['nick']['nick'] = 'A nickname!' self.check(msg, """ A nickname! """) suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_presence.py000066400000000000000000000042171157775340400232120ustar00rootroot00000000000000from sleekxmpp.test import * from sleekxmpp.stanza.presence import Presence class TestPresenceStanzas(SleekTest): def testPresenceShowRegression(self): """Regression check presence['type'] = 'dnd' show value working""" p = self.Presence() p['type'] = 'dnd' self.check(p, "dnd") def testPresenceType(self): """Test manipulating presence['type']""" p = self.Presence() p['type'] = 'available' self.check(p, "") self.failUnless(p['type'] == 'available', "Incorrect presence['type'] for type 'available': %s" % p['type']) for showtype in ['away', 'chat', 'dnd', 'xa']: p['type'] = showtype self.check(p, """ %s """ % showtype) self.failUnless(p['type'] == showtype, "Incorrect presence['type'] for type '%s'" % showtype) p['type'] = None self.check(p, "") def testPresenceUnsolicitedOffline(self): """ Unsolicted offline presence does not spawn changed_status or update the roster. """ p = self.Presence() p['type'] = 'unavailable' p['from'] = 'bill@chadmore.com/gmail15af' c = sleekxmpp.ClientXMPP('crap@wherever', 'password') happened = [] def handlechangedpresence(event): happened.append(True) c.add_event_handler("changed_status", handlechangedpresence) c._handle_presence(p) self.failUnless(happened == [], "changed_status event triggered for extra unavailable presence") self.failUnless(c.roster == {}, "Roster updated for superfulous unavailable presence") def testNickPlugin(self): """Test presence/nick/nick stanza.""" p = self.Presence() p['nick']['nick'] = 'A nickname!' self.check(p, """ A nickname! """) suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_roster.py000066400000000000000000000055001157775340400227200ustar00rootroot00000000000000from sleekxmpp.test import * from sleekxmpp.stanza.roster import Roster class TestRosterStanzas(SleekTest): def testAddItems(self): """Test adding items to a roster stanza.""" iq = self.Iq() iq['roster'].setItems({ 'user@example.com': { 'name': 'User', 'subscription': 'both', 'groups': ['Friends', 'Coworkers']}, 'otheruser@example.com': { 'name': 'Other User', 'subscription': 'both', 'groups': []}}) self.check(iq, """ Friends Coworkers """) def testGetItems(self): """Test retrieving items from a roster stanza.""" xml_string = """ Friends Coworkers """ iq = self.Iq(ET.fromstring(xml_string)) expected = { 'user@example.com': { 'name': 'User', 'subscription': 'both', 'groups': ['Friends', 'Coworkers']}, 'otheruser@example.com': { 'name': 'Other User', 'subscription': 'both', 'groups': []}} debug = "Roster items don't match after retrieval." debug += "\nReturned: %s" % str(iq['roster']['items']) debug += "\nExpected: %s" % str(expected) self.failUnless(iq['roster']['items'] == expected, debug) def testDelItems(self): """Test clearing items from a roster stanza.""" xml_string = """ Friends Coworkers """ iq = self.Iq(ET.fromstring(xml_string)) del iq['roster']['items'] self.check(iq, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestRosterStanzas) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_xep_0004.py000066400000000000000000000077711157775340400226550ustar00rootroot00000000000000from sleekxmpp.test import * import sleekxmpp.plugins.xep_0004 as xep_0004 class TestDataForms(SleekTest): def setUp(self): register_stanza_plugin(Message, xep_0004.Form) register_stanza_plugin(xep_0004.Form, xep_0004.FormField) register_stanza_plugin(xep_0004.FormField, xep_0004.FieldOption) def testMultipleInstructions(self): """Testing using multiple instructions elements in a data form.""" msg = self.Message() msg['form']['instructions'] = "Instructions\nSecond batch" self.check(msg, """ Instructions Second batch """) def testAddField(self): """Testing adding fields to a data form.""" msg = self.Message() form = msg['form'] form.addField(var='f1', ftype='text-single', label='Text', desc='A text field', required=True, value='Some text!') self.check(msg, """ A text field Some text! """) form['fields'] = [('f1', {'type': 'text-single', 'label': 'Username', 'required': True}), ('f2', {'type': 'text-private', 'label': 'Password', 'required': True}), ('f3', {'type': 'text-multi', 'label': 'Message', 'value': 'Enter message.\nA long one even.'}), ('f4', {'type': 'list-single', 'label': 'Message Type', 'options': [{'label': 'Cool!', 'value': 'cool'}, {'label': 'Urgh!', 'value': 'urgh'}]})] self.check(msg, """ Enter message. A long one even. """) def testSetValues(self): """Testing setting form values""" msg = self.Message() form = msg['form'] form.setFields([ ('foo', {'type': 'text-single'}), ('bar', {'type': 'list-multi'})]) form.setValues({'foo': 'Foo!', 'bar': ['a', 'b']}) self.check(msg, """ Foo! a b """) suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_xep_0009.py000066400000000000000000000033201157775340400226440ustar00rootroot00000000000000""" 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, """ system.exit """, use_values=False) def testMethodResponse(self): iq = self.Iq() iq['rpc_query']['method_response']['params'] = py2xml(*()) self.check(iq, """ """, use_values=False) suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberRPC) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_xep_0030.py000066400000000000000000000422631157775340400226470ustar00rootroot00000000000000from sleekxmpp.test import * import sleekxmpp.plugins.xep_0030 as xep_0030 class TestDisco(SleekTest): """ Test creating and manipulating the disco#info and disco#items stanzas from the XEP-0030 plugin. """ def setUp(self): register_stanza_plugin(Iq, xep_0030.DiscoInfo) register_stanza_plugin(Iq, xep_0030.DiscoItems) def testCreateInfoQueryNoNode(self): """Testing disco#info query with no node.""" iq = self.Iq() iq['disco_info']['node'] = '' self.check(iq, """ """) def testCreateInfoQueryWithNode(self): """Testing disco#info query with a node.""" iq = self.Iq() iq['disco_info']['node'] = 'foo' self.check(iq, """ """) def testCreateItemsQueryNoNode(self): """Testing disco#items query with no node.""" iq = self.Iq() iq['disco_items']['node'] = '' self.check(iq, """ """) def testCreateItemsQueryWithNode(self): """Testing disco#items query with a node.""" iq = self.Iq() iq['disco_items']['node'] = 'foo' self.check(iq, """ """) def testIdentities(self): """Testing adding identities to disco#info.""" iq = self.Iq() iq['disco_info'].add_identity('conference', 'text', name='Chatroom', lang='en') self.check(iq, """ """) def testDuplicateIdentities(self): """ Test adding multiple copies of the same category and type combination. Only the first identity should be kept. """ iq = self.Iq() iq['disco_info'].add_identity('conference', 'text', name='Chatroom') iq['disco_info'].add_identity('conference', 'text', name='MUC') self.check(iq, """ """) def testDuplicateIdentitiesWithLangs(self): """ Test adding multiple copies of the same category, type, and language combination. Only the first identity should be kept. """ iq = self.Iq() iq['disco_info'].add_identity('conference', 'text', name='Chatroom', lang='en') iq['disco_info'].add_identity('conference', 'text', name='MUC', lang='en') self.check(iq, """ """) def testRemoveIdentitiesNoLang(self): """Test removing identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'pc') iq['disco_info'].add_identity('client', 'bot') iq['disco_info'].del_identity('client', 'pc') self.check(iq, """ """) def testRemoveIdentitiesWithLang(self): """Test removing identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'pc') iq['disco_info'].add_identity('client', 'bot') iq['disco_info'].add_identity('client', 'pc', lang='no') iq['disco_info'].del_identity('client', 'pc') self.check(iq, """ """) def testRemoveAllIdentitiesNoLang(self): """Test removing all identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'bot', name='Bot') iq['disco_info'].add_identity('client', 'bot', lang='no') iq['disco_info'].add_identity('client', 'console') del iq['disco_info']['identities'] self.check(iq, """ """) def testRemoveAllIdentitiesWithLang(self): """Test removing all identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'bot', name='Bot') iq['disco_info'].add_identity('client', 'bot', lang='no') iq['disco_info'].add_identity('client', 'console') iq['disco_info'].del_identities(lang='no') self.check(iq, """ """) def testAddBatchIdentitiesNoLang(self): """Test adding multiple identities at once to a disco#info stanza.""" iq = self.Iq() identities = [('client', 'pc', 'no', 'PC Client'), ('client', 'bot', None, 'Bot'), ('client', 'console', None, None)] iq['disco_info']['identities'] = identities self.check(iq, """ """) def testAddBatchIdentitiesWithLang(self): """Test selectively replacing identities based on language.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'pc', lang='no') iq['disco_info'].add_identity('client', 'pc', lang='en') iq['disco_info'].add_identity('client', 'pc', lang='fr') identities = [('client', 'bot', 'fr', 'Bot'), ('client', 'bot', 'en', 'Bot')] iq['disco_info'].set_identities(identities, lang='fr') self.check(iq, """ """) def testGetIdentitiesNoLang(self): """Test getting all identities from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_identity('client', 'pc') iq['disco_info'].add_identity('client', 'pc', lang='no') iq['disco_info'].add_identity('client', 'pc', lang='en') iq['disco_info'].add_identity('client', 'pc', lang='fr') expected = set([('client', 'pc', None, None), ('client', 'pc', 'no', None), ('client', 'pc', 'en', None), ('client', 'pc', 'fr', None)]) self.failUnless(iq['disco_info']['identities'] == expected, "Identities do not match:\n%s\n%s" % ( expected, iq['disco_info']['identities'])) def testGetIdentitiesWithLang(self): """ Test getting all identities of a given lang from a disco#info stanza. """ iq = self.Iq() iq['disco_info'].add_identity('client', 'pc') iq['disco_info'].add_identity('client', 'pc', lang='no') iq['disco_info'].add_identity('client', 'pc', lang='en') iq['disco_info'].add_identity('client', 'pc', lang='fr') expected = set([('client', 'pc', 'no', None)]) result = iq['disco_info'].get_identities(lang='no') self.failUnless(result == expected, "Identities do not match:\n%s\n%s" % ( expected, result)) def testFeatures(self): """Testing adding features to disco#info.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') self.check(iq, """ """) def testFeaturesDuplicate(self): """Test adding duplicate features to disco#info.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') iq['disco_info'].add_feature('foo') self.check(iq, """ """) def testRemoveFeature(self): """Test removing a feature from disco#info.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') iq['disco_info'].add_feature('baz') iq['disco_info'].del_feature('foo') self.check(iq, """ """) def testGetFeatures(self): """Test getting all features from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') iq['disco_info'].add_feature('baz') expected = set(['foo', 'bar', 'baz']) self.failUnless(iq['disco_info']['features'] == expected, "Features do not match:\n%s\n%s" % ( expected, iq['disco_info']['features'])) def testRemoveAllFeatures(self): """Test removing all features from a disco#info stanza.""" iq = self.Iq() iq['disco_info'].add_feature('foo') iq['disco_info'].add_feature('bar') iq['disco_info'].add_feature('baz') del iq['disco_info']['features'] self.check(iq, """ """) def testAddBatchFeatures(self): """Test adding multiple features at once to a disco#info stanza.""" iq = self.Iq() features = ['foo', 'bar', 'baz'] iq['disco_info']['features'] = features self.check(iq, """ """) def testItems(self): """Testing adding features to disco#info.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', 'foo') iq['disco_items'].add_item('user@localhost', 'bar', name='Testing') self.check(iq, """ """) def testDuplicateItems(self): """Test adding items with the same JID without any nodes.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost', name='First') iq['disco_items'].add_item('user@localhost', name='Second') self.check(iq, """ """) def testDuplicateItemsWithNodes(self): """Test adding items with the same JID/node combination.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost', node='foo', name='First') iq['disco_items'].add_item('user@localhost', node='foo', name='Second') self.check(iq, """ """) def testRemoveItemsNoNode(self): """Test removing items without nodes from a disco#items stanza.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', node='foo') iq['disco_items'].add_item('test@localhost') iq['disco_items'].del_item('user@localhost') self.check(iq, """ """) def testRemoveItemsWithNode(self): """Test removing items with nodes from a disco#items stanza.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', node='foo') iq['disco_items'].add_item('test@localhost') iq['disco_items'].del_item('user@localhost', node='foo') self.check(iq, """ """) def testGetItems(self): """Test retrieving items from disco#items stanza.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', node='foo') iq['disco_items'].add_item('test@localhost', node='bar', name='Tester') expected = set([('user@localhost', None, None), ('user@localhost', 'foo', None), ('test@localhost', 'bar', 'Tester')]) self.failUnless(iq['disco_items']['items'] == expected, "Items do not match:\n%s\n%s" % ( expected, iq['disco_items']['items'])) def testRemoveAllItems(self): """Test removing all items from a disco#items stanza.""" iq = self.Iq() iq['disco_items'].add_item('user@localhost') iq['disco_items'].add_item('user@localhost', node='foo') iq['disco_items'].add_item('test@localhost', node='bar', name='Tester') del iq['disco_items']['items'] self.check(iq, """ """) def testAddBatchItems(self): """Test adding multiple items to a disco#items stanza.""" iq = self.Iq() items = [('user@localhost', 'foo', 'Test'), ('test@localhost', None, None), ('other@localhost', None, 'Other')] iq['disco_items']['items'] = items self.check(iq, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_xep_0033.py000066400000000000000000000072511157775340400226500ustar00rootroot00000000000000from sleekxmpp.test import * import sleekxmpp.plugins.xep_0033 as xep_0033 class TestAddresses(SleekTest): def setUp(self): register_stanza_plugin(Message, xep_0033.Addresses) def testAddAddress(self): """Testing adding extended stanza address.""" msg = self.Message() msg['addresses'].addAddress(atype='to', jid='to@header1.org') self.check(msg, """
""") msg = self.Message() msg['addresses'].addAddress(atype='replyto', jid='replyto@header1.org', desc='Reply address') self.check(msg, """
""") def testAddAddresses(self): """Testing adding multiple extended stanza addresses.""" xmlstring = """
""" msg = self.Message() msg['addresses'].setAddresses([ {'type':'replyto', 'jid':'replyto@header1.org', 'desc':'Reply address'}, {'type':'cc', 'jid':'cc@header2.org'}, {'type':'bcc', 'jid':'bcc@header2.org'}]) self.check(msg, xmlstring) msg = self.Message() msg['addresses']['replyto'] = [{'jid':'replyto@header1.org', 'desc':'Reply address'}] msg['addresses']['cc'] = [{'jid':'cc@header2.org'}] msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}] self.check(msg, xmlstring) def testAddURI(self): """Testing adding URI attribute to extended stanza address.""" msg = self.Message() addr = msg['addresses'].addAddress(atype='to', jid='to@header1.org', node='foo') self.check(msg, """
""") addr['uri'] = 'mailto:to@header2.org' self.check(msg, """
""") def testDelivered(self): """Testing delivered attribute of extended stanza addresses.""" xmlstring = """
""" msg = self.Message() addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to') self.check(msg, xmlstring % '') addr['delivered'] = True self.check(msg, xmlstring % 'delivered="true"') addr['delivered'] = False self.check(msg, xmlstring % '') suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_xep_0050.py000066400000000000000000000067111157775340400226470ustar00rootroot00000000000000from sleekxmpp import Iq from sleekxmpp.test import * from sleekxmpp.plugins.xep_0050 import Command class TestAdHocCommandStanzas(SleekTest): def setUp(self): register_stanza_plugin(Iq, Command) def testAction(self): """Test using the action attribute.""" iq = self.Iq() iq['type'] = 'set' iq['command']['node'] = 'foo' iq['command']['action'] = 'execute' self.failUnless(iq['command']['action'] == 'execute') iq['command']['action'] = 'complete' self.failUnless(iq['command']['action'] == 'complete') iq['command']['action'] = 'cancel' self.failUnless(iq['command']['action'] == 'cancel') def testSetActions(self): """Test setting next actions in a command stanza.""" iq = self.Iq() iq['type'] = 'result' iq['command']['node'] = 'foo' iq['command']['actions'] = ['prev', 'next'] self.check(iq, """ """) def testGetActions(self): """Test retrieving next actions from a command stanza.""" iq = self.Iq() iq['command']['node'] = 'foo' iq['command']['actions'] = ['prev', 'next'] results = iq['command']['actions'] expected = ['prev', 'next'] self.assertEqual(results, expected, "Incorrect next actions: %s" % results) def testDelActions(self): """Test removing next actions from a command stanza.""" iq = self.Iq() iq['type'] = 'result' iq['command']['node'] = 'foo' iq['command']['actions'] = ['prev', 'next'] del iq['command']['actions'] self.check(iq, """ """) def testAddNote(self): """Test adding a command note.""" iq = self.Iq() iq['type'] = 'result' iq['command']['node'] = 'foo' iq['command'].add_note('Danger!', ntype='warning') self.check(iq, """ Danger! """) def testNotes(self): """Test using command notes.""" iq = self.Iq() iq['type'] = 'result' iq['command']['node'] = 'foo' notes = [('info', 'Interesting...'), ('warning', 'Danger!'), ('error', "I can't let you do that")] iq['command']['notes'] = notes self.failUnless(iq['command']['notes'] == notes, "Notes don't match: %s %s" % (notes, iq['command']['notes'])) self.check(iq, """ Interesting... Danger! I can't let you do that """) suite = unittest.TestLoader().loadTestsFromTestCase(TestAdHocCommandStanzas) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_xep_0059.py000066400000000000000000000054171157775340400226620ustar00rootroot00000000000000from sleekxmpp.test import * from sleekxmpp.plugins.xep_0059 import Set class TestSetStanzas(SleekTest): def testSetFirstIndex(self): s = Set() s['first'] = 'id' s.set_first_index('10') self.check(s, """ id """) def testGetFirstIndex(self): xml_string = """ id """ s = Set(ET.fromstring(xml_string)) expected = '10' self.failUnless(s['first_index'] == expected) def testDelFirstIndex(self): xml_string = """ id """ s = Set(ET.fromstring(xml_string)) del s['first_index'] self.check(s, """ id """) def testSetBefore(self): s = Set() s['before'] = True self.check(s, """ """) def testGetBefore(self): xml_string = """ """ s = Set(ET.fromstring(xml_string)) expected = True self.failUnless(s['before'] == expected) def testGetBefore(self): xml_string = """ """ s = Set(ET.fromstring(xml_string)) del s['before'] self.check(s, """ """) def testSetBeforeVal(self): s = Set() s['before'] = 'id' self.check(s, """ id """) def testGetBeforeVal(self): xml_string = """ id """ s = Set(ET.fromstring(xml_string)) expected = 'id' self.failUnless(s['before'] == expected) def testGetBeforeVal(self): xml_string = """ id """ s = Set(ET.fromstring(xml_string)) del s['before'] self.check(s, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestSetStanzas) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_xep_0060.py000066400000000000000000000550701157775340400226520ustar00rootroot00000000000000from sleekxmpp.test import * import sleekxmpp.plugins.xep_0004 as xep_0004 import sleekxmpp.plugins.stanza_pubsub as pubsub class TestPubsubStanzas(SleekTest): def testAffiliations(self): "Testing iq/pubsub/affiliations/affiliation stanzas" iq = self.Iq() aff1 = pubsub.Affiliation() aff1['node'] = 'testnode' aff1['affiliation'] = 'owner' aff2 = pubsub.Affiliation() aff2['node'] = 'testnode2' aff2['affiliation'] = 'publisher' iq['pubsub']['affiliations'].append(aff1) iq['pubsub']['affiliations'].append(aff2) self.check(iq, """ """) def testSubscriptions(self): "Testing iq/pubsub/subscriptions/subscription stanzas" iq = self.Iq() sub1 = pubsub.Subscription() sub1['node'] = 'testnode' sub1['jid'] = 'steve@myserver.tld/someresource' sub2 = pubsub.Subscription() sub2['node'] = 'testnode2' sub2['jid'] = 'boogers@bork.top/bill' sub2['subscription'] = 'subscribed' iq['pubsub']['subscriptions'].append(sub1) iq['pubsub']['subscriptions'].append(sub2) self.check(iq, """ """) def testOptionalSettings(self): "Testing iq/pubsub/subscription/subscribe-options stanzas" iq = self.Iq() iq['pubsub']['subscription']['suboptions']['required'] = True iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas' iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp" iq['pubsub']['subscription']['subscription'] = 'unconfigured' self.check(iq, """ """) def testItems(self): "Testing iq/pubsub/items stanzas" iq = self.Iq() iq['pubsub']['items']['node'] = 'crap' payload = ET.fromstring(""" """) payload2 = ET.fromstring(""" """) item = pubsub.Item() item['id'] = 'asdf' item['payload'] = payload item2 = pubsub.Item() item2['id'] = 'asdf2' item2['payload'] = payload2 iq['pubsub']['items'].append(item) iq['pubsub']['items'].append(item2) self.check(iq, """ """) def testCreate(self): "Testing iq/pubsub/create&configure stanzas" iq = self.Iq() iq['pubsub']['create']['node'] = 'mynode' iq['pubsub']['configure']['form'].addField('pubsub#title', ftype='text-single', value='This thing is awesome') self.check(iq, """ This thing is awesome """) def testState(self): "Testing iq/psstate stanzas" iq = self.Iq() iq['psstate']['node']= 'mynode' iq['psstate']['item']= 'myitem' pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed') iq['psstate']['payload'] = pl self.check(iq, """ """) def testDefault(self): "Testing iq/pubsub_owner/default stanzas" iq = self.Iq() iq['pubsub_owner']['default'] iq['pubsub_owner']['default']['node'] = 'mynode' iq['pubsub_owner']['default']['type'] = 'leaf' iq['pubsub_owner']['default']['form'].addField('pubsub#title', ftype='text-single', value='This thing is awesome') self.check(iq, """ This thing is awesome """, use_values=False) def testSubscribe(self): "testing iq/pubsub/subscribe stanzas" iq = self.Iq() iq['pubsub']['subscribe']['options'] iq['pubsub']['subscribe']['node'] = 'cheese' iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp' iq['pubsub']['subscribe']['options']['node'] = 'cheese' iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp' form = xep_0004.Form() form.addField('pubsub#title', ftype='text-single', value='this thing is awesome') iq['pubsub']['subscribe']['options']['options'] = form self.check(iq, """ this thing is awesome """, use_values=False) def testPublish(self): "Testing iq/pubsub/publish stanzas" iq = self.Iq() iq['pubsub']['publish']['node'] = 'thingers' payload = ET.fromstring(""" """) payload2 = ET.fromstring(""" """) item = pubsub.Item() item['id'] = 'asdf' item['payload'] = payload item2 = pubsub.Item() item2['id'] = 'asdf2' item2['payload'] = payload2 iq['pubsub']['publish'].append(item) iq['pubsub']['publish'].append(item2) self.check(iq, """ """) def testDelete(self): "Testing iq/pubsub_owner/delete stanzas" iq = self.Iq() iq['pubsub_owner']['delete']['node'] = 'thingers' self.check(iq, """ """) def testCreateConfigGet(self): """Testing getting config from full create""" iq = self.Iq() iq['to'] = 'pubsub.asdf' iq['from'] = 'fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7' iq['type'] = 'set' iq['id'] = 'E' pub = iq['pubsub'] pub['create']['node'] = 'testnode2' pub['configure']['form']['type'] = 'submit' pub['configure']['form'].setFields([ ('FORM_TYPE', {'type': 'hidden', 'value': 'http://jabber.org/protocol/pubsub#node_config'}), ('pubsub#node_type', {'type': 'list-single', 'label': 'Select the node type', 'value': 'leaf'}), ('pubsub#title', {'type': 'text-single', 'label': 'A friendly name for the node'}), ('pubsub#deliver_notifications', {'type': 'boolean', 'label': 'Deliver event notifications', 'value': True}), ('pubsub#deliver_payloads', {'type': 'boolean', 'label': 'Deliver payloads with event notifications', 'value': True}), ('pubsub#notify_config', {'type': 'boolean', 'label': 'Notify subscribers when the node configuration changes'}), ('pubsub#notify_delete', {'type': 'boolean', 'label': 'Notify subscribers when the node is deleted'}), ('pubsub#notify_retract', {'type': 'boolean', 'label': 'Notify subscribers when items are removed from the node', 'value': True}), ('pubsub#notify_sub', {'type': 'boolean', 'label': 'Notify owners about new subscribers and unsubscribes'}), ('pubsub#persist_items', {'type': 'boolean', 'label': 'Persist items in storage'}), ('pubsub#max_items', {'type': 'text-single', 'label': 'Max # of items to persist', 'value': '10'}), ('pubsub#subscribe', {'type': 'boolean', 'label': 'Whether to allow subscriptions', 'value': True}), ('pubsub#access_model', {'type': 'list-single', 'label': 'Specify the subscriber model', 'value': 'open'}), ('pubsub#publish_model', {'type': 'list-single', 'label': 'Specify the publisher model', 'value': 'publishers'}), ('pubsub#send_last_published_item', {'type': 'list-single', 'label': 'Send last published item', 'value': 'never'}), ('pubsub#presence_based_delivery', {'type': 'boolean', 'label': 'Deliver notification only to available users'}), ]) self.check(iq, """ http://jabber.org/protocol/pubsub#node_config leaf 1 1 1 10 1 open publishers never """) def testItemEvent(self): """Testing message/pubsub_event/items/item""" msg = self.Message() item = pubsub.EventItem() pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) item['payload'] = pl item['id'] = 'abc123' msg['pubsub_event']['items'].append(item) msg['pubsub_event']['items']['node'] = 'cheese' msg['type'] = 'normal' self.check(msg, """ """) def testItemsEvent(self): """Testing multiple message/pubsub_event/items/item""" msg = self.Message() item = pubsub.EventItem() item2 = pubsub.EventItem() pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) item2['payload'] = pl2 item['payload'] = pl item['id'] = 'abc123' item2['id'] = '123abc' msg['pubsub_event']['items'].append(item) msg['pubsub_event']['items'].append(item2) msg['pubsub_event']['items']['node'] = 'cheese' msg['type'] = 'normal' self.check(msg, """ """) def testItemsEvent(self): """Testing message/pubsub_event/items/item & retract mix""" msg = self.Message() item = pubsub.EventItem() item2 = pubsub.EventItem() pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) item2['payload'] = pl2 retract = pubsub.EventRetract() retract['id'] = 'aabbcc' item['payload'] = pl item['id'] = 'abc123' item2['id'] = '123abc' msg['pubsub_event']['items'].append(item) msg['pubsub_event']['items'].append(retract) msg['pubsub_event']['items'].append(item2) msg['pubsub_event']['items']['node'] = 'cheese' msg['type'] = 'normal' self.check(msg, """ """) def testCollectionAssociate(self): """Testing message/pubsub_event/collection/associate""" msg = self.Message() msg['pubsub_event']['collection']['associate']['node'] = 'cheese' msg['pubsub_event']['collection']['node'] = 'cheeseburger' msg['type'] = 'headline' self.check(msg, """ """) def testCollectionDisassociate(self): """Testing message/pubsub_event/collection/disassociate""" msg = self.Message() msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese' msg['pubsub_event']['collection']['node'] = 'cheeseburger' msg['type'] = 'headline' self.check(msg, """ """) def testEventConfiguration(self): """Testing message/pubsub_event/configuration/config""" msg = self.Message() msg['pubsub_event']['configuration']['node'] = 'cheese' msg['pubsub_event']['configuration']['form'].addField('pubsub#title', ftype='text-single', value='This thing is awesome') msg['type'] = 'headline' self.check(msg, """ This thing is awesome """) def testEventPurge(self): """Testing message/pubsub_event/purge""" msg = self.Message() msg['pubsub_event']['purge']['node'] = 'pickles' msg['type'] = 'headline' self.check(msg, """ """) def testEventSubscription(self): """Testing message/pubsub_event/subscription""" msg = self.Message() msg['pubsub_event']['subscription']['node'] = 'pickles' msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test' msg['pubsub_event']['subscription']['subid'] = 'aabb1122' msg['pubsub_event']['subscription']['subscription'] = 'subscribed' msg['pubsub_event']['subscription']['expiry'] = 'presence' msg['type'] = 'headline' self.check(msg, """ """) suite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubStanzas) fritzy-SleekXMPP-5c4ee57/tests/test_stanza_xep_0085.py000066400000000000000000000022651157775340400226570ustar00rootroot00000000000000from sleekxmpp.test import * import sleekxmpp.plugins.xep_0085 as xep_0085 class TestChatStates(SleekTest): def setUp(self): register_stanza_plugin(Message, xep_0085.ChatState) def testCreateChatState(self): """Testing creating chat states.""" xmlstring = """ <%s xmlns="http://jabber.org/protocol/chatstates" /> """ msg = self.Message() self.assertEqual(msg['chat_state'], '') self.check(msg, "", use_values=False) msg['chat_state'] = 'active' self.check(msg, xmlstring % 'active', use_values=False) msg['chat_state'] = 'composing' self.check(msg, xmlstring % 'composing', use_values=False) msg['chat_state'] = 'gone' self.check(msg, xmlstring % 'gone', use_values=False) msg['chat_state'] = 'inactive' self.check(msg, xmlstring % 'inactive', use_values=False) msg['chat_state'] = 'paused' self.check(msg, xmlstring % 'paused', use_values=False) del msg['chat_state'] self.check(msg, "") suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates) fritzy-SleekXMPP-5c4ee57/tests/test_stream.py000066400000000000000000000041621157775340400213200ustar00rootroot00000000000000import time from sleekxmpp.test import * class TestStreamTester(SleekTest): """ Test that we can simulate and test a stanza stream. """ def tearDown(self): self.stream_close() def testClientEcho(self): """Test that we can interact with a ClientXMPP instance.""" self.stream_start(mode='client') def echo(msg): msg.reply('Thanks for sending: %(body)s' % msg).send() self.xmpp.add_event_handler('message', echo) self.recv(""" Hi! """) self.send(""" Thanks for sending: Hi! """) def testComponentEcho(self): """Test that we can interact with a ComponentXMPP instance.""" self.stream_start(mode='component') def echo(msg): msg.reply('Thanks for sending: %(body)s' % msg).send() self.xmpp.add_event_handler('message', echo) self.recv(""" Hi! """) self.send(""" Thanks for sending: Hi! """) def testSendStreamHeader(self): """Test that we can check a sent stream header.""" self.stream_start(mode='client', skip=False) self.send_header(sto='localhost') def testStreamDisconnect(self): """Test that the test socket can simulate disconnections.""" self.stream_start() events = set() def stream_error(event): events.add('socket_error') self.xmpp.add_event_handler('socket_error', stream_error) self.stream_disconnect() self.xmpp.send_raw(' ') time.sleep(.1) self.failUnless('socket_error' in events, "Stream error event not raised: %s" % events) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamTester) fritzy-SleekXMPP-5c4ee57/tests/test_stream_exceptions.py000066400000000000000000000130041157775340400235540ustar00rootroot00000000000000import sys import sleekxmpp from sleekxmpp.xmlstream.matcher import MatchXPath from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.exceptions import XMPPError from sleekxmpp.test import * class TestStreamExceptions(SleekTest): """ Test handling roster updates. """ def tearDown(self): sys.excepthook = sys.__excepthook__ self.stream_close() def testExceptionReply(self): """Test that raising an exception replies with the original stanza.""" def message(msg): msg.reply() msg['body'] = 'Body changed' raise XMPPError(clear=False) sys.excepthook = lambda *args, **kwargs: None self.stream_start() self.xmpp.add_event_handler('message', message) self.recv(""" This is going to cause an error. """) self.send(""" This is going to cause an error. """) def testXMPPErrorException(self): """Test raising an XMPPError exception.""" def message(msg): raise XMPPError(condition='feature-not-implemented', text="We don't do things that way here.", etype='cancel', extension='foo', extension_ns='foo:error', extension_args={'test': 'true'}) self.stream_start() self.xmpp.add_event_handler('message', message) self.recv(""" This is going to cause an error. """) self.send(""" We don't do things that way here. """, use_values=False) def testIqErrorException(self): """Test using error exceptions with Iq stanzas.""" def handle_iq(iq): raise XMPPError(condition='feature-not-implemented', text="We don't do things that way here.", etype='cancel', clear=False) self.stream_start() self.xmpp.register_handler( Callback( 'Test Iq', MatchXPath('{%s}iq/{test}query' % self.xmpp.default_ns), handle_iq)) self.recv(""" """) self.send(""" We don't do things that way here. """, use_values=False) def testThreadedXMPPErrorException(self): """Test raising an XMPPError exception in a threaded handler.""" def message(msg): raise XMPPError(condition='feature-not-implemented', text="We don't do things that way here.", etype='cancel') self.stream_start() self.xmpp.add_event_handler('message', message, threaded=True) self.recv(""" This is going to cause an error. """) self.send(""" We don't do things that way here. """) def testUnknownException(self): """Test raising an generic exception in a threaded handler.""" raised_errors = [] def message(msg): raise ValueError("Did something wrong") def catch_error(*args, **kwargs): raised_errors.append(True) sys.excepthook = catch_error self.stream_start() self.xmpp.add_event_handler('message', message) self.recv(""" This is going to cause an error. """) self.send(""" SleekXMPP got into trouble. """) self.assertEqual(raised_errors, [True], "Exception was not raised: %s" % raised_errors) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExceptions) fritzy-SleekXMPP-5c4ee57/tests/test_stream_handlers.py000066400000000000000000000077651157775340400232140ustar00rootroot00000000000000import time from sleekxmpp.test import * from sleekxmpp.xmlstream.handler import * from sleekxmpp.xmlstream.matcher import * class TestHandlers(SleekTest): """ Test using handlers and waiters. """ def setUp(self): self.stream_start() def tearDown(self): self.stream_close() def testCallback(self): """Test using stream callback handlers.""" def callback_handler(stanza): self.xmpp.sendRaw(""" Success! """) callback = Callback('Test Callback', MatchXPath('{test}tester'), callback_handler) self.xmpp.registerHandler(callback) self.recv("""""") msg = self.Message() msg['body'] = 'Success!' self.send(msg) def testWaiter(self): """Test using stream waiter handler.""" def waiter_handler(stanza): iq = self.xmpp.Iq() iq['id'] = 'test' iq['type'] = 'set' iq['query'] = 'test' reply = iq.send(block=True) if reply: self.xmpp.sendRaw(""" Successful: %s """ % reply['query']) self.xmpp.add_event_handler('message', waiter_handler, threaded=True) # Send message to trigger waiter_handler self.recv(""" Testing """) # Check that Iq was sent by waiter_handler iq = self.Iq() iq['id'] = 'test' iq['type'] = 'set' iq['query'] = 'test' self.send(iq) # Send the reply Iq self.recv(""" """) # Check that waiter_handler received the reply msg = self.Message() msg['body'] = 'Successful: test' self.send(msg) def testWaiterTimeout(self): """Test that waiter handler is removed after timeout.""" def waiter_handler(stanza): iq = self.xmpp.Iq() iq['id'] = 'test2' iq['type'] = 'set' iq['query'] = 'test2' reply = iq.send(block=True, timeout=0) self.xmpp.add_event_handler('message', waiter_handler, threaded=True) # Start test by triggerig waiter_handler self.recv("""Start Test""") # Check that Iq was sent to trigger start of timeout period iq = self.Iq() iq['id'] = 'test2' iq['type'] = 'set' iq['query'] = 'test2' self.send(iq) # Give the event queue time to process. time.sleep(0.1) # Check that the waiter is no longer registered waiter_exists = self.xmpp.removeHandler('IqWait_test2') self.failUnless(waiter_exists == False, "Waiter handler was not removed.") def testIqCallback(self): """Test that iq.send(callback=handle_foo) works.""" events = [] def handle_foo(iq): events.append('foo') iq = self.Iq() iq['type'] = 'get' iq['id'] = 'test-foo' iq['to'] = 'user@localhost' iq['query'] = 'foo' iq.send(callback=handle_foo) self.send(""" """) self.recv(""" """) # Give event queue time to process time.sleep(0.1) self.failUnless(events == ['foo'], "Iq callback was not executed: %s" % events) suite = unittest.TestLoader().loadTestsFromTestCase(TestHandlers) fritzy-SleekXMPP-5c4ee57/tests/test_stream_presence.py000066400000000000000000000127461157775340400232130ustar00rootroot00000000000000import time from sleekxmpp.test import * class TestStreamPresence(SleekTest): """ Test handling roster updates. """ def tearDown(self): self.stream_close() def testInitialUnavailablePresences(self): """ Test receiving unavailable presences from JIDs that are not online. """ events = set() def got_offline(presence): # The got_offline event should not be triggered. events.add('got_offline') def unavailable(presence): # The presence_unavailable event should be triggered. events.add('unavailable') self.stream_start() self.xmpp.add_event_handler('got_offline', got_offline) self.xmpp.add_event_handler('presence_unavailable', unavailable) self.recv(""" """) # Give event queue time to process. time.sleep(0.1) self.assertEqual(events, set(('unavailable',)), "Got offline incorrectly triggered: %s." % events) def testGotOffline(self): """Test that got_offline is triggered properly.""" events = [] def got_offline(presence): events.append('got_offline') self.stream_start() self.xmpp.add_event_handler('got_offline', got_offline) # Setup roster. Use a 'set' instead of 'result' so we # don't have to handle get_roster() blocking. # # We use the stream to initialize the roster to make # the test independent of the roster implementation. self.recv(""" Testers """) # Contact comes online. self.recv(""" """) # Contact goes offline, should trigger got_offline. self.recv(""" """) # Give event queue time to process. time.sleep(0.1) self.assertEqual(events, ['got_offline'], "Got offline incorrectly triggered: %s" % events) def testGotOnline(self): """Test that got_online is triggered properly.""" events = set() def presence_available(p): events.add('presence_available') def got_online(p): events.add('got_online') self.stream_start() self.xmpp.add_event_handler('presence_available', presence_available) self.xmpp.add_event_handler('got_online', got_online) self.recv(""" """) # Give event queue time to process. time.sleep(0.1) expected = set(('presence_available', 'got_online')) self.assertEqual(events, expected, "Incorrect events triggered: %s" % events) def testAutoAuthorizeAndSubscribe(self): """ Test auto authorizing and auto subscribing to subscription requests. """ events = set() def presence_subscribe(p): events.add('presence_subscribe') def changed_subscription(p): events.add('changed_subscription') self.stream_start(jid='tester@localhost') self.xmpp.add_event_handler('changed_subscription', changed_subscription) self.xmpp.add_event_handler('presence_subscribe', presence_subscribe) # With these settings we should accept a subscription # and request a subscription in return. self.xmpp.auto_authorize = True self.xmpp.auto_subscribe = True self.recv(""" """) self.send(""" """) self.send(""" """) expected = set(('presence_subscribe', 'changed_subscription')) self.assertEqual(events, expected, "Incorrect events triggered: %s" % events) def testNoAutoAuthorize(self): """Test auto rejecting subscription requests.""" events = set() def presence_subscribe(p): events.add('presence_subscribe') def changed_subscription(p): events.add('changed_subscription') self.stream_start(jid='tester@localhost') self.xmpp.add_event_handler('changed_subscription', changed_subscription) self.xmpp.add_event_handler('presence_subscribe', presence_subscribe) # With this setting we should reject all subscriptions. self.xmpp.auto_authorize = False self.recv(""" """) self.send(""" """) expected = set(('presence_subscribe', 'changed_subscription')) self.assertEqual(events, expected, "Incorrect events triggered: %s" % events) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamPresence) fritzy-SleekXMPP-5c4ee57/tests/test_stream_roster.py000066400000000000000000000153161157775340400227210ustar00rootroot00000000000000# -*- encoding:utf-8 -*- from __future__ import unicode_literals from sleekxmpp.test import * import time import threading class TestStreamRoster(SleekTest): """ Test handling roster updates. """ def tearDown(self): self.stream_close() def testGetRoster(self): """Test handling roster requests.""" self.stream_start(mode='client') self.failUnless(self.xmpp.roster == {}, "Initial roster not empty.") events = [] def roster_received(iq): events.append('roster_received') self.xmpp.add_event_handler('roster_received', roster_received) # Since get_roster blocks, we need to run it in a thread. t = threading.Thread(name='get_roster', target=self.xmpp.get_roster) t.start() self.send(""" """) self.recv(""" Friends Examples """) # Wait for get_roster to return. t.join() # Give the event queue time to process. time.sleep(.1) roster = {'user@localhost': {'name': 'User', 'subscription': 'both', 'groups': ['Friends', 'Examples'], 'presence': {}, 'in_roster': True}} self.failUnless(self.xmpp.roster == roster, "Unexpected roster values: %s" % self.xmpp.roster) self.failUnless('roster_received' in events, "Roster received event not triggered: %s" % events) def testRosterSet(self): """Test handling pushed roster updates.""" self.stream_start(mode='client') self.failUnless(self.xmpp.roster == {}, "Initial roster not empty.") events = [] def roster_update(e): events.append('roster_update') self.xmpp.add_event_handler('roster_update', roster_update) self.recv(""" Friends Examples """) self.send(""" """) # Give the event queue time to process. time.sleep(.1) roster = {'user@localhost': {'name': 'User', 'subscription': 'both', 'groups': ['Friends', 'Examples'], 'presence': {}, 'in_roster': True}} self.failUnless(self.xmpp.roster == roster, "Unexpected roster values: %s" % self.xmpp.roster) self.failUnless('roster_update' in events, "Roster updated event not triggered: %s" % events) def testRosterTimeout(self): """Test handling a timed out roster request.""" self.stream_start() events = [] def roster_timeout(event): events.append('roster_timeout') self.xmpp.add_event_handler('roster_timeout', roster_timeout) self.xmpp.get_roster(timeout=0) # Give the event queue time to process. time.sleep(.1) self.failUnless(events == ['roster_timeout'], "Roster timeout event not triggered: %s." % events) def testRosterCallback(self): """Test handling a roster request callback.""" self.stream_start() events = [] def roster_callback(iq): events.append('roster_callback') # Since get_roster blocks, we need to run it in a thread. t = threading.Thread(name='get_roster', target=self.xmpp.get_roster, kwargs={str('callback'): roster_callback}) t.start() self.send(""" """) self.recv(""" Friends Examples """) # Wait for get_roster to return. t.join() # Give the event queue time to process. time.sleep(.1) self.failUnless(events == ['roster_callback'], "Roster timeout event not triggered: %s." % events) def testRosterUnicode(self): """Test that JIDs with Unicode values are handled properly.""" self.stream_start() self.recv(""" Unicode """) # Give the event queue time to process. time.sleep(.1) roster = {'andré@foo': { 'name': '', 'subscription': 'both', 'groups': ['Unicode'], 'presence': {}, 'in_roster': True}} self.failUnless(self.xmpp.roster == roster, "Unexpected roster values: %s" % self.xmpp.roster) self.recv(""" away Testing """) # Give the event queue time to process. time.sleep(.1) roster = {'andré@foo': { 'name': '', 'subscription': 'both', 'groups': ['Unicode'], 'presence': { 'bar':{'priority':0, 'status':'Testing', 'show':'away'}}, 'in_roster': True}} self.failUnless(self.xmpp.roster == roster, "Unexpected roster values: %s" % self.xmpp.roster) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamRoster) fritzy-SleekXMPP-5c4ee57/tests/test_stream_xep_0030.py000066400000000000000000000454501157775340400226430ustar00rootroot00000000000000import sys import time import threading from sleekxmpp.test import * class TestStreamDisco(SleekTest): """ Test using the XEP-0030 plugin. """ def tearDown(self): sys.excepthook = sys.__excepthook__ self.stream_close() def testInfoEmptyDefaultNode(self): """ Info query result from an entity MUST have at least one identity and feature, namely http://jabber.org/protocol/disco#info. Since the XEP-0030 plugin is loaded, a disco response should be generated and not an error result. """ self.stream_start(mode='client', plugins=['xep_0030']) self.recv(""" """) self.send(""" """) def testInfoEmptyDefaultNodeComponent(self): """ Test requesting an empty, default node using a Component. """ self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) self.recv(""" """) self.send(""" """) def testInfoIncludeNode(self): """ Results for info queries directed to a particular node MUST include the node in the query response. """ self.stream_start(mode='client', plugins=['xep_0030']) self.xmpp['xep_0030'].static.add_node(node='testing') self.recv(""" """) self.send(""" """, method='mask') def testItemsIncludeNode(self): """ Results for items queries directed to a particular node MUST include the node in the query response. """ self.stream_start(mode='client', plugins=['xep_0030']) self.xmpp['xep_0030'].static.add_node(node='testing') self.recv(""" """) self.send(""" """, method='mask') def testDynamicInfoJID(self): """ Test using a dynamic info handler for a particular JID. """ self.stream_start(mode='client', plugins=['xep_0030']) def dynamic_jid(jid, node, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('client', 'console', name='Dynamic Info') return result self.xmpp['xep_0030'].set_node_handler('get_info', jid='tester@localhost', handler=dynamic_jid) self.recv(""" """) self.send(""" """) def testDynamicInfoGlobal(self): """ Test using a dynamic info handler for all requests. """ self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) def dynamic_global(jid, node, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('component', 'generic', name='Dynamic Info') return result self.xmpp['xep_0030'].set_node_handler('get_info', handler=dynamic_global) self.recv(""" """) self.send(""" """) def testOverrideJIDInfoHandler(self): """Test overriding a JID info handler.""" self.stream_start(mode='client', plugins=['xep_0030']) def dynamic_jid(jid, node, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('client', 'console', name='Dynamic Info') return result self.xmpp['xep_0030'].set_node_handler('get_info', jid='tester@localhost', handler=dynamic_jid) self.xmpp['xep_0030'].make_static(jid='tester@localhost', node='testing') self.xmpp['xep_0030'].add_identity(jid='tester@localhost', node='testing', category='automation', itype='command-list') self.recv(""" """) self.send(""" """) def testOverrideGlobalInfoHandler(self): """Test overriding the global JID info handler.""" self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) def dynamic_global(jid, node, iq): result = self.xmpp['xep_0030'].stanza.DiscoInfo() result['node'] = node result.add_identity('component', 'generic', name='Dynamic Info') return result self.xmpp['xep_0030'].set_node_handler('get_info', handler=dynamic_global) self.xmpp['xep_0030'].make_static(jid='user@tester.localhost', node='testing') self.xmpp['xep_0030'].add_feature(jid='user@tester.localhost', node='testing', feature='urn:xmpp:ping') self.recv(""" """) self.send(""" """) def testGetInfoRemote(self): """ Test sending a disco#info query to another entity and receiving the result. """ self.stream_start(mode='client', plugins=['xep_0030']) events = set() def handle_disco_info(iq): events.add('disco_info') self.xmpp.add_event_handler('disco_info', handle_disco_info) t = threading.Thread(name="get_info", target=self.xmpp['xep_0030'].get_info, args=('user@localhost', 'foo')) t.start() self.send(""" """) self.recv(""" """) # Wait for disco#info request to be received. t.join() time.sleep(0.1) self.assertEqual(events, set(('disco_info',)), "Disco info event was not triggered: %s" % events) def testDynamicItemsJID(self): """ Test using a dynamic items handler for a particular JID. """ self.stream_start(mode='client', plugins=['xep_0030']) def dynamic_jid(jid, node, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester@localhost', node='foo', name='JID') return result self.xmpp['xep_0030'].set_node_handler('get_items', jid='tester@localhost', handler=dynamic_jid) self.recv(""" """) self.send(""" """) def testDynamicItemsGlobal(self): """ Test using a dynamic items handler for all requests. """ self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) def dynamic_global(jid, node, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester@localhost', node='foo', name='Global') return result self.xmpp['xep_0030'].set_node_handler('get_items', handler=dynamic_global) self.recv(""" """) self.send(""" """) def testOverrideJIDItemsHandler(self): """Test overriding a JID items handler.""" self.stream_start(mode='client', plugins=['xep_0030']) def dynamic_jid(jid, node, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester@localhost', node='foo', name='Global') return result self.xmpp['xep_0030'].set_node_handler('get_items', jid='tester@localhost', handler=dynamic_jid) self.xmpp['xep_0030'].make_static(jid='tester@localhost', node='testing') self.xmpp['xep_0030'].add_item(ijid='tester@localhost', node='testing', jid='tester@localhost', subnode='foo', name='Test') self.recv(""" """) self.send(""" """) def testOverrideGlobalItemsHandler(self): """Test overriding the global JID items handler.""" self.stream_start(mode='component', jid='tester.localhost', plugins=['xep_0030']) def dynamic_global(jid, node, iq): result = self.xmpp['xep_0030'].stanza.DiscoItems() result['node'] = node result.add_item('tester.localhost', node='foo', name='Global') return result self.xmpp['xep_0030'].set_node_handler('get_items', handler=dynamic_global) self.xmpp['xep_0030'].make_static(jid='user@tester.localhost', node='testing') self.xmpp['xep_0030'].add_item(ijid='user@tester.localhost', node='testing', jid='user@tester.localhost', subnode='foo', name='Test') self.recv(""" """) self.send(""" """) def testGetItemsRemote(self): """ Test sending a disco#items query to another entity and receiving the result. """ self.stream_start(mode='client', plugins=['xep_0030']) events = set() results = set() def handle_disco_items(iq): events.add('disco_items') results.update(iq['disco_items']['items']) self.xmpp.add_event_handler('disco_items', handle_disco_items) t = threading.Thread(name="get_items", target=self.xmpp['xep_0030'].get_items, args=('user@localhost', 'foo')) t.start() self.send(""" """) self.recv(""" """) # Wait for disco#items request to be received. t.join() time.sleep(0.1) items = set([('user@localhost', 'bar', 'Test'), ('user@localhost', 'baz', 'Test 2')]) self.assertEqual(events, set(('disco_items',)), "Disco items event was not triggered: %s" % events) self.assertEqual(results, items, "Unexpected items: %s" % results) def testGetItemsIterator(self): """Test interaction between XEP-0030 and XEP-0059 plugins.""" raised_exceptions = [] def catch_exception(*args, **kwargs): raised_exceptions.append(True) sys.excepthook = catch_exception self.stream_start(mode='client', plugins=['xep_0030', 'xep_0059']) results = self.xmpp['xep_0030'].get_items(jid='foo@localhost', node='bar', iterator=True) results.amount = 10 t = threading.Thread(name="get_items_iterator", target=results.next) t.start() self.send(""" 10 """) self.recv(""" """) t.join() self.assertEqual(raised_exceptions, [True], "StopIteration was not raised: %s" % raised_exceptions) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamDisco) fritzy-SleekXMPP-5c4ee57/tests/test_stream_xep_0050.py000066400000000000000000000543331157775340400226450ustar00rootroot00000000000000import time import threading from sleekxmpp.test import * class TestAdHocCommands(SleekTest): def setUp(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0004', 'xep_0050']) # Real session IDs don't make for nice tests, so use # a dummy value. self.xmpp['xep_0050'].new_session = lambda: '_sessionid_' def tearDown(self): self.stream_close() def testZeroStepCommand(self): """Test running a command with no steps.""" def handle_command(iq, session): form = self.xmpp['xep_0004'].makeForm(ftype='result') form.addField(var='foo', ftype='text-single', label='Foo', value='bar') session['payload'] = form session['next'] = None session['has_next'] = False return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" bar """) def testOneStepCommand(self): """Test running a single step command.""" results = [] def handle_command(iq, session): def handle_form(form, session): results.append(form['values']['foo']) form = self.xmpp['xep_0004'].makeForm('form') form.addField(var='foo', ftype='text-single', label='Foo') session['payload'] = form session['next'] = handle_form session['has_next'] = False return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" """) self.recv(""" blah """) self.send(""" """) self.assertEqual(results, ['blah'], "Command handler was not executed: %s" % results) def testTwoStepCommand(self): """Test using a two-stage command.""" results = [] def handle_command(iq, session): def handle_step2(form, session): results.append(form['values']['bar']) def handle_step1(form, session): results.append(form['values']['foo']) form = self.xmpp['xep_0004'].makeForm('form') form.addField(var='bar', ftype='text-single', label='Bar') session['payload'] = form session['next'] = handle_step2 session['has_next'] = False return session form = self.xmpp['xep_0004'].makeForm('form') form.addField(var='foo', ftype='text-single', label='Foo') session['payload'] = form session['next'] = handle_step1 session['has_next'] = True return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" """) self.recv(""" blah """) self.send(""" """) self.recv(""" meh """) self.send(""" """) self.assertEqual(results, ['blah', 'meh'], "Command handler was not executed: %s" % results) def testCancelCommand(self): """Test canceling command.""" results = [] def handle_command(iq, session): def handle_form(form, session): results.append(form['values']['foo']) def handle_cancel(iq, session): results.append('canceled') form = self.xmpp['xep_0004'].makeForm('form') form.addField(var='foo', ftype='text-single', label='Foo') session['payload'] = form session['next'] = handle_form session['cancel'] = handle_cancel session['has_next'] = False return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" """) self.recv(""" blah """) self.send(""" """) self.assertEqual(results, ['canceled'], "Cancelation handler not executed: %s" % results) def testCommandNote(self): """Test adding notes to commands.""" def handle_command(iq, session): form = self.xmpp['xep_0004'].makeForm(ftype='result') form.addField(var='foo', ftype='text-single', label='Foo', value='bar') session['payload'] = form session['next'] = None session['has_next'] = False session['notes'] = [('info', 'testing notes')] return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" testing notes bar """) def testMultiPayloads(self): """Test using commands with multiple payloads.""" results = [] def handle_command(iq, session): def handle_form(forms, session): for form in forms: results.append(form['values']['FORM_TYPE']) form1 = self.xmpp['xep_0004'].makeForm('form') form1.addField(var='FORM_TYPE', ftype='hidden', value='form_1') form1.addField(var='foo', ftype='text-single', label='Foo') form2 = self.xmpp['xep_0004'].makeForm('form') form2.addField(var='FORM_TYPE', ftype='hidden', value='form_2') form2.addField(var='foo', ftype='text-single', label='Foo') session['payload'] = [form1, form2] session['next'] = handle_form session['has_next'] = False return session self.xmpp['xep_0050'].add_command('tester@localhost', 'foo', 'Do Foo', handle_command) self.recv(""" """) self.send(""" form_1 form_2 """) self.recv(""" form_1 bar form_2 bar """) self.send(""" """) self.assertEqual(results, [['form_1'], ['form_2']], "Command handler was not executed: %s" % results) def testClientAPI(self): """Test using client-side API for commands.""" results = [] def handle_complete(iq, session): for item in session['custom_data']: results.append(item) def handle_step2(iq, session): form = self.xmpp['xep_0004'].makeForm(ftype='submit') form.addField(var='bar', value='123') session['custom_data'].append('baz') session['payload'] = form session['next'] = handle_complete self.xmpp['xep_0050'].complete_command(session) def handle_step1(iq, session): form = self.xmpp['xep_0004'].makeForm(ftype='submit') form.addField(var='foo', value='42') session['custom_data'].append('bar') session['payload'] = form session['next'] = handle_step2 self.xmpp['xep_0050'].continue_command(session) session = {'custom_data': ['foo'], 'next': handle_step1} self.xmpp['xep_0050'].start_command( 'foo@example.com', 'test_client', session) self.send(""" """) self.recv(""" """) self.send(""" 42 """) self.recv(""" """) self.send(""" 123 """) self.recv(""" """) # Give the event queue time to process time.sleep(0.3) self.failUnless(results == ['foo', 'bar', 'baz'], 'Incomplete command workflow: %s' % results) def testClientAPICancel(self): """Test using client-side cancel API for commands.""" results = [] def handle_canceled(iq, session): for item in session['custom_data']: results.append(item) def handle_step1(iq, session): session['custom_data'].append('bar') session['next'] = handle_canceled self.xmpp['xep_0050'].cancel_command(session) session = {'custom_data': ['foo'], 'next': handle_step1} self.xmpp['xep_0050'].start_command( 'foo@example.com', 'test_client', session) self.send(""" """) self.recv(""" """) self.send(""" """) self.recv(""" """) # Give the event queue time to process time.sleep(0.3) self.failUnless(results == ['foo', 'bar'], 'Incomplete command workflow: %s' % results) def testClientAPIError(self): """Test using client-side error API for commands.""" results = [] def handle_error(iq, session): for item in session['custom_data']: results.append(item) session = {'custom_data': ['foo'], 'error': handle_error} self.xmpp['xep_0050'].start_command( 'foo@example.com', 'test_client', session) self.send(""" """) self.recv(""" """) # Give the event queue time to process time.sleep(0.3) self.failUnless(results == ['foo'], 'Incomplete command workflow: %s' % results) suite = unittest.TestLoader().loadTestsFromTestCase(TestAdHocCommands) fritzy-SleekXMPP-5c4ee57/tests/test_stream_xep_0059.py000066400000000000000000000115241157775340400226510ustar00rootroot00000000000000import threading from sleekxmpp.test import * from sleekxmpp.xmlstream import register_stanza_plugin from sleekxmpp.plugins.xep_0030 import DiscoItems from sleekxmpp.plugins.xep_0059 import ResultIterator, Set class TestStreamSet(SleekTest): def setUp(self): register_stanza_plugin(DiscoItems, Set) def tearDown(self): self.stream_close() def iter(self, rev=False): q = self.xmpp.Iq() q['type'] = 'get' it = ResultIterator(q, 'disco_items', '1', reverse=rev) for i in it: for j in i['disco_items']['items']: self.items.append(j[0]) def testResultIterator(self): self.items = [] self.stream_start(mode='client') t = threading.Thread(target=self.iter) t.start() self.send(""" 1 """) self.recv(""" item1 """) self.send(""" 1 item1 """) self.recv(""" item2 """) self.send(""" 1 item2 """) self.recv(""" """) t.join() self.failUnless(self.items == ['item1', 'item2']) def testResultIteratorReverse(self): self.items = [] self.stream_start(mode='client') t = threading.Thread(target=self.iter, args=(True,)) t.start() self.send(""" 1 """) self.recv(""" item2 """) self.send(""" 1 item2 """) self.recv(""" item1 """) self.send(""" 1 item1 """) self.recv(""" """) t.join() self.failUnless(self.items == ['item2', 'item1']) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamSet) fritzy-SleekXMPP-5c4ee57/tests/test_stream_xep_0085.py000066400000000000000000000033461157775340400226530ustar00rootroot00000000000000import threading import time from sleekxmpp.test import * class TestStreamChatStates(SleekTest): def tearDown(self): self.stream_close() def testChatStates(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0085']) results = [] def handle_state(msg): results.append(msg['chat_state']) self.xmpp.add_event_handler('chatstate_active', handle_state) self.xmpp.add_event_handler('chatstate_inactive', handle_state) self.xmpp.add_event_handler('chatstate_paused', handle_state) self.xmpp.add_event_handler('chatstate_gone', handle_state) self.xmpp.add_event_handler('chatstate_composing', handle_state) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) self.recv(""" """) # Give event queue time to process time.sleep(0.3) expected = ['active', 'inactive', 'paused', 'composing', 'gone'] self.failUnless(results == expected, "Chat state event not handled: %s" % results) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamChatStates) fritzy-SleekXMPP-5c4ee57/tests/test_stream_xep_0092.py000066400000000000000000000034211157775340400226430ustar00rootroot00000000000000import threading from sleekxmpp.test import * class TestStreamSet(SleekTest): def tearDown(self): self.stream_close() def testHandleSoftwareVersionRequest(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092']) self.xmpp['xep_0092'].name = 'SleekXMPP' self.xmpp['xep_0092'].version = 'dev' self.xmpp['xep_0092'].os = 'Linux' self.recv(""" """) self.send(""" SleekXMPP dev Linux """) def testMakeSoftwareVersionRequest(self): results = [] def query(): r = self.xmpp['xep_0092'].get_version('foo@bar') results.append(r) self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092']) t = threading.Thread(target=query) t.start() self.send(""" """) self.recv(""" Foo 1.0 Linux """) t.join() expected = [{'name': 'Foo', 'version': '1.0', 'os':'Linux'}] self.assertEqual(results, expected, "Did not receive expected results: %s" % results) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamSet) fritzy-SleekXMPP-5c4ee57/tests/test_stream_xep_0128.py000066400000000000000000000073051157775340400226500ustar00rootroot00000000000000import sys import time import threading from sleekxmpp.test import * from sleekxmpp.xmlstream import ElementBase class TestStreamExtendedDisco(SleekTest): """ Test using the XEP-0128 plugin. """ def tearDown(self): sys.excepthook = sys.__excepthook__ self.stream_close() def testUsingExtendedInfo(self): self.stream_start(mode='client', jid='tester@localhost', plugins=['xep_0030', 'xep_0004', 'xep_0128']) form = self.xmpp['xep_0004'].makeForm(ftype='result') form.addField(var='FORM_TYPE', ftype='hidden', value='testing') info_ns = 'http://jabber.org/protocol/disco#info' self.xmpp['xep_0030'].add_identity(node='test', category='client', itype='bot') self.xmpp['xep_0030'].add_feature(node='test', feature=info_ns) self.xmpp['xep_0128'].set_extended_info(node='test', data=form) self.recv(""" """) self.send(""" testing """) def testUsingMultipleExtendedInfo(self): self.stream_start(mode='client', jid='tester@localhost', plugins=['xep_0030', 'xep_0004', 'xep_0128']) form1 = self.xmpp['xep_0004'].makeForm(ftype='result') form1.addField(var='FORM_TYPE', ftype='hidden', value='testing') form2 = self.xmpp['xep_0004'].makeForm(ftype='result') form2.addField(var='FORM_TYPE', ftype='hidden', value='testing_2') info_ns = 'http://jabber.org/protocol/disco#info' self.xmpp['xep_0030'].add_identity(node='test', category='client', itype='bot') self.xmpp['xep_0030'].add_feature(node='test', feature=info_ns) self.xmpp['xep_0128'].set_extended_info(node='test', data=[form1, form2]) self.recv(""" """) self.send(""" testing testing_2 """) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExtendedDisco) fritzy-SleekXMPP-5c4ee57/tests/test_stream_xep_0249.py000066400000000000000000000033131157775340400226470ustar00rootroot00000000000000import sys import time import threading from sleekxmpp.test import * from sleekxmpp.xmlstream import ElementBase class TestStreamDirectInvite(SleekTest): """ Test using the XEP-0249 plugin. """ def tearDown(self): sys.excepthook = sys.__excepthook__ self.stream_close() def testReceiveInvite(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0249']) events = [] def handle_invite(msg): events.append(True) self.xmpp.add_event_handler('groupchat_direct_invite', handle_invite) self.recv(""" """) time.sleep(.5) self.failUnless(events == [True], "Event not raised: %s" % events) def testSentDirectInvite(self): self.stream_start(mode='client', plugins=['xep_0030', 'xep_0249']) self.xmpp['xep_0249'].send_invitation('user@example.com', 'sleek@conference.jabber.org', reason='Need to test Sleek') self.send(""" """) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamDirectInvite) fritzy-SleekXMPP-5c4ee57/tests/test_tostring.py000066400000000000000000000106441157775340400217000ustar00rootroot00000000000000from sleekxmpp.test import * from sleekxmpp.stanza import Message from sleekxmpp.xmlstream.stanzabase import ET, ElementBase from sleekxmpp.xmlstream.tostring import tostring, xml_escape class TestToString(SleekTest): """ Test the implementation of sleekxmpp.xmlstream.tostring """ def tearDown(self): self.stream_close() def tryTostring(self, original='', expected=None, message='', **kwargs): """ Compare the result of calling tostring against an expected result. """ if not expected: expected=original if isinstance(original, str): xml = ET.fromstring(original) else: xml=original result = tostring(xml, **kwargs) self.failUnless(result == expected, "%s: %s" % (message, result)) def testXMLEscape(self): """Test escaping XML special characters.""" original = """'Hi & welcome!'""" escaped = xml_escape(original) desired = """<foo bar="baz">'Hi""" desired += """ & welcome!'</foo>""" self.failUnless(escaped == desired, "XML escaping did not work: %s." % escaped) def testEmptyElement(self): """Test converting an empty element to a string.""" self.tryTostring( original='', message="Empty element not serialized correctly") def testEmptyElementWrapped(self): """Test converting an empty element inside another element.""" self.tryTostring( original='', message="Wrapped empty element not serialized correctly") def testEmptyElementWrappedText(self): """ Test converting an empty element wrapped with text inside another element. """ self.tryTostring( original='Some text. More text.', message="Text wrapped empty element serialized incorrectly") def testMultipleChildren(self): """Test converting multiple child elements to a Unicode string.""" self.tryTostring( original='', message="Multiple child elements not serialized correctly") def testXMLNS(self): """ Test using xmlns tostring parameter, which will prevent adding an xmlns attribute to the serialized element if the element's namespace is the same. """ self.tryTostring( original='', expected='', message="The xmlns parameter was not used properly.", xmlns='foo') def testTailContent(self): """ Test that elements of the form foo bar baz only include " baz" once. """ self.tryTostring( original='foo bar baz', message='Element tail content is incorrect.') def testStanzaNs(self): """ Test using the stanza_ns tostring parameter, which will prevent adding an xmlns attribute to the serialized element if the element's namespace is the same. """ self.tryTostring( original='', expected='', message="The stanza_ns parameter was not used properly.", stanza_ns='foo') def testStanzaStr(self): """ Test that stanza objects are serialized properly. """ utf8_message = '\xe0\xb2\xa0_\xe0\xb2\xa0' if not hasattr(utf8_message, 'decode'): # Python 3 utf8_message = bytes(utf8_message, encoding='utf-8') msg = Message() msg['body'] = utf8_message.decode('utf-8') expected = '\xe0\xb2\xa0_\xe0\xb2\xa0' result = msg.__str__() self.failUnless(result == expected, "Stanza Unicode handling is incorrect: %s" % result) def testXMLLang(self): """Test that serializing xml:lang works.""" self.stream_start() msg = self.Message() msg._set_attr('{%s}lang' % msg.xml_ns, "no") expected = '' result = msg.__str__() self.failUnless(expected == result, "Serialization with xml:lang failed: %s" % result) suite = unittest.TestLoader().loadTestsFromTestCase(TestToString) fritzy-SleekXMPP-5c4ee57/todo1.0000066400000000000000000000016161157775340400163620ustar00rootroot00000000000000Plugins: 0004 PEP8 Stream/Unit tests Fix serialization issue Use OrderedDict for fields/values 0009 Review contribution from dannmartens 0012 PEP8 Documentation Stream/Unit tests 0033 PEP8 Documentation Stream/Unit tests 0045 PEP8 Documentation Stream/Unit tests 0050 Review replacement in github.com/legastero/adhoc 0060 PEP8 Documentation Stream/Unit tests 0078 Will require new stream features handling, see stream_features branch. PEP8 Documentation Stream/Unit tests 0086 PEP8 Documentation Consider any simplifications. 0202 PEP8 Documentation Stream/Unit tests gmail_notify PEP8 Documentation Stream/Unit tests