summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--INSTALL3
-rw-r--r--LICENSE2
-rw-r--r--MANIFEST39
-rw-r--r--README5
-rw-r--r--example.py72
-rw-r--r--setup.py14
-rw-r--r--sleekxmpp/__init__.py427
-rw-r--r--sleekxmpp/basexmpp.py98
-rwxr-xr-xsleekxmpp/componentxmpp.py105
-rw-r--r--sleekxmpp/exceptions.py45
-rw-r--r--sleekxmpp/plugins/__init__.py20
-rw-r--r--sleekxmpp/plugins/base.py22
-rw-r--r--sleekxmpp/plugins/gmail_notify.py193
-rw-r--r--sleekxmpp/plugins/old_0004.py417
-rw-r--r--sleekxmpp/plugins/stanza_pubsub.py123
-rw-r--r--sleekxmpp/plugins/xep_0004.py732
-rw-r--r--sleekxmpp/plugins/xep_0009.py9
-rw-r--r--sleekxmpp/plugins/xep_0030.py23
-rw-r--r--sleekxmpp/plugins/xep_0033.py161
-rw-r--r--sleekxmpp/plugins/xep_0045.py34
-rw-r--r--sleekxmpp/plugins/xep_0050.py37
-rw-r--r--sleekxmpp/plugins/xep_0060.py2
-rw-r--r--sleekxmpp/plugins/xep_0078.py22
-rw-r--r--sleekxmpp/plugins/xep_0085.py101
-rw-r--r--sleekxmpp/plugins/xep_0092.py24
-rw-r--r--sleekxmpp/plugins/xep_0128.py51
-rw-r--r--sleekxmpp/plugins/xep_0199.py25
-rw-r--r--sleekxmpp/stanza/__init__.py9
-rw-r--r--sleekxmpp/stanza/atom.py2
-rw-r--r--sleekxmpp/stanza/error.py179
-rw-r--r--sleekxmpp/stanza/htmlim.py99
-rw-r--r--sleekxmpp/stanza/iq.py238
-rw-r--r--sleekxmpp/stanza/message.py190
-rw-r--r--sleekxmpp/stanza/nick.py82
-rw-r--r--sleekxmpp/stanza/presence.py193
-rw-r--r--sleekxmpp/stanza/rootstanza.py80
-rw-r--r--sleekxmpp/stanza/roster.py146
-rwxr-xr-xsleekxmpp/tests/testpubsub.py22
-rw-r--r--sleekxmpp/xmlstream/__init__.py11
-rw-r--r--sleekxmpp/xmlstream/filesocket.py2
-rw-r--r--sleekxmpp/xmlstream/handler/__init__.py12
-rw-r--r--sleekxmpp/xmlstream/handler/base.py2
-rw-r--r--sleekxmpp/xmlstream/handler/callback.py2
-rw-r--r--sleekxmpp/xmlstream/handler/waiter.py2
-rw-r--r--sleekxmpp/xmlstream/handler/xmlcallback.py2
-rw-r--r--sleekxmpp/xmlstream/handler/xmlwaiter.py2
-rw-r--r--sleekxmpp/xmlstream/jid.py121
-rw-r--r--sleekxmpp/xmlstream/matcher/__init__.py13
-rw-r--r--sleekxmpp/xmlstream/matcher/base.py2
-rw-r--r--sleekxmpp/xmlstream/matcher/id.py2
-rw-r--r--sleekxmpp/xmlstream/matcher/many.py2
-rw-r--r--sleekxmpp/xmlstream/matcher/stanzapath.py2
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py2
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py2
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py880
-rw-r--r--sleekxmpp/xmlstream/statemachine.py2
-rw-r--r--sleekxmpp/xmlstream/statemanager.py139
-rw-r--r--sleekxmpp/xmlstream/tostring/__init__.py75
-rw-r--r--sleekxmpp/xmlstream/tostring/tostring.py95
-rw-r--r--sleekxmpp/xmlstream/tostring/tostring26.py104
-rw-r--r--sleekxmpp/xmlstream/tostring26/__init__.py65
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py131
-rw-r--r--testall.py4
-rw-r--r--tests/sleektest.py519
-rw-r--r--tests/test_addresses.py111
-rw-r--r--tests/test_chatstates.py44
-rw-r--r--tests/test_disco.py203
-rw-r--r--tests/test_elementbase.py193
-rw-r--r--tests/test_errorstanzas.py56
-rw-r--r--tests/test_events.py12
-rw-r--r--tests/test_forms.py115
-rw-r--r--tests/test_gmail.py88
-rw-r--r--tests/test_iqstanzas.py90
-rw-r--r--tests/test_jid.py26
-rw-r--r--tests/test_messagestanzas.py101
-rw-r--r--tests/test_presencestanzas.py98
-rw-r--r--tests/test_pubsubstanzas.py806
-rw-r--r--tests/test_roster.py84
-rw-r--r--tests/test_stream.py34
-rw-r--r--tests/test_tostring.py104
-rw-r--r--tests/xmlcompare.py28
81 files changed, 5863 insertions, 2571 deletions
diff --git a/INSTALL b/INSTALL
index b73b5e5f..f081a35a 100644
--- a/INSTALL
+++ b/INSTALL
@@ -6,3 +6,6 @@ python3 setup.py install
Root install:
sudo python3 setup.py install
+
+To test:
+python example.py -v -j [USER@example.com] -p [PASSWORD]
diff --git a/LICENSE b/LICENSE
index 59c501bd..fb9f977c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2010 ICRL
+Copyright (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
diff --git a/MANIFEST b/MANIFEST
deleted file mode 100644
index 3a39d836..00000000
--- a/MANIFEST
+++ /dev/null
@@ -1,39 +0,0 @@
-setup.py
-sleekxmpp/__init__.py
-sleekxmpp/basexmpp.py
-sleekxmpp/clientxmpp.py
-sleekxmpp/example.py
-sleekxmpp/plugins/__init__.py
-sleekxmpp/plugins/base.py
-sleekxmpp/plugins/gmail_notify.py
-sleekxmpp/plugins/xep_0004.py
-sleekxmpp/plugins/xep_0009.py
-sleekxmpp/plugins/xep_0030.py
-sleekxmpp/plugins/xep_0045.py
-sleekxmpp/plugins/xep_0050.py
-sleekxmpp/plugins/xep_0060.py
-sleekxmpp/plugins/xep_0078.py
-sleekxmpp/plugins/xep_0086.py
-sleekxmpp/plugins/xep_0092.py
-sleekxmpp/plugins/xep_0199.py
-sleekxmpp/stanza/__init__.py
-sleekxmpp/stanza/iq.py
-sleekxmpp/stanza/message.py
-sleekxmpp/stanza/presence.py
-sleekxmpp/xmlstream/__init__.py
-sleekxmpp/xmlstream/stanzabase.py
-sleekxmpp/xmlstream/statemachine.py
-sleekxmpp/xmlstream/test.py
-sleekxmpp/xmlstream/testclient.py
-sleekxmpp/xmlstream/xmlstream.py
-sleekxmpp/xmlstream/handler/__init__.py
-sleekxmpp/xmlstream/handler/base.py
-sleekxmpp/xmlstream/handler/callback.py
-sleekxmpp/xmlstream/handler/waiter.py
-sleekxmpp/xmlstream/handler/xmlcallback.py
-sleekxmpp/xmlstream/handler/xmlwaiter.py
-sleekxmpp/xmlstream/matcher/__init__.py
-sleekxmpp/xmlstream/matcher/base.py
-sleekxmpp/xmlstream/matcher/many.py
-sleekxmpp/xmlstream/matcher/xmlmask.py
-sleekxmpp/xmlstream/matcher/xpath.py
diff --git a/README b/README
index abc2d090..da9fe8c8 100644
--- a/README
+++ b/README
@@ -4,6 +4,11 @@ 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.
diff --git a/example.py b/example.py
index c9b6559b..4eb88b3b 100644
--- a/example.py
+++ b/example.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
# coding=utf8
import sleekxmpp
@@ -8,41 +9,46 @@ import time
import sys
if sys.version_info < (3,0):
- reload(sys)
- sys.setdefaultencoding('utf8')
+ reload(sys)
+ sys.setdefaultencoding('utf8')
class Example(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)
-
- def start(self, event):
- self.getRoster()
- self.sendPresence()
-
- def message(self, msg):
- msg.reply("Thanks for sending\n%(body)s" % msg).send()
+
+ 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)
+
+ def start(self, event):
+ self.getRoster()
+ self.sendPresence()
+
+ def message(self, msg):
+ msg.reply("Thanks for sending\n%(body)s" % msg).send()
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")
- opts,args = optp.parse_args()
-
- logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
- xmpp = Example('user@gmail.com/sleekxmpp', 'password')
- xmpp.registerPlugin('xep_0030')
- xmpp.registerPlugin('xep_0004')
- xmpp.registerPlugin('xep_0060')
- xmpp.registerPlugin('xep_0199')
- if xmpp.connect(('talk.google.com', 5222)):
- xmpp.process(threaded=False)
- print("done")
- else:
- print("Unable to connect.")
+ #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("-j","--jid", dest="jid", help="JID to use")
+ optp.add_option("-p","--password", dest="password", help="password to use")
+ opts,args = optp.parse_args()
+
+ logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s')
+ xmpp = Example(opts.jid, opts.password)
+ xmpp.registerPlugin('xep_0030')
+ xmpp.registerPlugin('xep_0004')
+ xmpp.registerPlugin('xep_0060')
+ xmpp.registerPlugin('xep_0199')
+
+ # use this if you don't have pydns, and want to
+ # talk to GoogleTalk (e.g.)
+# if xmpp.connect(('talk.google.com', 5222)):
+ if xmpp.connect():
+ xmpp.process(threaded=False)
+ print("done")
+ else:
+ print("Unable to connect.")
diff --git a/setup.py b/setup.py
index 280ec3c0..e3acf18a 100644
--- a/setup.py
+++ b/setup.py
@@ -16,13 +16,13 @@ import sys
# 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 = '0.2.3.1'
@@ -37,17 +37,13 @@ CLASSIFIERS = [ 'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries :: Python Modules',
]
-packages = [ 'sleekxmpp',
+packages = [ 'sleekxmpp',
'sleekxmpp/plugins',
'sleekxmpp/stanza',
'sleekxmpp/xmlstream',
'sleekxmpp/xmlstream/matcher',
- 'sleekxmpp/xmlstream/handler' ]
-
-if sys.version_info < (3, 0):
- packages.append('sleekxmpp/xmlstream/tostring26')
-else:
- packages.append('sleekxmpp/xmlstream/tostring')
+ 'sleekxmpp/xmlstream/handler',
+ 'sleekxmpp/xmlstream/tostring']
setup(
name = "sleekxmpp",
diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py
index ccb43522..d2f5765f 100644
--- a/sleekxmpp/__init__.py
+++ b/sleekxmpp/__init__.py
@@ -1,11 +1,11 @@
-#!/usr/bin/python2.5
+#!/usr/bin/env python
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from __future__ import absolute_import, unicode_literals
from . basexmpp import basexmpp
@@ -30,223 +30,232 @@ from . import plugins
#from . import stanza
srvsupport = True
try:
- import dns.resolver
+ import dns.resolver
except ImportError:
- srvsupport = False
+ srvsupport = False
#class PresenceStanzaType(object):
-#
-# def fromXML(self, xml):
-# self.ptype = xml.get('type')
+#
+# def fromXML(self, xml):
+# self.ptype = xml.get('type')
class ClientXMPP(basexmpp, XMLStream):
- """SleekXMPP's client class. Use only for good, not evil."""
+ """SleekXMPP's client class. Use only for good, not evil."""
- def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], escape_quotes=True):
- global srvsupport
- XMLStream.__init__(self)
- self.default_ns = 'jabber:client'
- basexmpp.__init__(self)
- self.plugin_config = plugin_config
- self.escape_quotes = escape_quotes
- self.set_jid(jid)
- self.plugin_whitelist = plugin_whitelist
- self.auto_reconnect = True
- self.srvsupport = srvsupport
- self.password = password
- self.registered_features = []
- self.stream_header = """<stream:stream to='%s' xmlns:stream='http://etherx.jabber.org/streams' xmlns='%s' version='1.0'>""" % (self.server,self.default_ns)
- self.stream_footer = "</stream:stream>"
- #self.map_namespace('http://etherx.jabber.org/streams', 'stream')
- #self.map_namespace('jabber:client', '')
- self.features = []
- #TODO: Use stream state here
- self.authenticated = False
- self.sessionstarted = False
- self.bound = False
- self.bindfail = False
- self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, thread=True))
- self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster, thread=True))
- #self.registerHandler(Callback('Roster Update', MatchXMLMask("<presence xmlns='%s' type='subscribe' />" % self.default_ns), self._handlePresenceSubscribe, thread=True))
- self.registerFeature("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_starttls, True)
- self.registerFeature("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_sasl_auth, True)
- self.registerFeature("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", self.handler_bind_resource)
- self.registerFeature("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session)
-
- #self.registerStanzaExtension('PresenceStanza', PresenceStanzaType)
- #self.register_plugins()
-
- def __getitem__(self, key):
- if key in self.plugin:
- return self.plugin[key]
- else:
- logging.warning("""Plugin "%s" is not loaded.""" % key)
- return False
-
- def get(self, key, default):
- return self.plugin.get(key, default)
+ def __init__(self, jid, password, ssl=False, plugin_config = {}, plugin_whitelist=[], escape_quotes=True):
+ global srvsupport
+ XMLStream.__init__(self)
+ self.default_ns = 'jabber:client'
+ basexmpp.__init__(self)
+ self.plugin_config = plugin_config
+ self.escape_quotes = escape_quotes
+ self.set_jid(jid)
+ self.plugin_whitelist = plugin_whitelist
+ self.auto_reconnect = True
+ self.srvsupport = srvsupport
+ self.password = password
+ self.registered_features = []
+ self.stream_header = """<stream:stream to='%s' xmlns:stream='http://etherx.jabber.org/streams' xmlns='%s' version='1.0'>""" % (self.server,self.default_ns)
+ self.stream_footer = "</stream:stream>"
+ #self.map_namespace('http://etherx.jabber.org/streams', 'stream')
+ #self.map_namespace('jabber:client', '')
+ self.features = []
+ #TODO: Use stream state here
+ self.authenticated = False
+ self.sessionstarted = False
+ self.bound = False
+ self.bindfail = False
+ self.is_component = False
+ self.registerHandler(Callback('Stream Features', MatchXPath('{http://etherx.jabber.org/streams}features'), self._handleStreamFeatures, thread=True))
+ self.registerHandler(Callback('Roster Update', MatchXPath('{%s}iq/{jabber:iq:roster}query' % self.default_ns), self._handleRoster, thread=True))
+ #self.registerHandler(Callback('Roster Update', MatchXMLMask("<presence xmlns='%s' type='subscribe' />" % self.default_ns), self._handlePresenceSubscribe, thread=True))
+ self.registerFeature("<starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_starttls, True)
+ self.registerFeature("<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_sasl_auth, True)
+ self.registerFeature("<bind xmlns='urn:ietf:params:xml:ns:xmpp-bind' />", self.handler_bind_resource)
+ self.registerFeature("<session xmlns='urn:ietf:params:xml:ns:xmpp-session' />", self.handler_start_session)
+
+ #self.registerStanzaExtension('PresenceStanza', PresenceStanzaType)
+ #self.register_plugins()
+
+ def __getitem__(self, key):
+ if key in self.plugin:
+ return self.plugin[key]
+ else:
+ logging.warning("""Plugin "%s" is not loaded.""" % key)
+ return False
+
+ def get(self, key, default):
+ return self.plugin.get(key, default)
- def connect(self, address=tuple()):
- """Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses
- the JID server."""
- if not address or len(address) < 2:
- if not self.srvsupport:
- logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using server hostname from JID.")
- else:
- logging.debug("Since no address is supplied, attempting SRV lookup.")
- try:
- answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.server, dns.rdatatype.SRV)
- except dns.resolver.NXDOMAIN:
- logging.debug("No appropriate SRV record found. Using JID server name.")
- else:
- # pick a random answer, weighted by priority
- # there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway
- # suggestions are welcome
- addresses = {}
- intmax = 0
- priorities = []
- for answer in answers:
- intmax += answer.priority
- addresses[intmax] = (answer.target.to_text()[:-1], answer.port)
- priorities.append(intmax) # sure, I could just do priorities = addresses.keys()\n 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 take server from JID.
- address = (self.server, 5222)
- result = XMLStream.connect(self, address[0], address[1], use_tls=True)
- if result:
- self.event("connected")
- else:
- logging.warning("Failed to connect")
- self.event("disconnected")
- return result
-
- # overriding reconnect and disconnect so that we can get some events
- # should events be part of or required by xmlstream? Maybe that would be cleaner
- def reconnect(self):
- logging.info("Reconnecting")
- self.event("disconnected")
- XMLStream.reconnect(self)
-
- def disconnect(self, init=True, close=False, reconnect=False):
- self.event("disconnected")
- XMLStream.disconnect(self, reconnect)
-
- def registerFeature(self, mask, pointer, breaker = False):
- """Register a stream feature."""
- self.registered_features.append((MatchXMLMask(mask), pointer, breaker))
+ def connect(self, address=tuple()):
+ """Connect to the Jabber Server. Attempts SRV lookup, and if it fails, uses
+ the JID server."""
+ if not address or len(address) < 2:
+ if not self.srvsupport:
+ logging.debug("Did not supply (address, port) to connect to and no SRV support is installed (http://www.dnspython.org). Continuing to attempt connection, using server hostname from JID.")
+ else:
+ logging.debug("Since no address is supplied, attempting SRV lookup.")
+ try:
+ answers = dns.resolver.query("_xmpp-client._tcp.%s" % self.server, dns.rdatatype.SRV)
+ except dns.resolver.NXDOMAIN:
+ logging.debug("No appropriate SRV record found. Using JID server name.")
+ else:
+ # pick a random answer, weighted by priority
+ # there are less verbose ways of doing this (random.choice() with answer * priority), but I chose this way anyway
+ # suggestions are welcome
+ addresses = {}
+ intmax = 0
+ priorities = []
+ for answer in answers:
+ intmax += answer.priority
+ addresses[intmax] = (answer.target.to_text()[:-1], answer.port)
+ priorities.append(intmax) # sure, I could just do priorities = addresses.keys()\n 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 take server from JID.
+ address = (self.server, 5222)
+ result = XMLStream.connect(self, address[0], address[1], use_tls=True)
+ if result:
+ self.event("connected")
+ else:
+ logging.warning("Failed to connect")
+ self.event("disconnected")
+ return result
+
+ # overriding reconnect and disconnect so that we can get some events
+ # should events be part of or required by xmlstream? Maybe that would be cleaner
+ def reconnect(self):
+ logging.info("Reconnecting")
+ self.event("disconnected")
+ XMLStream.reconnect(self)
+
+ def disconnect(self, init=True, close=False, reconnect=False):
+ self.event("disconnected")
+ XMLStream.disconnect(self, reconnect)
+
+ def registerFeature(self, mask, pointer, breaker = False):
+ """Register a stream feature."""
+ self.registered_features.append((MatchXMLMask(mask), pointer, breaker))
- def updateRoster(self, jid, name=None, subscription=None, groups=[]):
- """Add or change a roster item."""
- iq = self.Iq().setValues({'type': 'set'})
- iq['roster'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}}
- #self.send(iq, self.Iq().setValues({'id': iq['id']}))
- r = iq.send()
- return r['type'] == 'result'
-
- def getRoster(self):
- """Request the roster be sent."""
- iq = self.Iq().setValues({'type': 'get'}).enable('roster').send()
- self._handleRoster(iq, request=True)
-
- def _handleStreamFeatures(self, features):
- self.features = []
- for sub in features.xml:
- self.features.append(sub.tag)
- for subelement in features.xml:
- for feature in self.registered_features:
- if feature[0].match(subelement):
- #if self.maskcmp(subelement, feature[0], True):
- if feature[1](subelement) and feature[2]: #if breaker, don't continue
- return True
-
- def handler_starttls(self, xml):
- if not self.authenticated and self.ssl_support:
- self.add_handler("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_tls_start, instream=True)
- self.sendXML(xml)
- return True
- else:
- logging.warning("The module tlslite is required in to some servers, and has not been found.")
- return False
+ def updateRoster(self, jid, name=None, subscription=None, groups=[]):
+ """Add or change a roster item."""
+ iq = self.Iq().setStanzaValues({'type': 'set'})
+ iq['roster']['items'] = {jid: {'name': name, 'subscription': subscription, 'groups': groups}}
+ #self.send(iq, self.Iq().setValues({'id': iq['id']}))
+ r = iq.send()
+ return r['type'] == 'result'
- def handler_tls_start(self, xml):
- logging.debug("Starting TLS")
- if self.startTLS():
- raise RestartStream()
-
- def handler_sasl_auth(self, xml):
- if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
- return False
- logging.debug("Starting SASL Auth")
- self.add_handler("<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_success, instream=True)
- self.add_handler("<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_fail, instream=True)
- sasl_mechs = xml.findall('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism')
- if len(sasl_mechs):
- for sasl_mech in sasl_mechs:
- self.features.append("sasl:%s" % sasl_mech.text)
- if 'sasl:PLAIN' in self.features:
- if sys.version_info < (3,0):
- self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username) + b'\x00' + bytes(self.password)).decode('utf-8'))
- else:
- self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username, 'utf-8') + b'\x00' + bytes(self.password, 'utf-8')).decode('utf-8'))
- else:
- logging.error("No appropriate login method.")
- self.disconnect()
- #if 'sasl:DIGEST-MD5' in self.features:
- # self._auth_digestmd5()
- return True
-
- def handler_auth_success(self, xml):
- self.authenticated = True
- self.features = []
- raise RestartStream()
+ def delRosterItem(self, jid):
+ iq = self.Iq()
+ iq['type'] = 'set'
+ iq['roster']['items'] = {jid: {'subscription': 'remove'}}
+ return iq.send()['type'] == 'result'
+
+ def getRoster(self):
+ """Request the roster be sent."""
+ iq = self.Iq().setStanzaValues({'type': 'get'}).enable('roster').send()
+ self._handleRoster(iq, request=True)
+
+ def _handleStreamFeatures(self, features):
+ self.features = []
+ for sub in features.xml:
+ self.features.append(sub.tag)
+ for subelement in features.xml:
+ for feature in self.registered_features:
+ if feature[0].match(subelement):
+ #if self.maskcmp(subelement, feature[0], True):
+ if feature[1](subelement) and feature[2]: #if breaker, don't continue
+ return True
+
+ def handler_starttls(self, xml):
+ if not self.authenticated and self.ssl_support:
+ self.add_handler("<proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls' />", self.handler_tls_start, name='TLS Proceed', instream=True)
+ self.sendXML(xml)
+ return True
+ else:
+ logging.warning("The module tlslite is required in to some servers, and has not been found.")
+ return False
- def handler_auth_fail(self, xml):
- logging.info("Authentication failed.")
- self.disconnect()
- self.event("failed_auth")
-
- def handler_bind_resource(self, xml):
- logging.debug("Requesting resource: %s" % self.resource)
- iq = self.Iq(stype='set')
- res = ET.Element('resource')
- res.text = self.resource
- xml.append(res)
- iq.append(xml)
- response = iq.send()
- #response = self.send(iq, self.Iq(sid=iq['id']))
- self.set_jid(response.xml.find('{urn:ietf:params:xml:ns:xmpp-bind}bind/{urn:ietf:params:xml:ns:xmpp-bind}jid').text)
- self.bound = True
- logging.info("Node set to: %s" % self.fulljid)
- if "{urn:ietf:params:xml:ns:xmpp-session}session" not in self.features or self.bindfail:
- logging.debug("Established Session")
- self.sessionstarted = True
- self.event("session_start")
-
- def handler_start_session(self, xml):
- if self.authenticated and self.bound:
- iq = self.makeIqSet(xml)
- response = iq.send()
- logging.debug("Established Session")
- self.sessionstarted = True
- self.event("session_start")
- else:
- #bind probably hasn't happened yet
- self.bindfail = True
-
- def _handleRoster(self, iq, request=False):
- 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])
- if iq['type'] == 'set':
- self.send(self.Iq().setValues({'type': 'result', 'id': iq['id']}).enable('roster'))
- self.event("roster_update", iq)
+ def handler_tls_start(self, xml):
+ logging.debug("Starting TLS")
+ if self.startTLS():
+ raise RestartStream()
+
+ def handler_sasl_auth(self, xml):
+ if '{urn:ietf:params:xml:ns:xmpp-tls}starttls' in self.features:
+ return False
+ logging.debug("Starting SASL Auth")
+ self.add_handler("<success xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_success, name='SASL Sucess', instream=True)
+ self.add_handler("<failure xmlns='urn:ietf:params:xml:ns:xmpp-sasl' />", self.handler_auth_fail, name='SASL Failure', instream=True)
+ sasl_mechs = xml.findall('{urn:ietf:params:xml:ns:xmpp-sasl}mechanism')
+ if len(sasl_mechs):
+ for sasl_mech in sasl_mechs:
+ self.features.append("sasl:%s" % sasl_mech.text)
+ if 'sasl:PLAIN' in self.features:
+ if sys.version_info < (3,0):
+ self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username) + b'\x00' + bytes(self.password)).decode('utf-8'))
+ else:
+ self.send("""<auth xmlns='urn:ietf:params:xml:ns:xmpp-sasl' mechanism='PLAIN'>%s</auth>""" % base64.b64encode(b'\x00' + bytes(self.username, 'utf-8') + b'\x00' + bytes(self.password, 'utf-8')).decode('utf-8'))
+ else:
+ logging.error("No appropriate login method.")
+ self.disconnect()
+ #if 'sasl:DIGEST-MD5' in self.features:
+ # self._auth_digestmd5()
+ return True
+
+ def handler_auth_success(self, xml):
+ self.authenticated = True
+ self.features = []
+ raise RestartStream()
+
+ def handler_auth_fail(self, xml):
+ logging.info("Authentication failed.")
+ self.disconnect()
+ self.event("failed_auth")
+
+ def handler_bind_resource(self, xml):
+ logging.debug("Requesting resource: %s" % self.resource)
+ xml.clear()
+ iq = self.Iq(stype='set')
+ if self.resource:
+ res = ET.Element('resource')
+ res.text = self.resource
+ xml.append(res)
+ iq.append(xml)
+ response = iq.send()
+ #response = self.send(iq, self.Iq(sid=iq['id']))
+ self.set_jid(response.xml.find('{urn:ietf:params:xml:ns:xmpp-bind}bind/{urn:ietf:params:xml:ns:xmpp-bind}jid').text)
+ self.bound = True
+ logging.info("Node set to: %s" % self.fulljid)
+ if "{urn:ietf:params:xml:ns:xmpp-session}session" not in self.features or self.bindfail:
+ logging.debug("Established Session")
+ self.sessionstarted = True
+ self.event("session_start")
+
+ def handler_start_session(self, xml):
+ if self.authenticated and self.bound:
+ iq = self.makeIqSet(xml)
+ response = iq.send()
+ logging.debug("Established Session")
+ self.sessionstarted = True
+ self.event("session_start")
+ else:
+ #bind probably hasn't happened yet
+ self.bindfail = True
+
+ def _handleRoster(self, iq, request=False):
+ 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])
+ if iq['type'] == 'set':
+ self.send(self.Iq().setStanzaValues({'type': 'result', 'id': iq['id']}).enable('roster'))
+ self.event("roster_update", iq)
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py
index 907067fa..b7b605b0 100644
--- a/sleekxmpp/basexmpp.py
+++ b/sleekxmpp/basexmpp.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from __future__ import with_statement, unicode_literals
@@ -16,6 +16,7 @@ from . xmlstream.handler.xmlcallback import XMLCallback
from . xmlstream.handler.xmlwaiter import XMLWaiter
from . xmlstream.handler.waiter import Waiter
from . xmlstream.handler.callback import Callback
+from . xmlstream.stanzabase import registerStanzaPlugin
from . import plugins
from . stanza.message import Message
from . stanza.iq import Iq
@@ -24,9 +25,11 @@ from . stanza.roster import Roster
from . stanza.nick import Nick
from . stanza.htmlim import HTMLIM
from . stanza.error import Error
+from sleekxmpp.xmlstream.tostring import tostring
import logging
import threading
+import copy
import sys
@@ -34,12 +37,6 @@ if sys.version_info < (3,0):
reload(sys)
sys.setdefaultencoding('utf8')
-
-def stanzaPlugin(stanza, plugin):
- stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
- stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
-
-
class basexmpp(object):
def __init__(self):
self.id = 0
@@ -61,14 +58,10 @@ class basexmpp(object):
self.registerStanza(Message)
self.registerStanza(Iq)
self.registerStanza(Presence)
- self.stanzaPlugin(Iq, Roster)
- self.stanzaPlugin(Message, Nick)
- self.stanzaPlugin(Message, HTMLIM)
-
- def stanzaPlugin(self, stanza, plugin):
- stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
- stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
-
+ registerStanzaPlugin(Iq, Roster)
+ registerStanzaPlugin(Message, Nick)
+ registerStanzaPlugin(Message, HTMLIM)
+
def Message(self, *args, **kwargs):
return Message(self, *args, **kwargs)
@@ -77,7 +70,7 @@ class basexmpp(object):
def Presence(self, *args, **kwargs):
return Presence(self, *args, **kwargs)
-
+
def set_jid(self, jid):
"""Rip a JID apart and claim it as our own."""
self.fulljid = jid
@@ -85,12 +78,12 @@ class basexmpp(object):
self.jid = self.getjidbare(jid)
self.username = jid.split('@', 1)[0]
self.server = jid.split('@',1)[-1].split('/', 1)[0]
-
+
def process(self, *args, **kwargs):
for idx in self.plugin:
if not self.plugin[idx].post_inited: self.plugin[idx].post_init()
return super(basexmpp, self).process(*args, **kwargs)
-
+
def registerPlugin(self, plugin, pconfig = {}):
"""Register a plugin not in plugins.__init__.__all__ but in the plugins
directory."""
@@ -105,7 +98,7 @@ class basexmpp(object):
if hasattr(self.plugin[plugin], 'xep'):
xep = "(XEP-%s) " % self.plugin[plugin].xep
logging.debug("Loaded Plugin %s%s" % (xep, self.plugin[plugin].description))
-
+
def register_plugins(self):
"""Initiates all plugins in the plugins/__init__.__all__"""
if self.plugin_whitelist:
@@ -120,22 +113,24 @@ class basexmpp(object):
# run post_init() for cross-plugin interaction
for plugin in self.plugin:
self.plugin[plugin].post_init()
-
+
def getNewId(self):
with self.id_lock:
self.id += 1
return self.getId()
-
- def add_handler(self, mask, pointer, disposable=False, threaded=False, filter=False, instream=False):
- #logging.warning("Deprecated add_handler used for %s: %s." % (mask, pointer))
- self.registerHandler(XMLCallback('add_handler_%s' % self.getNewId(), MatchXMLMask(mask), pointer, threaded, disposable, instream))
-
+
+ def add_handler(self, mask, pointer, name=None, disposable=False, threaded=False, filter=False, instream=False):
+ # threaded is no longer needed, but leaving it for backwards compatibility for now
+ if name is None:
+ name = 'add_handler_%s' % self.getNewId()
+ self.registerHandler(XMLCallback(name, MatchXMLMask(mask), pointer, threaded, disposable, instream))
+
def getId(self):
return "%x".upper() % self.id
def sendXML(self, data, mask=None, timeout=10):
- return self.send(self.tostring(data), mask, timeout)
-
+ return self.send(tostring(data), mask, timeout)
+
def send(self, data, mask=None, timeout=10):
#logging.warning("Deprecated send used for \"%s\"" % (data,))
#if not type(data) == type(''):
@@ -150,41 +145,41 @@ class basexmpp(object):
self.sendRaw(data)
if mask is not None:
return waitfor.wait(timeout)
-
+
def makeIq(self, id=0, ifrom=None):
- return self.Iq().setValues({'id': id, 'from': ifrom})
-
+ return self.Iq().setStanzaValues({'id': str(id), 'from': ifrom})
+
def makeIqGet(self, queryxmlns = None):
- iq = self.Iq().setValues({'type': 'get'})
+ iq = self.Iq().setStanzaValues({'type': 'get'})
if queryxmlns:
iq.append(ET.Element("{%s}query" % queryxmlns))
return iq
-
+
def makeIqResult(self, id):
- return self.Iq().setValues({'id': id, 'type': 'result'})
-
+ return self.Iq().setStanzaValues({'id': id, 'type': 'result'})
+
def makeIqSet(self, sub=None):
- iq = self.Iq().setValues({'type': 'set'})
+ iq = self.Iq().setStanzaValues({'type': 'set'})
if sub != None:
iq.append(sub)
return iq
def makeIqError(self, id, type='cancel', condition='feature-not-implemented', text=None):
- iq = self.Iq().setValues({'id': id})
- iq['error'].setValues({'type': type, 'condition': condition, 'text': text})
+ iq = self.Iq().setStanzaValues({'id': id})
+ iq['error'].setStanzaValues({'type': type, 'condition': condition, 'text': text})
return iq
def makeIqQuery(self, iq, xmlns):
query = ET.Element("{%s}query" % xmlns)
iq.append(query)
return iq
-
+
def makeQueryRoster(self, iq=None):
query = ET.Element("{jabber:iq:roster}query")
if iq:
iq.append(query)
return query
-
+
def add_event_handler(self, name, pointer, threaded=False, disposable=False):
if not name in self.event_handlers:
self.event_handlers[name] = []
@@ -194,27 +189,28 @@ class basexmpp(object):
"""Remove a handler for an event."""
if not name in self.event_handlers:
return
-
+
# Need to keep handlers that do not use
# the given function pointer
def filter_pointers(handler):
return handler[0] != pointer
- self.event_handlers[name] = filter(filter_pointers,
+ self.event_handlers[name] = filter(filter_pointers,
self.event_handlers[name])
def event(self, name, eventdata = {}): # called on an event
for handler in self.event_handlers.get(name, []):
+ handlerdata = copy.copy(eventdata)
if handler[1]: #if threaded
#thread.start_new(handler[0], (eventdata,))
- x = threading.Thread(name="Event_%s" % str(handler[0]), target=handler[0], args=(eventdata,))
+ x = threading.Thread(name="Event_%s" % str(handler[0]), target=handler[0], args=(handlerdata,))
x.start()
else:
- handler[0](eventdata)
+ handler[0](handlerdata)
if handler[2]: #disposable
with self.lock:
self.event_handlers[name].pop(self.event_handlers[name].index(handler))
-
+
def makeMessage(self, mto, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None):
message = self.Message(sto=mto, stype=mtype, sfrom=mfrom)
message['body'] = mbody
@@ -222,7 +218,7 @@ class basexmpp(object):
if mnick is not None: message['nick'] = mnick
if mhtml is not None: message['html']['html'] = mhtml
return message
-
+
def makePresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, ptype=None, pfrom=None):
presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto)
if pshow is not None: presence['type'] = pshow
@@ -231,10 +227,10 @@ class basexmpp(object):
presence['priority'] = ppriority
presence['status'] = pstatus
return presence
-
+
def sendMessage(self, mto, mbody, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None):
self.send(self.makeMessage(mto,mbody,msubject,mtype,mhtml,mfrom,mnick))
-
+
def sendPresence(self, pshow=None, pstatus=None, ppriority=None, pto=None, pfrom=None, ptype=None):
self.send(self.makePresence(pshow,pstatus,ppriority,pto, ptype=ptype, pfrom=pfrom))
if not self.sentpresence:
@@ -248,19 +244,19 @@ class basexmpp(object):
nick.text = pnick
presence.append(nick)
self.send(presence)
-
+
def getjidresource(self, fulljid):
if '/' in fulljid:
return fulljid.split('/', 1)[-1]
else:
return ''
-
+
def getjidbare(self, fulljid):
return fulljid.split('/', 1)[0]
def _handleMessage(self, msg):
self.event('message', msg)
-
+
def _handlePresence(self, presence):
"""Update roster items based on presence"""
self.event("presence_%s" % presence['type'], presence)
@@ -301,7 +297,7 @@ class basexmpp(object):
if name:
name = "(%s) " % name
logging.debug("STATUS: %s%s/%s[%s]: %s" % (name, jid, resource, show,status))
-
+
def _handlePresenceSubscribe(self, presence):
"""Handling subscriptions automatically."""
if self.auto_authorize == True:
diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py
index de125814..5534a457 100755
--- a/sleekxmpp/componentxmpp.py
+++ b/sleekxmpp/componentxmpp.py
@@ -1,11 +1,11 @@
-#!/usr/bin/python2.6
+#!/usr/bin/env python
"""
SleekXMPP: The Sleek XMPP Library
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from __future__ import absolute_import
from . basexmpp import basexmpp
@@ -30,59 +30,60 @@ from . import stanza
import hashlib
srvsupport = True
try:
- import dns.resolver
+ import dns.resolver
except ImportError:
- srvsupport = False
+ srvsupport = False
class ComponentXMPP(basexmpp, XMLStream):
- """SleekXMPP's client class. Use only for good, not evil."""
+ """SleekXMPP's client class. Use only for good, not evil."""
- def __init__(self, jid, secret, host, port, plugin_config = {}, plugin_whitelist=[], use_jc_ns=False):
- XMLStream.__init__(self)
- if use_jc_ns:
- self.default_ns = 'jabber:client'
- else:
- self.default_ns = 'jabber:component:accept'
- basexmpp.__init__(self)
- self.auto_authorize = None
- self.stream_header = "<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='%s'>" % jid
- self.stream_footer = "</stream:stream>"
- self.server_host = host
- self.server_port = port
- self.set_jid(jid)
- self.secret = secret
- self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake))
-
- def __getitem__(self, key):
- if key in self.plugin:
- return self.plugin[key]
- else:
- logging.warning("""Plugin "%s" is not loaded.""" % key)
- return False
-
- def get(self, key, default):
- return self.plugin.get(key, default)
-
- def incoming_filter(self, xmlobj):
- if xmlobj.tag.startswith('{jabber:client}'):
- xmlobj.tag = xmlobj.tag.replace('jabber:client', self.default_ns)
- for sub in xmlobj:
- self.incoming_filter(sub)
- return xmlobj
+ def __init__(self, jid, secret, host, port, plugin_config = {}, plugin_whitelist=[], use_jc_ns=False):
+ XMLStream.__init__(self)
+ if use_jc_ns:
+ self.default_ns = 'jabber:client'
+ else:
+ self.default_ns = 'jabber:component:accept'
+ basexmpp.__init__(self)
+ self.auto_authorize = None
+ self.stream_header = "<stream:stream xmlns='jabber:component:accept' xmlns:stream='http://etherx.jabber.org/streams' to='%s'>" % jid
+ self.stream_footer = "</stream:stream>"
+ self.server_host = host
+ self.server_port = port
+ self.set_jid(jid)
+ self.secret = secret
+ self.is_component = True
+ self.registerHandler(Callback('Handshake', MatchXPath('{jabber:component:accept}handshake'), self._handleHandshake))
+
+ def __getitem__(self, key):
+ if key in self.plugin:
+ return self.plugin[key]
+ else:
+ logging.warning("""Plugin "%s" is not loaded.""" % key)
+ return False
+
+ def get(self, key, default):
+ return self.plugin.get(key, default)
+
+ def incoming_filter(self, xmlobj):
+ if xmlobj.tag.startswith('{jabber:client}'):
+ xmlobj.tag = xmlobj.tag.replace('jabber:client', self.default_ns)
+ for sub in xmlobj:
+ self.incoming_filter(sub)
+ return xmlobj
- def start_stream_handler(self, xml):
- sid = xml.get('id', '')
- handshake = ET.Element('{jabber:component:accept}handshake')
- if sys.version_info < (3,0):
- handshake.text = hashlib.sha1("%s%s" % (sid, self.secret)).hexdigest().lower()
- else:
- handshake.text = hashlib.sha1(bytes("%s%s" % (sid, self.secret), 'utf-8')).hexdigest().lower()
- self.sendXML(handshake)
-
- def _handleHandshake(self, xml):
- self.event("session_start")
-
- def connect(self):
- logging.debug("Connecting to %s:%s" % (self.server_host, self.server_port))
- return xmlstreammod.XMLStream.connect(self, self.server_host, self.server_port)
+ def start_stream_handler(self, xml):
+ sid = xml.get('id', '')
+ handshake = ET.Element('{jabber:component:accept}handshake')
+ if sys.version_info < (3,0):
+ handshake.text = hashlib.sha1("%s%s" % (sid, self.secret)).hexdigest().lower()
+ else:
+ handshake.text = hashlib.sha1(bytes("%s%s" % (sid, self.secret), 'utf-8')).hexdigest().lower()
+ self.sendXML(handshake)
+
+ def _handleHandshake(self, xml):
+ self.event("session_start")
+
+ def connect(self):
+ logging.debug("Connecting to %s:%s" % (self.server_host, self.server_port))
+ return xmlstreammod.XMLStream.connect(self, self.server_host, self.server_port)
diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py
index 5b761cf3..40217ef6 100644
--- a/sleekxmpp/exceptions.py
+++ b/sleekxmpp/exceptions.py
@@ -3,14 +3,43 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
-See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
class XMPPError(Exception):
- def __init__(self, condition='undefined-condition', text=None, etype=None, extension=None, extension_ns=None, extension_args=None):
- self.condition = condition
- self.text = text
- self.etype = etype
- self.extension = extension
- self.extension_ns = extension_ns
- self.extension_args = extension_args
+
+ """
+ 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):
+ """
+ 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.
+ """
+ self.condition = condition
+ self.text = text
+ self.etype = etype
+ self.extension = extension
+ self.extension_ns = extension_ns
+ self.extension_args = extension_args
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py
index 1868365e..b51977b8 100644
--- a/sleekxmpp/plugins/__init__.py
+++ b/sleekxmpp/plugins/__init__.py
@@ -1,20 +1,8 @@
"""
SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2007 Nathanael C. Fritz
+ Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
-
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+ See the file LICENSE for copying permission.
"""
-__all__ = ['xep_0004', 'xep_0030', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060']
+__all__ = ['xep_0004', 'xep_0030', 'xep_0033', 'xep_0045', 'xep_0050', 'xep_0078', 'xep_0085', 'xep_0092', 'xep_0199', 'gmail_notify', 'xep_0060']
diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py
index 4223646a..a5260b0c 100644
--- a/sleekxmpp/plugins/base.py
+++ b/sleekxmpp/plugins/base.py
@@ -1,22 +1,12 @@
"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2007 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
+"""
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
-"""
class base_plugin(object):
def __init__(self, xmpp, config):
diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py
index b709ef69..7e442346 100644
--- a/sleekxmpp/plugins/gmail_notify.py
+++ b/sleekxmpp/plugins/gmail_notify.py
@@ -1,57 +1,146 @@
"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2007 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ 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 __future__ import with_statement
-from . import base
+
import logging
-from xml.etree import cElementTree as ET
-import traceback
-import time
+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
+
+
+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):
-
- def plugin_init(self):
- self.description = 'Google Talk Gmail Notification'
- self.xmpp.add_event_handler('sent_presence', self.handler_gmailcheck, threaded=True)
- self.emails = []
-
- def handler_gmailcheck(self, payload):
- #TODO XEP 30 should cache results and have getFeature
- result = self.xmpp['xep_0030'].getInfo(self.xmpp.server)
- features = []
- for feature in result.findall('{http://jabber.org/protocol/disco#info}query/{http://jabber.org/protocol/disco#info}feature'):
- features.append(feature.get('var'))
- if 'google:mail:notify' in features:
- logging.debug("Server supports Gmail Notify")
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><new-mail xmlns='google:mail:notify' /></iq>" % self.xmpp.default_ns, self.handler_notify)
- self.getEmail()
-
- def handler_notify(self, xml):
- logging.info("New Gmail recieved!")
- self.xmpp.event('gmail_notify')
-
- def getEmail(self):
- iq = self.xmpp.makeIqGet()
- iq.attrib['from'] = self.xmpp.fulljid
- iq.attrib['to'] = self.xmpp.jid
- self.xmpp.makeIqQuery(iq, 'google:mail:notify')
- emails = iq.send()
- mailbox = emails.find('{google:mail:notify}mailbox')
- total = int(mailbox.get('total-matched', 0))
- logging.info("%s New Gmail Messages" % total)
+ """
+ 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 ''
+ logging.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):
+ logging.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:
+ logging.info("Gmail: Checking for new emails")
+ else:
+ logging.info('Gmail: Searching for emails matching: "%s"' % query)
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = self.xmpp.jid
+ iq['gmail']['q'] = query
+ iq['gmail']['newer-than-time'] = newer
+ return iq.send()
diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py
new file mode 100644
index 00000000..651408ae
--- /dev/null
+++ b/sleekxmpp/plugins/old_0004.py
@@ -0,0 +1,417 @@
+"""
+ 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
+
+class old_0004(base.base_plugin):
+
+ def plugin_init(self):
+ self.xep = '0004'
+ self.description = '*Deprecated Data Forms'
+ self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", 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')
+ logging.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
+
diff --git a/sleekxmpp/plugins/stanza_pubsub.py b/sleekxmpp/plugins/stanza_pubsub.py
index 1a1526f0..96d02f93 100644
--- a/sleekxmpp/plugins/stanza_pubsub.py
+++ b/sleekxmpp/plugins/stanza_pubsub.py
@@ -1,4 +1,4 @@
-from .. xmlstream.stanzabase import ElementBase, ET, JID
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.iq import Iq
from .. stanza.message import Message
from .. basexmpp import basexmpp
@@ -6,9 +6,6 @@ from .. xmlstream.xmlstream import XMLStream
import logging
from . import xep_0004
-def stanzaPlugin(stanza, plugin):
- stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
- stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
class PubsubState(ElementBase):
namespace = 'http://jabber.org/protocol/psstate'
@@ -30,7 +27,7 @@ class PubsubState(ElementBase):
for child in self.xml.getchildren():
self.xml.remove(child)
-stanzaPlugin(Iq, PubsubState)
+registerStanzaPlugin(Iq, PubsubState)
class PubsubStateEvent(ElementBase):
namespace = 'http://jabber.org/protocol/psstate#event'
@@ -40,8 +37,8 @@ class PubsubStateEvent(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(Message, PubsubStateEvent)
-stanzaPlugin(PubsubStateEvent, PubsubState)
+registerStanzaPlugin(Message, PubsubStateEvent)
+registerStanzaPlugin(PubsubStateEvent, PubsubState)
class Pubsub(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -51,7 +48,7 @@ class Pubsub(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(Iq, Pubsub)
+registerStanzaPlugin(Iq, Pubsub)
class PubsubOwner(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -61,7 +58,7 @@ class PubsubOwner(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(Iq, PubsubOwner)
+registerStanzaPlugin(Iq, PubsubOwner)
class Affiliation(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -86,7 +83,7 @@ class Affiliations(ElementBase):
self.xml.append(affiliation.xml)
return self.iterables.append(affiliation)
-stanzaPlugin(Pubsub, Affiliations)
+registerStanzaPlugin(Pubsub, Affiliations)
class Subscription(ElementBase):
@@ -103,7 +100,7 @@ class Subscription(ElementBase):
def getjid(self):
return jid(self._getattr('jid'))
-stanzaPlugin(Pubsub, Subscription)
+registerStanzaPlugin(Pubsub, Subscription)
class Subscriptions(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -114,7 +111,7 @@ class Subscriptions(ElementBase):
plugin_tag_map = {}
subitem = (Subscription,)
-stanzaPlugin(Pubsub, Subscriptions)
+registerStanzaPlugin(Pubsub, Subscriptions)
class OptionalSetting(object):
interfaces = set(('required',))
@@ -147,7 +144,7 @@ class SubscribeOptions(ElementBase, OptionalSetting):
plugin_tag_map = {}
interfaces = set(('required',))
-stanzaPlugin(Subscription, SubscribeOptions)
+registerStanzaPlugin(Subscription, SubscribeOptions)
class Item(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -178,7 +175,7 @@ class Items(ElementBase):
plugin_tag_map = {}
subitem = (Item,)
-stanzaPlugin(Pubsub, Items)
+registerStanzaPlugin(Pubsub, Items)
class Create(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -188,7 +185,7 @@ class Create(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(Pubsub, Create)
+registerStanzaPlugin(Pubsub, Create)
#class Default(ElementBase):
# namespace = 'http://jabber.org/protocol/pubsub'
@@ -203,7 +200,7 @@ stanzaPlugin(Pubsub, Create)
# if not t: t == 'leaf'
# return t
#
-#stanzaPlugin(Pubsub, Default)
+#registerStanzaPlugin(Pubsub, Default)
class Publish(Items):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -214,7 +211,7 @@ class Publish(Items):
plugin_tag_map = {}
subitem = (Item,)
-stanzaPlugin(Pubsub, Publish)
+registerStanzaPlugin(Pubsub, Publish)
class Retract(Items):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -224,7 +221,7 @@ class Retract(Items):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(Pubsub, Retract)
+registerStanzaPlugin(Pubsub, Retract)
class Unsubscribe(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -254,13 +251,13 @@ class Subscribe(ElementBase):
def getJid(self):
return JID(self._getAttr('jid'))
-stanzaPlugin(Pubsub, Subscribe)
+registerStanzaPlugin(Pubsub, Subscribe)
class Configure(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
name = 'configure'
plugin_attrib = name
- interfaces = set(('node', 'type', 'config'))
+ interfaces = set(('node', 'type'))
plugin_attrib_map = {}
plugin_tag_map = {}
@@ -269,22 +266,8 @@ class Configure(ElementBase):
if not t: t == 'leaf'
return t
- def getConfig(self):
- config = self.xml.find('{jabber:x:data}x')
- form = xep_0004.Form()
- if config is not None:
- form.fromXML(config)
- return form
-
- def setConfig(self, value):
- self.xml.append(value.getXML())
- return self
-
- def delConfig(self):
- config = self.xml.find('{jabber:x:data}x')
- self.xml.remove(config)
-
-stanzaPlugin(Pubsub, Configure)
+registerStanzaPlugin(Pubsub, Configure)
+registerStanzaPlugin(Configure, xep_0004.Form)
class DefaultConfig(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -296,28 +279,14 @@ class DefaultConfig(ElementBase):
def __init__(self, *args, **kwargs):
ElementBase.__init__(self, *args, **kwargs)
-
- def getConfig(self):
- config = self.xml.find('{jabber:x:data}x')
- form = xep_0004.Form()
- if config is not None:
- form.fromXML(config)
- return form
-
- def setConfig(self, value):
- self.xml.append(value.getXML())
- return self
-
- def delConfig(self):
- config = self.xml.find('{jabber:x:data}x')
- self.xml.remove(config)
def getType(self):
t = self._getAttr('type')
if not t: t = 'leaf'
return t
-stanzaPlugin(PubsubOwner, DefaultConfig)
+registerStanzaPlugin(PubsubOwner, DefaultConfig)
+registerStanzaPlugin(DefaultConfig, xep_0004.Form)
class Options(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub'
@@ -351,8 +320,8 @@ class Options(ElementBase):
def getJid(self):
return JID(self._getAttr('jid'))
-stanzaPlugin(Pubsub, Options)
-stanzaPlugin(Subscribe, Options)
+registerStanzaPlugin(Pubsub, Options)
+registerStanzaPlugin(Subscribe, Options)
class OwnerAffiliations(Affiliations):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -366,7 +335,7 @@ class OwnerAffiliations(Affiliations):
self.xml.append(affiliation.xml)
return self.affiliations.append(affiliation)
-stanzaPlugin(PubsubOwner, OwnerAffiliations)
+registerStanzaPlugin(PubsubOwner, OwnerAffiliations)
class OwnerAffiliation(Affiliation):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -380,7 +349,7 @@ class OwnerConfigure(Configure):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(PubsubOwner, OwnerConfigure)
+registerStanzaPlugin(PubsubOwner, OwnerConfigure)
class OwnerDefault(OwnerConfigure):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -388,7 +357,7 @@ class OwnerDefault(OwnerConfigure):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(PubsubOwner, OwnerDefault)
+registerStanzaPlugin(PubsubOwner, OwnerDefault)
class OwnerDelete(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -398,7 +367,7 @@ class OwnerDelete(ElementBase, OptionalSetting):
plugin_tag_map = {}
interfaces = set(('node',))
-stanzaPlugin(PubsubOwner, OwnerDelete)
+registerStanzaPlugin(PubsubOwner, OwnerDelete)
class OwnerPurge(ElementBase, OptionalSetting):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -407,7 +376,7 @@ class OwnerPurge(ElementBase, OptionalSetting):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(PubsubOwner, OwnerPurge)
+registerStanzaPlugin(PubsubOwner, OwnerPurge)
class OwnerRedirect(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -423,7 +392,7 @@ class OwnerRedirect(ElementBase):
def getJid(self):
return JID(self._getAttr('jid'))
-stanzaPlugin(OwnerDelete, OwnerRedirect)
+registerStanzaPlugin(OwnerDelete, OwnerRedirect)
class OwnerSubscriptions(Subscriptions):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -437,7 +406,7 @@ class OwnerSubscriptions(Subscriptions):
self.xml.append(subscription.xml)
return self.subscriptions.append(subscription)
-stanzaPlugin(PubsubOwner, OwnerSubscriptions)
+registerStanzaPlugin(PubsubOwner, OwnerSubscriptions)
class OwnerSubscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#owner'
@@ -461,7 +430,7 @@ class Event(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(Message, Event)
+registerStanzaPlugin(Message, Event)
class EventItem(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -501,7 +470,7 @@ class EventItems(ElementBase):
plugin_tag_map = {}
subitem = (EventItem, EventRetract)
-stanzaPlugin(Event, EventItems)
+registerStanzaPlugin(Event, EventItems)
class EventCollection(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -511,7 +480,7 @@ class EventCollection(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(Event, EventCollection)
+registerStanzaPlugin(Event, EventCollection)
class EventAssociate(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -521,7 +490,7 @@ class EventAssociate(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(EventCollection, EventAssociate)
+registerStanzaPlugin(EventCollection, EventAssociate)
class EventDisassociate(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -531,7 +500,7 @@ class EventDisassociate(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(EventCollection, EventDisassociate)
+registerStanzaPlugin(EventCollection, EventDisassociate)
class EventConfiguration(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -541,22 +510,8 @@ class EventConfiguration(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
- def getConfig(self):
- config = self.xml.find('{jabber:x:data}x')
- form = xep_0004.Form()
- if config is not None:
- form.fromXML(config)
- return form
-
- def setConfig(self, value):
- self.xml.append(value.getXML())
- return self
-
- def delConfig(self):
- config = self.xml.find('{jabber:x:data}x')
- self.xml.remove(config)
-
-stanzaPlugin(Event, EventConfiguration)
+registerStanzaPlugin(Event, EventConfiguration)
+registerStanzaPlugin(EventConfiguration, xep_0004.Form)
class EventPurge(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -566,7 +521,7 @@ class EventPurge(ElementBase):
plugin_attrib_map = {}
plugin_tag_map = {}
-stanzaPlugin(Event, EventPurge)
+registerStanzaPlugin(Event, EventPurge)
class EventSubscription(ElementBase):
namespace = 'http://jabber.org/protocol/pubsub#event'
@@ -582,4 +537,4 @@ class EventSubscription(ElementBase):
def getJid(self):
return JID(self._getAttr('jid'))
-stanzaPlugin(Event, EventSubscription)
+registerStanzaPlugin(Event, EventSubscription)
diff --git a/sleekxmpp/plugins/xep_0004.py b/sleekxmpp/plugins/xep_0004.py
index 015bd8bc..037fc090 100644
--- a/sleekxmpp/plugins/xep_0004.py
+++ b/sleekxmpp/plugins/xep_0004.py
@@ -1,427 +1,347 @@
"""
SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2007 Nathanael C. Fritz
+ Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ See the file LICENSE for copying permission.
"""
-from . import base
+
import logging
-from xml.etree import cElementTree as ET
import copy
-#TODO support item groups and results
+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 xep_0004(base.base_plugin):
-
- def plugin_init(self):
- self.xep = '0004'
- self.description = 'Data Forms'
- self.xmpp.add_handler("<message><x xmlns='jabber:x:data' /></message>", self.handler_message_xform)
-
- def post_init(self):
- base.base_plugin.post_init(self)
- self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data')
-
- def handler_message_xform(self, xml):
- object = self.handle_form(xml)
- 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 = []
+
+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 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):
+ logging.warning("Form.getXML() is deprecated API compatibility with plugins/old_0004.py")
+ return self.xml
- 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)
+ def fromXML(self, xml):
+ logging.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:
- 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])
-
+ 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):
- 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
-
+ 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):
- 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
+ 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]
+
+
+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:
- self.islinebreak = False
- if value:
- self.setValue(value)
-
- def addOption(self, value, label):
- if self.islist:
- self.options.append((value, label))
+ 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:
- raise ValueError("Cannot add options to non-list type field.")
-
- def setTrue(self):
- if self.type == 'boolean':
- self.value = [True]
+ return valsXML[0].text
+
+ def setAnswer(self, answer):
+ self.setValue(answer)
def setFalse(self):
- if self.type == 'boolean':
- self.value = [False]
+ self.setValue(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
+ def setOptions(self, options):
+ for value in options:
+ if isinstance(value, dict):
+ self.addOption(**value)
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]
+ 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:
- 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
+ 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:
- 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
+ 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 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
-
+ 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)
diff --git a/sleekxmpp/plugins/xep_0009.py b/sleekxmpp/plugins/xep_0009.py
index 49ffac41..625b03fb 100644
--- a/sleekxmpp/plugins/xep_0009.py
+++ b/sleekxmpp/plugins/xep_0009.py
@@ -178,9 +178,12 @@ class xep_0009(base.base_plugin):
def plugin_init(self):
self.xep = '0009'
self.description = 'Jabber-RPC'
- self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>", self._callMethod)
- self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>", self._callResult)
- self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>", self._callError)
+ self.xmpp.add_handler("<iq type='set'><query xmlns='jabber:iq:rpc' /></iq>",
+ self._callMethod, name='Jabber RPC Call')
+ self.xmpp.add_handler("<iq type='result'><query xmlns='jabber:iq:rpc' /></iq>",
+ self._callResult, name='Jabber RPC Result')
+ self.xmpp.add_handler("<iq type='error'><query xmlns='jabber:iq:rpc' /></iq>",
+ self._callError, name='Jabber RPC Error')
self.entries = {}
self.activeCalls = []
diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py
index 6a31d243..a9d8d6a7 100644
--- a/sleekxmpp/plugins/xep_0030.py
+++ b/sleekxmpp/plugins/xep_0030.py
@@ -3,14 +3,14 @@
Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
This file is part of SleekXMPP.
- See the file license.txt for copying permissio
+ 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 ElementBase, ET, JID
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from .. stanza.iq import Iq
class DiscoInfo(ElementBase):
@@ -138,6 +138,9 @@ class DiscoNode(object):
self.info = DiscoInfo()
self.items = DiscoItems()
+ self.info['node'] = name
+ self.items['node'] = name
+
# This is a bit like poor man's inheritance, but
# to simplify adding information to the node we
# map node functions to either the info or items
@@ -201,8 +204,8 @@ class xep_0030(base.base_plugin):
DiscoInfo.namespace)),
self.handle_info_query))
- self.xmpp.stanzaPlugin(Iq, DiscoInfo)
- self.xmpp.stanzaPlugin(Iq, DiscoItems)
+ registerStanzaPlugin(Iq, DiscoInfo)
+ registerStanzaPlugin(Iq, DiscoItems)
self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items)
self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info)
@@ -290,21 +293,21 @@ class xep_0030(base.base_plugin):
# Older interface methods for backwards compatibility
- def getInfo(self, jid, node=''):
+ def getInfo(self, jid, node='', dfrom=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = jid
- iq['from'] = self.xmpp.fulljid
+ iq['from'] = dfrom
iq['disco_info']['node'] = node
- iq.send()
+ return iq.send()
- def getItems(self, jid, node=''):
+ def getItems(self, jid, node='', dfrom=None):
iq = self.xmpp.Iq()
iq['type'] = 'get'
iq['to'] = jid
- iq['from'] = self.xmpp.fulljid
+ iq['from'] = dfrom
iq['disco_items']['node'] = node
- iq.send()
+ return iq.send()
def add_feature(self, feature, node='main'):
self.add_node(node)
diff --git a/sleekxmpp/plugins/xep_0033.py b/sleekxmpp/plugins/xep_0033.py
new file mode 100644
index 00000000..ea0b10b7
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0033.py
@@ -0,0 +1,161 @@
+"""
+ 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_0030(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)
diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py
index 937c6f96..1892eeab 100644
--- a/sleekxmpp/plugins/xep_0045.py
+++ b/sleekxmpp/plugins/xep_0045.py
@@ -1,27 +1,15 @@
"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2007 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ 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 ElementBase, JID
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, JID
from .. stanza.presence import Presence
from .. xmlstream.handler.callback import Callback
from .. xmlstream.matcher.xpath import MatchXPath
@@ -125,7 +113,7 @@ class xep_0045(base.base_plugin):
self.xep = '0045'
self.description = 'Multi User Chat'
# load MUC support in presence stanzas
- self.xmpp.stanzaPlugin(Presence, MUCPresence)
+ registerStanzaPlugin(Presence, MUCPresence)
self.xmpp.registerHandler(Callback('MUCPresence', MatchXMLMask("<presence xmlns='%s' />" % self.xmpp.default_ns), self.handle_groupchat_presence))
self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("<message xmlns='%s' type='groupchat'><body/></message>" % self.xmpp.default_ns), self.handle_groupchat_message))
@@ -134,7 +122,7 @@ class xep_0045(base.base_plugin):
"""
if pr['muc']['room'] not in self.rooms.keys():
return
- entry = pr['muc'].getValues()
+ entry = pr['muc'].getStanzaValues()
if pr['type'] == 'unavailable':
del self.rooms[entry['room']][entry['nick']]
else:
@@ -166,13 +154,13 @@ class xep_0045(base.base_plugin):
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['xep_0004'].buildForm(xform)
+ 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['xep_0004'].makeForm(ftype='submit')
+ #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
@@ -274,7 +262,7 @@ class xep_0045(base.base_plugin):
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)
+ return self.xmpp.plugin['old_0004'].buildForm(form)
def cancelConfig(self, room):
query = ET.Element('{http://jabber.org/protocol/muc#owner}query')
diff --git a/sleekxmpp/plugins/xep_0050.py b/sleekxmpp/plugins/xep_0050.py
index 2f356e17..5efb9116 100644
--- a/sleekxmpp/plugins/xep_0050.py
+++ b/sleekxmpp/plugins/xep_0050.py
@@ -1,27 +1,14 @@
"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2007 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ 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 traceback
import time
class xep_0050(base.base_plugin):
@@ -32,11 +19,11 @@ class xep_0050(base.base_plugin):
def plugin_init(self):
self.xep = '0050'
self.description = 'Ad-Hoc Commands'
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command)
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command)
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, threaded=True)
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel)
- self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete)
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='__None__'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None')
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='execute'/></iq>" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute')
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='next'/></iq>" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True)
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='cancel'/></iq>" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel')
+ self.xmpp.add_handler("<iq type='set' xmlns='%s'><command xmlns='http://jabber.org/protocol/commands' action='complete'/></iq>" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete')
self.commands = {}
self.sessions = {}
self.sd = self.xmpp.plugin['xep_0030']
@@ -83,7 +70,7 @@ class xep_0050(base.base_plugin):
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['xep_0004'].makeForm('result')
+ 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=[]))
@@ -94,7 +81,7 @@ class xep_0050(base.base_plugin):
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['xep_0004'].makeForm('result')
+ 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
diff --git a/sleekxmpp/plugins/xep_0060.py b/sleekxmpp/plugins/xep_0060.py
index bff158a0..a92a3844 100644
--- a/sleekxmpp/plugins/xep_0060.py
+++ b/sleekxmpp/plugins/xep_0060.py
@@ -2,7 +2,7 @@ from __future__ import with_statement
from . import base
import logging
#from xml.etree import cElementTree as ET
-from .. xmlstream.stanzabase import ElementBase, ET
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET
from . import stanza_pubsub
class xep_0060(base.base_plugin):
diff --git a/sleekxmpp/plugins/xep_0078.py b/sleekxmpp/plugins/xep_0078.py
index f8732905..4b3ab829 100644
--- a/sleekxmpp/plugins/xep_0078.py
+++ b/sleekxmpp/plugins/xep_0078.py
@@ -1,21 +1,9 @@
"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2007 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ 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
diff --git a/sleekxmpp/plugins/xep_0085.py b/sleekxmpp/plugins/xep_0085.py
new file mode 100644
index 00000000..b7b5d6dd
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0085.py
@@ -0,0 +1,101 @@
+"""
+ 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 permissio
+"""
+
+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 ChatState(ElementBase):
+ namespace = 'http://jabber.org/protocol/chatstates'
+ plugin_attrib = 'chat_state'
+ interface = set(('state',))
+ states = set(('active', 'composing', 'gone', 'inactive', 'paused'))
+
+ def active(self):
+ self.setState('active')
+
+ def composing(self):
+ self.setState('composing')
+
+ def gone(self):
+ self.setState('gone')
+
+ def inactive(self):
+ self.setState('inactive')
+
+ def paused(self):
+ self.setState('paused')
+
+ def setState(self, state):
+ if state in self.states:
+ self.name = state
+ self.xml.tag = '{%s}%s' % (self.namespace, state)
+ else:
+ raise ValueError('Invalid chat state')
+
+ def getState(self):
+ return self.name
+
+# In order to match the various chat state elements,
+# we need one stanza object per state, even though
+# they are all the same except for the initial name
+# value. Do not depend on the type of the chat state
+# stanza object for the actual state.
+
+class Active(ChatState):
+ name = 'active'
+class Composing(ChatState):
+ name = 'composing'
+class Gone(ChatState):
+ name = 'gone'
+class Inactive(ChatState):
+ name = 'inactive'
+class Paused(ChatState):
+ name = 'paused'
+
+
+class xep_0085(base.base_plugin):
+ """
+ XEP-0085 Chat State Notifications
+ """
+
+ def plugin_init(self):
+ self.xep = '0085'
+ self.description = 'Chat State Notifications'
+
+ handlers = [('Active Chat State', 'active'),
+ ('Composing Chat State', 'composing'),
+ ('Gone Chat State', 'gone'),
+ ('Inactive Chat State', 'inactive'),
+ ('Paused Chat State', 'paused')]
+ for handler in handlers:
+ self.xmpp.registerHandler(
+ Callback(handler[0],
+ MatchXPath("{%s}message/{%s}%s" % (self.xmpp.default_ns,
+ ChatState.namespace,
+ handler[1])),
+ self._handleChatState))
+
+ registerStanzaPlugin(Message, Active)
+ registerStanzaPlugin(Message, Composing)
+ registerStanzaPlugin(Message, Gone)
+ registerStanzaPlugin(Message, Inactive)
+ registerStanzaPlugin(Message, Paused)
+
+ def post_init(self):
+ base.base_plugin.post_init(self)
+ self.xmpp.plugin['xep_0030'].add_feature('http://jabber.org/protocol/chatstates')
+
+ def _handleChatState(self, msg):
+ state = msg['chat_state'].name
+ logging.debug("Chat State: %s, %s" % (state, msg['from'].jid))
+ self.xmpp.event('chatstate_%s' % state, msg)
diff --git a/sleekxmpp/plugins/xep_0092.py b/sleekxmpp/plugins/xep_0092.py
index aeebbe0c..ca02c4a8 100644
--- a/sleekxmpp/plugins/xep_0092.py
+++ b/sleekxmpp/plugins/xep_0092.py
@@ -1,21 +1,9 @@
"""
- SleekXMPP: The Sleek XMPP Library
- Copyright (C) 2007 Nathanael C. Fritz
- This file is part of SleekXMPP.
-
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
"""
from xml.etree import cElementTree as ET
from . import base
@@ -30,7 +18,7 @@ class xep_0092(base.base_plugin):
self.xep = "0092"
self.name = self.config.get('name', 'SleekXMPP')
self.version = self.config.get('version', '0.1-dev')
- self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version)
+ self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version, name='Sofware Version')
def post_init(self):
base.base_plugin.post_init(self)
diff --git a/sleekxmpp/plugins/xep_0128.py b/sleekxmpp/plugins/xep_0128.py
new file mode 100644
index 00000000..824977b6
--- /dev/null
+++ b/sleekxmpp/plugins/xep_0128.py
@@ -0,0 +1,51 @@
+"""
+ 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
+from . xep_0030 import DiscoInfo, DiscoItems
+from . xep_0004 import Form
+
+
+class xep_0128(base.base_plugin):
+ """
+ XEP-0128 Service Discovery Extensions
+ """
+
+ def plugin_init(self):
+ self.xep = '0128'
+ self.description = 'Service Discovery Extensions'
+
+ registerStanzaPlugin(DiscoInfo, Form)
+ registerStanzaPlugin(DiscoItems, Form)
+
+ def extend_info(self, node, data=None):
+ if data is None:
+ data = {}
+ node = self.xmpp['xep_0030'].nodes.get(node, None)
+ if node is None:
+ self.xmpp['xep_0030'].add_node(node)
+
+ info = node.info
+ info['form']['type'] = 'result'
+ info['form'].setFields(data, default=None)
+
+ def extend_items(self, node, data=None):
+ if data is None:
+ data = {}
+ node = self.xmpp['xep_0030'].nodes.get(node, None)
+ if node is None:
+ self.xmpp['xep_0030'].add_node(node)
+
+ items = node.items
+ items['form']['type'] = 'result'
+ items['form'].setFields(data, default=None)
diff --git a/sleekxmpp/plugins/xep_0199.py b/sleekxmpp/plugins/xep_0199.py
index ccaf0b3a..3fc62f55 100644
--- a/sleekxmpp/plugins/xep_0199.py
+++ b/sleekxmpp/plugins/xep_0199.py
@@ -1,22 +1,9 @@
"""
- SleekXMPP: The Sleek XMPP Library
- XEP-0199 (Ping) support
- Copyright (C) 2007 Kevin Smith
- This file is part of SleekXMPP.
-
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
+
+ See the file LICENSE for copying permission.
"""
from xml.etree import cElementTree as ET
from . import base
@@ -29,7 +16,7 @@ class xep_0199(base.base_plugin):
def plugin_init(self):
self.description = "XMPP Ping"
self.xep = "0199"
- self.xmpp.add_handler("<iq type='get' xmlns='%s'><ping xmlns='http://www.xmpp.org/extensions/xep-0199.html#ns'/></iq>" % self.xmpp.default_ns, self.handler_ping)
+ self.xmpp.add_handler("<iq type='get' xmlns='%s'><ping xmlns='http://www.xmpp.org/extensions/xep-0199.html#ns'/></iq>" % self.xmpp.default_ns, self.handler_ping, name='XMPP Ping')
self.running = False
#if self.config.get('keepalive', True):
#self.xmpp.add_event_handler('session_start', self.handler_pingserver, threaded=True)
diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py
index c3d8a318..8302c43d 100644
--- a/sleekxmpp/stanza/__init__.py
+++ b/sleekxmpp/stanza/__init__.py
@@ -3,6 +3,11 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-__all__ = ['presence']
+
+
+from sleekxmpp.stanza.error import Error
+from sleekxmpp.stanza.iq import Iq
+from sleekxmpp.stanza.message import Message
+from sleekxmpp.stanza.presence import Presence
diff --git a/sleekxmpp/stanza/atom.py b/sleekxmpp/stanza/atom.py
index 5e82cb98..9df85a2b 100644
--- a/sleekxmpp/stanza/atom.py
+++ b/sleekxmpp/stanza/atom.py
@@ -1,4 +1,4 @@
-from .. xmlstream.stanzabase import ElementBase, ET, JID
+from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID
from xml.etree import cElementTree as ET
class AtomEntry(ElementBase):
diff --git a/sleekxmpp/stanza/error.py b/sleekxmpp/stanza/error.py
index ee46722a..6d18c297 100644
--- a/sleekxmpp/stanza/error.py
+++ b/sleekxmpp/stanza/error.py
@@ -3,60 +3,131 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-from .. xmlstream.stanzabase import ElementBase, ET
+
+from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin
+from sleekxmpp.xmlstream.stanzabase import ElementBase, ET
+
class Error(ElementBase):
- namespace = 'jabber:client'
- name = 'error'
- plugin_attrib = 'error'
- conditions = set(('bad-request', 'conflict', 'feature-not-implemented', 'forbidden', 'gone', 'internal-server-error', 'item-not-found', 'jid-malformed', 'not-acceptable', 'not-allowed', 'not-authorized', 'payment-required', 'recipient-unavailable', 'redirect', 'registration-required', 'remote-server-not-found', 'remote-server-timeout', 'resource-constraint', 'service-unavailable', 'subscription-required', 'undefined-condition', 'unexpected-request'))
- interfaces = set(('code', 'condition', 'text', 'type'))
- types = set(('cancel', 'continue', 'modify', 'auth', 'wait'))
- sub_interfaces = set(('text',))
- condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas'
-
- def setup(self, xml=None):
- if ElementBase.setup(self, xml): #if we had to generate xml
- self['type'] = 'cancel'
- self['condition'] = 'feature-not-implemented'
- if self.parent is not None:
- self.parent()['type'] = 'error'
-
- def getCondition(self):
- for child in self.xml.getchildren():
- if "{%s}" % self.condition_ns in child.tag:
- return child.tag.split('}', 1)[-1]
- return ''
-
- def setCondition(self, value):
- if value in self.conditions:
- for child in self.xml.getchildren():
- if "{%s}" % self.condition_ns in child.tag:
- self.xml.remove(child)
- condition = ET.Element("{%s}%s" % (self.condition_ns, value))
- self.xml.append(condition)
- return self
-
- def delCondition(self):
- return self
-
- def getText(self):
- text = ''
- textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text")
- if textxml is not None:
- text = textxml.text
- return text
-
- def setText(self, value):
- self.delText()
- textxml = ET.Element('{urn:ietf:params:xml:ns:xmpp-stanzas}text')
- textxml.text = value
- self.xml.append(textxml)
- return self
-
- def delText(self):
- textxml = self.xml.find("{urn:ietf:params:xml:ns:xmpp-stanzas}text")
- if textxml is not None:
- self.xml.remove(textxml)
+
+ """
+ XMPP stanzas of type 'error' should include an <error> 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:
+ <error type="cancel" code="404">
+ <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ The item was not found.
+ </text>
+ </error>
+
+ 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.
+ getCondition -- Retrieve the name of the condition element.
+ setCondition -- Add a condition element.
+ delCondition -- Remove the condition element.
+ getText -- Retrieve the contents of the <text> element.
+ setText -- Set the contents of the <text> element.
+ delText -- Remove the <text> 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 getCondition(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 setCondition(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 delCondition(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 getText(self):
+ """Retrieve the contents of the <text> element."""
+ return self._getSubText('{%s}text' % self.condition_ns)
+
+ def setText(self, value):
+ """
+ Set the contents of the <text> element.
+
+ Arguments:
+ value -- The new contents for the <text> element.
+ """
+ self._setSubText('{%s}text' % self.condition_ns, text=value)
+ return self
+
+ def delText(self):
+ """Remove the <text> element."""
+ self._delSub('{%s}text' % self.condition_ns)
+ return self
diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py
index 60686e4a..c2f2f0c8 100644
--- a/sleekxmpp/stanza/htmlim.py
+++ b/sleekxmpp/stanza/htmlim.py
@@ -3,33 +3,78 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-from .. xmlstream.stanzabase import ElementBase, ET
+
+from sleekxmpp.stanza import Message
+from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin
+from sleekxmpp.xmlstream.stanzabase import ElementBase, ET
+
class HTMLIM(ElementBase):
- namespace = 'http://jabber.org/protocol/xhtml-im'
- name = 'html'
- plugin_attrib = 'html'
- interfaces = set(('html',))
- plugin_attrib_map = set()
- plugin_xml_map = set()
-
- def setHtml(self, html):
- 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 getHtml(self):
- html = self.xml.find('{http://www.w3.org/1999/xhtml}body')
- if html is None: return ''
- return html
-
- def delHtml(self):
- if self.parent is not None:
- self.parent().xml.remove(self.xml)
+
+ """
+ XEP-0071: XHTML-IM defines a method for embedding XHTML content
+ within a <message> 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:
+ <message to="user@example.com">
+ <body>Non-html message content.</body>
+ <html xmlns="http://jabber.org/protocol/xhtml-im">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p><b>HTML!</b></p>
+ </body>
+ </html>
+ </message>
+
+ Stanza Interface:
+ body -- The contents of the HTML body tag.
+
+ Methods:
+ getBody -- Return the HTML body contents.
+ setBody -- Set the HTML body contents.
+ delBody -- Remove the HTML body contents.
+ """
+
+ namespace = 'http://jabber.org/protocol/xhtml-im'
+ name = 'html'
+ interfaces = set(('body',))
+ plugin_attrib = name
+
+ def setBody(self, html):
+ """
+ Set the contents of the HTML body.
+
+ Arguments:
+ html -- Either a string or XML object. If the top level
+ element is not <body> 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 getBody(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 delBody(self):
+ """Remove the HTML body contents."""
+ if self.parent is not None:
+ self.parent().xml.remove(self.xml)
+
+
+registerStanzaPlugin(Message, HTMLIM)
diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py
index ded7515f..c5ef8bb4 100644
--- a/sleekxmpp/stanza/iq.py
+++ b/sleekxmpp/stanza/iq.py
@@ -3,75 +3,175 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-from .. xmlstream.stanzabase import StanzaBase
-from xml.etree import cElementTree as ET
-from . error import Error
-from .. xmlstream.handler.waiter import Waiter
-from .. xmlstream.matcher.id import MatcherId
-from . rootstanza import RootStanza
+
+from sleekxmpp.stanza import Error
+from sleekxmpp.stanza.rootstanza import RootStanza
+from sleekxmpp.xmlstream import RESPONSE_TIMEOUT
+from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
+from sleekxmpp.xmlstream.handler import Waiter
+from sleekxmpp.xmlstream.matcher import MatcherId
+
class Iq(RootStanza):
- interfaces = set(('type', 'to', 'from', 'id','query'))
- types = set(('get', 'result', 'set', 'error'))
- name = 'iq'
- plugin_attrib = name
- namespace = 'jabber:client'
-
- def __init__(self, *args, **kwargs):
- StanzaBase.__init__(self, *args, **kwargs)
- if self['id'] == '':
- if self.stream is not None:
- self['id'] = self.stream.getNewId()
- else:
- self['id'] = '0'
-
- def unhandled(self):
- 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 setPayload(self, value):
- self.clear()
- StanzaBase.setPayload(self, value)
- return self
-
- def setQuery(self, value):
- 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 getQuery(self):
- 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 reply(self):
- self['type'] = 'result'
- StanzaBase.reply(self)
- return self
-
- def delQuery(self):
- for child in self.getchildren():
- if child.tag.endswith('query'):
- self.xml.remove(child)
- return self
-
- def send(self, block=True, timeout=10):
- if block and self['type'] in ('get', 'set'):
- waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
- self.stream.registerHandler(waitfor)
- StanzaBase.send(self)
- return waitfor.wait(timeout)
- else:
- return StanzaBase.send(self)
+
+ """
+ XMPP <iq> stanzas, or info/query stanzas, are XMPP's method of
+ requesting and modifying information, similar to HTTP's GET and
+ POST methods.
+
+ Each <iq> stanza must have an 'id' value which associates the
+ stanza with the response stanza. XMPP entities must always
+ be given a response <iq> stanza with a type of 'result' after
+ sending a stanza of type 'get' or 'set'.
+
+ Most uses cases for <iq> stanzas will involve adding a <query>
+ element whose namespace indicates the type of information
+ desired. However, some custom XMPP applications use <iq> stanzas
+ as a carrier stanza for an application-specific protocol instead.
+
+ Example <iq> Stanzas:
+ <iq to="user@example.com" type="get" id="314">
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+
+ <iq to="user@localhost" type="result" id="17">
+ <query xmlns='jabber:iq:roster'>
+ <item jid='otheruser@example.net'
+ name='John Doe'
+ subscription='both'>
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>
+
+ Stanza Interface:
+ query -- The namespace of the <query> 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.
+ setPayload -- Overrides StanzaBase.setPayload.
+ setQuery -- Add or modify a <query> element.
+ getQuery -- Return the namespace of the <query> element.
+ delQuery -- Remove the <query> 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 <iq> 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.getNewId()
+ 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 setPayload(self, value):
+ """
+ Set the XML contents of the <iq> stanza.
+
+ Arguments:
+ value -- An XML object to use as the <iq> stanza's contents
+ """
+ self.clear()
+ StanzaBase.setPayload(self, value)
+ return self
+
+ def setQuery(self, value):
+ """
+ Add or modify a <query> element.
+
+ Query elements are differentiated by their namespace.
+
+ Arguments:
+ value -- The namespace of the <query> 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 getQuery(self):
+ """Return the namespace of the <query> 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 delQuery(self):
+ """Remove the <query> element."""
+ for child in self.xml.getchildren():
+ if child.tag.endswith('query'):
+ self.xml.remove(child)
+ return self
+
+ def reply(self):
+ """
+ Send a reply <iq> stanza.
+
+ Overrides StanzaBase.reply
+
+ Sets the 'type' to 'result' in addition to the default
+ StanzaBase.reply behavior.
+ """
+ self['type'] = 'result'
+ StanzaBase.reply(self)
+ return self
+
+ def send(self, block=True, timeout=RESPONSE_TIMEOUT):
+ """
+ Send an <iq> 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.
+
+ 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
+ """
+ if block and self['type'] in ('get', 'set'):
+ waitfor = Waiter('IqWait_%s' % self['id'], MatcherId(self['id']))
+ self.stream.registerHandler(waitfor)
+ StanzaBase.send(self)
+ return waitfor.wait(timeout)
+ else:
+ return StanzaBase.send(self)
diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py
index 38341809..560e1d47 100644
--- a/sleekxmpp/stanza/message.py
+++ b/sleekxmpp/stanza/message.py
@@ -3,61 +3,141 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-from .. xmlstream.stanzabase import StanzaBase
-from xml.etree import cElementTree as ET
-from . error import Error
-from . rootstanza import RootStanza
+
+from sleekxmpp.stanza import Error
+from sleekxmpp.stanza.rootstanza import RootStanza
+from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
+
class Message(RootStanza):
- interfaces = set(('type', 'to', 'from', 'id', 'body', 'subject', 'mucroom', 'mucnick'))
- types = set((None, 'normal', 'chat', 'headline', 'error', 'groupchat'))
- sub_interfaces = set(('body', 'subject'))
- name = 'message'
- plugin_attrib = name
- namespace = 'jabber:client'
-
- def getType(self):
- return self.xml.attrib.get('type', 'normal')
-
- def chat(self):
- self['type'] = 'chat'
- return self
-
- def normal(self):
- self['type'] = 'normal'
- return self
-
- def reply(self, body=None):
- StanzaBase.reply(self)
- if self['type'] == 'groupchat':
- self['to'] = self['to'].bare
- del self['id']
- if body is not None:
- self['body'] = body
- return self
-
- def getMucroom(self):
- if self['type'] == 'groupchat':
- return self['from'].bare
- else:
- return ''
-
- def setMucroom(self, value):
- pass
-
- def delMucroom(self):
- pass
-
- def getMucnick(self):
- if self['type'] == 'groupchat':
- return self['from'].resource
- else:
- return ''
-
- def setMucnick(self, value):
- pass
-
- def delMucnick(self):
- pass
+
+ """
+ XMPP's <message> stanzas are a "push" mechanism to send information
+ to other XMPP entities without requiring a response.
+
+ Chat clients will typically use <message> 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 <message> stanzas:
+ <message to="user1@example.com" from="user2@example.com">
+ <body>Hi!</body>
+ </message>
+
+ <message type="groupchat" to="room@conference.example.com">
+ <body>Hi everyone!</body>
+ </message>
+
+ 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:
+ chat -- Set the message type to 'chat'.
+ normal -- Set the message type to 'normal'.
+ reply -- Overrides StanzaBase.reply
+ getType -- Overrides StanzaBase interface
+ getMucroom -- Return the name of the MUC room of the message.
+ setMucroom -- Dummy method to prevent assignment.
+ delMucroom -- Dummy method to prevent deletion.
+ getMucnick -- Return the MUC nickname of the message's sender.
+ setMucnick -- Dummy method to prevent assignment.
+ delMucnick -- 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 getType(self):
+ """
+ Return the message type.
+
+ Overrides default stanza interface behavior.
+
+ Returns 'normal' if no type attribute is present.
+ """
+ return self._getAttr('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):
+ """
+ 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.
+ """
+ StanzaBase.reply(self)
+ if self['type'] == 'groupchat':
+ self['to'] = self['to'].bare
+
+ del self['id']
+
+ if body is not None:
+ self['body'] = body
+ return self
+
+ def getMucroom(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 getMucnick(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 setMucroom(self, value):
+ """Dummy method to prevent modification."""
+ pass
+
+ def delMucroom(self):
+ """Dummy method to prevent deletion."""
+ pass
+
+ def setMucnick(self, value):
+ """Dummy method to prevent modification."""
+ pass
+
+ def delMucnick(self):
+ """Dummy method to prevent deletion."""
+ pass
diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py
index ac7e3604..de54b307 100644
--- a/sleekxmpp/stanza/nick.py
+++ b/sleekxmpp/stanza/nick.py
@@ -3,24 +3,70 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-from .. xmlstream.stanzabase import ElementBase, ET
+
+from sleekxmpp.stanza import Message, Presence
+from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin
+from sleekxmpp.xmlstream.stanzabase import ElementBase, ET
+
class Nick(ElementBase):
- namespace = 'http://jabber.org/nick/nick'
- name = 'nick'
- plugin_attrib = 'nick'
- interfaces = set(('nick'))
- plugin_attrib_map = set()
- plugin_xml_map = set()
-
- def setNick(self, nick):
- self.xml.text = nick
-
- def getNick(self):
- return self.xml.text
-
- def delNick(self):
- if self.parent is not None:
- self.parent().xml.remove(self.xml)
+
+ """
+ XEP-0172: User Nickname allows the addition of a <nick> element
+ in several stanza types, including <message> and <presence> stanzas.
+
+ The nickname contained in a <nick> should be the global, friendly or
+ informal name chosen by the owner of a bare JID. The <nick> element
+ may be included when establishing communications with new entities,
+ such as normal XMPP users or MUC services.
+
+ The nickname contained in a <nick> element will not necessarily be
+ the same as the nickname used in a MUC.
+
+ Example stanzas:
+ <message to="user@example.com">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ <body>...</body>
+ </message>
+
+ <presence to="otheruser@example.com" type="subscribe">
+ <nick xmlns="http://jabber.org/nick/nick">The User</nick>
+ </presence>
+
+ Stanza Interface:
+ nick -- A global, friendly or informal name chosen by a user.
+
+ Methods:
+ getNick -- Return the nickname in the <nick> element.
+ setNick -- Add a <nick> element with the given nickname.
+ delNick -- Remove the <nick> element.
+ """
+
+ namespace = 'http://jabber.org/nick/nick'
+ name = 'nick'
+ plugin_attrib = name
+ interfaces = set(('nick',))
+
+ def setNick(self, nick):
+ """
+ Add a <nick> element with the given nickname.
+
+ Arguments:
+ nick -- A human readable, informal name.
+ """
+ self.xml.text = nick
+
+ def getNick(self):
+ """Return the nickname in the <nick> element."""
+ return self.xml.text
+
+ def delNick(self):
+ """Remove the <nick> element."""
+ if self.parent is not None:
+ self.parent().xml.remove(self.xml)
+
+
+registerStanzaPlugin(Message, Nick)
+registerStanzaPlugin(Presence, Nick)
diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py
index c66246c9..651bf34d 100644
--- a/sleekxmpp/stanza/presence.py
+++ b/sleekxmpp/stanza/presence.py
@@ -3,61 +3,144 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-from .. xmlstream.stanzabase import StanzaBase
-from xml.etree import cElementTree as ET
-from . error import Error
-from . rootstanza import RootStanza
+
+from sleekxmpp.stanza import Error
+from sleekxmpp.stanza.rootstanza import RootStanza
+from sleekxmpp.xmlstream.stanzabase import StanzaBase, ET
+
class Presence(RootStanza):
- interfaces = set(('type', 'to', 'from', 'id', 'status', 'priority'))
- types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', 'subscribed', 'unsubscribe', 'unsubscribed'))
- showtypes = set(('dnd', 'chat', 'xa', 'away'))
- sub_interfaces = set(('status', 'priority'))
- name = 'presence'
- plugin_attrib = name
- namespace = 'jabber:client'
-
- def getShowElement(self):
- return self.xml.find("{%s}show" % self.namespace)
-
- def setType(self, value):
- show = self.getShowElement()
- if value in self.types:
- if show is not None:
- self.xml.remove(show)
- if value == 'available':
- value = ''
- self._setAttr('type', value)
- elif value in self.showtypes:
- if show is None:
- show = ET.Element("{%s}show" % self.namespace)
- self.xml.append(show)
- show.text = value
- return self
-
- def setPriority(self, value):
- self._setSubText('priority', text = str(value))
-
- def getPriority(self):
- p = self._getSubText('priority')
- if not p: p = 0
- return int(p)
-
- def getType(self):
- out = self._getAttr('type')
- if not out:
- show = self.getShowElement()
- if show is not None:
- out = show.text
- if not out or out is None:
- out = 'available'
- return out
-
- def reply(self):
- if self['type'] == 'unsubscribe':
- self['type'] = 'unsubscribed'
- elif self['type'] == 'subscribe':
- self['type'] = 'subscribed'
- return StanzaBase.reply(self)
+
+ """
+ XMPP's <presence> 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 <presence>
+ 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 <presence> stanzas are broadcast when an XMPP entity changes
+ its status, the bulk of the traffic in an XMPP network will be from
+ <presence> stanzas. Therefore, do not include more information than
+ necessary in a status message or within a <presence> stanza in order
+ to help keep the network running smoothly.
+
+ Example <presence> stanzas:
+ <presence />
+
+ <presence from="user@example.com">
+ <show>away</show>
+ <status>Getting lunch.</status>
+ <priority>5</priority>
+ </presence>
+
+ <presence type="unavailable" />
+
+ <presence to="user@otherhost.com" type="subscribe" />
+
+ 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:
+ reply -- Overrides StanzaBase.reply
+ setShow -- Set the value of the <show> element.
+ getType -- Get the value of the type attribute or <show> element.
+ setType -- Set the value of the type attribute or <show> element.
+ getPriority -- Get the value of the <priority> element.
+ setPriority -- Set the value of the <priority> 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 setShow(self, show):
+ """
+ Set the value of the <show> element.
+
+ Arguments:
+ show -- Must be one of: away, chat, dnd, or xa.
+ """
+ if show in self.showtypes:
+ self._setSubText('show', text=show)
+ return self
+
+ def setType(self, value):
+ """
+ Set the type attribute's value, and the <show> 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._setAttr('type', value)
+ elif value in self.showtypes:
+ self['show'] = value
+ return self
+
+ def setPriority(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._setSubText('priority', text=str(value))
+
+ def getPriority(self):
+ """
+ Return the value of the <presence> element as an integer.
+ """
+ p = self._getSubText('priority')
+ if not p:
+ p = 0
+ return int(p)
+
+ def getType(self):
+ """
+ Return the value of the <presence> stanza's type attribute, or
+ the value of the <show> element.
+ """
+ out = self._getAttr('type')
+ if not out:
+ out = self['show']
+ if not out or out is None:
+ out = 'available'
+ return out
+
+ def reply(self):
+ """
+ Set the appropriate presence reply type.
+
+ Overrides StanzaBase.reply.
+ """
+ if self['type'] == 'unsubscribe':
+ self['type'] = 'unsubscribed'
+ elif self['type'] == 'subscribe':
+ self['type'] = 'subscribed'
+ return StanzaBase.reply(self)
diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py
index 3b4822d8..eafc79a2 100644
--- a/sleekxmpp/stanza/rootstanza.py
+++ b/sleekxmpp/stanza/rootstanza.py
@@ -3,34 +3,64 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-from .. xmlstream.stanzabase import StanzaBase
-from xml.etree import cElementTree as ET
-from . error import Error
-from .. exceptions import XMPPError
+
+import logging
import traceback
import sys
+from sleekxmpp.exceptions import XMPPError
+from sleekxmpp.stanza import Error
+from sleekxmpp.xmlstream.stanzabase import ET, StanzaBase, registerStanzaPlugin
+
+
class RootStanza(StanzaBase):
- def exception(self, e): #called when a handler raises an exception
- self.reply()
- if isinstance(e, XMPPError): # 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'].xml.append(extxml)
- self['error']['type'] = e.etype
- else: # we probably didn't raise this on purpose, so send back a traceback
- self['error']['condition'] = 'undefined-condition'
- if sys.version_info < (3,0):
- self['error']['text'] = "SleekXMPP got into trouble."
- else:
- self['error']['text'] = traceback.format_tb(e.__traceback__)
- self.send()
-
-# all jabber:client root stanzas should have the error plugin
-RootStanza.plugin_attrib_map['error'] = Error
-RootStanza.plugin_tag_map["{%s}%s" % (Error.namespace, Error.name)] = Error
+ """
+ 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
+ """
+ self.reply()
+ if isinstance(e, XMPPError):
+ # 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
+ else:
+ # We probably didn't raise this on purpose, so send a traceback
+ self['error']['condition'] = 'undefined-condition'
+ if sys.version_info < (3, 0):
+ self['error']['text'] = "SleekXMPP got into trouble."
+ else:
+ self['error']['text'] = traceback.format_tb(e.__traceback__)
+ logging.exception('Error handling {%s}%s stanza' %
+ (self.namespace, self.name))
+ self.send()
+
+
+registerStanzaPlugin(RootStanza, Error)
diff --git a/sleekxmpp/stanza/roster.py b/sleekxmpp/stanza/roster.py
index 1fefc180..292c8956 100644
--- a/sleekxmpp/stanza/roster.py
+++ b/sleekxmpp/stanza/roster.py
@@ -3,51 +3,107 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-from .. xmlstream.stanzabase import ElementBase, ET, JID
-import logging
+
+from sleekxmpp.stanza import Iq
+from sleekxmpp.xmlstream import JID
+from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin
+from sleekxmpp.xmlstream.stanzabase import ET, ElementBase
+
class Roster(ElementBase):
- namespace = 'jabber:iq:roster'
- name = 'query'
- plugin_attrib = 'roster'
- interfaces = set(('items',))
- sub_interfaces = set()
-
- def setItems(self, items):
- self.delItems()
- 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]:
- item.attrib['name'] = items[jid]['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 getItems(self):
- 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 delItems(self):
- for child in self.xml.getchildren():
- self.xml.remove(child)
+
+ """
+ Example roster stanzas:
+ <iq type="set">
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" subscription="both" name="User">
+ <group>Friends</group>
+ </item>
+ </query>
+ </iq>
+
+ Stanza Inteface:
+ items -- A dictionary of roster entries contained
+ in the stanza.
+
+ Methods:
+ getItems -- Return a dictionary of roster entries.
+ setItems -- Add <item> elements.
+ delItems -- Remove all <item> elements.
+ """
+
+ namespace = 'jabber:iq:roster'
+ name = 'query'
+ plugin_attrib = 'roster'
+ interfaces = set(('items',))
+
+ def setItems(self, items):
+ """
+ Set the roster entries in the <roster> 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.delItems()
+ 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 getItems(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 delItems(self):
+ """
+ Remove all <item> elements from the roster stanza.
+ """
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+
+
+registerStanzaPlugin(Iq, Roster)
diff --git a/sleekxmpp/tests/testpubsub.py b/sleekxmpp/tests/testpubsub.py
index ed9dd5c2..24855c90 100755
--- a/sleekxmpp/tests/testpubsub.py
+++ b/sleekxmpp/tests/testpubsub.py
@@ -1,19 +1,9 @@
"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
-
- SleekXMPP is free software; you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation; either version 2 of the License, or
- (at your option) any later version.
-
- SleekXMPP is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with SleekXMPP; if not, write to the Free Software
- Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
+
+ See the file LICENSE for copying permission.
"""
import logging
@@ -34,9 +24,9 @@ class testps(sleekxmpp.ClientXMPP):
self.registerPlugin('xep_0030')
self.registerPlugin('xep_0060')
self.registerPlugin('xep_0092')
- self.add_handler("<message xmlns='jabber:client'><event xmlns='http://jabber.org/protocol/pubsub#event' /></message>", self.pubsubEventHandler, threaded=True)
+ self.add_handler("<message xmlns='jabber:client'><event xmlns='http://jabber.org/protocol/pubsub#event' /></message>", self.pubsubEventHandler, name='Pubsub Event', threaded=True)
self.add_event_handler("session_start", self.start, threaded=True)
- self.add_handler("<iq type='error' />", self.handleError)
+ self.add_handler("<iq type='error' />", self.handleError, name='Iq Error')
self.events = Queue.Queue()
self.default_config = None
self.ps = self.plugin['xep_0060']
diff --git a/sleekxmpp/xmlstream/__init__.py b/sleekxmpp/xmlstream/__init__.py
index e69de29b..c82ab346 100644
--- a/sleekxmpp/xmlstream/__init__.py
+++ b/sleekxmpp/xmlstream/__init__.py
@@ -0,0 +1,11 @@
+"""
+ 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.stanzabase import StanzaBase, ElementBase
+from sleekxmpp.xmlstream.xmlstream import XMLStream, RESPONSE_TIMEOUT
diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py
index f60c5b8e..07b395dc 100644
--- a/sleekxmpp/xmlstream/filesocket.py
+++ b/sleekxmpp/xmlstream/filesocket.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from socket import _fileobject
import socket
diff --git a/sleekxmpp/xmlstream/handler/__init__.py b/sleekxmpp/xmlstream/handler/__init__.py
index e69de29b..50e286a3 100644
--- a/sleekxmpp/xmlstream/handler/__init__.py
+++ b/sleekxmpp/xmlstream/handler/__init__.py
@@ -0,0 +1,12 @@
+"""
+ 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
diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py
index 5d55f4ee..720846d6 100644
--- a/sleekxmpp/xmlstream/handler/base.py
+++ b/sleekxmpp/xmlstream/handler/base.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
class BaseHandler(object):
diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py
index 49cfa14d..889b0aa7 100644
--- a/sleekxmpp/xmlstream/handler/callback.py
+++ b/sleekxmpp/xmlstream/handler/callback.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from . import base
import logging
diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py
index c85a0c46..7c4330a4 100644
--- a/sleekxmpp/xmlstream/handler/waiter.py
+++ b/sleekxmpp/xmlstream/handler/waiter.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from . import base
try:
diff --git a/sleekxmpp/xmlstream/handler/xmlcallback.py b/sleekxmpp/xmlstream/handler/xmlcallback.py
index 632c142b..67879dfe 100644
--- a/sleekxmpp/xmlstream/handler/xmlcallback.py
+++ b/sleekxmpp/xmlstream/handler/xmlcallback.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
import threading
from . callback import Callback
diff --git a/sleekxmpp/xmlstream/handler/xmlwaiter.py b/sleekxmpp/xmlstream/handler/xmlwaiter.py
index 2344403b..cf90751d 100644
--- a/sleekxmpp/xmlstream/handler/xmlwaiter.py
+++ b/sleekxmpp/xmlstream/handler/xmlwaiter.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from . waiter import Waiter
diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py
new file mode 100644
index 00000000..292abd92
--- /dev/null
+++ b/sleekxmpp/xmlstream/jid.py
@@ -0,0 +1,121 @@
+"""
+ 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 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.
+ """
+ self._full = self._jid = str(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:
+ self._resource = self._jid.split('/', 1)[-1]
+ return self._resource
+ 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
+ elif name in ('server', 'domain'):
+ if self._domain is None:
+ self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0]
+ return self._domain
+ elif name == 'full':
+ return self._jid
+ elif name == 'bare':
+ if self._bare is None:
+ self._bare = self._jid.split('/', 1)[0]
+ return self._bare
+
+ 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 == 'server':
+ self.domain = value
+ elif name in ('full', 'jid'):
+ self.reset(value)
+ 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, "_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
diff --git a/sleekxmpp/xmlstream/matcher/__init__.py b/sleekxmpp/xmlstream/matcher/__init__.py
index e69de29b..91cb8d6e 100644
--- a/sleekxmpp/xmlstream/matcher/__init__.py
+++ b/sleekxmpp/xmlstream/matcher/__init__.py
@@ -0,0 +1,13 @@
+"""
+ 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
diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py
index 8185bdc5..51da0942 100644
--- a/sleekxmpp/xmlstream/matcher/base.py
+++ b/sleekxmpp/xmlstream/matcher/base.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
class MatcherBase(object):
diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py
index bb858fc4..43972c23 100644
--- a/sleekxmpp/xmlstream/matcher/id.py
+++ b/sleekxmpp/xmlstream/matcher/id.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from . import base
diff --git a/sleekxmpp/xmlstream/matcher/many.py b/sleekxmpp/xmlstream/matcher/many.py
index cf860e62..ff0c4e4d 100644
--- a/sleekxmpp/xmlstream/matcher/many.py
+++ b/sleekxmpp/xmlstream/matcher/many.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from . import base
from xml.etree import cElementTree
diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py
index bd091c0e..e315445d 100644
--- a/sleekxmpp/xmlstream/matcher/stanzapath.py
+++ b/sleekxmpp/xmlstream/matcher/stanzapath.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from . import base
from xml.etree import cElementTree
diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py
index eba3e954..89fd6422 100644
--- a/sleekxmpp/xmlstream/matcher/xmlmask.py
+++ b/sleekxmpp/xmlstream/matcher/xmlmask.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from . import base
from xml.etree import cElementTree
diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py
index f6d04243..7f3d20be 100644
--- a/sleekxmpp/xmlstream/matcher/xpath.py
+++ b/sleekxmpp/xmlstream/matcher/xpath.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from . import base
from xml.etree import cElementTree
diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py
index 64020c8f..83a8ddfc 100644
--- a/sleekxmpp/xmlstream/stanzabase.py
+++ b/sleekxmpp/xmlstream/stanzabase.py
@@ -3,386 +3,516 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
-from xml.etree import cElementTree as ET
+
+import copy
import logging
-import traceback
import sys
import weakref
+from xml.etree import cElementTree as ET
+
+from sleekxmpp.xmlstream import JID
+from sleekxmpp.xmlstream.tostring import tostring
+
+
+# Used to check if an argument is an XML object.
+XML_TYPE = type(ET.Element('xml'))
+
+
+def registerStanzaPlugin(stanza, plugin):
+ """
+ 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.
+ """
+ tag = "{%s}%s" % (plugin.namespace, plugin.name)
+ stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
+ stanza.plugin_tag_map[tag] = plugin
+
+
+class ElementBase(object):
+ 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()
+ plugin_attrib_map = {}
+ plugin_tag_map = {}
+ subitem = None
+
+ 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 = {}
+ self.iterables = []
+ self.idx = 0
+ if parent is None:
+ self.parent = None
+ else:
+ self.parent = weakref.ref(parent)
+
+ 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)
+ if self.subitem is not None:
+ for sub in self.subitem:
+ 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 initPlugin.
+
+ Arguments:
+ attrib -- The stanza interface for the plugin.
+ """
+ return self.initPlugin(attrib)
+
+ def initPlugin(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 getStanzaValues(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.getStanzaValues()
+ if self.iterables:
+ iterables = []
+ for stanza in self.iterables:
+ iterables.append(stanza.getStanzaValues())
+ iterables[-1].update({
+ '__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)
+ })
+ values['substanzas'] = iterables
+ return values
+
+ def setStanzaValues(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.
+ """
+ for interface, value in values.items():
+ if interface == 'substanzas':
+ for subdict in value:
+ if '__childtag__' in subdict:
+ for subclass in self.subitem:
+ child_tag = "{%s}%s" % (subclass.namespace,
+ subclass.name)
+ if subdict['__childtag__'] == child_tag:
+ sub = subclass(parent=self)
+ sub.setStanzaValues(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 self.plugins:
+ self.initPlugin(interface)
+ self.plugins[interface].setStanzaValues(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 getAttrib method
+ (or getFoo 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 getFoo.
+ 3. The contents of the foo subelement, if foo is a sub interface.
+ 4. The value of the foo attribute of the XML object.
+ 5. The plugin named 'foo'
+ 6. 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.title()
+ if hasattr(self, get_method):
+ return getattr(self, get_method)()
+ else:
+ if attrib in self.sub_interfaces:
+ return self._getSubText(attrib)
+ else:
+ return self._getAttr(attrib)
+ elif attrib in self.plugin_attrib_map:
+ if attrib not in self.plugins:
+ self.initPlugin(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 setAttrib method
+ (or setFoo 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 setFoo, if it exists.
+ 3. Set the text of a foo element, if foo is in sub_interfaces.
+ 4. Set the value of a top level XML attribute name foo.
+ 5. Attempt to pass value to a plugin named foo using the plugin's
+ foo interface.
+ 6. 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:
+ if hasattr(self, "set%s" % attrib.title()):
+ getattr(self, "set%s" % attrib.title())(value,)
+ else:
+ if attrib in self.sub_interfaces:
+ return self._setSubText(attrib, text=value)
+ else:
+ self._setAttr(attrib, value)
+ else:
+ self.__delitem__(attrib)
+ elif attrib in self.plugin_attrib_map:
+ if attrib not in self.plugins:
+ self.initPlugin(attrib)
+ self.plugins[attrib][attrib] = value
+ return self
+
+ @property
+ def attrib(self): #backwards compatibility
+ return self
-if sys.version_info < (3,0):
- from . import tostring26 as tostring
-else:
- from . import tostring
-
-xmltester = type(ET.Element('xml'))
-
-class JID(object):
- def __init__(self, jid):
- self.jid = jid
-
- def __getattr__(self, name):
- if name == 'resource':
- return self.jid.split('/', 1)[-1]
- elif name == 'user':
- if '@' in self.jid:
- return self.jid.split('@', 1)[0]
- else:
- return ''
- elif name == 'server':
- return self.jid.split('@', 1)[-1].split('/', 1)[0]
- elif name == 'full':
- return self.jid
- elif name == 'bare':
- return self.jid.split('/', 1)[0]
-
- def __str__(self):
- return self.jid
-
-class ElementBase(tostring.ToString):
- 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()
- plugin_attrib_map = {}
- plugin_tag_map = {}
- subitem = None
-
- def __init__(self, xml=None, parent=None):
- if parent is None:
- self.parent = None
- else:
- self.parent = weakref.ref(parent)
- self.xml = xml
- self.plugins = {}
- self.iterables = []
- self.idx = 0
- if not self.setup(xml):
- for child in self.xml.getchildren():
- if child.tag in self.plugin_tag_map:
- self.plugins[self.plugin_tag_map[child.tag].plugin_attrib] = self.plugin_tag_map[child.tag](xml=child, parent=self)
- if self.subitem is not None:
- for sub in self.subitem:
- if child.tag == "{%s}%s" % (sub.namespace, sub.name):
- self.iterables.append(sub(xml=child, parent=self))
- break
-
-
- @property
- def attrib(self): #backwards compatibility
- return self
-
- def __iter__(self):
- self.idx = 0
- return self
-
- def __bool__(self):
- return True
-
- def __next__(self):
- self.idx += 1
- if self.idx > len(self.iterables):
- self.idx = 0
- raise StopIteration
- return self.iterables[self.idx - 1]
-
- def next(self):
- return self.__next__()
-
- def __len__(self):
- return len(self.iterables)
-
- def append(self, item):
- if not isinstance(item, ElementBase):
- if type(item) == xmltester:
- return self.appendxml(item)
- else:
- raise TypeError
- self.xml.append(item.xml)
- self.iterables.append(item)
- return self
-
- def pop(self, idx=0):
- aff = self.iterables.pop(idx)
- self.xml.remove(aff.xml)
- return aff
-
- def get(self, key, defaultvalue=None):
- value = self[key]
- if value is None or value == '':
- return defaultvalue
- return value
-
- def keys(self):
- out = []
- out += [x for x in self.interfaces]
- out += [x for x in self.plugins]
- if self.iterables:
- out.append('substanzas')
- return tuple(out)
-
- def match(self, matchstring):
- if isinstance(matchstring, str):
- nodes = matchstring.split('/')
- else:
- nodes = matchstring
- tagargs = nodes[0].split('@')
- if tagargs[0] not in (self.plugins, self.plugin_attrib): return False
- founditerable = False
- for iterable in self.iterables:
- if nodes[1:] == []:
- break
- founditerable = iterable.match(nodes[1:])
- if founditerable: break;
- for evals in tagargs[1:]:
- x,y = evals.split('=')
- if self[x] != y: return False
- if not founditerable and len(nodes) > 1:
- next = nodes[1].split('@')[0]
- if next in self.plugins:
- return self.plugins[next].match(nodes[1:])
- else:
- return False
- return True
-
- def find(self, xpath): # for backwards compatiblity, expose elementtree interface
- return self.xml.find(xpath)
-
- def findall(self, xpath):
- return self.xml.findall(xpath)
-
- def setup(self, xml=None):
- if self.xml is None:
- self.xml = xml
- if self.xml is None:
- for ename in self.name.split('/'):
- new = ET.Element("{%(namespace)s}%(name)s" % {'name': self.name, 'namespace': self.namespace})
- if self.xml is None:
- self.xml = new
- else:
- self.xml.append(new)
- if self.parent is not None:
- self.parent().xml.append(self.xml)
- return True #had to generate XML
- else:
- return False
-
- def enable(self, attrib):
- self.initPlugin(attrib)
- return self
-
- def initPlugin(self, attrib):
- if attrib not in self.plugins:
- self.plugins[attrib] = self.plugin_attrib_map[attrib](parent=self)
-
- def __getitem__(self, attrib):
- if attrib == 'substanzas':
- return self.iterables
- elif attrib in self.interfaces:
- if hasattr(self, "get%s" % attrib.title()):
- return getattr(self, "get%s" % attrib.title())()
- else:
- if attrib in self.sub_interfaces:
- return self._getSubText(attrib)
- else:
- return self._getAttr(attrib)
- elif attrib in self.plugin_attrib_map:
- if attrib not in self.plugins: self.initPlugin(attrib)
- return self.plugins[attrib]
- else:
- return ''
-
- def __setitem__(self, attrib, value):
- if attrib in self.interfaces:
- if value is not None:
- if hasattr(self, "set%s" % attrib.title()):
- getattr(self, "set%s" % attrib.title())(value,)
- else:
- if attrib in self.sub_interfaces:
- return self._setSubText(attrib, text=value)
- else:
- self._setAttr(attrib, value)
- else:
- self.__delitem__(attrib)
- elif attrib in self.plugin_attrib_map:
- if attrib not in self.plugins: self.initPlugin(attrib)
- self.initPlugin(attrib)
- self.plugins[attrib][attrib] = value
- return self
-
- def __delitem__(self, attrib):
- if attrib.lower() in self.interfaces:
- if hasattr(self, "del%s" % attrib.title()):
- getattr(self, "del%s" % attrib.title())()
- else:
- if attrib in self.sub_interfaces:
- return self._delSub(attrib)
- else:
- self._delAttr(attrib)
- elif attrib in self.plugin_attrib_map:
- if attrib in self.plugins:
- del self.plugins[attrib]
- return self
-
- def __eq__(self, other):
- if not isinstance(other, ElementBase):
- return False
- values = self.getValues()
- for key in other:
- if key not in values or values[key] != other[key]:
- return False
- return True
-
- def _setAttr(self, name, value):
- if value is None or value == '':
- self.__delitem__(name)
- else:
- self.xml.attrib[name] = value
-
- def _delAttr(self, name):
- if name in self.xml.attrib:
- del self.xml.attrib[name]
-
- def _getAttr(self, name):
- return self.xml.attrib.get(name, '')
-
- def _getSubText(self, name):
- stanza = self.xml.find("{%s}%s" % (self.namespace, name))
- if stanza is None or stanza.text is None:
- return ''
- else:
- return stanza.text
-
- def _setSubText(self, name, attrib={}, text=None):
- if text is None or text == '':
- return self.__delitem__(name)
- stanza = self.xml.find("{%s}%s" % (self.namespace, name))
- if stanza is None:
- #self.xml.append(ET.Element("{%s}%s" % (self.namespace, name), attrib))
- stanza = ET.Element("{%s}%s" % (self.namespace, name))
- self.xml.append(stanza)
- stanza.text = text
- return stanza
-
- def _delSub(self, name):
- for child in self.xml.getchildren():
- if child.tag == "{%s}%s" % (self.namespace, name):
- self.xml.remove(child)
-
- def getValues(self):
- out = {}
- for interface in self.interfaces:
- out[interface] = self[interface]
- for pluginkey in self.plugins:
- out[pluginkey] = self.plugins[pluginkey].getValues()
- if self.iterables:
- iterables = []
- for stanza in self.iterables:
- iterables.append(stanza.getValues())
- iterables[-1].update({'__childtag__': "{%s}%s" % (stanza.namespace, stanza.name)})
- out['substanzas'] = iterables
- return out
-
- def setValues(self, attrib):
- for interface in attrib:
- if interface == 'substanzas':
- for subdict in attrib['substanzas']:
- if '__childtag__' in subdict:
- for subclass in self.subitem:
- if subdict['__childtag__'] == "{%s}%s" % (subclass.namespace, subclass.name):
- sub = subclass(parent=self)
- sub.setValues(subdict)
- self.iterables.append(sub)
- break
- elif interface in self.interfaces:
- self[interface] = attrib[interface]
- elif interface in self.plugin_attrib_map and interface not in self.plugins:
- self.initPlugin(interface)
- if interface in self.plugins:
- self.plugins[interface].setValues(attrib[interface])
- return self
-
- def appendxml(self, xml):
- self.xml.append(xml)
- return self
-
- #def __del__(self): #prevents garbage collection of reference cycle
- # if self.parent is not None:
- # self.parent.xml.remove(self.xml)
+ def __iter__(self):
+ self.idx = 0
+ return self
+
+ def __bool__(self):
+ return True
+
+ def __next__(self):
+ self.idx += 1
+ if self.idx > len(self.iterables):
+ self.idx = 0
+ raise StopIteration
+ return self.iterables[self.idx - 1]
+
+ def next(self):
+ return self.__next__()
+
+ def __len__(self):
+ return len(self.iterables)
+
+ def append(self, item):
+ 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 pop(self, idx=0):
+ aff = self.iterables.pop(idx)
+ self.xml.remove(aff.xml)
+ return aff
+
+ def get(self, key, defaultvalue=None):
+ value = self[key]
+ if value is None or value == '':
+ return defaultvalue
+ return value
+
+ def keys(self):
+ out = []
+ out += [x for x in self.interfaces]
+ out += [x for x in self.plugins]
+ if self.iterables:
+ out.append('substanzas')
+ return tuple(out)
+
+ def match(self, matchstring):
+ if isinstance(matchstring, str):
+ nodes = matchstring.split('/')
+ else:
+ nodes = matchstring
+ tagargs = nodes[0].split('@')
+ if tagargs[0] not in (self.plugins, self.plugin_attrib): return False
+ founditerable = False
+ for iterable in self.iterables:
+ if nodes[1:] == []:
+ break
+ founditerable = iterable.match(nodes[1:])
+ if founditerable: break;
+ for evals in tagargs[1:]:
+ x,y = evals.split('=')
+ if self[x] != y: return False
+ if not founditerable and len(nodes) > 1:
+ next = nodes[1].split('@')[0]
+ if next in self.plugins:
+ return self.plugins[next].match(nodes[1:])
+ else:
+ return False
+ return True
+
+ def find(self, xpath): # for backwards compatiblity, expose elementtree interface
+ return self.xml.find(xpath)
+
+ def findall(self, xpath):
+ return self.xml.findall(xpath)
+
+ def __delitem__(self, attrib):
+ if attrib.lower() in self.interfaces:
+ if hasattr(self, "del%s" % attrib.title()):
+ getattr(self, "del%s" % attrib.title())()
+ else:
+ if attrib in self.sub_interfaces:
+ return self._delSub(attrib)
+ else:
+ self._delAttr(attrib)
+ elif attrib in self.plugin_attrib_map:
+ if attrib in self.plugins:
+ del self.plugins[attrib]
+ return self
+
+ def __eq__(self, other):
+ if not isinstance(other, ElementBase):
+ return False
+ values = self.getStanzaValues()
+ for key in other:
+ if key not in values or values[key] != other[key]:
+ return False
+ return True
+
+ def _setAttr(self, name, value):
+ if value is None or value == '':
+ self.__delitem__(name)
+ else:
+ self.xml.attrib[name] = value
+
+ def _delAttr(self, name):
+ if name in self.xml.attrib:
+ del self.xml.attrib[name]
+
+ def _getAttr(self, name, default=''):
+ return self.xml.attrib.get(name, default)
+
+ def _getSubText(self, name):
+ if '}' not in name:
+ name = "{%s}%s" % (self.namespace, name)
+ stanza = self.xml.find(name)
+ if stanza is None or stanza.text is None:
+ return ''
+ else:
+ return stanza.text
+
+ def _setSubText(self, name, attrib={}, text=None):
+ if '}' not in name:
+ name = "{%s}%s" % (self.namespace, name)
+ if text is None or text == '':
+ return self.__delitem__(name)
+ stanza = self.xml.find(name)
+ if stanza is None:
+ stanza = ET.Element(name)
+ self.xml.append(stanza)
+ stanza.text = text
+ return stanza
+
+ def _delSub(self, name):
+ if '}' not in name:
+ name = "{%s}%s" % (self.namespace, name)
+ for child in self.xml.getchildren():
+ if child.tag == name:
+ self.xml.remove(child)
+
+ def appendxml(self, xml):
+ self.xml.append(xml)
+ return self
+
+ def __copy__(self):
+ return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent)
+
+ def __str__(self):
+ return tostring(self.xml, xmlns='', stanza_ns=self.namespace)
+
+ def __repr__(self):
+ return self.__str__()
+
+#def __del__(self): #prevents garbage collection of reference cycle
+# if self.parent is not None:
+# self.parent.xml.remove(self.xml)
class StanzaBase(ElementBase):
- 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):
- 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 setType(self, value):
- if value in self.types:
- self.xml.attrib['type'] = value
- return self
-
- def getPayload(self):
- return self.xml.getchildren()
-
- def setPayload(self, value):
- self.xml.append(value)
- return self
-
- def delPayload(self):
- self.clear()
- return self
-
- def clear(self):
- for child in self.xml.getchildren():
- self.xml.remove(child)
- for plugin in list(self.plugins.keys()):
- del self.plugins[plugin]
- return self
-
- def reply(self):
- self['from'], self['to'] = self['to'], self['from']
- self.clear()
- return self
-
- def error(self):
- self['type'] = 'error'
- return self
-
- def getTo(self):
- return JID(self._getAttr('to'))
-
- def setTo(self, value):
- return self._setAttr('to', str(value))
-
- def getFrom(self):
- return JID(self._getAttr('from'))
-
- def setFrom(self, value):
- return self._setAttr('from', str(value))
-
- def unhandled(self):
- pass
-
- def exception(self, e):
- logging.error(traceback.format_tb(e))
-
- def send(self):
- self.stream.sendRaw(self.__str__())
+ 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):
+ 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 setType(self, value):
+ if value in self.types:
+ self.xml.attrib['type'] = value
+ return self
+
+ def getPayload(self):
+ return self.xml.getchildren()
+
+ def setPayload(self, value):
+ self.xml.append(value)
+ return self
+
+ def delPayload(self):
+ self.clear()
+ return self
+
+ def clear(self):
+ for child in self.xml.getchildren():
+ self.xml.remove(child)
+ for plugin in list(self.plugins.keys()):
+ del self.plugins[plugin]
+ return self
+
+ def reply(self):
+ # 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']
+ self.clear()
+ return self
+
+ def error(self):
+ self['type'] = 'error'
+ return self
+
+ def getTo(self):
+ return JID(self._getAttr('to'))
+
+ def setTo(self, value):
+ return self._setAttr('to', str(value))
+
+ def getFrom(self):
+ return JID(self._getAttr('from'))
+
+ def setFrom(self, value):
+ return self._setAttr('from', str(value))
+
+ def unhandled(self):
+ pass
+
+ def exception(self, e):
+ logging.exception('Error handling {%s}%s stanza' % (self.namespace, self.name))
+
+ def send(self):
+ self.stream.sendRaw(self.__str__())
+
+ def __copy__(self):
+ return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream)
+ def __str__(self):
+ return tostring(self.xml, xmlns='', stanza_ns=self.namespace, stream=self.stream)
diff --git a/sleekxmpp/xmlstream/statemachine.py b/sleekxmpp/xmlstream/statemachine.py
index fb7d1508..8a1aa22d 100644
--- a/sleekxmpp/xmlstream/statemachine.py
+++ b/sleekxmpp/xmlstream/statemachine.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from __future__ import with_statement
import threading
diff --git a/sleekxmpp/xmlstream/statemanager.py b/sleekxmpp/xmlstream/statemanager.py
new file mode 100644
index 00000000..c7f76e75
--- /dev/null
+++ b/sleekxmpp/xmlstream/statemanager.py
@@ -0,0 +1,139 @@
+"""
+ 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 __future__ import with_statement
+import threading
+
+
+class StateError(Exception):
+ """Raised whenever a state transition was attempted but failed."""
+
+
+class StateManager(object):
+ """
+ At the very core of SleekXMPP there is a need to track various
+ library configuration settings, XML stream features, and the
+ network connection status. The state manager is responsible for
+ tracking this information in a thread-safe manner.
+
+ State 'variables' store the current state of these items as simple
+ string values or booleans. Changing those values must be done
+ according to transitions defined when creating the state variable.
+
+ If a state variable is given a value that is not allowed according
+ to the transition definitions, a StateError is raised. When a
+ valid value is assigned an event is raised named:
+
+ _state_changed_nameofthestatevariable
+
+ The event carries a dictionary containing the previous and the new
+ state values.
+ """
+
+ def __init__(self, event_func=None):
+ """
+ Initialize the state manager. The parameter event_func should be
+ the event() method of a SleekXMPP object in order to enable
+ _state_changed_* events.
+ """
+ self.main_lock = threading.Lock()
+ self.locks = {}
+ self.state_variables = {}
+
+ if event_func is not None:
+ self.event = event_func
+ else:
+ self.event = lambda name, data: None
+
+ def add(self, name, default=False, values=None, transitions=None):
+ """
+ Create a new state variable.
+
+ When transitions is specified, only those defined state change
+ transitions will be allowed.
+
+ When values is specified (and not transitions), any state changes
+ between those values are allowed.
+
+ If neither values nor transitions are defined, then the state variable
+ will be a binary switch between True and False.
+ """
+ if name in self.state_variables:
+ raise IndexError("State variable %s already exists" % name)
+
+ self.locks[name] = threading.Lock()
+ with self.locks[name]:
+ var = {'value': default,
+ 'default': default,
+ 'transitions': {}}
+
+ if transitions is not None:
+ for start in transitions:
+ var['transitions'][start] = set(transitions[start])
+ elif values is not None:
+ values = set(values)
+ for value in values:
+ var['transitions'][value] = values
+ elif values is None:
+ var['transitions'] = {True: [False],
+ False: [True]}
+
+ self.state_variables[name] = var
+
+ def addStates(self, var_defs):
+ """
+ Create multiple state variables at once.
+ """
+ for var, data in var_defs:
+ self.add(var,
+ default=data.get('default', False),
+ values=data.get('values', None),
+ transitions=data.get('transitions', None))
+
+ def force_set(self, name, val):
+ """
+ Force setting a state variable's value by overriding transition checks.
+ """
+ with self.locks[name]:
+ self.state_variables[name]['value'] = val
+
+ def reset(self, name):
+ """
+ Reset a state variable to its default value.
+ """
+ with self.locks[name]:
+ default = self.state_variables[name]['default']
+ self.state_variables[name]['value'] = default
+
+ def __getitem__(self, name):
+ """
+ Get the value of a state variable if it exists.
+ """
+ with self.locks[name]:
+ if name not in self.state_variables:
+ raise IndexError("State variable %s does not exist" % name)
+ return self.state_variables[name]['value']
+
+ def __setitem__(self, name, val):
+ """
+ Attempt to set the value of a state variable, but raise StateError
+ if the transition is undefined.
+
+ A _state_changed_* event is triggered after a successful transition.
+ """
+ with self.locks[name]:
+ if name not in self.state_variables:
+ raise IndexError("State variable %s does not exist" % name)
+ current = self.state_variables[name]['value']
+ if current == val:
+ return
+ if val in self.state_variables[name]['transitions'][current]:
+ self.state_variables[name]['value'] = val
+ self.event('_state_changed_%s' % name, {'from': current, 'to': val})
+ else:
+ raise StateError("Can not transition from '%s' to '%s'" % (str(current), str(val)))
diff --git a/sleekxmpp/xmlstream/tostring/__init__.py b/sleekxmpp/xmlstream/tostring/__init__.py
index 6603cbb8..5852cba2 100644
--- a/sleekxmpp/xmlstream/tostring/__init__.py
+++ b/sleekxmpp/xmlstream/tostring/__init__.py
@@ -1,60 +1,19 @@
+"""
+ SleekXMPP: The Sleek XMPP Library
+ Copyright (C) 2010 Nathanael C. Fritz
+ This file is part of SleekXMPP.
-class ToString(object):
- def __str__(self, xml=None, xmlns='', stringbuffer=''):
- if xml is None:
- xml = self.xml
- newoutput = [stringbuffer]
- #TODO respect ET mapped namespaces
- itag = xml.tag.split('}', 1)[-1]
- if '}' in xml.tag:
- ixmlns = xml.tag.split('}', 1)[0][1:]
- else:
- ixmlns = ''
- nsbuffer = ''
- if xmlns != ixmlns and ixmlns != '' and ixmlns != self.namespace:
- if self.stream is not None and ixmlns in self.stream.namespace_map:
- if self.stream.namespace_map[ixmlns] != '':
- itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag)
- else:
- nsbuffer = """ xmlns="%s\"""" % ixmlns
- if ixmlns not in ('', xmlns, self.namespace):
- nsbuffer = """ xmlns="%s\"""" % ixmlns
- newoutput.append("<%s" % itag)
- newoutput.append(nsbuffer)
- for attrib in xml.attrib:
- if '{' not in attrib:
- newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib])))
- if len(xml) or xml.text or xml.tail:
- newoutput.append(">")
- if xml.text:
- newoutput.append(self.xmlesc(xml.text))
- if len(xml):
- for child in xml.getchildren():
- newoutput.append(self.__str__(child, ixmlns))
- newoutput.append("</%s>" % (itag, ))
- if xml.tail:
- newoutput.append(self.xmlesc(xml.tail))
- elif xml.text:
- newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag))
- else:
- newoutput.append(" />")
- return ''.join(newoutput)
+ See the file LICENSE for copying permission.
+"""
- def xmlesc(self, text):
- text = list(text)
- cc = 0
- matches = ('&', '<', '"', '>', "'")
- for c in text:
- if c in matches:
- if c == '&':
- text[cc] = '&amp;'
- elif c == '<':
- text[cc] = '&lt;'
- elif c == '>':
- text[cc] = '&gt;'
- elif c == "'":
- text[cc] = '&apos;'
- else:
- text[cc] = '&quot;'
- cc += 1
- return ''.join(text)
+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']
diff --git a/sleekxmpp/xmlstream/tostring/tostring.py b/sleekxmpp/xmlstream/tostring/tostring.py
new file mode 100644
index 00000000..c2696321
--- /dev/null
+++ b/sleekxmpp/xmlstream/tostring/tostring.py
@@ -0,0 +1,95 @@
+"""
+ 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 = "%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():
+ if '{' not in attrib:
+ value = xml_escape(value)
+ output.append(' %s="%s"' % (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("</%s>" % tag_name)
+ elif xml.text:
+ # If we only have text content.
+ output.append(">%s</%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 = {'&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ "'": '&apos;',
+ '"': '&quot;'}
+ for i, c in enumerate(text):
+ text[i] = escapes.get(c, c)
+ return ''.join(text)
diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py
new file mode 100644
index 00000000..7a376374
--- /dev/null
+++ b/sleekxmpp/xmlstream/tostring/tostring26.py
@@ -0,0 +1,104 @@
+"""
+ 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 = 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():
+ if '{' not in attrib:
+ value = xml_escape(value)
+ output.append(u' %s="%s"' % (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"</%s>" % tag_name)
+ if xml.tail:
+ # If there is additional text after the element.
+ output.append(xml_escape(xml.tail))
+ elif xml.text:
+ # If we only have text content.
+ output.append(u">%s</%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'&amp;',
+ u'<': u'&lt;',
+ u'>': u'&gt;',
+ u"'": u'&apos;',
+ u'"': u'&quot;'}
+ for i, c in enumerate(text):
+ text[i] = escapes.get(c, c)
+ return u''.join(text)
diff --git a/sleekxmpp/xmlstream/tostring26/__init__.py b/sleekxmpp/xmlstream/tostring26/__init__.py
deleted file mode 100644
index 9711c300..00000000
--- a/sleekxmpp/xmlstream/tostring26/__init__.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import types
-
-class ToString(object):
- def __str__(self, xml=None, xmlns='', stringbuffer=''):
- if xml is None:
- xml = self.xml
- newoutput = [stringbuffer]
- #TODO respect ET mapped namespaces
- itag = xml.tag.split('}', 1)[-1]
- if '}' in xml.tag:
- ixmlns = xml.tag.split('}', 1)[0][1:]
- else:
- ixmlns = ''
- nsbuffer = ''
- if xmlns != ixmlns and ixmlns != u'' and ixmlns != self.namespace:
- if self.stream is not None and ixmlns in self.stream.namespace_map:
- if self.stream.namespace_map[ixmlns] != u'':
- itag = "%s:%s" % (self.stream.namespace_map[ixmlns], itag)
- else:
- nsbuffer = """ xmlns="%s\"""" % ixmlns
- if ixmlns not in ('', xmlns, self.namespace):
- nsbuffer = """ xmlns="%s\"""" % ixmlns
- newoutput.append("<%s" % itag)
- newoutput.append(nsbuffer)
- for attrib in xml.attrib:
- if '{' not in attrib:
- newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib])))
- if len(xml) or xml.text or xml.tail:
- newoutput.append(u">")
- if xml.text:
- newoutput.append(self.xmlesc(xml.text))
- if len(xml):
- for child in xml.getchildren():
- newoutput.append(self.__str__(child, ixmlns))
- newoutput.append(u"</%s>" % (itag, ))
- if xml.tail:
- newoutput.append(self.xmlesc(xml.tail))
- elif xml.text:
- newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag))
- else:
- newoutput.append(" />")
- return u''.join(newoutput)
-
- def xmlesc(self, text):
- if type(text) != types.UnicodeType:
- text = list(unicode(text, 'utf-8', 'ignore'))
- else:
- text = list(text)
-
- cc = 0
- matches = (u'&', u'<', u'"', u'>', u"'")
- for c in text:
- if c in matches:
- if c == u'&':
- text[cc] = u'&amp;'
- elif c == u'<':
- text[cc] = u'&lt;'
- elif c == u'>':
- text[cc] = u'&gt;'
- elif c == u"'":
- text[cc] = u'&apos;'
- else:
- text[cc] = u'&quot;'
- cc += 1
- return ''.join(text)
diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py
index 6b92abca..bf39bb33 100644
--- a/sleekxmpp/xmlstream/xmlstream.py
+++ b/sleekxmpp/xmlstream/xmlstream.py
@@ -3,7 +3,7 @@
Copyright (C) 2010 Nathanael C. Fritz
This file is part of SleekXMPP.
- See the file license.txt for copying permission.
+ See the file LICENSE for copying permission.
"""
from __future__ import with_statement, unicode_literals
@@ -19,11 +19,13 @@ import logging
import socket
import threading
import time
-import traceback
import types
+import copy
import xml.sax.saxutils
from . import scheduler
+from sleekxmpp.xmlstream.tostring import tostring
+RESPONSE_TIMEOUT = 10
HANDLER_THREADS = 1
ssl_support = True
@@ -36,7 +38,7 @@ if sys.version_info < (3, 0):
#monkey patch broken filesocket object
from . import filesocket
#socket._fileobject = filesocket.filesocket
-
+
class RestartStream(Exception):
pass
@@ -71,6 +73,7 @@ class XMLStream(object):
self.use_ssl = False
self.use_tls = False
+ self.default_ns = ''
self.stream_header = "<stream>"
self.stream_footer = "</stream>"
@@ -81,7 +84,7 @@ class XMLStream(object):
self.namespace_map = {}
self.run = True
-
+
def setSocket(self, socket):
"Set the socket"
self.socket = socket
@@ -89,10 +92,10 @@ class XMLStream(object):
self.filesocket = socket.makefile('rb', 0) # ElementTree.iterparse requires a file. 0 buffer files have to be binary
self.state.set('connected', True)
-
+
def setFileSocket(self, filesocket):
self.filesocket = filesocket
-
+
def connect(self, host='', port=0, use_ssl=False, use_tls=True):
"Link to connectTCP"
return self.connectTCP(host, port, use_ssl, use_tls)
@@ -124,7 +127,7 @@ class XMLStream(object):
except socket.error as serr:
logging.error("Could not connect. Socket Error #%s: %s" % (serr.errno, serr.strerror))
time.sleep(1)
-
+
def connectUnix(self, filepath):
"Connect to Unix file and create socket"
@@ -145,7 +148,7 @@ class XMLStream(object):
logging.warning("Tried to enable TLS, but ssl module not found.")
return False
raise RestartStream()
-
+
def process(self, threaded=True):
self.scheduler.process(threaded=True)
for t in range(0, HANDLER_THREADS):
@@ -159,10 +162,10 @@ class XMLStream(object):
self.__thread['process'].start()
else:
self._process()
-
+
def schedule(self, name, seconds, callback, args=None, kwargs=None, repeat=False):
self.scheduler.add(name, seconds, callback, args, kwargs, repeat, qpointer=self.eventqueue)
-
+
def _process(self):
"Start processing the socket."
firstrun = True
@@ -194,14 +197,14 @@ class XMLStream(object):
return
else:
self.state.set('processing', False)
- traceback.print_exc()
+ logging.exception('Socket Error')
self.disconnect(reconnect=True)
except:
if not self.state.reconnect:
return
else:
self.state.set('processing', False)
- traceback.print_exc()
+ logging.exception('Connection error. Reconnecting.')
self.disconnect(reconnect=True)
if self.state['reconnect']:
self.reconnect()
@@ -211,7 +214,7 @@ class XMLStream(object):
#self.__thread['readXML'].start()
#self.__thread['spawnEvents'] = threading.Thread(name='spawnEvents', target=self.__spawnEvents)
#self.__thread['spawnEvents'].start()
-
+
def __readXML(self):
"Parses the incoming stream, adding to xmlin queue as it goes"
#build cElementTree object from expat was we go
@@ -244,7 +247,7 @@ class XMLStream(object):
if event == b'start':
edepth += 1
logging.debug("Ending readXML loop")
-
+
def _sendThread(self):
while self.run:
data = self.sendqueue.get(True)
@@ -257,14 +260,13 @@ class XMLStream(object):
logging.warning("Failed to send %s" % data)
self.state.set('connected', False)
if self.state.reconnect:
- logging.error("Disconnected. Socket Error.")
- traceback.print_exc()
+ logging.exception("Disconnected. Socket Error.")
self.disconnect(reconnect=True)
-
+
def sendRaw(self, data):
self.sendqueue.put(data)
return True
-
+
def disconnect(self, reconnect=False):
self.state.set('reconnect', reconnect)
if self.state['disconnecting']:
@@ -290,41 +292,40 @@ class XMLStream(object):
if self.state['processing']:
#raise CloseStream
pass
-
+
def reconnect(self):
self.state.set('tls',False)
self.state.set('ssl',False)
time.sleep(1)
self.connect()
-
+
def incoming_filter(self, xmlobj):
return xmlobj
-
+
def __spawnEvent(self, xmlobj):
"watching xmlOut and processes handlers"
#convert XML into Stanza
- logging.debug("RECV: %s" % cElementTree.tostring(xmlobj))
+ logging.debug("RECV: %s" % tostring(xmlobj, xmlns=self.default_ns, stream=self))
xmlobj = self.incoming_filter(xmlobj)
- stanza = None
+ stanza_type = StanzaBase
for stanza_class in self.__root_stanza:
if xmlobj.tag == "{%s}%s" % (self.default_ns, stanza_class.name):
- #if self.__root_stanza[stanza_class].match(xmlobj):
- stanza = stanza_class(self, xmlobj)
+ stanza_type = stanza_class
break
- if stanza is None:
- stanza = StanzaBase(self, xmlobj)
unhandled = True
+ stanza = stanza_type(self, xmlobj)
for handler in self.__handlers:
if handler.match(stanza):
- handler.prerun(stanza)
- self.eventqueue.put(('stanza', handler, stanza))
+ stanza_copy = stanza_type(self, copy.deepcopy(xmlobj))
+ handler.prerun(stanza_copy)
+ self.eventqueue.put(('stanza', handler, stanza_copy))
if handler.checkDelete(): self.__handlers.pop(self.__handlers.index(handler))
unhandled = False
if unhandled:
stanza.unhandled()
#loop through handlers and test match
#spawn threads as necessary, call handlers, sending Stanza
-
+
def _eventRunner(self):
logging.debug("Loading event runner")
while self.run:
@@ -344,22 +345,22 @@ class XMLStream(object):
try:
handler.run(args[0])
except Exception as e:
- traceback.print_exc()
+ logging.exception('Error processing event handler: %s' % handler.name)
args[0].exception(e)
elif etype == 'schedule':
try:
logging.debug(args)
handler(*args[0])
except:
- logging.error(traceback.format_exc())
+ logging.exception('Error processing scheduled task')
elif etype == 'quit':
logging.debug("Quitting eventRunner thread")
return False
-
+
def registerHandler(self, handler, before=None, after=None):
"Add handler with matcher class and parameters."
self.__handlers.append(handler)
-
+
def removeHandler(self, name):
"Removes the handler."
idx = 0
@@ -368,81 +369,27 @@ class XMLStream(object):
self.__handlers.pop(idx)
return
idx += 1
-
+
def registerStanza(self, stanza_class):
"Adds stanza. If root stanzas build stanzas sent in events while non-root stanzas build substanza objects."
self.__root_stanza.append(stanza_class)
-
+
def registerStanzaExtension(self, stanza_class, stanza_extension):
if stanza_class not in stanza_extensions:
stanza_extensions[stanza_class] = [stanza_extension]
else:
stanza_extensions[stanza_class].append(stanza_extension)
-
+
def removeStanza(self, stanza_class, root=False):
"Removes the stanza's registration."
if root:
del self.__root_stanza[stanza_class]
else:
del self.__stanza[stanza_class]
-
+
def removeStanzaExtension(self, stanza_class, stanza_extension):
stanza_extension[stanza_class].pop(stanza_extension)
- def tostring(self, xml, xmlns='', stringbuffer=''):
- newoutput = [stringbuffer]
- #TODO respect ET mapped namespaces
- itag = xml.tag.split('}', 1)[-1]
- if '}' in xml.tag:
- ixmlns = xml.tag.split('}', 1)[0][1:]
- else:
- ixmlns = ''
- nsbuffer = ''
- if xmlns != ixmlns and ixmlns != '':
- if ixmlns in self.namespace_map:
- if self.namespace_map[ixmlns] != '':
- itag = "%s:%s" % (self.namespace_map[ixmlns], itag)
- else:
- nsbuffer = """ xmlns="%s\"""" % ixmlns
- newoutput.append("<%s" % itag)
- newoutput.append(nsbuffer)
- for attrib in xml.attrib:
- newoutput.append(""" %s="%s\"""" % (attrib, self.xmlesc(xml.attrib[attrib])))
- if len(xml) or xml.text or xml.tail:
- newoutput.append(">")
- if xml.text:
- newoutput.append(self.xmlesc(xml.text))
- if len(xml):
- for child in xml.getchildren():
- newoutput.append(self.tostring(child, ixmlns))
- newoutput.append("</%s>" % (itag, ))
- if xml.tail:
- newoutput.append(self.xmlesc(xml.tail))
- elif xml.text:
- newoutput.append(">%s</%s>" % (self.xmlesc(xml.text), itag))
- else:
- newoutput.append(" />")
- return ''.join(newoutput)
-
- def xmlesc(self, text):
- text = list(text)
- cc = 0
- matches = ('&', '<', '"', '>', "'")
- for c in text:
- if c in matches:
- if c == '&':
- text[cc] = '&amp;'
- elif c == '<':
- text[cc] = '&lt;'
- elif c == '>':
- text[cc] = '&gt;'
- elif c == "'":
- text[cc] = '&apos;'
- elif self.escape_quotes:
- text[cc] = '&quot;'
- cc += 1
- return ''.join(text)
-
def start_stream_handler(self, xml):
"""Meant to be overridden"""
pass
diff --git a/testall.py b/testall.py
index bf0b4c76..d3d049e6 100644
--- a/testall.py
+++ b/testall.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python2.6
+#!/usr/bin/env python
import unittest
import logging
import sys
@@ -21,7 +21,7 @@ class testoverall(unittest.TestCase):
self.failIf(tabnanny.check("." + os.sep + 'sleekxmpp'))
#raise "Help!"
- def testMethodLength(self):
+ def disabled_testMethodLength(self):
"""Testing for excessive method lengths"""
import re
dirs = os.walk(sys.path[0] + os.sep + 'sleekxmpp')
diff --git a/tests/sleektest.py b/tests/sleektest.py
new file mode 100644
index 00000000..801253d3
--- /dev/null
+++ b/tests/sleektest.py
@@ -0,0 +1,519 @@
+"""
+ 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
+import socket
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+import sleekxmpp
+from sleekxmpp import ClientXMPP
+from sleekxmpp.stanza import Message, Iq, Presence
+from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ET
+from sleekxmpp.xmlstream.tostring import tostring
+
+
+class TestSocket(object):
+
+ """
+ A dummy socket that reads and writes to queues instead
+ of an actual networking socket.
+
+ Methods:
+ nextSent -- Return the next sent stanza.
+ recvData -- 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()
+
+ 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 nextSent(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 recvData(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)
+
+ # ------------------------------------------------------------------
+ # Socket Interface
+
+ def recv(self, *args, **kwargs):
+ """
+ Read a value from the received queue.
+
+ Arguments:
+ Placeholders. Same as for socket.Socket.recv.
+ """
+ return self.read(block=True)
+
+ def send(self, data):
+ """
+ Send data by placing it in the send queue.
+
+ Arguments:
+ data -- String value to write.
+ """
+ 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 timeout is not None:
+ block = True
+ try:
+ return self.recv_queue.get(block, timeout)
+ except:
+ return None
+
+
+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.
+ checkMessage -- Compare a Message stanza against an XML string.
+ checkIq -- Compare an Iq stanza against an XML string.
+ checkPresence -- Compare a Presence stanza against an XML string.
+ streamStart -- Initialize a dummy XMPP client.
+ streamRecv -- Queue data for XMPP client to receive.
+ streamSendMessage -- Check that the XMPP client sent the given
+ Message stanza.
+ streamSendIq -- Check that the XMPP client sent the given
+ Iq stanza.
+ streamSendPresence -- Check taht the XMPP client sent the given
+ Presence stanza.
+ streamClose -- Disconnect the XMPP client.
+ fix_namespaces -- Add top-level namespace to an XML object.
+ compare -- Compare XML objects against each other.
+ """
+
+ # ------------------------------------------------------------------
+ # 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(None, *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(None, *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(None, *args, **kwargs)
+
+ # ------------------------------------------------------------------
+ # Methods for comparing stanza objects to XML strings
+
+ def checkStanza(self, stanza_class, stanza, xml_string,
+ defaults=None, use_values=True):
+ """
+ Create and compare several stanza objects to a correct XML string.
+
+ If use_values is False, test using getStanzaValues() and
+ setStanzaValues() 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_class -- The class of the stanza being tested.
+ stanza -- The stanza object to test.
+ xml_string -- A string version of the correct XML expected.
+ 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 getStanzaValues() and
+ setStanzaValues() should be used. Defaults to
+ True.
+ """
+ xml = ET.fromstring(xml_string)
+
+ # 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 getStanzaValues() and setStanzaValues() 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:
+ defaults = []
+ 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.getStanzaValues()
+ stanza3 = stanza_class()
+ stanza3.setStanzaValues(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)
+
+ def checkMessage(self, msg, xml_string, use_values=True):
+ """
+ Create and compare several message stanza objects to a
+ correct XML string.
+
+ If use_values is False, the test using getStanzaValues() and
+ setStanzaValues() will not be used.
+
+ Arguments:
+ msg -- The Message stanza object to check.
+ xml_string -- The XML contents to compare against.
+ use_values -- Indicates if the test using getStanzaValues
+ and setStanzaValues should be used. Defaults
+ to True.
+ """
+
+ return self.checkStanza(Message, msg, xml_string,
+ defaults=['type'],
+ use_values = use_values)
+
+ def checkIq(self, iq, xml_string, use_values=True):
+ """
+ Create and compare several iq stanza objects to a
+ correct XML string.
+
+ If use_values is False, the test using getStanzaValues() and
+ setStanzaValues() will not be used.
+
+ Arguments:
+ iq -- The Iq stanza object to check.
+ xml_string -- The XML contents to compare against.
+ use_values -- Indicates if the test using getStanzaValues
+ and setStanzaValues should be used. Defaults
+ to True.
+ """
+ return self.checkStanza(Iq, iq, xml_string, use_values=use_values)
+
+ def checkPresence(self, pres, xml_string, use_values=True):
+ """
+ Create and compare several presence stanza objects to a
+ correct XML string.
+
+ If use_values is False, the test using getStanzaValues() and
+ setStanzaValues() will not be used.
+
+ Arguments:
+ iq -- The Iq stanza object to check.
+ xml_string -- The XML contents to compare against.
+ use_values -- Indicates if the test using getStanzaValues
+ and setStanzaValues should be used. Defaults
+ to True.
+ """
+ return self.checkStanza(Presence, pres, xml_string,
+ defaults=['priority'],
+ use_values=use_values)
+
+ # ------------------------------------------------------------------
+ # Methods for simulating stanza streams.
+
+ def streamStart(self, mode='client', skip=True):
+ """
+ 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.
+ """
+ if mode == 'client':
+ self.xmpp = ClientXMPP('tester@localhost', 'test')
+ self.xmpp.setSocket(TestSocket())
+
+ self.xmpp.state.set('reconnect', False)
+ self.xmpp.state.set('is client', True)
+ self.xmpp.state.set('connected', True)
+
+ # Must have the stream header ready for xmpp.process() to work
+ self.xmpp.socket.recvData(self.xmpp.stream_header)
+
+ self.xmpp.connectTCP = lambda a, b, c, d: True
+ self.xmpp.startTLS = lambda: True
+ self.xmpp.process(threaded=True)
+ if skip:
+ # Clear startup stanzas
+ self.xmpp.socket.nextSent(timeout=0.01)
+
+ def streamRecv(self, data):
+ """
+ Pass data to the dummy XMPP client as if it came from an XMPP server.
+
+ Arguments:
+ data -- String stanza XML to be received and processed by the
+ XMPP client or component.
+ """
+ data = str(data)
+ self.xmpp.socket.recvData(data)
+
+ def streamSendMessage(self, data, use_values=True, timeout=.1):
+ """
+ Check that the XMPP client sent the given stanza XML.
+
+ Extracts the next sent stanza and compares it with the given
+ XML using checkMessage.
+
+ Arguments:
+ data -- The XML string of the expected Message stanza,
+ or an equivalent stanza object.
+ use_values -- Modifies the type of tests used by checkMessage.
+ timeout -- Time in seconds to wait for a stanza before
+ failing the check.
+ """
+ if isinstance(data, str):
+ data = self.Message(xml=ET.fromstring(data))
+ sent = self.xmpp.socket.nextSent(timeout)
+ self.checkMessage(data, sent, use_values)
+
+ def streamSendIq(self, data, use_values=True, timeout=.1):
+ """
+ Check that the XMPP client sent the given stanza XML.
+
+ Extracts the next sent stanza and compares it with the given
+ XML using checkIq.
+
+ Arguments:
+ data -- The XML string of the expected Iq stanza,
+ or an equivalent stanza object.
+ use_values -- Modifies the type of tests used by checkIq.
+ timeout -- Time in seconds to wait for a stanza before
+ failing the check.
+ """
+ if isinstance(data, str):
+ data = self.Iq(xml=ET.fromstring(data))
+ sent = self.xmpp.socket.nextSent(timeout)
+ self.checkIq(data, sent, use_values)
+
+ def streamSendPresence(self, data, use_values=True, timeout=.1):
+ """
+ Check that the XMPP client sent the given stanza XML.
+
+ Extracts the next sent stanza and compares it with the given
+ XML using checkPresence.
+
+ Arguments:
+ data -- The XML string of the expected Presence stanza,
+ or an equivalent stanza object.
+ use_values -- Modifies the type of tests used by checkPresence.
+ timeout -- Time in seconds to wait for a stanza before
+ failing the check.
+ """
+ if isinstance(data, str):
+ data = self.Presence(xml=ET.fromstring(data))
+ sent = self.xmpp.socket.nextSent(timeout)
+ self.checkPresence(data, sent, use_values)
+
+ def streamClose(self):
+ """
+ Disconnect the dummy XMPP client.
+
+ Can be safely called even if streamStart 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.disconnect()
+ self.xmpp.socket.recvData(self.xmpp.stream_footer)
+
+ # ------------------------------------------------------------------
+ # 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: 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
+
+ # Everything matches
+ return True
diff --git a/tests/test_addresses.py b/tests/test_addresses.py
new file mode 100644
index 00000000..450e1362
--- /dev/null
+++ b/tests/test_addresses.py
@@ -0,0 +1,111 @@
+from . sleektest import *
+import sleekxmpp.plugins.xep_0033 as xep_0033
+
+
+class TestAddresses(SleekTest):
+
+ def setUp(self):
+ registerStanzaPlugin(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.checkMessage(msg, """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address jid="to@header1.org" type="to" />
+ </addresses>
+ </message>
+ """)
+
+ msg = self.Message()
+ msg['addresses'].addAddress(atype='replyto',
+ jid='replyto@header1.org',
+ desc='Reply address')
+ self.checkMessage(msg, """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address jid="replyto@header1.org" type="replyto" desc="Reply address" />
+ </addresses>
+ </message>
+ """)
+
+ def testAddAddresses(self):
+ """Testing adding multiple extended stanza addresses."""
+
+ xmlstring = """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address jid="replyto@header1.org" type="replyto" desc="Reply address" />
+ <address jid="cc@header2.org" type="cc" />
+ <address jid="bcc@header2.org" type="bcc" />
+ </addresses>
+ </message>
+ """
+
+ 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.checkMessage(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.checkMessage(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.checkMessage(msg, """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address node="foo" jid="to@header1.org" type="to" />
+ </addresses>
+ </message>
+ """)
+
+ addr['uri'] = 'mailto:to@header2.org'
+ self.checkMessage(msg, """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address type="to" uri="mailto:to@header2.org" />
+ </addresses>
+ </message>
+ """)
+
+ def testDelivered(self):
+ """Testing delivered attribute of extended stanza addresses."""
+
+ xmlstring = """
+ <message>
+ <addresses xmlns="http://jabber.org/protocol/address">
+ <address %s jid="to@header1.org" type="to" />
+ </addresses>
+ </message>
+ """
+
+ msg = self.Message()
+ addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to')
+ self.checkMessage(msg, xmlstring % '')
+
+ addr['delivered'] = True
+ self.checkMessage(msg, xmlstring % 'delivered="true"')
+
+ addr['delivered'] = False
+ self.checkMessage(msg, xmlstring % '')
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses)
diff --git a/tests/test_chatstates.py b/tests/test_chatstates.py
new file mode 100644
index 00000000..74359df9
--- /dev/null
+++ b/tests/test_chatstates.py
@@ -0,0 +1,44 @@
+from . sleektest import *
+import sleekxmpp.plugins.xep_0085 as xep_0085
+
+class TestChatStates(SleekTest):
+
+ def setUp(self):
+ registerStanzaPlugin(Message, xep_0085.Active)
+ registerStanzaPlugin(Message, xep_0085.Composing)
+ registerStanzaPlugin(Message, xep_0085.Gone)
+ registerStanzaPlugin(Message, xep_0085.Inactive)
+ registerStanzaPlugin(Message, xep_0085.Paused)
+
+ def testCreateChatState(self):
+ """Testing creating chat states."""
+
+ xmlstring = """
+ <message>
+ <%s xmlns="http://jabber.org/protocol/chatstates" />
+ </message>
+ """
+
+ msg = self.Message()
+ msg['chat_state'].active()
+ self.checkMessage(msg, xmlstring % 'active',
+ use_values=False)
+
+ msg['chat_state'].composing()
+ self.checkMessage(msg, xmlstring % 'composing',
+ use_values=False)
+
+
+ msg['chat_state'].gone()
+ self.checkMessage(msg, xmlstring % 'gone',
+ use_values=False)
+
+ msg['chat_state'].inactive()
+ self.checkMessage(msg, xmlstring % 'inactive',
+ use_values=False)
+
+ msg['chat_state'].paused()
+ self.checkMessage(msg, xmlstring % 'paused',
+ use_values=False)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates)
diff --git a/tests/test_disco.py b/tests/test_disco.py
index bbe285a6..2cc50ee0 100644
--- a/tests/test_disco.py
+++ b/tests/test_disco.py
@@ -1,155 +1,176 @@
-import unittest
-from xml.etree import cElementTree as ET
-from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
-from . import xmlcompare
+from . sleektest import *
+import sleekxmpp.plugins.xep_0030 as xep_0030
-import sleekxmpp.plugins.xep_0030 as sd
-def stanzaPlugin(stanza, plugin):
- stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin
- stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin
-
-class testdisco(unittest.TestCase):
+class TestDisco(SleekTest):
def setUp(self):
- self.sd = sd
- stanzaPlugin(self.sd.Iq, self.sd.DiscoInfo)
- stanzaPlugin(self.sd.Iq, self.sd.DiscoItems)
-
- def try3Methods(self, xmlstring, iq):
- iq2 = self.sd.Iq(None, self.sd.ET.fromstring(xmlstring))
- values = iq2.getValues()
- iq3 = self.sd.Iq()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), str(iq)+"3 methods for creating stanza don't match")
-
+ registerStanzaPlugin(Iq, xep_0030.DiscoInfo)
+ registerStanzaPlugin(Iq, xep_0030.DiscoItems)
+
def testCreateInfoQueryNoNode(self):
"""Testing disco#info query with no node."""
- iq = self.sd.Iq()
+ iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = ''
- xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" /></iq>"""
- self.try3Methods(xmlstring, iq)
+
+ self.checkIq(iq, """
+ <iq id="0">
+ <query xmlns="http://jabber.org/protocol/disco#info" />
+ </iq>
+ """)
def testCreateInfoQueryWithNode(self):
"""Testing disco#info query with a node."""
- iq = self.sd.Iq()
+ iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
- xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo" /></iq>"""
- self.try3Methods(xmlstring, iq)
+
+ self.checkIq(iq, """
+ <iq id="0">
+ <query xmlns="http://jabber.org/protocol/disco#info" node="foo" />
+ </iq>
+ """)
def testCreateInfoQueryNoNode(self):
"""Testing disco#items query with no node."""
- iq = self.sd.Iq()
+ iq = self.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = ''
- xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" /></iq>"""
- self.try3Methods(xmlstring, iq)
+
+ self.checkIq(iq, """
+ <iq id="0">
+ <query xmlns="http://jabber.org/protocol/disco#items" />
+ </iq>
+ """)
def testCreateItemsQueryWithNode(self):
"""Testing disco#items query with a node."""
- iq = self.sd.Iq()
+ iq = self.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = 'foo'
- xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo" /></iq>"""
- self.try3Methods(xmlstring, iq)
+
+ self.checkIq(iq, """
+ <iq id="0">
+ <query xmlns="http://jabber.org/protocol/disco#items" node="foo" />
+ </iq>
+ """)
def testInfoIdentities(self):
"""Testing adding identities to disco#info."""
- iq = self.sd.Iq()
+ iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
- iq['disco_info'].addIdentity('conference', 'text', 'Chatroom')
- xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><identity category="conference" type="text" name="Chatroom" /></query></iq>"""
- self.try3Methods(xmlstring, iq)
+ iq['disco_info'].addIdentity('conference', 'text', 'Chatroom')
+
+ self.checkIq(iq, """
+ <iq id="0">
+ <query xmlns="http://jabber.org/protocol/disco#info" node="foo">
+ <identity category="conference" type="text" name="Chatroom" />
+ </query>
+ </iq>
+ """)
def testInfoFeatures(self):
"""Testing adding features to disco#info."""
- iq = self.sd.Iq()
+ iq = self.Iq()
iq['id'] = "0"
iq['disco_info']['node'] = 'foo'
- iq['disco_info'].addFeature('foo')
- iq['disco_info'].addFeature('bar')
- xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#info" node="foo"><feature var="foo" /><feature var="bar" /></query></iq>"""
- self.try3Methods(xmlstring, iq)
+ iq['disco_info'].addFeature('foo')
+ iq['disco_info'].addFeature('bar')
+
+ self.checkIq(iq, """
+ <iq id="0">
+ <query xmlns="http://jabber.org/protocol/disco#info" node="foo">
+ <feature var="foo" />
+ <feature var="bar" />
+ </query>
+ </iq>
+ """)
def testItems(self):
"""Testing adding features to disco#info."""
- iq = self.sd.Iq()
+ iq = self.Iq()
iq['id'] = "0"
iq['disco_items']['node'] = 'foo'
- iq['disco_items'].addItem('user@localhost')
- iq['disco_items'].addItem('user@localhost', 'foo')
- iq['disco_items'].addItem('user@localhost', 'bar', 'Testing')
- xmlstring = """<iq id="0"><query xmlns="http://jabber.org/protocol/disco#items" node="foo"><item jid="user@localhost" /><item node="foo" jid="user@localhost" /><item node="bar" jid="user@localhost" name="Testing" /></query></iq>"""
- self.try3Methods(xmlstring, iq)
+ iq['disco_items'].addItem('user@localhost')
+ iq['disco_items'].addItem('user@localhost', 'foo')
+ iq['disco_items'].addItem('user@localhost', 'bar', 'Testing')
+
+ self.checkIq(iq, """
+ <iq id="0">
+ <query xmlns="http://jabber.org/protocol/disco#items" node="foo">
+ <item jid="user@localhost" />
+ <item node="foo" jid="user@localhost" />
+ <item node="bar" jid="user@localhost" name="Testing" />
+ </query>
+ </iq>
+ """)
def testAddRemoveIdentities(self):
"""Test adding and removing identities to disco#info stanza"""
- ids = [('automation', 'commands', 'AdHoc'),
- ('conference', 'text', 'ChatRoom')]
+ ids = [('automation', 'commands', 'AdHoc'),
+ ('conference', 'text', 'ChatRoom')]
- info = self.sd.DiscoInfo()
- info.addIdentity(*ids[0])
- self.failUnless(info.getIdentities() == [ids[0]])
+ info = xep_0030.DiscoInfo()
+ info.addIdentity(*ids[0])
+ self.failUnless(info.getIdentities() == [ids[0]])
- info.delIdentity('automation', 'commands')
- self.failUnless(info.getIdentities() == [])
+ info.delIdentity('automation', 'commands')
+ self.failUnless(info.getIdentities() == [])
- info.setIdentities(ids)
- self.failUnless(info.getIdentities() == ids)
+ info.setIdentities(ids)
+ self.failUnless(info.getIdentities() == ids)
- info.delIdentity('automation', 'commands')
- self.failUnless(info.getIdentities() == [ids[1]])
+ info.delIdentity('automation', 'commands')
+ self.failUnless(info.getIdentities() == [ids[1]])
- info.delIdentities()
- self.failUnless(info.getIdentities() == [])
+ info.delIdentities()
+ self.failUnless(info.getIdentities() == [])
def testAddRemoveFeatures(self):
"""Test adding and removing features to disco#info stanza"""
- features = ['foo', 'bar', 'baz']
+ features = ['foo', 'bar', 'baz']
- info = self.sd.DiscoInfo()
- info.addFeature(features[0])
- self.failUnless(info.getFeatures() == [features[0]])
+ info = xep_0030.DiscoInfo()
+ info.addFeature(features[0])
+ self.failUnless(info.getFeatures() == [features[0]])
- info.delFeature('foo')
- self.failUnless(info.getFeatures() == [])
+ info.delFeature('foo')
+ self.failUnless(info.getFeatures() == [])
- info.setFeatures(features)
- self.failUnless(info.getFeatures() == features)
+ info.setFeatures(features)
+ self.failUnless(info.getFeatures() == features)
- info.delFeature('bar')
- self.failUnless(info.getFeatures() == ['foo', 'baz'])
+ info.delFeature('bar')
+ self.failUnless(info.getFeatures() == ['foo', 'baz'])
- info.delFeatures()
- self.failUnless(info.getFeatures() == [])
+ info.delFeatures()
+ self.failUnless(info.getFeatures() == [])
def testAddRemoveItems(self):
"""Test adding and removing items to disco#items stanza"""
- items = [('user@localhost', None, None),
- ('user@localhost', 'foo', None),
- ('user@localhost', 'bar', 'Test')]
+ items = [('user@localhost', None, None),
+ ('user@localhost', 'foo', None),
+ ('user@localhost', 'bar', 'Test')]
+
+ info = xep_0030.DiscoItems()
+ self.failUnless(True, ""+str(items[0]))
- info = self.sd.DiscoItems()
- self.failUnless(True, ""+str(items[0]))
+ info.addItem(*(items[0]))
+ self.failUnless(info.getItems() == [items[0]], info.getItems())
- info.addItem(*(items[0]))
- self.failUnless(info.getItems() == [items[0]], info.getItems())
+ info.delItem('user@localhost')
+ self.failUnless(info.getItems() == [])
- info.delItem('user@localhost')
- self.failUnless(info.getItems() == [])
+ info.setItems(items)
+ self.failUnless(info.getItems() == items)
- info.setItems(items)
- self.failUnless(info.getItems() == items)
+ info.delItem('user@localhost', 'foo')
+ self.failUnless(info.getItems() == [items[0], items[2]])
- info.delItem('user@localhost', 'foo')
- self.failUnless(info.getItems() == [items[0], items[2]])
+ info.delItems()
+ self.failUnless(info.getItems() == [])
- info.delItems()
- self.failUnless(info.getItems() == [])
-
-
-suite = unittest.TestLoader().loadTestsFromTestCase(testdisco)
+suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco)
diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py
new file mode 100644
index 00000000..95502f54
--- /dev/null
+++ b/tests/test_elementbase.py
@@ -0,0 +1,193 @@
+from . sleektest import *
+from sleekxmpp.xmlstream.stanzabase import ElementBase
+
+class TestElementBase(SleekTest):
+
+ def testExtendedName(self):
+ """Test element names of the form tag1/tag2/tag3."""
+
+ class TestStanza(ElementBase):
+ name = "foo/bar/baz"
+ namespace = "test"
+
+ stanza = TestStanza()
+ self.checkStanza(TestStanza, stanza, """
+ <foo xmlns="test">
+ <bar>
+ <baz />
+ </bar>
+ </foo>
+ """)
+
+ 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'))
+ subitem = set((TestSubStanza,))
+
+ registerStanzaPlugin(TestStanza, TestStanzaPlugin)
+
+ 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'))
+ subitem = set((TestSubStanza,))
+
+ registerStanzaPlugin(TestStanza, TestStanzaPlugin)
+ registerStanzaPlugin(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.setStanzaValues(values)
+
+ self.checkStanza(TestStanza, stanza, """
+ <foo xmlns="foo" bar="a">
+ <pluginfoo baz="b" />
+ <pluginfoo2 bar="d" baz="e" />
+ <subfoo bar="c" />
+ </foo>
+ """)
+
+ 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',))
+
+ TestStanza.subitem = (TestStanza,)
+ registerStanzaPlugin(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',))
+
+ registerStanzaPlugin(TestStanza, TestStanzaPlugin)
+
+ stanza = TestStanza()
+
+ stanza['bar'] = 'attribute!'
+ stanza['baz'] = 'element!'
+ stanza['qux'] = 'overridden'
+ stanza['foobar'] = 'plugin'
+
+ self.checkStanza(TestStanza, stanza, """
+ <foo xmlns="foo" bar="attribute!">
+ <baz>element!</baz>
+ <foobar foobar="plugin" />
+ </foo>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase)
diff --git a/tests/test_errorstanzas.py b/tests/test_errorstanzas.py
new file mode 100644
index 00000000..d6fafc59
--- /dev/null
+++ b/tests/test_errorstanzas.py
@@ -0,0 +1,56 @@
+from . sleektest import *
+
+class TestErrorStanzas(SleekTest):
+
+ def testSetup(self):
+ """Test setting initial values in error stanza."""
+ msg = self.Message()
+ msg.enable('error')
+ self.checkMessage(msg, """
+ <message type="error">
+ <error type="cancel">
+ <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </message>
+ """)
+
+ def testCondition(self):
+ """Test modifying the error condition."""
+ msg = self.Message()
+ msg['error']['condition'] = 'item-not-found'
+
+ self.checkMessage(msg, """
+ <message type="error">
+ <error type="cancel">
+ <item-not-found xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ </error>
+ </message>
+ """)
+
+ self.failUnless(msg['error']['condition'] == 'item-not-found', "Error condition doesn't match.")
+
+ del msg['error']['condition']
+
+ self.checkMessage(msg, """
+ <message type="error">
+ <error type="cancel" />
+ </message>
+ """)
+
+ 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.checkMessage(msg, """
+ <message type="error">
+ <error type="cancel">
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">Error!</text>
+ </error>
+ </message>
+ """)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorStanzas)
diff --git a/tests/test_events.py b/tests/test_events.py
index 11821dbb..bbc5832e 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -1,14 +1,11 @@
-import unittest
+import sleekxmpp
+from . sleektest import *
-class testevents(unittest.TestCase):
- def setUp(self):
- import sleekxmpp.stanza.presence as p
- self.p = p
+class TestEvents(SleekTest):
def testEventHappening(self):
"Test handler working"
- import sleekxmpp
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
happened = []
def handletestevent(event):
@@ -20,7 +17,6 @@ class testevents(unittest.TestCase):
def testDelEvent(self):
"Test handler working, then deleted and not triggered"
- import sleekxmpp
c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
happened = []
def handletestevent(event):
@@ -32,4 +28,4 @@ class testevents(unittest.TestCase):
self.failUnless(happened == [True], "event did not get triggered the correct number of times")
-suite = unittest.TestLoader().loadTestsFromTestCase(testevents)
+suite = unittest.TestLoader().loadTestsFromTestCase(TestEvents)
diff --git a/tests/test_forms.py b/tests/test_forms.py
new file mode 100644
index 00000000..d5710633
--- /dev/null
+++ b/tests/test_forms.py
@@ -0,0 +1,115 @@
+from . sleektest import *
+import sleekxmpp.plugins.xep_0004 as xep_0004
+
+
+class TestDataForms(SleekTest):
+
+ def setUp(self):
+ registerStanzaPlugin(Message, xep_0004.Form)
+ registerStanzaPlugin(xep_0004.Form, xep_0004.FormField)
+ registerStanzaPlugin(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.checkMessage(msg, """
+ <message>
+ <x xmlns="jabber:x:data" type="form">
+ <instructions>Instructions</instructions>
+ <instructions>Second batch</instructions>
+ </x>
+ </message>
+ """)
+
+ 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.checkMessage(msg, """
+ <message>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="f1" type="text-single" label="Text">
+ <desc>A text field</desc>
+ <required />
+ <value>Some text!</value>
+ </field>
+ </x>
+ </message>
+ """)
+
+ 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.checkMessage(msg, """
+ <message>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="f1" type="text-single" label="Username">
+ <required />
+ </field>
+ <field var="f2" type="text-private" label="Password">
+ <required />
+ </field>
+ <field var="f3" type="text-multi" label="Message">
+ <value>Enter message.</value>
+ <value>A long one even.</value>
+ </field>
+ <field var="f4" type="list-single" label="Message Type">
+ <option label="Cool!">
+ <value>cool</value>
+ </option>
+ <option label="Urgh!">
+ <value>urgh</value>
+ </option>
+ </field>
+ </x>
+ </message>
+ """)
+
+ 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.checkMessage(msg, """
+ <message>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="foo" type="text-single">
+ <value>Foo!</value>
+ </field>
+ <field var="bar" type="list-multi">
+ <value>a</value>
+ <value>b</value>
+ </field>
+ </x>
+ </message>""")
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms)
diff --git a/tests/test_gmail.py b/tests/test_gmail.py
new file mode 100644
index 00000000..dd256e27
--- /dev/null
+++ b/tests/test_gmail.py
@@ -0,0 +1,88 @@
+from . sleektest import *
+import sleekxmpp.plugins.gmail_notify as gmail
+
+
+class TestGmail(SleekTest):
+
+ def setUp(self):
+ registerStanzaPlugin(Iq, gmail.GmailQuery)
+ registerStanzaPlugin(Iq, gmail.MailBox)
+ registerStanzaPlugin(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.checkIq(iq, """
+ <iq type="get">
+ <query xmlns="google:mail:notify"
+ newer-than-time="1140638252542"
+ newer-than-tid="11134623426430234"
+ q="is:starred" />
+ </iq>
+ """)
+
+ 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("""
+ <iq type="result">
+ <mailbox xmlns="google:mail:notify"
+ result-time='1118012394209'
+ url='http://mail.google.com/mail'
+ total-matched='95'
+ total-estimate='0'>
+ <mail-thread-info tid='1172320964060972012'
+ participation='1'
+ messages='28'
+ date='1118012394209'
+ url='http://mail.google.com/mail?view=cv'>
+ <senders>
+ <sender name='Me' address='romeo@gmail.com' originator='1' />
+ <sender name='Benvolio' address='benvolio@gmail.com' />
+ <sender name='Mercutio' address='mercutio@gmail.com' unread='1'/>
+ </senders>
+ <labels>act1scene3</labels>
+ <subject>Put thy rapier up.</subject>
+ <snippet>Ay, ay, a scratch, a scratch; marry, 'tis enough.</snippet>
+ </mail-thread-info>
+ </mailbox>
+ </iq>
+ """)
+
+ 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)
diff --git a/tests/test_iqstanzas.py b/tests/test_iqstanzas.py
new file mode 100644
index 00000000..2dabc5e9
--- /dev/null
+++ b/tests/test_iqstanzas.py
@@ -0,0 +1,90 @@
+from . sleektest import *
+from sleekxmpp.xmlstream.stanzabase import ET
+
+
+class TestIqStanzas(SleekTest):
+
+ def tearDown(self):
+ """Shutdown the XML stream after testing."""
+ self.streamClose()
+
+ def testSetup(self):
+ """Test initializing default Iq values."""
+ iq = self.Iq()
+ self.checkIq(iq, """
+ <iq id="0" />
+ """)
+
+ def testPayload(self):
+ """Test setting Iq stanza payload."""
+ iq = self.Iq()
+ iq.setPayload(ET.Element('{test}tester'))
+ self.checkIq(iq, """
+ <iq id="0">
+ <tester xmlns="test" />
+ </iq>
+ """, use_values=False)
+
+
+ def testUnhandled(self):
+ """Test behavior for Iq.unhandled."""
+ self.streamStart()
+ self.streamRecv("""
+ <iq id="test" type="get">
+ <query xmlns="test" />
+ </iq>
+ """)
+
+ iq = self.Iq()
+ iq['id'] = 'test'
+ iq['error']['condition'] = 'feature-not-implemented'
+ iq['error']['text'] = 'No handlers registered for this request.'
+
+ self.streamSendIq(iq, """
+ <iq id="test" type="error">
+ <error type="cancel">
+ <feature-not-implemented xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
+ <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
+ No handlers registered for this request.
+ </text>
+ </error>
+ </iq>
+ """)
+
+ def testQuery(self):
+ """Test modifying query element of Iq stanzas."""
+ iq = self.Iq()
+
+ iq['query'] = 'query_ns'
+ self.checkIq(iq, """
+ <iq id="0">
+ <query xmlns="query_ns" />
+ </iq>
+ """)
+
+ iq['query'] = 'query_ns2'
+ self.checkIq(iq, """
+ <iq id="0">
+ <query xmlns="query_ns2" />
+ </iq>
+ """)
+
+ self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match")
+
+ del iq['query']
+ self.checkIq(iq, """
+ <iq id="0" />
+ """)
+
+ def testReply(self):
+ """Test setting proper result type in Iq replies."""
+ iq = self.Iq()
+ iq['to'] = 'user@localhost'
+ iq['type'] = 'get'
+ iq.reply()
+
+ self.checkIq(iq, """
+ <iq id="0" type="result" />
+ """)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestIqStanzas)
diff --git a/tests/test_jid.py b/tests/test_jid.py
new file mode 100644
index 00000000..cddac424
--- /dev/null
+++ b/tests/test_jid.py
@@ -0,0 +1,26 @@
+from . sleektest import *
+from sleekxmpp.xmlstream.jid import JID
+
+class TestJIDClass(SleekTest):
+ def testJIDfromfull(self):
+ j = JID('user@someserver/some/resource')
+ self.assertEqual(j.user, 'user', "User does not match")
+ self.assertEqual(j.domain, 'someserver', "Domain does not match")
+ self.assertEqual(j.resource, 'some/resource', "Resource does not match")
+ self.assertEqual(j.bare, 'user@someserver', "Bare does not match")
+ self.assertEqual(j.full, 'user@someserver/some/resource', "Full does not match")
+ self.assertEqual(str(j), 'user@someserver/some/resource', "String does not match")
+
+ def testJIDchange(self):
+ j = JID('user1@someserver1/some1/resource1')
+ j.user = 'user'
+ j.domain = 'someserver'
+ j.resource = 'some/resource'
+ self.assertEqual(j.user, 'user', "User does not match")
+ self.assertEqual(j.domain, 'someserver', "Domain does not match")
+ self.assertEqual(j.resource, 'some/resource', "Resource does not match")
+ self.assertEqual(j.bare, 'user@someserver', "Bare does not match")
+ self.assertEqual(j.full, 'user@someserver/some/resource', "Full does not match")
+ self.assertEqual(str(j), 'user@someserver/some/resource', "String does not match")
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass)
diff --git a/tests/test_messagestanzas.py b/tests/test_messagestanzas.py
index 08488ce3..2a1567da 100644
--- a/tests/test_messagestanzas.py
+++ b/tests/test_messagestanzas.py
@@ -1,44 +1,57 @@
-import unittest
-from xml.etree import cElementTree as ET
-
-class testmessagestanzas(unittest.TestCase):
-
- def setUp(self):
- import sleekxmpp.stanza.message as m
- from sleekxmpp.basexmpp import stanzaPlugin
- from sleekxmpp.stanza.htmlim import HTMLIM
- stanzaPlugin(m.Message, HTMLIM)
- self.m = m
-
- def testGroupchatReplyRegression(self):
- "Regression groupchat reply should be to barejid"
- msg = self.m.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.m.Message()
- msg.attrib.attrib.attrib['to'] = 'usr@server.tld'
- self.failUnless(str(msg['to']) == 'usr@server.tld')
-
- def testHTMLPlugin(self):
- "Test message/html/html stanza"
- msgtxt = """<message to="fritzy@netflint.net/sleekxmpp" type="chat"><body>this is the plaintext message</body><html xmlns="http://jabber.org/protocol/xhtml-im"><body xmlns="http://www.w3.org/1999/xhtml"><p>This is the htmlim message</p></body></html></message>"""
- msg = self.m.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']['html'] = p
- msg2 = self.m.Message()
- values = msg.getValues()
- msg2.setValues(values)
- self.failUnless(msgtxt == str(msg) == str(msg2))
-
-suite = unittest.TestLoader().loadTestsFromTestCase(testmessagestanzas)
+from . sleektest import *
+from sleekxmpp.stanza.message import Message
+from sleekxmpp.stanza.htmlim import HTMLIM
+
+
+class TestMessageStanzas(SleekTest):
+
+ def setUp(self):
+ registerStanzaPlugin(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.checkMessage(msg, """
+ <message to="fritzy@netflint.net/sleekxmpp" type="chat">
+ <body>this is the plaintext message</body>
+ <html xmlns="http://jabber.org/protocol/xhtml-im">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p>This is the htmlim message</p>
+ </body>
+ </html>
+ </message>""")
+
+ def testNickPlugin(self):
+ "Test message/nick/nick stanza."
+ msg = self.Message()
+ msg['nick']['nick'] = 'A nickname!'
+ self.checkMessage(msg, """
+ <message>
+ <nick xmlns="http://jabber.org/nick/nick">A nickname!</nick>
+ </message>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas)
diff --git a/tests/test_presencestanzas.py b/tests/test_presencestanzas.py
index 23eb911e..d6a5a388 100644
--- a/tests/test_presencestanzas.py
+++ b/tests/test_presencestanzas.py
@@ -1,31 +1,67 @@
-import unittest
-
-class testpresencestanzas(unittest.TestCase):
-
- def setUp(self):
- import sleekxmpp.stanza.presence as p
- self.p = p
-
- def testPresenceShowRegression(self):
- "Regression check presence['type'] = 'dnd' show value working"
- p = self.p.Presence()
- p['type'] = 'dnd'
- self.failUnless(str(p) == "<presence><show>dnd</show></presence>")
-
- def testPresenceUnsolicitedOffline(self):
- "Unsolicted offline presence does not spawn changed_status or update roster"
- p = self.p.Presence()
- p['type'] = 'unavailable'
- p['from'] = 'bill@chadmore.com/gmail15af'
- import sleekxmpp
- c = sleekxmpp.ClientXMPP('crap@wherever', 'password')
- happened = []
- def handlechangedpresence(event):
- happened.append(True)
- c.add_event_handler("changed_status", handlechangedpresence)
- c._handlePresence(p)
- self.failUnless(happened == [], "changed_status event triggered for superfulous unavailable presence")
- self.failUnless(c.roster == {}, "Roster updated for superfulous unavailable presence")
-
-
-suite = unittest.TestLoader().loadTestsFromTestCase(testpresencestanzas)
+import sleekxmpp
+from . sleektest 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.checkPresence(p, "<presence><show>dnd</show></presence>")
+
+ def testPresenceType(self):
+ """Test manipulating presence['type']"""
+ p = self.Presence()
+ p['type'] = 'available'
+ self.checkPresence(p, "<presence />")
+ self.failUnless(p['type'] == 'available',
+ "Incorrect presence['type'] for type 'available'")
+
+ for showtype in ['away', 'chat', 'dnd', 'xa']:
+ p['type'] = showtype
+ self.checkPresence(p, """
+ <presence><show>%s</show></presence>
+ """ % showtype)
+ self.failUnless(p['type'] == showtype,
+ "Incorrect presence['type'] for type '%s'" % showtype)
+
+ p['type'] = None
+ self.checkPresence(p, "<presence />")
+
+ 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._handlePresence(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.checkPresence(p, """
+ <presence>
+ <nick xmlns="http://jabber.org/nick/nick">A nickname!</nick>
+ </presence>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas)
diff --git a/tests/test_pubsubstanzas.py b/tests/test_pubsubstanzas.py
index 089ee180..cddfd12b 100644
--- a/tests/test_pubsubstanzas.py
+++ b/tests/test_pubsubstanzas.py
@@ -1,315 +1,511 @@
-import unittest
-from xml.etree import cElementTree as ET
-from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath
-from . import xmlcompare
+from . sleektest import *
+import sleekxmpp.plugins.xep_0004 as xep_0004
+import sleekxmpp.plugins.stanza_pubsub as pubsub
-class testpubsubstanzas(unittest.TestCase):
- def setUp(self):
- import sleekxmpp.plugins.stanza_pubsub as ps
- self.ps = ps
+class TestPubsubStanzas(SleekTest):
- def testAffiliations(self):
- "Testing iq/pubsub/affiliations/affiliation stanzas"
- iq = self.ps.Iq()
- aff1 = self.ps.Affiliation()
- aff1['node'] = 'testnode'
- aff1['affiliation'] = 'owner'
- aff2 = self.ps.Affiliation()
- aff2['node'] = 'testnode2'
- aff2['affiliation'] = 'publisher'
- iq['pubsub']['affiliations'].append(aff1)
- iq['pubsub']['affiliations'].append(aff2)
- xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><affiliations><affiliation node="testnode" affiliation="owner" /><affiliation node="testnode2" affiliation="publisher" /></affiliations></pubsub></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- values = iq2.getValues()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), "3 methods for creating stanza don't match")
- self.failUnless(iq.match('iq@id=0/pubsub/affiliations/affiliation@node=testnode2@affiliation=publisher'), 'Match path failed')
-
- def testSubscriptions(self):
- "Testing iq/pubsub/subscriptions/subscription stanzas"
- iq = self.ps.Iq()
- sub1 = self.ps.Subscription()
- sub1['node'] = 'testnode'
- sub1['jid'] = 'steve@myserver.tld/someresource'
- sub2 = self.ps.Subscription()
- sub2['node'] = 'testnode2'
- sub2['jid'] = 'boogers@bork.top/bill'
- sub2['subscription'] = 'subscribed'
- iq['pubsub']['subscriptions'].append(sub1)
- iq['pubsub']['subscriptions'].append(sub2)
- xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><subscriptions><subscription node="testnode" jid="steve@myserver.tld/someresource" /><subscription node="testnode2" jid="boogers@bork.top/bill" subscription="subscribed" /></subscriptions></pubsub></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- values = iq2.getValues()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
-
- def testOptionalSettings(self):
- "Testing iq/pubsub/subscription/subscribe-options stanzas"
- iq = self.ps.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'
- xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><subscription node="testnode alsdkjfas" jid="fritzy@netflint.net/sleekxmpp" subscription="unconfigured"><subscribe-options><required /></subscribe-options></subscription></pubsub></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- values = iq2.getValues()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
-
- def testItems(self):
- "Testing iq/pubsub/items stanzas"
- iq = self.ps.Iq()
- iq['pubsub']['items']
- payload = ET.fromstring("""<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'><child1 /><child2 normandy='cheese' foo='bar' /></thinger>""")
- payload2 = ET.fromstring("""<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'><child12 /><child22 normandy='cheese2' foo='bar2' /></thinger2>""")
- item = self.ps.Item()
- item['id'] = 'asdf'
- item['payload'] = payload
- item2 = self.ps.Item()
- item2['id'] = 'asdf2'
- item2['payload'] = payload2
- iq['pubsub']['items'].append(item)
- iq['pubsub']['items'].append(item2)
- xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><items><item id="asdf"><thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1"><child1 /><child2 foo="bar" normandy="cheese" /></thinger></item><item id="asdf2"><thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12"><child12 /><child22 foo="bar2" normandy="cheese2" /></thinger2></item></items></pubsub></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- values = iq2.getValues()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
-
- def testCreate(self):
- "Testing iq/pubsub/create&configure stanzas"
- from sleekxmpp.plugins import xep_0004
- iq = self.ps.Iq()
- iq['pubsub']['create']['node'] = 'mynode'
- form = xep_0004.Form()
- form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
- iq['pubsub']['configure']['config'] = form
- xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><create node="mynode" /><configure><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></configure></pubsub></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- values = iq2.getValues()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
-
- def testState(self):
- "Testing iq/psstate stanzas"
- from sleekxmpp.plugins import xep_0004
- iq = self.ps.Iq()
- iq['psstate']['node']= 'mynode'
- iq['psstate']['item']= 'myitem'
- pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed')
- iq['psstate']['payload'] = pl
- xmlstring = """<iq id="0"><state xmlns="http://jabber.org/protocol/psstate" node="mynode" item="myitem"><claimed xmlns="http://andyet.net/protocol/pubsubqueue" /></state></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- values = iq2.getValues()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
-
- def testDefault(self):
- "Testing iq/pubsub_owner/default stanzas"
- from sleekxmpp.plugins import xep_0004
- iq = self.ps.Iq()
- iq['pubsub_owner']['default']
- iq['pubsub_owner']['default']['node'] = 'mynode'
- iq['pubsub_owner']['default']['type'] = 'leaf'
- form = xep_0004.Form()
- form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
- iq['pubsub_owner']['default']['config'] = form
- xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub#owner"><default node="mynode" type="leaf"><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></default></pubsub></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- values = iq2.getValues()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
-
- def testSubscribe(self):
- "Testing iq/pubsub/subscribe stanzas"
- from sleekxmpp.plugins import xep_0004
- iq = self.ps.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
- xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp"><options node="cheese" jid="fritzy@netflint.net/sleekxmpp"><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></options></subscribe></pubsub></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- values = iq2.getValues()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
-
- def testPublish(self):
- "Testing iq/pubsub/publish stanzas"
- iq = self.ps.Iq()
- iq['pubsub']['publish']['node'] = 'thingers'
- payload = ET.fromstring("""<thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'><child1 /><child2 normandy='cheese' foo='bar' /></thinger>""")
- payload2 = ET.fromstring("""<thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'><child12 /><child22 normandy='cheese2' foo='bar2' /></thinger2>""")
- item = self.ps.Item()
- item['id'] = 'asdf'
- item['payload'] = payload
- item2 = self.ps.Item()
- item2['id'] = 'asdf2'
- item2['payload'] = payload2
- iq['pubsub']['publish'].append(item)
- iq['pubsub']['publish'].append(item2)
- xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub"><publish node="thingers"><item id="asdf"><thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1"><child1 /><child2 foo="bar" normandy="cheese" /></thinger></item><item id="asdf2"><thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12"><child12 /><child22 foo="bar2" normandy="cheese2" /></thinger2></item></publish></pubsub></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- values = iq2.getValues()
- iq3.setValues(values)
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
+ def 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.checkIq(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <affiliations>
+ <affiliation node="testnode" affiliation="owner" />
+ <affiliation node="testnode2" affiliation="publisher" />
+ </affiliations>
+ </pubsub>
+ </iq>""")
- def testDelete(self):
- "Testing iq/pubsub_owner/delete stanzas"
- iq = self.ps.Iq()
- iq['pubsub_owner']['delete']['node'] = 'thingers'
- xmlstring = """<iq id="0"><pubsub xmlns="http://jabber.org/protocol/pubsub#owner"><delete node="thingers" /></pubsub></iq>"""
- iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring))
- iq3 = self.ps.Iq()
- iq3.setValues(iq2.getValues())
- self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3))
-
- def testCreateConfigGet(self):
- """Testing getting config from full create"""
- xml = """<iq to="pubsub.asdf" type="set" id="E" from="fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7"><pubsub xmlns="http://jabber.org/protocol/pubsub"><create node="testnode2" /><configure><x xmlns="jabber:x:data" type="submit"><field var="FORM_TYPE" type="hidden"><value>http://jabber.org/protocol/pubsub#node_config</value></field><field var="pubsub#node_type" type="list-single" label="Select the node type"><value>leaf</value></field><field var="pubsub#title" type="text-single" label="A friendly name for the node" /><field var="pubsub#deliver_notifications" type="boolean" label="Deliver event notifications"><value>1</value></field><field var="pubsub#deliver_payloads" type="boolean" label="Deliver payloads with event notifications"><value>1</value></field><field var="pubsub#notify_config" type="boolean" label="Notify subscribers when the node configuration changes" /><field var="pubsub#notify_delete" type="boolean" label="Notify subscribers when the node is deleted" /><field var="pubsub#notify_retract" type="boolean" label="Notify subscribers when items are removed from the node"><value>1</value></field><field var="pubsub#notify_sub" type="boolean" label="Notify owners about new subscribers and unsubscribes" /><field var="pubsub#persist_items" type="boolean" label="Persist items in storage" /><field var="pubsub#max_items" type="text-single" label="Max # of items to persist"><value>10</value></field><field var="pubsub#subscribe" type="boolean" label="Whether to allow subscriptions"><value>1</value></field><field var="pubsub#access_model" type="list-single" label="Specify the subscriber model"><value>open</value></field><field var="pubsub#publish_model" type="list-single" label="Specify the publisher model"><value>publishers</value></field><field var="pubsub#send_last_published_item" type="list-single" label="Send last published item"><value>never</value></field><field var="pubsub#presence_based_delivery" type="boolean" label="Deliver notification only to available users" /></x></configure></pubsub></iq>"""
- iq = self.ps.Iq(None, self.ps.ET.fromstring(xml))
- config = iq['pubsub']['configure']['config']
- self.failUnless(config.getValues() != {})
+ 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.checkIq(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscriptions>
+ <subscription node="testnode" jid="steve@myserver.tld/someresource" />
+ <subscription node="testnode2" jid="boogers@bork.top/bill" subscription="subscribed" />
+ </subscriptions>
+ </pubsub>
+ </iq>""")
- def testItemEvent(self):
- """Testing message/pubsub_event/items/item"""
- msg = self.ps.Message()
- item = self.ps.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'
- xmlstring = """<message type="normal"><event xmlns="http://jabber.org/protocol/pubsub#event"><items node="cheese"><item id="abc123"><test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" /></item></items></event></message>"""
- msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
- msg3 = self.ps.Message()
- msg3.setValues(msg2.getValues())
- self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
+ 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.checkIq(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscription node="testnode alsdkjfas" jid="fritzy@netflint.net/sleekxmpp" subscription="unconfigured">
+ <subscribe-options>
+ <required />
+ </subscribe-options>
+ </subscription>
+ </pubsub>
+ </iq>""")
- def testItemsEvent(self):
- """Testing multiple message/pubsub_event/items/item"""
- msg = self.ps.Message()
- item = self.ps.EventItem()
- item2 = self.ps.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'
- xmlstring = """<message type="normal"><event xmlns="http://jabber.org/protocol/pubsub#event"><items node="cheese"><item id="abc123"><test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" /></item><item id="123abc"><test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" /></item></items></event></message>"""
- msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
- msg3 = self.ps.Message()
- msg3.setValues(msg2.getValues())
- self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
+ def testItems(self):
+ "Testing iq/pubsub/items stanzas"
+ iq = self.Iq()
+ iq['pubsub']['items']
+ payload = ET.fromstring("""
+ <thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'>
+ <child1 />
+ <child2 normandy='cheese' foo='bar' />
+ </thinger>""")
+ payload2 = ET.fromstring("""
+ <thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'>
+ <child12 />
+ <child22 normandy='cheese2' foo='bar2' />
+ </thinger2>""")
+ 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.checkIq(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <items>
+ <item id="asdf">
+ <thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1">
+ <child1 />
+ <child2 foo="bar" normandy="cheese" />
+ </thinger>
+ </item>
+ <item id="asdf2">
+ <thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12">
+ <child12 />
+ <child22 foo="bar2" normandy="cheese2" />
+ </thinger2>
+ </item>
+ </items>
+ </pubsub>
+ </iq>""")
- def testItemsEvent(self):
- """Testing message/pubsub_event/items/item & retract mix"""
- msg = self.ps.Message()
- item = self.ps.EventItem()
- item2 = self.ps.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 = self.ps.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'
- xmlstring = """<message type="normal"><event xmlns="http://jabber.org/protocol/pubsub#event"><items node="cheese"><item id="abc123"><test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" /></item><retract id="aabbcc" /><item id="123abc"><test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" /></item></items></event></message>"""
- msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
- msg3 = self.ps.Message()
- msg3.setValues(msg2.getValues())
- self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
-
- def testCollectionAssociate(self):
- """Testing message/pubsub_event/collection/associate"""
- msg = self.ps.Message()
- msg['pubsub_event']['collection']['associate']['node'] = 'cheese'
- msg['pubsub_event']['collection']['node'] = 'cheeseburger'
- msg['type'] = 'headline'
- xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><collection node="cheeseburger"><associate node="cheese" /></collection></event></message>"""
- msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
- msg3 = self.ps.Message()
- msg3.setValues(msg2.getValues())
- self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
+ 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.checkIq(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="mynode" />
+ <configure>
+ <x xmlns="jabber:x:data" type="form">
+ <field var="pubsub#title" type="text-single">
+ <value>This thing is awesome</value>
+ </field>
+ </x>
+ </configure>
+ </pubsub>
+ </iq>""")
- def testCollectionDisassociate(self):
- """Testing message/pubsub_event/collection/disassociate"""
- msg = self.ps.Message()
- msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese'
- msg['pubsub_event']['collection']['node'] = 'cheeseburger'
- msg['type'] = 'headline'
- xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><collection node="cheeseburger"><disassociate node="cheese" /></collection></event></message>"""
- msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
- msg3 = self.ps.Message()
- msg3.setValues(msg2.getValues())
- self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
+ 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.checkIq(iq, """
+ <iq id="0">
+ <state xmlns="http://jabber.org/protocol/psstate" node="mynode" item="myitem">
+ <claimed xmlns="http://andyet.net/protocol/pubsubqueue" />
+ </state>
+ </iq>""")
- def testEventConfiguration(self):
- """Testing message/pubsub_event/configuration/config"""
- msg = self.ps.Message()
- from sleekxmpp.plugins import xep_0004
- form = xep_0004.Form()
- form.addField('pubsub#title', ftype='text-single', value='This thing is awesome')
- msg['pubsub_event']['configuration']['node'] = 'cheese'
- msg['pubsub_event']['configuration']['config'] = form
- msg['type'] = 'headline'
- xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><configuration node="cheese"><x xmlns="jabber:x:data" type="form"><field var="pubsub#title" type="text-single"><value>This thing is awesome</value></field></x></configuration></event></message>"""
- msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
- msg3 = self.ps.Message()
- msg3.setValues(msg2.getValues())
- self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
-
- def testEventPurge(self):
- """Testing message/pubsub_event/purge"""
- msg = self.ps.Message()
- msg['pubsub_event']['purge']['node'] = 'pickles'
- msg['type'] = 'headline'
- xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><purge node="pickles" /></event></message>"""
- msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
- msg3 = self.ps.Message()
- msg3.setValues(msg2.getValues())
- self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3))
-
- def testEventSubscription(self):
- """Testing message/pubsub_event/subscription"""
- msg = self.ps.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'
- xmlstring = """<message type="headline"><event xmlns="http://jabber.org/protocol/pubsub#event"><subscription node="pickles" subid="aabb1122" jid="fritzy@netflint.net/test" subscription="subscribed" expiry="presence" /></event></message>"""
- msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring))
- msg3 = self.ps.Message()
- msg3.setValues(msg2.getValues())
- self.failUnless(xmlcompare.comparemany([xmlstring, str(msg), str(msg2), str(msg3)]))
+ 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.checkIq(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <default node="mynode" type="leaf">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="pubsub#title" type="text-single">
+ <value>This thing is awesome</value>
+ </field>
+ </x>
+ </default>
+ </pubsub>
+ </iq>""", use_values=False)
-suite = unittest.TestLoader().loadTestsFromTestCase(testpubsubstanzas)
+ 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.checkIq(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <subscribe node="cheese" jid="fritzy@netflint.net/sleekxmpp">
+ <options node="cheese" jid="fritzy@netflint.net/sleekxmpp">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="pubsub#title" type="text-single">
+ <value>this thing is awesome</value>
+ </field>
+ </x>
+ </options>
+ </subscribe>
+ </pubsub>
+ </iq>""", use_values=False)
+
+ def testPublish(self):
+ "Testing iq/pubsub/publish stanzas"
+ iq = self.Iq()
+ iq['pubsub']['publish']['node'] = 'thingers'
+ payload = ET.fromstring("""
+ <thinger xmlns="http://andyet.net/protocol/thinger" x="1" y='2'>
+ <child1 />
+ <child2 normandy='cheese' foo='bar' />
+ </thinger>""")
+ payload2 = ET.fromstring("""
+ <thinger2 xmlns="http://andyet.net/protocol/thinger2" x="12" y='22'>
+ <child12 />
+ <child22 normandy='cheese2' foo='bar2' />
+ </thinger2>""")
+ 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.checkIq(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <publish node="thingers">
+ <item id="asdf">
+ <thinger xmlns="http://andyet.net/protocol/thinger" y="2" x="1">
+ <child1 />
+ <child2 foo="bar" normandy="cheese" />
+ </thinger>
+ </item>
+ <item id="asdf2">
+ <thinger2 xmlns="http://andyet.net/protocol/thinger2" y="22" x="12">
+ <child12 />
+ <child22 foo="bar2" normandy="cheese2" />
+ </thinger2>
+ </item>
+ </publish>
+ </pubsub>
+ </iq>""")
+
+ def testDelete(self):
+ "Testing iq/pubsub_owner/delete stanzas"
+ iq = self.Iq()
+ iq['pubsub_owner']['delete']['node'] = 'thingers'
+ self.checkIq(iq, """
+ <iq id="0">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub#owner">
+ <delete node="thingers" />
+ </pubsub>
+ </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.checkIq(iq, """
+ <iq to="pubsub.asdf" type="set" id="E" from="fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7">
+ <pubsub xmlns="http://jabber.org/protocol/pubsub">
+ <create node="testnode2" />
+ <configure>
+ <x xmlns="jabber:x:data" type="submit">
+ <field var="FORM_TYPE" type="hidden">
+ <value>http://jabber.org/protocol/pubsub#node_config</value>
+ </field>
+ <field var="pubsub#node_type" type="list-single" label="Select the node type">
+ <value>leaf</value>
+ </field>
+ <field var="pubsub#title" type="text-single" label="A friendly name for the node" />
+ <field var="pubsub#deliver_notifications" type="boolean" label="Deliver event notifications">
+ <value>1</value>
+ </field>
+ <field var="pubsub#deliver_payloads" type="boolean" label="Deliver payloads with event notifications">
+ <value>1</value>
+ </field>
+ <field var="pubsub#notify_config" type="boolean" label="Notify subscribers when the node configuration changes" />
+ <field var="pubsub#notify_delete" type="boolean" label="Notify subscribers when the node is deleted" />
+ <field var="pubsub#notify_retract" type="boolean" label="Notify subscribers when items are removed from the node">
+ <value>1</value>
+ </field>
+ <field var="pubsub#notify_sub" type="boolean" label="Notify owners about new subscribers and unsubscribes" />
+ <field var="pubsub#persist_items" type="boolean" label="Persist items in storage" />
+ <field var="pubsub#max_items" type="text-single" label="Max # of items to persist">
+ <value>10</value>
+ </field>
+ <field var="pubsub#subscribe" type="boolean" label="Whether to allow subscriptions">
+ <value>1</value>
+ </field>
+ <field var="pubsub#access_model" type="list-single" label="Specify the subscriber model">
+ <value>open</value>
+ </field>
+ <field var="pubsub#publish_model" type="list-single" label="Specify the publisher model">
+ <value>publishers</value>
+ </field>
+ <field var="pubsub#send_last_published_item" type="list-single" label="Send last published item">
+ <value>never</value>
+ </field>
+ <field var="pubsub#presence_based_delivery" type="boolean" label="Deliver notification only to available users" />
+ </x>
+ </configure>
+ </pubsub>
+ </iq>""")
+
+ 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.checkMessage(msg, """
+ <message type="normal">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <items node="cheese">
+ <item id="abc123">
+ <test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
+ </item>
+ </items>
+ </event>
+ </message>""")
+
+ 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.checkMessage(msg, """
+ <message type="normal">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <items node="cheese">
+ <item id="abc123">
+ <test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
+ </item>
+ <item id="123abc">
+ <test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" />
+ </item>
+ </items>
+ </event>
+ </message>""")
+
+ 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.checkMessage(msg, """
+ <message type="normal">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <items node="cheese">
+ <item id="abc123">
+ <test xmlns="http://netflint.net/protocol/test" failed="3" passed="24" />
+ </item><retract id="aabbcc" />
+ <item id="123abc">
+ <test xmlns="http://netflint.net/protocol/test-other" failed="3" total="27" />
+ </item>
+ </items>
+ </event>
+ </message>""")
+
+ 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.checkMessage(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <collection node="cheeseburger">
+ <associate node="cheese" />
+ </collection>
+ </event>
+ </message>""")
+
+ 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.checkMessage(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <collection node="cheeseburger">
+ <disassociate node="cheese" />
+ </collection>
+ </event>
+ </message>""")
+
+ 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.checkMessage(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <configuration node="cheese">
+ <x xmlns="jabber:x:data" type="form">
+ <field var="pubsub#title" type="text-single">
+ <value>This thing is awesome</value>
+ </field>
+ </x>
+ </configuration>
+ </event>
+ </message>""")
+
+ def testEventPurge(self):
+ """Testing message/pubsub_event/purge"""
+ msg = self.Message()
+ msg['pubsub_event']['purge']['node'] = 'pickles'
+ msg['type'] = 'headline'
+ self.checkMessage(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <purge node="pickles" />
+ </event>
+ </message>""")
+
+ 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.checkMessage(msg, """
+ <message type="headline">
+ <event xmlns="http://jabber.org/protocol/pubsub#event">
+ <subscription node="pickles" subid="aabb1122" jid="fritzy@netflint.net/test" subscription="subscribed" expiry="presence" />
+ </event>
+ </message>""")
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubStanzas)
diff --git a/tests/test_roster.py b/tests/test_roster.py
new file mode 100644
index 00000000..6f9fa3d6
--- /dev/null
+++ b/tests/test_roster.py
@@ -0,0 +1,84 @@
+from . sleektest 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.checkIq(iq, """
+ <iq>
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" name="User" subscription="both">
+ <group>Friends</group>
+ <group>Coworkers</group>
+ </item>
+ <item jid="otheruser@example.com" name="Other User"
+ subscription="both" />
+ </query>
+ </iq>
+ """)
+
+ def testGetItems(self):
+ """Test retrieving items from a roster stanza."""
+ xml_string = """
+ <iq>
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" name="User" subscription="both">
+ <group>Friends</group>
+ <group>Coworkers</group>
+ </item>
+ <item jid="otheruser@example.com" name="Other User"
+ subscription="both" />
+ </query>
+ </iq>
+ """
+ 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 = """
+ <iq>
+ <query xmlns="jabber:iq:roster">
+ <item jid="user@example.com" name="User" subscription="both">
+ <group>Friends</group>
+ <group>Coworkers</group>
+ </item>
+ <item jid="otheruser@example.com" name="Other User"
+ subscription="both" />
+ </query>
+ </iq>
+ """
+ iq = self.Iq(ET.fromstring(xml_string))
+ del iq['roster']['items']
+ self.checkIq(iq, """
+ <iq>
+ <query xmlns="jabber:iq:roster" />
+ </iq>
+ """)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestRosterStanzas)
diff --git a/tests/test_stream.py b/tests/test_stream.py
new file mode 100644
index 00000000..6e240747
--- /dev/null
+++ b/tests/test_stream.py
@@ -0,0 +1,34 @@
+from . sleektest import *
+import sleekxmpp.plugins.xep_0033 as xep_0033
+
+
+class TestStreamTester(SleekTest):
+ """
+ Test that we can simulate and test a stanza stream.
+ """
+
+ def setUp(self):
+ self.streamStart()
+
+ def tearDown(self):
+ self.streamClose()
+
+ def testEcho(self):
+ def echo(msg):
+ msg.reply('Thanks for sending: %(body)s' % msg).send()
+
+ self.xmpp.add_event_handler('message', echo)
+
+ self.streamRecv("""
+ <message to="tester@localhost" from="user@localhost">
+ <body>Hi!</body>
+ </message>
+ """)
+
+ self.streamSendMessage("""
+ <message to="user@localhost">
+ <body>Thanks for sending: Hi!</body>
+ </message>
+ """)
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamTester)
diff --git a/tests/test_tostring.py b/tests/test_tostring.py
new file mode 100644
index 00000000..2999949a
--- /dev/null
+++ b/tests/test_tostring.py
@@ -0,0 +1,104 @@
+from . sleektest import *
+from sleekxmpp.stanza import Message
+from sleekxmpp.xmlstream.stanzabase import ET
+from sleekxmpp.xmlstream.tostring import tostring, xml_escape
+
+
+class TestToString(SleekTest):
+
+ """
+ Test the implementation of sleekxmpp.xmlstream.tostring
+ """
+
+ 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 = """<foo bar="baz">'Hi & welcome!'</foo>"""
+ escaped = xml_escape(original)
+ desired = """&lt;foo bar=&quot;baz&quot;&gt;&apos;Hi"""
+ desired += """ &amp; welcome!&apos;&lt;/foo&gt;"""
+
+ 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='<bar xmlns="foo" />',
+ message="Empty element not serialized correctly")
+
+ def testEmptyElementWrapped(self):
+ """Test converting an empty element inside another element."""
+ self.tryTostring(
+ original='<bar xmlns="foo"><baz /></bar>',
+ message="Wrapped empty element not serialized correctly")
+
+ def testEmptyElementWrappedText(self):
+ """
+ Test converting an empty element wrapped with text
+ inside another element.
+ """
+ self.tryTostring(
+ original='<bar xmlns="foo">Some text. <baz /> More text.</bar>',
+ message="Text wrapped empty element serialized incorrectly")
+
+ def testMultipleChildren(self):
+ """Test converting multiple child elements to a Unicode string."""
+ self.tryTostring(
+ original='<bar xmlns="foo"><baz><qux /></baz><quux /></bar>',
+ 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='<bar xmlns="foo" />',
+ expected='<bar />',
+ message="The xmlns parameter was not used properly.",
+ xmlns='foo')
+
+ 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='<bar xmlns="foo" />',
+ expected='<bar />',
+ 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 = '<message><body>\xe0\xb2\xa0_\xe0\xb2\xa0</body></message>'
+ result = msg.__str__()
+ self.failUnless(result == expected,
+ "Stanza Unicode handling is incorrect: %s" % result)
+
+
+suite = unittest.TestLoader().loadTestsFromTestCase(TestToString)
diff --git a/tests/xmlcompare.py b/tests/xmlcompare.py
deleted file mode 100644
index d97af971..00000000
--- a/tests/xmlcompare.py
+++ /dev/null
@@ -1,28 +0,0 @@
-from xml.etree import cElementTree as ET
-
-def comparemany(xmls):
- xml1 = xmls[0]
- if type(xml1) == type(''):
- xml1 = ET.fromstring(xml1)
- for xml in xmls[1:]:
- xml2 = xml
- if type(xml2) == type(''):
- xml2 = ET.fromstring(xml2)
- if not compare(xml1, xml2): return False
- return True
-
-def compare(xml1, xml2):
- if xml1.tag != xml2.tag:
- return False
- if xml1.attrib != xml2.attrib:
- return False
- for child in xml1:
- child2s = xml2.findall("%s" % child.tag)
- if child2s is None:
- return False
- found = False
- for child2 in child2s:
- found = compare(child, child2)
- if found: break
- if not found: return False
- return True