summaryrefslogtreecommitdiff
path: root/sleekxmpp
diff options
context:
space:
mode:
authorNathan Fritz <nathan@andyet.net>2010-08-19 16:09:47 -0700
committerNathan Fritz <nathan@andyet.net>2010-08-19 16:09:47 -0700
commitd150b35464742de7af9b3105bc7eeb55171b96ee (patch)
treef1dce9115efbe4c85d37bc27d6ae35f1e937a043 /sleekxmpp
parent21b7109c06695955632692814fed11b3717e0fc7 (diff)
parente4240dd593207a5912de996c42451b3946f113b2 (diff)
downloadslixmpp-d150b35464742de7af9b3105bc7eeb55171b96ee.tar.gz
slixmpp-d150b35464742de7af9b3105bc7eeb55171b96ee.tar.bz2
slixmpp-d150b35464742de7af9b3105bc7eeb55171b96ee.tar.xz
slixmpp-d150b35464742de7af9b3105bc7eeb55171b96ee.zip
fixed todo merge
Diffstat (limited to 'sleekxmpp')
-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
56 files changed, 3603 insertions, 1980 deletions
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