diff --git a/INSTALL b/INSTALL index b73b5e5..f081a35 100644 --- a/INSTALL +++ b/INSTALL @@ -6,3 +6,6 @@ python3 setup.py install Root install: sudo python3 setup.py install + +To test: +python example.py -v -j [USER@example.com] -p [PASSWORD] diff --git a/LICENSE b/LICENSE index 59c501b..fb9f977 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010 ICRL +Copyright (c) 2010 Nathanael C. Fritz Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index 3a39d83..0000000 --- a/MANIFEST +++ /dev/null @@ -1,39 +0,0 @@ -setup.py -sleekxmpp/__init__.py -sleekxmpp/basexmpp.py -sleekxmpp/clientxmpp.py -sleekxmpp/example.py -sleekxmpp/plugins/__init__.py -sleekxmpp/plugins/base.py -sleekxmpp/plugins/gmail_notify.py -sleekxmpp/plugins/xep_0004.py -sleekxmpp/plugins/xep_0009.py -sleekxmpp/plugins/xep_0030.py -sleekxmpp/plugins/xep_0045.py -sleekxmpp/plugins/xep_0050.py -sleekxmpp/plugins/xep_0060.py -sleekxmpp/plugins/xep_0078.py -sleekxmpp/plugins/xep_0086.py -sleekxmpp/plugins/xep_0092.py -sleekxmpp/plugins/xep_0199.py -sleekxmpp/stanza/__init__.py -sleekxmpp/stanza/iq.py -sleekxmpp/stanza/message.py -sleekxmpp/stanza/presence.py -sleekxmpp/xmlstream/__init__.py -sleekxmpp/xmlstream/stanzabase.py -sleekxmpp/xmlstream/statemachine.py -sleekxmpp/xmlstream/test.py -sleekxmpp/xmlstream/testclient.py -sleekxmpp/xmlstream/xmlstream.py -sleekxmpp/xmlstream/handler/__init__.py -sleekxmpp/xmlstream/handler/base.py -sleekxmpp/xmlstream/handler/callback.py -sleekxmpp/xmlstream/handler/waiter.py -sleekxmpp/xmlstream/handler/xmlcallback.py -sleekxmpp/xmlstream/handler/xmlwaiter.py -sleekxmpp/xmlstream/matcher/__init__.py -sleekxmpp/xmlstream/matcher/base.py -sleekxmpp/xmlstream/matcher/many.py -sleekxmpp/xmlstream/matcher/xmlmask.py -sleekxmpp/xmlstream/matcher/xpath.py diff --git a/README b/README index abc2d09..da9fe8c 100644 --- a/README +++ b/README @@ -4,6 +4,11 @@ Hosted at http://wiki.github.com/fritzy/SleekXMPP/ Featured in examples in XMPP: The Definitive Guide by Kevin Smith, Remko Tronçon, and Peter Saint-Andre If you're coming here from The Definitive Guide, please read http://wiki.github.com/fritzy/SleekXMPP/xmpp-the-definitive-guide +Requirements: +We try to keep requirements to a minimum, but we suggest that you install http://dnspython.org although it isn't strictly required. +If you do not install this library, you may need to specify the server/port for services that use SRV records (like GTalk). +"sudo pip install dnspython" on a *nix system with pip installed. + SleekXMPP has several design goals/philosophies: - Low number of dependencies. - Every XEP as a plugin. diff --git a/example.py b/example.py index c9b6559..4eb88b3 100644 --- a/example.py +++ b/example.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python # coding=utf8 import sleekxmpp @@ -8,41 +9,46 @@ import time import sys if sys.version_info < (3,0): - reload(sys) - sys.setdefaultencoding('utf8') + reload(sys) + sys.setdefaultencoding('utf8') class Example(sleekxmpp.ClientXMPP): - - def __init__(self, jid, password): - sleekxmpp.ClientXMPP.__init__(self, jid, password) - self.add_event_handler("session_start", self.start) - self.add_event_handler("message", self.message) - - def start(self, event): - self.getRoster() - self.sendPresence() + + def __init__(self, jid, password): + sleekxmpp.ClientXMPP.__init__(self, jid, password) + self.add_event_handler("session_start", self.start) + self.add_event_handler("message", self.message) + + def start(self, event): + self.getRoster() + self.sendPresence() - def message(self, msg): - msg.reply("Thanks for sending\n%(body)s" % msg).send() + def message(self, msg): + msg.reply("Thanks for sending\n%(body)s" % msg).send() if __name__ == '__main__': - #parse command line arguements - optp = OptionParser() - optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO) - optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO) - optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO) - optp.add_option("-c","--config", dest="configfile", default="config.xml", help="set config file to use") - opts,args = optp.parse_args() - - logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') - xmpp = Example('user@gmail.com/sleekxmpp', 'password') - xmpp.registerPlugin('xep_0030') - xmpp.registerPlugin('xep_0004') - xmpp.registerPlugin('xep_0060') - xmpp.registerPlugin('xep_0199') - if xmpp.connect(('talk.google.com', 5222)): - xmpp.process(threaded=False) - print("done") - else: - print("Unable to connect.") + #parse command line arguements + optp = OptionParser() + optp.add_option('-q','--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO) + optp.add_option('-d','--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO) + optp.add_option('-v','--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO) + optp.add_option("-j","--jid", dest="jid", help="JID to use") + optp.add_option("-p","--password", dest="password", help="password to use") + opts,args = optp.parse_args() + + logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') + xmpp = Example(opts.jid, opts.password) + xmpp.registerPlugin('xep_0030') + xmpp.registerPlugin('xep_0004') + xmpp.registerPlugin('xep_0060') + xmpp.registerPlugin('xep_0199') + + # use this if you don't have pydns, and want to + # talk to GoogleTalk (e.g.) +# if xmpp.connect(('talk.google.com', 5222)): + if xmpp.connect(): + xmpp.process(threaded=False) + print("done") + else: + print("Unable to connect.") diff --git a/setup.py b/setup.py index 280ec3c..e3acf18 100644 --- a/setup.py +++ b/setup.py @@ -16,13 +16,13 @@ import sys # min_version = '0.6c6' # else: # min_version = '0.6a9' -# +# # try: # use_setuptools(min_version=min_version) # except TypeError: # # locally installed ez_setup won't have min_version # use_setuptools() -# +# # from setuptools import setup, find_packages, Extension, Feature VERSION = '0.2.3.1' @@ -37,17 +37,13 @@ CLASSIFIERS = [ 'Intended Audience :: Developers', 'Topic :: Software Development :: Libraries :: Python Modules', ] -packages = [ 'sleekxmpp', +packages = [ 'sleekxmpp', 'sleekxmpp/plugins', 'sleekxmpp/stanza', 'sleekxmpp/xmlstream', 'sleekxmpp/xmlstream/matcher', - 'sleekxmpp/xmlstream/handler' ] - -if sys.version_info < (3, 0): - packages.append('sleekxmpp/xmlstream/tostring26') -else: - packages.append('sleekxmpp/xmlstream/tostring') + 'sleekxmpp/xmlstream/handler', + 'sleekxmpp/xmlstream/tostring'] setup( name = "sleekxmpp", diff --git a/sleekxmpp/__init__.py b/sleekxmpp/__init__.py index ccb4352..d2f5765 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 = """""" % (self.server,self.default_ns) - self.stream_footer = "" - #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("" % self.default_ns), self._handlePresenceSubscribe, thread=True)) - self.registerFeature("", self.handler_starttls, True) - self.registerFeature("", self.handler_sasl_auth, True) - self.registerFeature("", self.handler_bind_resource) - self.registerFeature("", 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 = """""" % (self.server,self.default_ns) + self.stream_footer = "" + #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("" % self.default_ns), self._handlePresenceSubscribe, thread=True)) + self.registerFeature("", self.handler_starttls, True) + self.registerFeature("", self.handler_sasl_auth, True) + self.registerFeature("", self.handler_bind_resource) + self.registerFeature("", 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("", 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("", self.handler_auth_success, instream=True) - self.add_handler("", 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("""%s""" % base64.b64encode(b'\x00' + bytes(self.username) + b'\x00' + bytes(self.password)).decode('utf-8')) - else: - self.send("""%s""" % 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("", 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("", self.handler_auth_success, name='SASL Sucess', instream=True) + self.add_handler("", 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("""%s""" % base64.b64encode(b'\x00' + bytes(self.username) + b'\x00' + bytes(self.password)).decode('utf-8')) + else: + self.send("""%s""" % 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 907067f..b7b605b 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) + registerStanzaPlugin(Iq, Roster) + registerStanzaPlugin(Message, Nick) + registerStanzaPlugin(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 - 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 de12581..5534a45 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 = "" % jid - self.stream_footer = "" - 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 = "" % jid + self.stream_footer = "" + 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 5b761cf..40217ef 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 1868365..b51977b 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 4223646..a5260b0 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 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. """ + + class base_plugin(object): def __init__(self, xmpp, config): diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py index b709ef6..7e44234 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: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. - SleekXMPP is free software; you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation; either version 2 of the License, or - (at your option) any later version. - - SleekXMPP is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with SleekXMPP; if not, write to the Free Software - Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + See the file LICENSE 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("" % 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 0000000..651408a --- /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("", 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 1a1526f..96d02f9 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 015bd8b..037fc09 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 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 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: + fields.append((field['var'], field)) + return fields + + def getInstructions(self): + instructions = '' + instsXML = self.xml.findall('{%s}instructions' % self.namespace) + return "\n".join([instXML.text for instXML in instsXML]) + + def getItems(self): + items = [] + itemsXML = self.xml.findall('{%s}item' % self.namespace) + for itemXML in itemsXML: + item = {} + fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + item[field['var']] = field['value'] + items.append(item) + return items + + def getReported(self): + fields = {} + fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, + FormField.namespace)) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + fields[field['var']] = field + return fields + + def getValues(self): + values = {} + fields = self.getFields(use_dict=True) + for var in fields: + values[var] = fields[var]['value'] + return values + + def reply(self): + if self['type'] == 'form': + self['type'] = 'submit' + elif self['type'] == 'submit': + self['type'] = 'result' + + def setFields(self, fields, default=None): + del self['fields'] + for field_data in fields: + var = field_data[0] + field = field_data[1] + field['var'] = var + + self.addField(**field) + + def setInstructions(self, instructions): + del self['instructions'] + if instructions in [None, '']: + return + instructions = instructions.split('\n') + for instruction in instructions: + inst = ET.Element('{%s}instructions' % self.namespace) + inst.text = instruction + self.xml.append(inst) + + def setItems(self, items): + for item in items: + self.addItem(item) + + def setReported(self, reported, default=None): + for var in reported: + field = reported[var] + field['var'] = var + self.addReported(var, **field) + + def setValues(self, values): + fields = self.getFields(use_dict=True) + for field in values: + fields[field]['value'] = values[field] + + +class FormField(ElementBase): + namespace = 'jabber:x:data' + name = 'field' + plugin_attrib = 'field' + interfaces = set(('answer', 'desc', 'required', 'value', 'options', 'label', 'type', 'var')) + sub_interfaces = set(('desc',)) + field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', + 'list-single', 'text-multi', 'text-private', 'text-single')) + multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi')) + multi_line_types = set(('hidden', 'text-multi')) + option_types = set(('list-multi', 'list-single')) + true_values = set((True, '1', 'true')) + + def addOption(self, label='', value=''): + if self['type'] in self.option_types: + opt = FieldOption(parent=self) + opt['label'] = label + opt['value'] = value + else: + raise ValueError("Cannot add options to a %s field." % self['type']) + + def delOptions(self): + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + self.xml.remove(optXML) + + def delRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + if reqXML is not None: + self.xml.remove(reqXML) + + def delValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + for valXML in valsXML: + self.xml.remove(valXML) + + def getAnswer(self): + return self.getValue() + + def getOptions(self): + options = [] + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + opt = FieldOption(xml=optXML) + options.append({'label': opt['label'], 'value':opt['value']}) + return options + + def getRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + return reqXML is not None + + def getValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + if len(valsXML) == 0: + return None + elif self['type'] == 'boolean': + return valsXML[0].text in self.true_values + elif self['type'] in self.multi_value_types: + values = [] + for valXML in valsXML: + if valXML.text is None: + valXML.text = '' + values.append(valXML.text) + if self['type'] == 'text-multi': + values = "\n".join(values) + return values + else: + return valsXML[0].text + + def setAnswer(self, answer): + self.setValue(answer) + + def setFalse(self): + self.setValue(False) + + def setOptions(self, options): + for value in options: + if isinstance(value, dict): + self.addOption(**value) + else: + self.addOption(value=value) + + def setRequired(self, required): + exists = self.getRequired() + if not exists and required: + self.xml.append(ET.Element('{%s}required' % self.namespace)) + elif exists and not required: + self.delRequired() + + def setTrue(self): + self.setValue(True) + + def setValue(self, value): + self.delValue() + valXMLName = '{%s}value' % self.namespace + + if self['type'] == 'boolean': + if value in self.true_values: + valXML = ET.Element(valXMLName) + valXML.text = '1' + self.xml.append(valXML) + else: + valXML = ET.Element(valXMLName) + valXML.text = '0' + self.xml.append(valXML) + elif self['type'] in self.multi_value_types or self['type'] in ['', None]: + if self['type'] in self.multi_line_types and isinstance(value, str): + value = value.split('\n') + if not isinstance(value, list): + value = [value] + for val in value: + if self['type'] in ['', None] and val in self.true_values: + val = '1' + valXML = ET.Element(valXMLName) + valXML.text = val + self.xml.append(valXML) + else: + if isinstance(value, list): + raise ValueError("Cannot add multiple values to a %s field." % self['type']) + valXML = ET.Element(valXMLName) + valXML.text = value + self.xml.append(valXML) + + +class FieldOption(ElementBase): + namespace = 'jabber:x:data' + name = 'option' + plugin_attrib = 'option' + interfaces = set(('label', 'value')) + sub_interfaces = set(('value',)) + class xep_0004(base.base_plugin): - + """ + XEP-0004: Data Forms + """ + def plugin_init(self): self.xep = '0004' self.description = 'Data Forms' - self.xmpp.add_handler("", self.handler_message_xform) + + 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 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 = [] - - 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 - + 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 49ffac4..625b03f 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("", self._callMethod) - self.xmpp.add_handler("", self._callResult) - self.xmpp.add_handler("", self._callError) + self.xmpp.add_handler("", + self._callMethod, name='Jabber RPC Call') + self.xmpp.add_handler("", + self._callResult, name='Jabber RPC Result') + self.xmpp.add_handler("", + self._callError, name='Jabber RPC Error') self.entries = {} self.activeCalls = [] diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py index 6a31d24..a9d8d6a 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 0000000..ea0b10b --- /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 937c6f9..1892eea 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("" % self.xmpp.default_ns), self.handle_groupchat_presence)) self.xmpp.registerHandler(Callback('MUCMessage', MatchXMLMask("" % 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 2f356e1..5efb911 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("" % self.xmpp.default_ns, self.handler_command) - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command) - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_next, threaded=True) - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_cancel) - self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_complete) + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc None') + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command, name='Ad-Hoc Execute') + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_next, name='Ad-Hoc Next', threaded=True) + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_cancel, name='Ad-Hoc Cancel') + self.xmpp.add_handler("" % self.xmpp.default_ns, self.handler_command_complete, name='Ad-Hoc Complete') self.commands = {} self.sessions = {} self.sd = self.xmpp.plugin['xep_0030'] @@ -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 bff158a..a92a384 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 f873290..4b3ab82 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 0000000..b7b5d6d --- /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 aeebbe0..ca02c4a 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("" % self.xmpp.default_ns, self.report_version) + self.xmpp.add_handler("" % 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 0000000..824977b --- /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 ccaf0b3..3fc62f5 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("" % self.xmpp.default_ns, self.handler_ping) + self.xmpp.add_handler("" % 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 c3d8a31..8302c43 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 5e82cb9..9df85a2 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 ee46722..6d18c29 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 stanza that + describes the nature of the error and how it should be handled. + + Use the 'XEP-0086: Error Condition Mappings' plugin to include error + codes used in older XMPP versions. + + Example error stanza: + + + + The item was not found. + + + + Stanza Interface: + code -- The error code used in older XMPP versions. + condition -- The name of the condition element. + text -- Human readable description of the error. + type -- Error type indicating how the error should be handled. + + Attributes: + conditions -- The set of allowable error condition elements. + condition_ns -- The namespace for the condition element. + types -- A set of values indicating how the error + should be treated. + + Methods: + setup -- Overrides ElementBase.setup. + getCondition -- Retrieve the name of the condition element. + setCondition -- Add a condition element. + delCondition -- Remove the condition element. + getText -- Retrieve the contents of the element. + setText -- Set the contents of the element. + delText -- Remove the element. + """ + + namespace = 'jabber:client' + name = 'error' + plugin_attrib = 'error' + interfaces = set(('code', 'condition', 'text', 'type')) + sub_interfaces = set(('text',)) + conditions = set(('bad-request', 'conflict', 'feature-not-implemented', + 'forbidden', 'gone', 'internal-server-error', + 'item-not-found', 'jid-malformed', 'not-acceptable', + 'not-allowed', 'not-authorized', 'payment-required', + 'recipient-unavailable', 'redirect', + 'registration-required', 'remote-server-not-found', + 'remote-server-timeout', 'resource-constraint', + 'service-unavailable', 'subscription-required', + 'undefined-condition', 'unexpected-request')) + condition_ns = 'urn:ietf:params:xml:ns:xmpp-stanzas' + types = set(('cancel', 'continue', 'modify', 'auth', 'wait')) + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup. + + Sets a default error type and condition, and changes the + parent stanza's type to 'error'. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + if ElementBase.setup(self, xml): + #If we had to generate XML then set default values. + self['type'] = 'cancel' + self['condition'] = 'feature-not-implemented' + if self.parent is not None: + self.parent()['type'] = 'error' + + def 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 element.""" + return self._getSubText('{%s}text' % self.condition_ns) + + def setText(self, value): + """ + Set the contents of the element. + + Arguments: + value -- The new contents for the element. + """ + self._setSubText('{%s}text' % self.condition_ns, text=value) + return self + + def delText(self): + """Remove the element.""" + self._delSub('{%s}text' % self.condition_ns) + return self diff --git a/sleekxmpp/stanza/htmlim.py b/sleekxmpp/stanza/htmlim.py index 60686e4..c2f2f0c 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 stanza so that lightweight markup can be used + to format the message contents and to create links. + + Only a subset of XHTML is recommended for use with XHTML-IM. + See the full spec at 'http://xmpp.org/extensions/xep-0071.html' + for more information. + + Example stanza: + + Non-html message content. + + +

HTML!

+ + +
+ + Stanza Interface: + body -- The contents of the HTML body tag. + + Methods: + 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 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 ded7515..c5ef8bb 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 stanzas, or info/query stanzas, are XMPP's method of + requesting and modifying information, similar to HTTP's GET and + POST methods. + + Each stanza must have an 'id' value which associates the + stanza with the response stanza. XMPP entities must always + be given a response stanza with a type of 'result' after + sending a stanza of type 'get' or 'set'. + + Most uses cases for stanzas will involve adding a + element whose namespace indicates the type of information + desired. However, some custom XMPP applications use stanzas + as a carrier stanza for an application-specific protocol instead. + + Example Stanzas: + + + + + + + + Friends + + + + + Stanza Interface: + query -- The namespace of the element if one exists. + + Attributes: + types -- May be one of: get, set, result, or error. + + Methods: + __init__ -- Overrides StanzaBase.__init__. + unhandled -- Send error if there are no handlers. + setPayload -- Overrides StanzaBase.setPayload. + setQuery -- Add or modify a element. + getQuery -- Return the namespace of the element. + delQuery -- Remove the element. + reply -- Overrides StanzaBase.reply + send -- Overrides StanzaBase.send + """ + + namespace = 'jabber:client' + name = 'iq' + interfaces = set(('type', 'to', 'from', 'id', 'query')) + types = set(('get', 'result', 'set', 'error')) + plugin_attrib = name + + def __init__(self, *args, **kwargs): + """ + Initialize a new stanza with an 'id' value. + + Overrides StanzaBase.__init__. + """ + StanzaBase.__init__(self, *args, **kwargs) + if self['id'] == '': + if self.stream is not None: + self['id'] = self.stream.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 stanza. + + Arguments: + value -- An XML object to use as the stanza's contents + """ + self.clear() + StanzaBase.setPayload(self, value) + return self + + def setQuery(self, value): + """ + Add or modify a element. + + Query elements are differentiated by their namespace. + + Arguments: + value -- The namespace of the element. + """ + query = self.xml.find("{%s}query" % value) + if query is None and value: + self.clear() + query = ET.Element("{%s}query" % value) + self.xml.append(query) + return self + + def getQuery(self): + """Return the namespace of the element.""" + for child in self.xml.getchildren(): + if child.tag.endswith('query'): + ns = child.tag.split('}')[0] + if '{' in ns: + ns = ns[1:] + return ns + return '' + + def delQuery(self): + """Remove the element.""" + for child in self.xml.getchildren(): + if child.tag.endswith('query'): + self.xml.remove(child) + return self + + def reply(self): + """ + Send a reply 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 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 3834180..560e1d4 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 stanzas are a "push" mechanism to send information + to other XMPP entities without requiring a response. + + Chat clients will typically use stanzas that have a type + of either "chat" or "groupchat". + + When handling a message event, be sure to check if the message is + an error response. + + Example stanzas: + + Hi! + + + + Hi everyone! + + + Stanza Interface: + body -- The main contents of the message. + subject -- An optional description of the message's contents. + mucroom -- (Read-only) The name of the MUC room that sent the message. + mucnick -- (Read-only) The MUC nickname of message's sender. + + Attributes: + types -- May be one of: normal, chat, headline, groupchat, or error. + + Methods: + 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 ac7e360..de54b30 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 element + in several stanza types, including and stanzas. + + The nickname contained in a should be the global, friendly or + informal name chosen by the owner of a bare JID. The element + may be included when establishing communications with new entities, + such as normal XMPP users or MUC services. + + The nickname contained in a element will not necessarily be + the same as the nickname used in a MUC. + + Example stanzas: + + The User + ... + + + + The User + + + Stanza Interface: + nick -- A global, friendly or informal name chosen by a user. + + Methods: + getNick -- Return the nickname in the element. + setNick -- Add a element with the given nickname. + delNick -- Remove the element. + """ + + namespace = 'http://jabber.org/nick/nick' + name = 'nick' + plugin_attrib = name + interfaces = set(('nick',)) + + def setNick(self, nick): + """ + Add a element with the given nickname. + + Arguments: + nick -- A human readable, informal name. + """ + self.xml.text = nick + + def getNick(self): + """Return the nickname in the element.""" + return self.xml.text + + def delNick(self): + """Remove the 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 c66246c..651bf34 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) + """ + XMPP's stanza allows entities to know the status of other + clients and components. Since it is currently the only multi-cast + stanza in XMPP, many extensions add more information to + stanzas to broadcast to every entry in the roster, such as + capabilities, music choices, or locations (XEP-0115: Entity Capabilities + and XEP-0163: Personal Eventing Protocol). - 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 + Since stanzas are broadcast when an XMPP entity changes + its status, the bulk of the traffic in an XMPP network will be from + stanzas. Therefore, do not include more information than + necessary in a status message or within a stanza in order + to help keep the network running smoothly. - 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) + Example stanzas: + + + + away + Getting lunch. + 5 + + + + + + + Stanza Interface: + priority -- A value used by servers to determine message routing. + show -- The type of status, such as away or available for chat. + status -- Custom, human readable status message. + + Attributes: + types -- One of: available, unavailable, error, probe, + subscribe, subscribed, unsubscribe, + and unsubscribed. + showtypes -- One of: away, chat, dnd, and xa. + + Methods: + reply -- Overrides StanzaBase.reply + setShow -- Set the value of the element. + getType -- Get the value of the type attribute or element. + setType -- Set the value of the type attribute or element. + getPriority -- Get the value of the element. + setPriority -- Set the value of the element. + """ + + namespace = 'jabber:client' + name = 'presence' + interfaces = set(('type', 'to', 'from', 'id', 'show', + 'status', 'priority')) + sub_interfaces = set(('show', 'status', 'priority')) + plugin_attrib = name + + types = set(('available', 'unavailable', 'error', 'probe', 'subscribe', + 'subscribed', 'unsubscribe', 'unsubscribed')) + showtypes = set(('dnd', 'chat', 'xa', 'away')) + + def setShow(self, show): + """ + Set the value of the 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 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 element as an integer. + """ + p = self._getSubText('priority') + if not p: + p = 0 + return int(p) + + def getType(self): + """ + Return the value of the stanza's type attribute, or + the value of the 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 3b4822d..eafc79a 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() + """ + A top-level XMPP stanza in an XMLStream. -# 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 + 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 1fefc18..292c895 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: + + + + Friends + + + + + Stanza Inteface: + items -- A dictionary of roster entries contained + in the stanza. + + Methods: + getItems -- Return a dictionary of roster entries. + setItems -- Add elements. + delItems -- Remove all elements. + """ + + namespace = 'jabber:iq:roster' + name = 'query' + plugin_attrib = 'roster' + interfaces = set(('items',)) + + def setItems(self, items): + """ + Set the roster entries in the stanza. + + Uses a dictionary using JIDs as keys, where each entry is itself + a dictionary that contains: + name -- An alias or nickname for the JID. + subscription -- The subscription type. Can be one of 'to', + 'from', 'both', 'none', or 'remove'. + groups -- A list of group names to which the JID + has been assigned. + + Arguments: + items -- A dictionary of roster entries. + """ + self.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 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 ed9dd5c..24855c9 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("", self.pubsubEventHandler, threaded=True) + self.add_handler("", self.pubsubEventHandler, name='Pubsub Event', threaded=True) self.add_event_handler("session_start", self.start, threaded=True) - self.add_handler("", self.handleError) + self.add_handler("", 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 e69de29..c82ab34 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 f60c5b8..07b395d 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 e69de29..50e286a 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 5d55f4e..720846d 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 49cfa14..889b0aa 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 c85a0c4..7c4330a 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 632c142..67879df 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 2344403..cf90751 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 0000000..292abd9 --- /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 e69de29..91cb8d6 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 8185bdc..51da094 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 bb858fc..43972c2 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 cf860e6..ff0c4e4 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 bd091c0..e315445 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 eba3e95..89fd642 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 f6d0424..7f3d20b 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 64020c8..83a8ddf 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 -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 +from sleekxmpp.xmlstream import JID +from sleekxmpp.xmlstream.tostring import tostring - @property - def attrib(self): #backwards compatibility - return self +# Used to check if an argument is an XML object. +XML_TYPE = type(ET.Element('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 registerStanzaPlugin(stanza, plugin): + """ + Associate a stanza object as a plugin for another stanza. - 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) + 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 - 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) +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 + + 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() + 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 __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 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__()) + 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 fb7d150..8a1aa22 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 0000000..c7f76e7 --- /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 6603cbb..5852cba 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("" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%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] = '&' - elif c == '<': - text[cc] = '<' - elif c == '>': - text[cc] = '>' - elif c == "'": - text[cc] = ''' - else: - text[cc] = '"' - 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 0000000..c269632 --- /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("" % tag_name) + elif xml.text: + # If we only have text content. + output.append(">%s" % (xml_escape(xml.text), tag_name)) + else: + # Empty element. + output.append(" />") + if xml.tail: + # If there is additional text after the element. + output.append(xml_escape(xml.tail)) + return ''.join(output) + + +def xml_escape(text): + """ + Convert special characters in XML to escape sequences. + + Arguments: + text -- The XML text to convert. + """ + text = list(text) + escapes = {'&': '&', + '<': '<', + '>': '>', + "'": ''', + '"': '"'} + for i, c in enumerate(text): + text[i] = escapes.get(c, c) + return ''.join(text) diff --git a/sleekxmpp/xmlstream/tostring/tostring26.py b/sleekxmpp/xmlstream/tostring/tostring26.py new file mode 100644 index 0000000..7a37637 --- /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"" % 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" % (xml_escape(xml.text), tag_name)) + else: + # Empty element. + output.append(u" />") + if xml.tail: + # If there is additional text after the element. + output.append(xml_escape(xml.tail)) + return u''.join(output) + + +def xml_escape(text): + """ + Convert special characters in XML to escape sequences. + + Arguments: + text -- The XML text to convert. + """ + if type(text) != types.UnicodeType: + text = list(unicode(text, 'utf-8', 'ignore')) + else: + text = list(text) + escapes = {u'&': u'&', + u'<': u'<', + u'>': u'>', + u"'": u''', + u'"': u'"'} + for i, c in enumerate(text): + text[i] = escapes.get(c, c) + return u''.join(text) diff --git a/sleekxmpp/xmlstream/tostring26/__init__.py b/sleekxmpp/xmlstream/tostring26/__init__.py deleted file mode 100644 index 9711c30..0000000 --- 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"" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%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'&' - elif c == u'<': - text[cc] = u'<' - elif c == u'>': - text[cc] = u'>' - elif c == u"'": - text[cc] = u''' - else: - text[cc] = u'"' - cc += 1 - return ''.join(text) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 6b92abc..bf39bb3 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 = "" self.stream_footer = "" @@ -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("" % (itag, )) - if xml.tail: - newoutput.append(self.xmlesc(xml.tail)) - elif xml.text: - newoutput.append(">%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] = '&' - elif c == '<': - text[cc] = '<' - elif c == '>': - text[cc] = '>' - elif c == "'": - text[cc] = ''' - elif self.escape_quotes: - text[cc] = '"' - cc += 1 - return ''.join(text) - def start_stream_handler(self, xml): """Meant to be overridden""" pass diff --git a/testall.py b/testall.py index bf0b4c7..d3d049e 100644 --- a/testall.py +++ b/testall.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.6 +#!/usr/bin/env python import unittest import logging import sys @@ -21,7 +21,7 @@ class testoverall(unittest.TestCase): self.failIf(tabnanny.check("." + os.sep + 'sleekxmpp')) #raise "Help!" - def testMethodLength(self): + def disabled_testMethodLength(self): """Testing for excessive method lengths""" import re dirs = os.walk(sys.path[0] + os.sep + 'sleekxmpp') diff --git a/tests/sleektest.py b/tests/sleektest.py new file mode 100644 index 0000000..801253d --- /dev/null +++ b/tests/sleektest.py @@ -0,0 +1,519 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import unittest +import socket +try: + import queue +except ImportError: + import Queue as queue + +import sleekxmpp +from sleekxmpp import ClientXMPP +from sleekxmpp.stanza import Message, Iq, Presence +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ET +from sleekxmpp.xmlstream.tostring import tostring + + +class TestSocket(object): + + """ + A dummy socket that reads and writes to queues instead + of an actual networking socket. + + Methods: + nextSent -- Return the next sent stanza. + recvData -- Make a stanza available to read next. + recv -- Read the next stanza from the socket. + send -- Write a stanza to the socket. + makefile -- Dummy call, returns self. + read -- Read the next stanza from the socket. + """ + + def __init__(self, *args, **kwargs): + """ + Create a new test socket. + + Arguments: + Same as arguments for socket.socket + """ + self.socket = socket.socket(*args, **kwargs) + self.recv_queue = queue.Queue() + self.send_queue = queue.Queue() + + def __getattr__(self, name): + """ + Return attribute values of internal, dummy socket. + + Some attributes and methods are disabled to prevent the + socket from connecting to the network. + + Arguments: + name -- Name of the attribute requested. + """ + + def dummy(*args): + """Method to do nothing and prevent actual socket connections.""" + return None + + overrides = {'connect': dummy, + 'close': dummy, + 'shutdown': dummy} + + return overrides.get(name, getattr(self.socket, name)) + + # ------------------------------------------------------------------ + # Testing Interface + + def nextSent(self, timeout=None): + """ + Get the next stanza that has been 'sent'. + + Arguments: + timeout -- Optional timeout for waiting for a new value. + """ + args = {'block': False} + if timeout is not None: + args = {'block': True, 'timeout': timeout} + try: + return self.send_queue.get(**args) + except: + return None + + def recvData(self, data): + """ + Add data to the receiving queue. + + Arguments: + data -- String data to 'write' to the socket to be received + by the XMPP client. + """ + self.recv_queue.put(data) + + # ------------------------------------------------------------------ + # Socket Interface + + def recv(self, *args, **kwargs): + """ + Read a value from the received queue. + + Arguments: + Placeholders. Same as for socket.Socket.recv. + """ + return self.read(block=True) + + def send(self, data): + """ + Send data by placing it in the send queue. + + Arguments: + data -- String value to write. + """ + self.send_queue.put(data) + + # ------------------------------------------------------------------ + # File Socket + + def makefile(self, *args, **kwargs): + """ + File socket version to use with ElementTree. + + Arguments: + Placeholders, same as socket.Socket.makefile() + """ + return self + + def read(self, block=True, timeout=None, **kwargs): + """ + Implement the file socket interface. + + Arguments: + block -- Indicate if the read should block until a + value is ready. + timeout -- Time in seconds a block should last before + returning None. + """ + if timeout is not None: + block = True + try: + return self.recv_queue.get(block, timeout) + except: + return None + + +class SleekTest(unittest.TestCase): + + """ + A SleekXMPP specific TestCase class that provides + methods for comparing message, iq, and presence stanzas. + + Methods: + Message -- Create a Message stanza object. + Iq -- Create an Iq stanza object. + Presence -- Create a Presence stanza object. + checkMessage -- Compare a Message stanza against an XML string. + checkIq -- Compare an Iq stanza against an XML string. + checkPresence -- Compare a Presence stanza against an XML string. + streamStart -- Initialize a dummy XMPP client. + streamRecv -- Queue data for XMPP client to receive. + streamSendMessage -- Check that the XMPP client sent the given + Message stanza. + streamSendIq -- Check that the XMPP client sent the given + Iq stanza. + streamSendPresence -- Check taht the XMPP client sent the given + Presence stanza. + streamClose -- Disconnect the XMPP client. + fix_namespaces -- Add top-level namespace to an XML object. + compare -- Compare XML objects against each other. + """ + + # ------------------------------------------------------------------ + # Shortcut methods for creating stanza objects + + def Message(self, *args, **kwargs): + """ + Create a Message stanza. + + Uses same arguments as StanzaBase.__init__ + + Arguments: + xml -- An XML object to use for the Message's values. + """ + return Message(None, *args, **kwargs) + + def Iq(self, *args, **kwargs): + """ + Create an Iq stanza. + + Uses same arguments as StanzaBase.__init__ + + Arguments: + xml -- An XML object to use for the Iq's values. + """ + return Iq(None, *args, **kwargs) + + def Presence(self, *args, **kwargs): + """ + Create a Presence stanza. + + Uses same arguments as StanzaBase.__init__ + + Arguments: + xml -- An XML object to use for the Iq's values. + """ + return Presence(None, *args, **kwargs) + + # ------------------------------------------------------------------ + # Methods for comparing stanza objects to XML strings + + def checkStanza(self, stanza_class, stanza, xml_string, + defaults=None, use_values=True): + """ + Create and compare several stanza objects to a correct XML string. + + If use_values is False, test using getStanzaValues() and + setStanzaValues() will not be used. + + Some stanzas provide default values for some interfaces, but + these defaults can be problematic for testing since they can easily + be forgotten when supplying the XML string. A list of interfaces that + use defaults may be provided and the generated stanzas will use the + default values for those interfaces if needed. + + However, correcting the supplied XML is not possible for interfaces + that add or remove XML elements. Only interfaces that map to XML + attributes may be set using the defaults parameter. The supplied XML + must take into account any extra elements that are included by default. + + Arguments: + stanza_class -- The class of the stanza being tested. + stanza -- The stanza object to test. + xml_string -- A string version of the correct XML expected. + defaults -- A list of stanza interfaces that have default + values. These interfaces will be set to their + defaults for the given and generated stanzas to + prevent unexpected test failures. + use_values -- Indicates if testing using getStanzaValues() and + setStanzaValues() should be used. Defaults to + True. + """ + xml = ET.fromstring(xml_string) + + # Ensure that top level namespaces are used, even if they + # were not provided. + self.fix_namespaces(stanza.xml, 'jabber:client') + self.fix_namespaces(xml, 'jabber:client') + + stanza2 = stanza_class(xml=xml) + + if use_values: + # Using getStanzaValues() and setStanzaValues() will add + # XML for any interface that has a default value. We need + # to set those defaults on the existing stanzas and XML + # so that they will compare correctly. + default_stanza = stanza_class() + if defaults is None: + defaults = [] + for interface in defaults: + stanza[interface] = stanza[interface] + stanza2[interface] = stanza2[interface] + # Can really only automatically add defaults for top + # level attribute values. Anything else must be accounted + # for in the provided XML string. + if interface not in xml.attrib: + if interface in default_stanza.xml.attrib: + value = default_stanza.xml.attrib[interface] + xml.attrib[interface] = value + + values = stanza2.getStanzaValues() + stanza3 = stanza_class() + stanza3.setStanzaValues(values) + + debug = "Three methods for creating stanzas do not match.\n" + debug += "Given XML:\n%s\n" % tostring(xml) + debug += "Given stanza:\n%s\n" % tostring(stanza.xml) + debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml) + debug += "Second generated stanza:\n%s\n" % tostring(stanza3.xml) + result = self.compare(xml, stanza.xml, stanza2.xml, stanza3.xml) + else: + debug = "Two methods for creating stanzas do not match.\n" + debug += "Given XML:\n%s\n" % tostring(xml) + debug += "Given stanza:\n%s\n" % tostring(stanza.xml) + debug += "Generated stanza:\n%s\n" % tostring(stanza2.xml) + result = self.compare(xml, stanza.xml, stanza2.xml) + + self.failUnless(result, debug) + + def checkMessage(self, msg, xml_string, use_values=True): + """ + Create and compare several message stanza objects to a + correct XML string. + + If use_values is False, the test using getStanzaValues() and + setStanzaValues() will not be used. + + Arguments: + msg -- The Message stanza object to check. + xml_string -- The XML contents to compare against. + use_values -- Indicates if the test using getStanzaValues + and setStanzaValues should be used. Defaults + to True. + """ + + return self.checkStanza(Message, msg, xml_string, + defaults=['type'], + use_values = use_values) + + def checkIq(self, iq, xml_string, use_values=True): + """ + Create and compare several iq stanza objects to a + correct XML string. + + If use_values is False, the test using getStanzaValues() and + setStanzaValues() will not be used. + + Arguments: + iq -- The Iq stanza object to check. + xml_string -- The XML contents to compare against. + use_values -- Indicates if the test using getStanzaValues + and setStanzaValues should be used. Defaults + to True. + """ + return self.checkStanza(Iq, iq, xml_string, use_values=use_values) + + def checkPresence(self, pres, xml_string, use_values=True): + """ + Create and compare several presence stanza objects to a + correct XML string. + + If use_values is False, the test using getStanzaValues() and + setStanzaValues() will not be used. + + Arguments: + iq -- The Iq stanza object to check. + xml_string -- The XML contents to compare against. + use_values -- Indicates if the test using getStanzaValues + and setStanzaValues should be used. Defaults + to True. + """ + return self.checkStanza(Presence, pres, xml_string, + defaults=['priority'], + use_values=use_values) + + # ------------------------------------------------------------------ + # Methods for simulating stanza streams. + + def streamStart(self, mode='client', skip=True): + """ + Initialize an XMPP client or component using a dummy XML stream. + + Arguments: + mode -- Either 'client' or 'component'. Defaults to 'client'. + skip -- Indicates if the first item in the sent queue (the + stream header) should be removed. Tests that wish + to test initializing the stream should set this to + False. Otherwise, the default of True should be used. + """ + if mode == 'client': + self.xmpp = ClientXMPP('tester@localhost', 'test') + self.xmpp.setSocket(TestSocket()) + + self.xmpp.state.set('reconnect', False) + self.xmpp.state.set('is client', True) + self.xmpp.state.set('connected', True) + + # Must have the stream header ready for xmpp.process() to work + self.xmpp.socket.recvData(self.xmpp.stream_header) + + self.xmpp.connectTCP = lambda a, b, c, d: True + self.xmpp.startTLS = lambda: True + self.xmpp.process(threaded=True) + if skip: + # Clear startup stanzas + self.xmpp.socket.nextSent(timeout=0.01) + + def streamRecv(self, data): + """ + Pass data to the dummy XMPP client as if it came from an XMPP server. + + Arguments: + data -- String stanza XML to be received and processed by the + XMPP client or component. + """ + data = str(data) + self.xmpp.socket.recvData(data) + + def streamSendMessage(self, data, use_values=True, timeout=.1): + """ + Check that the XMPP client sent the given stanza XML. + + Extracts the next sent stanza and compares it with the given + XML using checkMessage. + + Arguments: + data -- The XML string of the expected Message stanza, + or an equivalent stanza object. + use_values -- Modifies the type of tests used by checkMessage. + timeout -- Time in seconds to wait for a stanza before + failing the check. + """ + if isinstance(data, str): + data = self.Message(xml=ET.fromstring(data)) + sent = self.xmpp.socket.nextSent(timeout) + self.checkMessage(data, sent, use_values) + + def streamSendIq(self, data, use_values=True, timeout=.1): + """ + Check that the XMPP client sent the given stanza XML. + + Extracts the next sent stanza and compares it with the given + XML using checkIq. + + Arguments: + data -- The XML string of the expected Iq stanza, + or an equivalent stanza object. + use_values -- Modifies the type of tests used by checkIq. + timeout -- Time in seconds to wait for a stanza before + failing the check. + """ + if isinstance(data, str): + data = self.Iq(xml=ET.fromstring(data)) + sent = self.xmpp.socket.nextSent(timeout) + self.checkIq(data, sent, use_values) + + def streamSendPresence(self, data, use_values=True, timeout=.1): + """ + Check that the XMPP client sent the given stanza XML. + + Extracts the next sent stanza and compares it with the given + XML using checkPresence. + + Arguments: + data -- The XML string of the expected Presence stanza, + or an equivalent stanza object. + use_values -- Modifies the type of tests used by checkPresence. + timeout -- Time in seconds to wait for a stanza before + failing the check. + """ + if isinstance(data, str): + data = self.Presence(xml=ET.fromstring(data)) + sent = self.xmpp.socket.nextSent(timeout) + self.checkPresence(data, sent, use_values) + + def streamClose(self): + """ + Disconnect the dummy XMPP client. + + Can be safely called even if streamStart has not been called. + + Must be placed in the tearDown method of a test class to ensure + that the XMPP client is disconnected after an error. + """ + if hasattr(self, 'xmpp') and self.xmpp is not None: + self.xmpp.disconnect() + self.xmpp.socket.recvData(self.xmpp.stream_footer) + + # ------------------------------------------------------------------ + # XML Comparison and Cleanup + + def fix_namespaces(self, xml, ns): + """ + Assign a namespace to an element and any children that + don't have a namespace. + + Arguments: + xml -- The XML object to fix. + ns -- The namespace to add to the XML object. + """ + if xml.tag.startswith('{'): + return + xml.tag = '{%s}%s' % (ns, xml.tag) + for child in xml.getchildren(): + self.fix_namespaces(child, ns) + + def compare(self, xml, *other): + """ + Compare XML objects. + + Arguments: + xml -- The XML object to compare against. + *other -- The list of XML objects to compare. + """ + if not other: + return False + + # Compare multiple objects + if len(other) > 1: + for xml2 in other: + if not self.compare(xml, xml2): + return False + return True + + other = other[0] + + # Step 1: Check tags + if xml.tag != other.tag: + return False + + # Step 2: Check attributes + if xml.attrib != other.attrib: + return False + + # Step 3: Recursively check children + for child in xml: + child2s = other.findall("%s" % child.tag) + if child2s is None: + return False + for child2 in child2s: + if self.compare(child, child2): + break + else: + return False + + # Everything matches + return True diff --git a/tests/test_addresses.py b/tests/test_addresses.py new file mode 100644 index 0000000..450e136 --- /dev/null +++ b/tests/test_addresses.py @@ -0,0 +1,111 @@ +from . sleektest import * +import sleekxmpp.plugins.xep_0033 as xep_0033 + + +class TestAddresses(SleekTest): + + def setUp(self): + registerStanzaPlugin(Message, xep_0033.Addresses) + + def testAddAddress(self): + """Testing adding extended stanza address.""" + msg = self.Message() + msg['addresses'].addAddress(atype='to', jid='to@header1.org') + self.checkMessage(msg, """ + + +
+ + + """) + + msg = self.Message() + msg['addresses'].addAddress(atype='replyto', + jid='replyto@header1.org', + desc='Reply address') + self.checkMessage(msg, """ + + +
+ + + """) + + def testAddAddresses(self): + """Testing adding multiple extended stanza addresses.""" + + xmlstring = """ + + +
+
+
+ + + """ + + msg = self.Message() + msg['addresses'].setAddresses([ + {'type':'replyto', + 'jid':'replyto@header1.org', + 'desc':'Reply address'}, + {'type':'cc', + 'jid':'cc@header2.org'}, + {'type':'bcc', + 'jid':'bcc@header2.org'}]) + self.checkMessage(msg, xmlstring) + + msg = self.Message() + msg['addresses']['replyto'] = [{'jid':'replyto@header1.org', + 'desc':'Reply address'}] + msg['addresses']['cc'] = [{'jid':'cc@header2.org'}] + msg['addresses']['bcc'] = [{'jid':'bcc@header2.org'}] + self.checkMessage(msg, xmlstring) + + def testAddURI(self): + """Testing adding URI attribute to extended stanza address.""" + + msg = self.Message() + addr = msg['addresses'].addAddress(atype='to', + jid='to@header1.org', + node='foo') + self.checkMessage(msg, """ + + +
+ + + """) + + addr['uri'] = 'mailto:to@header2.org' + self.checkMessage(msg, """ + + +
+ + + """) + + def testDelivered(self): + """Testing delivered attribute of extended stanza addresses.""" + + xmlstring = """ + + +
+ + + """ + + msg = self.Message() + addr = msg['addresses'].addAddress(jid='to@header1.org', atype='to') + self.checkMessage(msg, xmlstring % '') + + addr['delivered'] = True + self.checkMessage(msg, xmlstring % 'delivered="true"') + + addr['delivered'] = False + self.checkMessage(msg, xmlstring % '') + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestAddresses) diff --git a/tests/test_chatstates.py b/tests/test_chatstates.py new file mode 100644 index 0000000..74359df --- /dev/null +++ b/tests/test_chatstates.py @@ -0,0 +1,44 @@ +from . sleektest import * +import sleekxmpp.plugins.xep_0085 as xep_0085 + +class TestChatStates(SleekTest): + + def setUp(self): + registerStanzaPlugin(Message, xep_0085.Active) + registerStanzaPlugin(Message, xep_0085.Composing) + registerStanzaPlugin(Message, xep_0085.Gone) + registerStanzaPlugin(Message, xep_0085.Inactive) + registerStanzaPlugin(Message, xep_0085.Paused) + + def testCreateChatState(self): + """Testing creating chat states.""" + + xmlstring = """ + + <%s xmlns="http://jabber.org/protocol/chatstates" /> + + """ + + msg = self.Message() + msg['chat_state'].active() + self.checkMessage(msg, xmlstring % 'active', + use_values=False) + + msg['chat_state'].composing() + self.checkMessage(msg, xmlstring % 'composing', + use_values=False) + + + msg['chat_state'].gone() + self.checkMessage(msg, xmlstring % 'gone', + use_values=False) + + msg['chat_state'].inactive() + self.checkMessage(msg, xmlstring % 'inactive', + use_values=False) + + msg['chat_state'].paused() + self.checkMessage(msg, xmlstring % 'paused', + use_values=False) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestChatStates) diff --git a/tests/test_disco.py b/tests/test_disco.py index bbe285a..2cc50ee 100644 --- a/tests/test_disco.py +++ b/tests/test_disco.py @@ -1,155 +1,176 @@ -import unittest -from xml.etree import cElementTree as ET -from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath -from . import xmlcompare +from . sleektest import * +import sleekxmpp.plugins.xep_0030 as xep_0030 -import sleekxmpp.plugins.xep_0030 as sd -def stanzaPlugin(stanza, plugin): - stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin - stanza.plugin_tag_map["{%s}%s" % (plugin.namespace, plugin.name)] = plugin - -class testdisco(unittest.TestCase): +class TestDisco(SleekTest): def setUp(self): - self.sd = sd - stanzaPlugin(self.sd.Iq, self.sd.DiscoInfo) - stanzaPlugin(self.sd.Iq, self.sd.DiscoItems) + registerStanzaPlugin(Iq, xep_0030.DiscoInfo) + registerStanzaPlugin(Iq, xep_0030.DiscoItems) - def try3Methods(self, xmlstring, iq): - iq2 = self.sd.Iq(None, self.sd.ET.fromstring(xmlstring)) - values = iq2.getValues() - iq3 = self.sd.Iq() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), str(iq)+"3 methods for creating stanza don't match") - def testCreateInfoQueryNoNode(self): """Testing disco#info query with no node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = '' - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + """) def testCreateInfoQueryWithNode(self): """Testing disco#info query with a node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + """) def testCreateInfoQueryNoNode(self): """Testing disco#items query with no node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_items']['node'] = '' - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + """) def testCreateItemsQueryWithNode(self): """Testing disco#items query with a node.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_items']['node'] = 'foo' - xmlstring = """""" - self.try3Methods(xmlstring, iq) + + self.checkIq(iq, """ + + + + """) def testInfoIdentities(self): """Testing adding identities to disco#info.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' - iq['disco_info'].addIdentity('conference', 'text', 'Chatroom') - xmlstring = """""" - self.try3Methods(xmlstring, iq) + iq['disco_info'].addIdentity('conference', 'text', 'Chatroom') + + self.checkIq(iq, """ + + + + + + """) def testInfoFeatures(self): """Testing adding features to disco#info.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_info']['node'] = 'foo' - iq['disco_info'].addFeature('foo') - iq['disco_info'].addFeature('bar') - xmlstring = """""" - self.try3Methods(xmlstring, iq) + iq['disco_info'].addFeature('foo') + iq['disco_info'].addFeature('bar') + + self.checkIq(iq, """ + + + + + + + """) def testItems(self): """Testing adding features to disco#info.""" - iq = self.sd.Iq() + iq = self.Iq() iq['id'] = "0" iq['disco_items']['node'] = 'foo' - iq['disco_items'].addItem('user@localhost') - iq['disco_items'].addItem('user@localhost', 'foo') - iq['disco_items'].addItem('user@localhost', 'bar', 'Testing') - xmlstring = """""" - self.try3Methods(xmlstring, iq) + iq['disco_items'].addItem('user@localhost') + iq['disco_items'].addItem('user@localhost', 'foo') + iq['disco_items'].addItem('user@localhost', 'bar', 'Testing') + + self.checkIq(iq, """ + + + + + + + + """) def testAddRemoveIdentities(self): """Test adding and removing identities to disco#info stanza""" - ids = [('automation', 'commands', 'AdHoc'), - ('conference', 'text', 'ChatRoom')] + ids = [('automation', 'commands', 'AdHoc'), + ('conference', 'text', 'ChatRoom')] - info = self.sd.DiscoInfo() - info.addIdentity(*ids[0]) - self.failUnless(info.getIdentities() == [ids[0]]) + info = xep_0030.DiscoInfo() + info.addIdentity(*ids[0]) + self.failUnless(info.getIdentities() == [ids[0]]) - info.delIdentity('automation', 'commands') - self.failUnless(info.getIdentities() == []) + info.delIdentity('automation', 'commands') + self.failUnless(info.getIdentities() == []) - info.setIdentities(ids) - self.failUnless(info.getIdentities() == ids) + info.setIdentities(ids) + self.failUnless(info.getIdentities() == ids) - info.delIdentity('automation', 'commands') - self.failUnless(info.getIdentities() == [ids[1]]) + info.delIdentity('automation', 'commands') + self.failUnless(info.getIdentities() == [ids[1]]) - info.delIdentities() - self.failUnless(info.getIdentities() == []) + info.delIdentities() + self.failUnless(info.getIdentities() == []) def testAddRemoveFeatures(self): """Test adding and removing features to disco#info stanza""" - features = ['foo', 'bar', 'baz'] + features = ['foo', 'bar', 'baz'] - info = self.sd.DiscoInfo() - info.addFeature(features[0]) - self.failUnless(info.getFeatures() == [features[0]]) + info = xep_0030.DiscoInfo() + info.addFeature(features[0]) + self.failUnless(info.getFeatures() == [features[0]]) - info.delFeature('foo') - self.failUnless(info.getFeatures() == []) + info.delFeature('foo') + self.failUnless(info.getFeatures() == []) - info.setFeatures(features) - self.failUnless(info.getFeatures() == features) + info.setFeatures(features) + self.failUnless(info.getFeatures() == features) - info.delFeature('bar') - self.failUnless(info.getFeatures() == ['foo', 'baz']) + info.delFeature('bar') + self.failUnless(info.getFeatures() == ['foo', 'baz']) - info.delFeatures() - self.failUnless(info.getFeatures() == []) + info.delFeatures() + self.failUnless(info.getFeatures() == []) def testAddRemoveItems(self): """Test adding and removing items to disco#items stanza""" - items = [('user@localhost', None, None), - ('user@localhost', 'foo', None), - ('user@localhost', 'bar', 'Test')] + items = [('user@localhost', None, None), + ('user@localhost', 'foo', None), + ('user@localhost', 'bar', 'Test')] - info = self.sd.DiscoItems() - self.failUnless(True, ""+str(items[0])) + info = xep_0030.DiscoItems() + self.failUnless(True, ""+str(items[0])) - info.addItem(*(items[0])) - self.failUnless(info.getItems() == [items[0]], info.getItems()) + info.addItem(*(items[0])) + self.failUnless(info.getItems() == [items[0]], info.getItems()) - info.delItem('user@localhost') - self.failUnless(info.getItems() == []) + info.delItem('user@localhost') + self.failUnless(info.getItems() == []) - info.setItems(items) - self.failUnless(info.getItems() == items) + info.setItems(items) + self.failUnless(info.getItems() == items) - info.delItem('user@localhost', 'foo') - self.failUnless(info.getItems() == [items[0], items[2]]) + info.delItem('user@localhost', 'foo') + self.failUnless(info.getItems() == [items[0], items[2]]) - info.delItems() - self.failUnless(info.getItems() == []) - + info.delItems() + self.failUnless(info.getItems() == []) - -suite = unittest.TestLoader().loadTestsFromTestCase(testdisco) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestDisco) diff --git a/tests/test_elementbase.py b/tests/test_elementbase.py new file mode 100644 index 0000000..95502f5 --- /dev/null +++ b/tests/test_elementbase.py @@ -0,0 +1,193 @@ +from . sleektest import * +from sleekxmpp.xmlstream.stanzabase import ElementBase + +class TestElementBase(SleekTest): + + def testExtendedName(self): + """Test element names of the form tag1/tag2/tag3.""" + + class TestStanza(ElementBase): + name = "foo/bar/baz" + namespace = "test" + + stanza = TestStanza() + self.checkStanza(TestStanza, stanza, """ + + + + + + """) + + def testGetStanzaValues(self): + """Test getStanzaValues using plugins and substanzas.""" + + class TestStanzaPlugin(ElementBase): + name = "foo2" + namespace = "foo" + interfaces = set(('bar', 'baz')) + plugin_attrib = "foo2" + + class TestSubStanza(ElementBase): + name = "subfoo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + + class TestStanza(ElementBase): + name = "foo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + subitem = set((TestSubStanza,)) + + registerStanzaPlugin(TestStanza, TestStanzaPlugin) + + stanza = TestStanza() + stanza['bar'] = 'a' + stanza['foo2']['baz'] = 'b' + substanza = TestSubStanza() + substanza['bar'] = 'c' + stanza.append(substanza) + + values = stanza.getStanzaValues() + expected = {'bar': 'a', + 'baz': '', + 'foo2': {'bar': '', + 'baz': 'b'}, + 'substanzas': [{'__childtag__': '{foo}subfoo', + 'bar': 'c', + 'baz': ''}]} + self.failUnless(values == expected, + "Unexpected stanza values:\n%s\n%s" % (str(expected), str(values))) + + + def testSetStanzaValues(self): + """Test using setStanzaValues with substanzas and plugins.""" + + class TestStanzaPlugin(ElementBase): + name = "pluginfoo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + plugin_attrib = "plugin_foo" + + class TestStanzaPlugin2(ElementBase): + name = "pluginfoo2" + namespace = "foo" + interfaces = set(('bar', 'baz')) + plugin_attrib = "plugin_foo2" + + class TestSubStanza(ElementBase): + name = "subfoo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + + class TestStanza(ElementBase): + name = "foo" + namespace = "foo" + interfaces = set(('bar', 'baz')) + subitem = set((TestSubStanza,)) + + registerStanzaPlugin(TestStanza, TestStanzaPlugin) + registerStanzaPlugin(TestStanza, TestStanzaPlugin2) + + stanza = TestStanza() + values = {'bar': 'a', + 'baz': '', + 'plugin_foo': {'bar': '', + 'baz': 'b'}, + 'plugin_foo2': {'bar': 'd', + 'baz': 'e'}, + 'substanzas': [{'__childtag__': '{foo}subfoo', + 'bar': 'c', + 'baz': ''}]} + stanza.setStanzaValues(values) + + self.checkStanza(TestStanza, stanza, """ + + + + + + """) + + def testGetItem(self): + """Test accessing stanza interfaces.""" + + class TestStanza(ElementBase): + name = "foo" + namespace = "foo" + interfaces = set(('bar', 'baz', 'qux')) + sub_interfaces = set(('baz',)) + + def getQux(self): + return 'qux' + + class TestStanzaPlugin(ElementBase): + name = "foobar" + namespace = "foo" + plugin_attrib = "foobar" + interfaces = set(('fizz',)) + + TestStanza.subitem = (TestStanza,) + registerStanzaPlugin(TestStanza, TestStanzaPlugin) + + stanza = TestStanza() + substanza = TestStanza() + stanza.append(substanza) + stanza.setStanzaValues({'bar': 'a', + 'baz': 'b', + 'qux': 42, + 'foobar': {'fizz': 'c'}}) + + # Test non-plugin interfaces + expected = {'substanzas': [substanza], + 'bar': 'a', + 'baz': 'b', + 'qux': 'qux', + 'meh': ''} + for interface, value in expected.items(): + result = stanza[interface] + self.failUnless(result == value, + "Incorrect stanza interface access result: %s" % result) + + # Test plugin interfaces + self.failUnless(isinstance(stanza['foobar'], TestStanzaPlugin), + "Incorrect plugin object result.") + self.failUnless(stanza['foobar']['fizz'] == 'c', + "Incorrect plugin subvalue result.") + + def testSetItem(self): + """Test assigning to stanza interfaces.""" + + class TestStanza(ElementBase): + name = "foo" + namespace = "foo" + interfaces = set(('bar', 'baz', 'qux')) + sub_interfaces = set(('baz',)) + + def setQux(self, value): + pass + + class TestStanzaPlugin(ElementBase): + name = "foobar" + namespace = "foo" + plugin_attrib = "foobar" + interfaces = set(('foobar',)) + + registerStanzaPlugin(TestStanza, TestStanzaPlugin) + + stanza = TestStanza() + + stanza['bar'] = 'attribute!' + stanza['baz'] = 'element!' + stanza['qux'] = 'overridden' + stanza['foobar'] = 'plugin' + + self.checkStanza(TestStanza, stanza, """ + + element! + + + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestElementBase) diff --git a/tests/test_errorstanzas.py b/tests/test_errorstanzas.py new file mode 100644 index 0000000..d6fafc5 --- /dev/null +++ b/tests/test_errorstanzas.py @@ -0,0 +1,56 @@ +from . sleektest import * + +class TestErrorStanzas(SleekTest): + + def testSetup(self): + """Test setting initial values in error stanza.""" + msg = self.Message() + msg.enable('error') + self.checkMessage(msg, """ + + + + + + """) + + def testCondition(self): + """Test modifying the error condition.""" + msg = self.Message() + msg['error']['condition'] = 'item-not-found' + + self.checkMessage(msg, """ + + + + + + """) + + self.failUnless(msg['error']['condition'] == 'item-not-found', "Error condition doesn't match.") + + del msg['error']['condition'] + + self.checkMessage(msg, """ + + + + """) + + def testDelCondition(self): + """Test that deleting error conditions doesn't remove extra elements.""" + msg = self.Message() + msg['error']['text'] = 'Error!' + msg['error']['condition'] = 'internal-server-error' + + del msg['error']['condition'] + + self.checkMessage(msg, """ + + + Error! + + + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestErrorStanzas) diff --git a/tests/test_events.py b/tests/test_events.py index 11821db..bbc5832 100644 --- a/tests/test_events.py +++ b/tests/test_events.py @@ -1,14 +1,11 @@ -import unittest +import sleekxmpp +from . sleektest import * -class testevents(unittest.TestCase): - def setUp(self): - import sleekxmpp.stanza.presence as p - self.p = p +class TestEvents(SleekTest): def testEventHappening(self): "Test handler working" - import sleekxmpp c = sleekxmpp.ClientXMPP('crap@wherever', 'password') happened = [] def handletestevent(event): @@ -20,7 +17,6 @@ class testevents(unittest.TestCase): def testDelEvent(self): "Test handler working, then deleted and not triggered" - import sleekxmpp c = sleekxmpp.ClientXMPP('crap@wherever', 'password') happened = [] def handletestevent(event): @@ -32,4 +28,4 @@ class testevents(unittest.TestCase): self.failUnless(happened == [True], "event did not get triggered the correct number of times") -suite = unittest.TestLoader().loadTestsFromTestCase(testevents) +suite = unittest.TestLoader().loadTestsFromTestCase(TestEvents) diff --git a/tests/test_forms.py b/tests/test_forms.py new file mode 100644 index 0000000..d571063 --- /dev/null +++ b/tests/test_forms.py @@ -0,0 +1,115 @@ +from . sleektest import * +import sleekxmpp.plugins.xep_0004 as xep_0004 + + +class TestDataForms(SleekTest): + + def setUp(self): + registerStanzaPlugin(Message, xep_0004.Form) + registerStanzaPlugin(xep_0004.Form, xep_0004.FormField) + registerStanzaPlugin(xep_0004.FormField, xep_0004.FieldOption) + + def testMultipleInstructions(self): + """Testing using multiple instructions elements in a data form.""" + msg = self.Message() + msg['form']['instructions'] = "Instructions\nSecond batch" + + self.checkMessage(msg, """ + + + Instructions + Second batch + + + """) + + def testAddField(self): + """Testing adding fields to a data form.""" + + msg = self.Message() + form = msg['form'] + form.addField(var='f1', + ftype='text-single', + label='Text', + desc='A text field', + required=True, + value='Some text!') + + self.checkMessage(msg, """ + + + + A text field + + Some text! + + + + """) + + form['fields'] = [('f1', {'type': 'text-single', + 'label': 'Username', + 'required': True}), + ('f2', {'type': 'text-private', + 'label': 'Password', + 'required': True}), + ('f3', {'type': 'text-multi', + 'label': 'Message', + 'value': 'Enter message.\nA long one even.'}), + ('f4', {'type': 'list-single', + 'label': 'Message Type', + 'options': [{'label': 'Cool!', + 'value': 'cool'}, + {'label': 'Urgh!', + 'value': 'urgh'}]})] + self.checkMessage(msg, """ + + + + + + + + + + Enter message. + A long one even. + + + + + + + + """) + + def testSetValues(self): + """Testing setting form values""" + + msg = self.Message() + form = msg['form'] + form.setFields([ + ('foo', {'type': 'text-single'}), + ('bar', {'type': 'list-multi'})]) + + form.setValues({'foo': 'Foo!', + 'bar': ['a', 'b']}) + + self.checkMessage(msg, """ + + + + Foo! + + + a + b + + + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms) diff --git a/tests/test_gmail.py b/tests/test_gmail.py new file mode 100644 index 0000000..dd256e2 --- /dev/null +++ b/tests/test_gmail.py @@ -0,0 +1,88 @@ +from . sleektest import * +import sleekxmpp.plugins.gmail_notify as gmail + + +class TestGmail(SleekTest): + + def setUp(self): + registerStanzaPlugin(Iq, gmail.GmailQuery) + registerStanzaPlugin(Iq, gmail.MailBox) + registerStanzaPlugin(Iq, gmail.NewMail) + + def testCreateQuery(self): + """Testing querying Gmail for emails.""" + + iq = self.Iq() + iq['type'] = 'get' + iq['gmail']['search'] = 'is:starred' + iq['gmail']['newer-than-time'] = '1140638252542' + iq['gmail']['newer-than-tid'] = '11134623426430234' + + self.checkIq(iq, """ + + + + """) + + def testMailBox(self): + """Testing reading from Gmail mailbox result""" + + # Use the example from Google's documentation at + # http://code.google.com/apis/talk/jep_extensions/gmail.html#notifications + xml = ET.fromstring(""" + + + + + + + + + act1scene3 + Put thy rapier up. + Ay, ay, a scratch, a scratch; marry, 'tis enough. + + + + """) + + iq = self.Iq(xml=xml) + mailbox = iq['mailbox'] + self.failUnless(mailbox['result-time'] == '1118012394209', "result-time doesn't match") + self.failUnless(mailbox['url'] == 'http://mail.google.com/mail', "url doesn't match") + self.failUnless(mailbox['matched'] == '95', "total-matched incorrect") + self.failUnless(mailbox['estimate'] == False, "total-estimate incorrect") + self.failUnless(len(mailbox['threads']) == 1, "could not extract message threads") + + thread = mailbox['threads'][0] + self.failUnless(thread['tid'] == '1172320964060972012', "thread tid doesn't match") + self.failUnless(thread['participation'] == '1', "thread participation incorrect") + self.failUnless(thread['messages'] == '28', "thread message count incorrect") + self.failUnless(thread['date'] == '1118012394209', "thread date doesn't match") + self.failUnless(thread['url'] == 'http://mail.google.com/mail?view=cv', "thread url doesn't match") + self.failUnless(thread['labels'] == 'act1scene3', "thread labels incorrect") + self.failUnless(thread['subject'] == 'Put thy rapier up.', "thread subject doesn't match") + self.failUnless(thread['snippet'] == "Ay, ay, a scratch, a scratch; marry, 'tis enough.", "snippet doesn't match") + self.failUnless(len(thread['senders']) == 3, "could not extract senders") + + sender1 = thread['senders'][0] + self.failUnless(sender1['name'] == 'Me', "sender name doesn't match") + self.failUnless(sender1['address'] == 'romeo@gmail.com', "sender address doesn't match") + self.failUnless(sender1['originator'] == True, "sender originator incorrect") + self.failUnless(sender1['unread'] == False, "sender unread incorrectly True") + + sender2 = thread['senders'][2] + self.failUnless(sender2['unread'] == True, "sender unread incorrectly False") + +suite = unittest.TestLoader().loadTestsFromTestCase(TestGmail) diff --git a/tests/test_iqstanzas.py b/tests/test_iqstanzas.py new file mode 100644 index 0000000..2dabc5e --- /dev/null +++ b/tests/test_iqstanzas.py @@ -0,0 +1,90 @@ +from . sleektest import * +from sleekxmpp.xmlstream.stanzabase import ET + + +class TestIqStanzas(SleekTest): + + def tearDown(self): + """Shutdown the XML stream after testing.""" + self.streamClose() + + def testSetup(self): + """Test initializing default Iq values.""" + iq = self.Iq() + self.checkIq(iq, """ + + """) + + def testPayload(self): + """Test setting Iq stanza payload.""" + iq = self.Iq() + iq.setPayload(ET.Element('{test}tester')) + self.checkIq(iq, """ + + + + """, use_values=False) + + + def testUnhandled(self): + """Test behavior for Iq.unhandled.""" + self.streamStart() + self.streamRecv(""" + + + + """) + + iq = self.Iq() + iq['id'] = 'test' + iq['error']['condition'] = 'feature-not-implemented' + iq['error']['text'] = 'No handlers registered for this request.' + + self.streamSendIq(iq, """ + + + + + No handlers registered for this request. + + + + """) + + def testQuery(self): + """Test modifying query element of Iq stanzas.""" + iq = self.Iq() + + iq['query'] = 'query_ns' + self.checkIq(iq, """ + + + + """) + + iq['query'] = 'query_ns2' + self.checkIq(iq, """ + + + + """) + + self.failUnless(iq['query'] == 'query_ns2', "Query namespace doesn't match") + + del iq['query'] + self.checkIq(iq, """ + + """) + + def testReply(self): + """Test setting proper result type in Iq replies.""" + iq = self.Iq() + iq['to'] = 'user@localhost' + iq['type'] = 'get' + iq.reply() + + self.checkIq(iq, """ + + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestIqStanzas) diff --git a/tests/test_jid.py b/tests/test_jid.py new file mode 100644 index 0000000..cddac42 --- /dev/null +++ b/tests/test_jid.py @@ -0,0 +1,26 @@ +from . sleektest import * +from sleekxmpp.xmlstream.jid import JID + +class TestJIDClass(SleekTest): + def testJIDfromfull(self): + j = JID('user@someserver/some/resource') + self.assertEqual(j.user, 'user', "User does not match") + self.assertEqual(j.domain, 'someserver', "Domain does not match") + self.assertEqual(j.resource, 'some/resource', "Resource does not match") + self.assertEqual(j.bare, 'user@someserver', "Bare does not match") + self.assertEqual(j.full, 'user@someserver/some/resource', "Full does not match") + self.assertEqual(str(j), 'user@someserver/some/resource', "String does not match") + + def testJIDchange(self): + j = JID('user1@someserver1/some1/resource1') + j.user = 'user' + j.domain = 'someserver' + j.resource = 'some/resource' + self.assertEqual(j.user, 'user', "User does not match") + self.assertEqual(j.domain, 'someserver', "Domain does not match") + self.assertEqual(j.resource, 'some/resource', "Resource does not match") + self.assertEqual(j.bare, 'user@someserver', "Bare does not match") + self.assertEqual(j.full, 'user@someserver/some/resource', "Full does not match") + self.assertEqual(str(j), 'user@someserver/some/resource', "String does not match") + +suite = unittest.TestLoader().loadTestsFromTestCase(TestJIDClass) diff --git a/tests/test_messagestanzas.py b/tests/test_messagestanzas.py index 08488ce..2a1567d 100644 --- a/tests/test_messagestanzas.py +++ b/tests/test_messagestanzas.py @@ -1,44 +1,57 @@ -import unittest -from xml.etree import cElementTree as ET +from . sleektest import * +from sleekxmpp.stanza.message import Message +from sleekxmpp.stanza.htmlim import HTMLIM -class testmessagestanzas(unittest.TestCase): - def setUp(self): - import sleekxmpp.stanza.message as m - from sleekxmpp.basexmpp import stanzaPlugin - from sleekxmpp.stanza.htmlim import HTMLIM - stanzaPlugin(m.Message, HTMLIM) - self.m = m - - def testGroupchatReplyRegression(self): - "Regression groupchat reply should be to barejid" - msg = self.m.Message() - msg['to'] = 'me@myserver.tld' - msg['from'] = 'room@someservice.someserver.tld/somenick' - msg['type'] = 'groupchat' - msg['body'] = "this is a message" - msg.reply() - self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld') +class TestMessageStanzas(SleekTest): - def testAttribProperty(self): - "Test attrib property returning self" - msg = self.m.Message() - msg.attrib.attrib.attrib['to'] = 'usr@server.tld' - self.failUnless(str(msg['to']) == 'usr@server.tld') - - def testHTMLPlugin(self): - "Test message/html/html stanza" - msgtxt = """this is the plaintext message

This is the htmlim message

""" - msg = self.m.Message() - msg['to'] = "fritzy@netflint.net/sleekxmpp" - msg['body'] = "this is the plaintext message" - msg['type'] = 'chat' - p = ET.Element('{http://www.w3.org/1999/xhtml}p') - p.text = "This is the htmlim message" - msg['html']['html'] = p - msg2 = self.m.Message() - values = msg.getValues() - msg2.setValues(values) - self.failUnless(msgtxt == str(msg) == str(msg2)) + def setUp(self): + registerStanzaPlugin(Message, HTMLIM) -suite = unittest.TestLoader().loadTestsFromTestCase(testmessagestanzas) + def testGroupchatReplyRegression(self): + "Regression groupchat reply should be to barejid" + msg = self.Message() + msg['to'] = 'me@myserver.tld' + msg['from'] = 'room@someservice.someserver.tld/somenick' + msg['type'] = 'groupchat' + msg['body'] = "this is a message" + msg.reply() + self.failUnless(str(msg['to']) == 'room@someservice.someserver.tld') + + def testAttribProperty(self): + "Test attrib property returning self" + msg = self.Message() + msg.attrib.attrib.attrib['to'] = 'usr@server.tld' + self.failUnless(str(msg['to']) == 'usr@server.tld') + + def testHTMLPlugin(self): + "Test message/html/body stanza" + msg = self.Message() + msg['to'] = "fritzy@netflint.net/sleekxmpp" + msg['body'] = "this is the plaintext message" + msg['type'] = 'chat' + p = ET.Element('{http://www.w3.org/1999/xhtml}p') + p.text = "This is the htmlim message" + msg['html']['body'] = p + self.checkMessage(msg, """ + + this is the plaintext message + + +

This is the htmlim message

+ + +
""") + + def testNickPlugin(self): + "Test message/nick/nick stanza." + msg = self.Message() + msg['nick']['nick'] = 'A nickname!' + self.checkMessage(msg, """ + + A nickname! + + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestMessageStanzas) diff --git a/tests/test_presencestanzas.py b/tests/test_presencestanzas.py index 23eb911..d6a5a38 100644 --- a/tests/test_presencestanzas.py +++ b/tests/test_presencestanzas.py @@ -1,31 +1,67 @@ -import unittest +import sleekxmpp +from . sleektest import * +from sleekxmpp.stanza.presence import Presence -class testpresencestanzas(unittest.TestCase): - def setUp(self): - import sleekxmpp.stanza.presence as p - self.p = p - - def testPresenceShowRegression(self): - "Regression check presence['type'] = 'dnd' show value working" - p = self.p.Presence() - p['type'] = 'dnd' - self.failUnless(str(p) == "dnd") - - def testPresenceUnsolicitedOffline(self): - "Unsolicted offline presence does not spawn changed_status or update roster" - p = self.p.Presence() - p['type'] = 'unavailable' - p['from'] = 'bill@chadmore.com/gmail15af' - import sleekxmpp - c = sleekxmpp.ClientXMPP('crap@wherever', 'password') - happened = [] - def handlechangedpresence(event): - happened.append(True) - c.add_event_handler("changed_status", handlechangedpresence) - c._handlePresence(p) - self.failUnless(happened == [], "changed_status event triggered for superfulous unavailable presence") - self.failUnless(c.roster == {}, "Roster updated for superfulous unavailable presence") - +class TestPresenceStanzas(SleekTest): -suite = unittest.TestLoader().loadTestsFromTestCase(testpresencestanzas) + def testPresenceShowRegression(self): + """Regression check presence['type'] = 'dnd' show value working""" + p = self.Presence() + p['type'] = 'dnd' + self.checkPresence(p, "dnd") + + def testPresenceType(self): + """Test manipulating presence['type']""" + p = self.Presence() + p['type'] = 'available' + self.checkPresence(p, "") + self.failUnless(p['type'] == 'available', + "Incorrect presence['type'] for type 'available'") + + for showtype in ['away', 'chat', 'dnd', 'xa']: + p['type'] = showtype + self.checkPresence(p, """ + %s + """ % showtype) + self.failUnless(p['type'] == showtype, + "Incorrect presence['type'] for type '%s'" % showtype) + + p['type'] = None + self.checkPresence(p, "") + + def testPresenceUnsolicitedOffline(self): + """ + Unsolicted offline presence does not spawn changed_status + or update the roster. + """ + p = self.Presence() + p['type'] = 'unavailable' + p['from'] = 'bill@chadmore.com/gmail15af' + + c = sleekxmpp.ClientXMPP('crap@wherever', 'password') + happened = [] + + def handlechangedpresence(event): + happened.append(True) + + c.add_event_handler("changed_status", handlechangedpresence) + c._handlePresence(p) + + self.failUnless(happened == [], + "changed_status event triggered for extra unavailable presence") + self.failUnless(c.roster == {}, + "Roster updated for superfulous unavailable presence") + + def testNickPlugin(self): + """Test presence/nick/nick stanza.""" + p = self.Presence() + p['nick']['nick'] = 'A nickname!' + self.checkPresence(p, """ + + A nickname! + + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestPresenceStanzas) diff --git a/tests/test_pubsubstanzas.py b/tests/test_pubsubstanzas.py index 089ee18..cddfd12 100644 --- a/tests/test_pubsubstanzas.py +++ b/tests/test_pubsubstanzas.py @@ -1,315 +1,511 @@ -import unittest -from xml.etree import cElementTree as ET -from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath -from . import xmlcompare +from . sleektest import * +import sleekxmpp.plugins.xep_0004 as xep_0004 +import sleekxmpp.plugins.stanza_pubsub as pubsub -class testpubsubstanzas(unittest.TestCase): - def setUp(self): - import sleekxmpp.plugins.stanza_pubsub as ps - self.ps = ps +class TestPubsubStanzas(SleekTest): - def testAffiliations(self): - "Testing iq/pubsub/affiliations/affiliation stanzas" - iq = self.ps.Iq() - aff1 = self.ps.Affiliation() - aff1['node'] = 'testnode' - aff1['affiliation'] = 'owner' - aff2 = self.ps.Affiliation() - aff2['node'] = 'testnode2' - aff2['affiliation'] = 'publisher' - iq['pubsub']['affiliations'].append(aff1) - iq['pubsub']['affiliations'].append(aff2) - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3), "3 methods for creating stanza don't match") - self.failUnless(iq.match('iq@id=0/pubsub/affiliations/affiliation@node=testnode2@affiliation=publisher'), 'Match path failed') - - def testSubscriptions(self): - "Testing iq/pubsub/subscriptions/subscription stanzas" - iq = self.ps.Iq() - sub1 = self.ps.Subscription() - sub1['node'] = 'testnode' - sub1['jid'] = 'steve@myserver.tld/someresource' - sub2 = self.ps.Subscription() - sub2['node'] = 'testnode2' - sub2['jid'] = 'boogers@bork.top/bill' - sub2['subscription'] = 'subscribed' - iq['pubsub']['subscriptions'].append(sub1) - iq['pubsub']['subscriptions'].append(sub2) - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testOptionalSettings(self): - "Testing iq/pubsub/subscription/subscribe-options stanzas" - iq = self.ps.Iq() - iq['pubsub']['subscription']['suboptions']['required'] = True - iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas' - iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp" - iq['pubsub']['subscription']['subscription'] = 'unconfigured' - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testItems(self): - "Testing iq/pubsub/items stanzas" - iq = self.ps.Iq() - iq['pubsub']['items'] - payload = ET.fromstring("""""") - payload2 = ET.fromstring("""""") - item = self.ps.Item() - item['id'] = 'asdf' - item['payload'] = payload - item2 = self.ps.Item() - item2['id'] = 'asdf2' - item2['payload'] = payload2 - iq['pubsub']['items'].append(item) - iq['pubsub']['items'].append(item2) - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testCreate(self): - "Testing iq/pubsub/create&configure stanzas" - from sleekxmpp.plugins import xep_0004 - iq = self.ps.Iq() - iq['pubsub']['create']['node'] = 'mynode' - form = xep_0004.Form() - form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') - iq['pubsub']['configure']['config'] = form - xmlstring = """This thing is awesome""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testState(self): - "Testing iq/psstate stanzas" - from sleekxmpp.plugins import xep_0004 - iq = self.ps.Iq() - iq['psstate']['node']= 'mynode' - iq['psstate']['item']= 'myitem' - pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed') - iq['psstate']['payload'] = pl - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testDefault(self): - "Testing iq/pubsub_owner/default stanzas" - from sleekxmpp.plugins import xep_0004 - iq = self.ps.Iq() - iq['pubsub_owner']['default'] - iq['pubsub_owner']['default']['node'] = 'mynode' - iq['pubsub_owner']['default']['type'] = 'leaf' - form = xep_0004.Form() - form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') - iq['pubsub_owner']['default']['config'] = form - xmlstring = """This thing is awesome""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testSubscribe(self): - "Testing iq/pubsub/subscribe stanzas" - from sleekxmpp.plugins import xep_0004 - iq = self.ps.Iq() - iq['pubsub']['subscribe']['options'] - iq['pubsub']['subscribe']['node'] = 'cheese' - iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp' - iq['pubsub']['subscribe']['options']['node'] = 'cheese' - iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp' - form = xep_0004.Form() - form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') - iq['pubsub']['subscribe']['options']['options'] = form - xmlstring = """This thing is awesome""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testPublish(self): - "Testing iq/pubsub/publish stanzas" - iq = self.ps.Iq() - iq['pubsub']['publish']['node'] = 'thingers' - payload = ET.fromstring("""""") - payload2 = ET.fromstring("""""") - item = self.ps.Item() - item['id'] = 'asdf' - item['payload'] = payload - item2 = self.ps.Item() - item2['id'] = 'asdf2' - item2['payload'] = payload2 - iq['pubsub']['publish'].append(item) - iq['pubsub']['publish'].append(item2) - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - values = iq2.getValues() - iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) + def testAffiliations(self): + "Testing iq/pubsub/affiliations/affiliation stanzas" + iq = self.Iq() + aff1 = pubsub.Affiliation() + aff1['node'] = 'testnode' + aff1['affiliation'] = 'owner' + aff2 = pubsub.Affiliation() + aff2['node'] = 'testnode2' + aff2['affiliation'] = 'publisher' + iq['pubsub']['affiliations'].append(aff1) + iq['pubsub']['affiliations'].append(aff2) + self.checkIq(iq, """ + + + + + + + + """) - def testDelete(self): - "Testing iq/pubsub_owner/delete stanzas" - iq = self.ps.Iq() - iq['pubsub_owner']['delete']['node'] = 'thingers' - xmlstring = """""" - iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) - iq3 = self.ps.Iq() - iq3.setValues(iq2.getValues()) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) - - def testCreateConfigGet(self): - """Testing getting config from full create""" - xml = """http://jabber.org/protocol/pubsub#node_configleaf111101openpublishersnever""" - iq = self.ps.Iq(None, self.ps.ET.fromstring(xml)) - config = iq['pubsub']['configure']['config'] - self.failUnless(config.getValues() != {}) + def testSubscriptions(self): + "Testing iq/pubsub/subscriptions/subscription stanzas" + iq = self.Iq() + sub1 = pubsub.Subscription() + sub1['node'] = 'testnode' + sub1['jid'] = 'steve@myserver.tld/someresource' + sub2 = pubsub.Subscription() + sub2['node'] = 'testnode2' + sub2['jid'] = 'boogers@bork.top/bill' + sub2['subscription'] = 'subscribed' + iq['pubsub']['subscriptions'].append(sub1) + iq['pubsub']['subscriptions'].append(sub2) + self.checkIq(iq, """ + + + + + + + + """) - def testItemEvent(self): - """Testing message/pubsub_event/items/item""" - msg = self.ps.Message() - item = self.ps.EventItem() - pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) - item['payload'] = pl - item['id'] = 'abc123' - msg['pubsub_event']['items'].append(item) - msg['pubsub_event']['items']['node'] = 'cheese' - msg['type'] = 'normal' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) + def testOptionalSettings(self): + "Testing iq/pubsub/subscription/subscribe-options stanzas" + iq = self.Iq() + iq['pubsub']['subscription']['suboptions']['required'] = True + iq['pubsub']['subscription']['node'] = 'testnode alsdkjfas' + iq['pubsub']['subscription']['jid'] = "fritzy@netflint.net/sleekxmpp" + iq['pubsub']['subscription']['subscription'] = 'unconfigured' + self.checkIq(iq, """ + + + + + + + + + """) - def testItemsEvent(self): - """Testing multiple message/pubsub_event/items/item""" - msg = self.ps.Message() - item = self.ps.EventItem() - item2 = self.ps.EventItem() - pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) - pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) - item2['payload'] = pl2 - item['payload'] = pl - item['id'] = 'abc123' - item2['id'] = '123abc' - msg['pubsub_event']['items'].append(item) - msg['pubsub_event']['items'].append(item2) - msg['pubsub_event']['items']['node'] = 'cheese' - msg['type'] = 'normal' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) + def testItems(self): + "Testing iq/pubsub/items stanzas" + iq = self.Iq() + iq['pubsub']['items'] + payload = ET.fromstring(""" + + + + """) + payload2 = ET.fromstring(""" + + + + """) + item = pubsub.Item() + item['id'] = 'asdf' + item['payload'] = payload + item2 = pubsub.Item() + item2['id'] = 'asdf2' + item2['payload'] = payload2 + iq['pubsub']['items'].append(item) + iq['pubsub']['items'].append(item2) + self.checkIq(iq, """ + + + + + + + + + + + + + + + + + + """) - def testItemsEvent(self): - """Testing message/pubsub_event/items/item & retract mix""" - msg = self.ps.Message() - item = self.ps.EventItem() - item2 = self.ps.EventItem() - pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) - pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) - item2['payload'] = pl2 - retract = self.ps.EventRetract() - retract['id'] = 'aabbcc' - item['payload'] = pl - item['id'] = 'abc123' - item2['id'] = '123abc' - msg['pubsub_event']['items'].append(item) - msg['pubsub_event']['items'].append(retract) - msg['pubsub_event']['items'].append(item2) - msg['pubsub_event']['items']['node'] = 'cheese' - msg['type'] = 'normal' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) - - def testCollectionAssociate(self): - """Testing message/pubsub_event/collection/associate""" - msg = self.ps.Message() - msg['pubsub_event']['collection']['associate']['node'] = 'cheese' - msg['pubsub_event']['collection']['node'] = 'cheeseburger' - msg['type'] = 'headline' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) + def testCreate(self): + "Testing iq/pubsub/create&configure stanzas" + iq = self.Iq() + iq['pubsub']['create']['node'] = 'mynode' + iq['pubsub']['configure']['form'].addField('pubsub#title', + ftype='text-single', + value='This thing is awesome') + self.checkIq(iq, """ + + + + + + + This thing is awesome + + + + + """) - def testCollectionDisassociate(self): - """Testing message/pubsub_event/collection/disassociate""" - msg = self.ps.Message() - msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese' - msg['pubsub_event']['collection']['node'] = 'cheeseburger' - msg['type'] = 'headline' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) + def testState(self): + "Testing iq/psstate stanzas" + iq = self.Iq() + iq['psstate']['node']= 'mynode' + iq['psstate']['item']= 'myitem' + pl = ET.Element('{http://andyet.net/protocol/pubsubqueue}claimed') + iq['psstate']['payload'] = pl + self.checkIq(iq, """ + + + + + """) - def testEventConfiguration(self): - """Testing message/pubsub_event/configuration/config""" - msg = self.ps.Message() - from sleekxmpp.plugins import xep_0004 - form = xep_0004.Form() - form.addField('pubsub#title', ftype='text-single', value='This thing is awesome') - msg['pubsub_event']['configuration']['node'] = 'cheese' - msg['pubsub_event']['configuration']['config'] = form - msg['type'] = 'headline' - xmlstring = """This thing is awesome""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) - - def testEventPurge(self): - """Testing message/pubsub_event/purge""" - msg = self.ps.Message() - msg['pubsub_event']['purge']['node'] = 'pickles' - msg['type'] = 'headline' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlstring == str(msg) == str(msg2) == str(msg3)) - - def testEventSubscription(self): - """Testing message/pubsub_event/subscription""" - msg = self.ps.Message() - msg['pubsub_event']['subscription']['node'] = 'pickles' - msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test' - msg['pubsub_event']['subscription']['subid'] = 'aabb1122' - msg['pubsub_event']['subscription']['subscription'] = 'subscribed' - msg['pubsub_event']['subscription']['expiry'] = 'presence' - msg['type'] = 'headline' - xmlstring = """""" - msg2 = self.ps.Message(None, self.ps.ET.fromstring(xmlstring)) - msg3 = self.ps.Message() - msg3.setValues(msg2.getValues()) - self.failUnless(xmlcompare.comparemany([xmlstring, str(msg), str(msg2), str(msg3)])) + def testDefault(self): + "Testing iq/pubsub_owner/default stanzas" + iq = self.Iq() + iq['pubsub_owner']['default'] + iq['pubsub_owner']['default']['node'] = 'mynode' + iq['pubsub_owner']['default']['type'] = 'leaf' + iq['pubsub_owner']['default']['form'].addField('pubsub#title', + ftype='text-single', + value='This thing is awesome') + self.checkIq(iq, """ + + + + + + This thing is awesome + + + + + """, use_values=False) -suite = unittest.TestLoader().loadTestsFromTestCase(testpubsubstanzas) + def testSubscribe(self): + "testing iq/pubsub/subscribe stanzas" + iq = self.Iq() + iq['pubsub']['subscribe']['options'] + iq['pubsub']['subscribe']['node'] = 'cheese' + iq['pubsub']['subscribe']['jid'] = 'fritzy@netflint.net/sleekxmpp' + iq['pubsub']['subscribe']['options']['node'] = 'cheese' + iq['pubsub']['subscribe']['options']['jid'] = 'fritzy@netflint.net/sleekxmpp' + form = xep_0004.Form() + form.addField('pubsub#title', ftype='text-single', value='this thing is awesome') + iq['pubsub']['subscribe']['options']['options'] = form + self.checkIq(iq, """ + + + + + + + this thing is awesome + + + + + + """, use_values=False) + + def testPublish(self): + "Testing iq/pubsub/publish stanzas" + iq = self.Iq() + iq['pubsub']['publish']['node'] = 'thingers' + payload = ET.fromstring(""" + + + + """) + payload2 = ET.fromstring(""" + + + + """) + item = pubsub.Item() + item['id'] = 'asdf' + item['payload'] = payload + item2 = pubsub.Item() + item2['id'] = 'asdf2' + item2['payload'] = payload2 + iq['pubsub']['publish'].append(item) + iq['pubsub']['publish'].append(item2) + + self.checkIq(iq, """ + + + + + + + + + + + + + + + + + + """) + + def testDelete(self): + "Testing iq/pubsub_owner/delete stanzas" + iq = self.Iq() + iq['pubsub_owner']['delete']['node'] = 'thingers' + self.checkIq(iq, """ + + + + + """) + + def testCreateConfigGet(self): + """Testing getting config from full create""" + iq = self.Iq() + iq['to'] = 'pubsub.asdf' + iq['from'] = 'fritzy@asdf/87292ede-524d-4117-9076-d934ed3db8e7' + iq['type'] = 'set' + iq['id'] = 'E' + + pub = iq['pubsub'] + pub['create']['node'] = 'testnode2' + pub['configure']['form']['type'] = 'submit' + pub['configure']['form'].setFields([ + ('FORM_TYPE', {'type': 'hidden', + 'value': 'http://jabber.org/protocol/pubsub#node_config'}), + ('pubsub#node_type', {'type': 'list-single', + 'label': 'Select the node type', + 'value': 'leaf'}), + ('pubsub#title', {'type': 'text-single', + 'label': 'A friendly name for the node'}), + ('pubsub#deliver_notifications', {'type': 'boolean', + 'label': 'Deliver event notifications', + 'value': True}), + ('pubsub#deliver_payloads', {'type': 'boolean', + 'label': 'Deliver payloads with event notifications', + 'value': True}), + ('pubsub#notify_config', {'type': 'boolean', + 'label': 'Notify subscribers when the node configuration changes'}), + ('pubsub#notify_delete', {'type': 'boolean', + 'label': 'Notify subscribers when the node is deleted'}), + ('pubsub#notify_retract', {'type': 'boolean', + 'label': 'Notify subscribers when items are removed from the node', + 'value': True}), + ('pubsub#notify_sub', {'type': 'boolean', + 'label': 'Notify owners about new subscribers and unsubscribes'}), + ('pubsub#persist_items', {'type': 'boolean', + 'label': 'Persist items in storage'}), + ('pubsub#max_items', {'type': 'text-single', + 'label': 'Max # of items to persist', + 'value': '10'}), + ('pubsub#subscribe', {'type': 'boolean', + 'label': 'Whether to allow subscriptions', + 'value': True}), + ('pubsub#access_model', {'type': 'list-single', + 'label': 'Specify the subscriber model', + 'value': 'open'}), + ('pubsub#publish_model', {'type': 'list-single', + 'label': 'Specify the publisher model', + 'value': 'publishers'}), + ('pubsub#send_last_published_item', {'type': 'list-single', + 'label': 'Send last published item', + 'value': 'never'}), + ('pubsub#presence_based_delivery', {'type': 'boolean', + 'label': 'Deliver notification only to available users'}), + ]) + + self.checkIq(iq, """ + + + + + + + http://jabber.org/protocol/pubsub#node_config + + + leaf + + + + 1 + + + 1 + + + + + 1 + + + + + 10 + + + 1 + + + open + + + publishers + + + never + + + + + + """) + + def testItemEvent(self): + """Testing message/pubsub_event/items/item""" + msg = self.Message() + item = pubsub.EventItem() + pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) + item['payload'] = pl + item['id'] = 'abc123' + msg['pubsub_event']['items'].append(item) + msg['pubsub_event']['items']['node'] = 'cheese' + msg['type'] = 'normal' + self.checkMessage(msg, """ + + + + + + + + + """) + + def testItemsEvent(self): + """Testing multiple message/pubsub_event/items/item""" + msg = self.Message() + item = pubsub.EventItem() + item2 = pubsub.EventItem() + pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) + pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) + item2['payload'] = pl2 + item['payload'] = pl + item['id'] = 'abc123' + item2['id'] = '123abc' + msg['pubsub_event']['items'].append(item) + msg['pubsub_event']['items'].append(item2) + msg['pubsub_event']['items']['node'] = 'cheese' + msg['type'] = 'normal' + self.checkMessage(msg, """ + + + + + + + + + + + + """) + + def testItemsEvent(self): + """Testing message/pubsub_event/items/item & retract mix""" + msg = self.Message() + item = pubsub.EventItem() + item2 = pubsub.EventItem() + pl = ET.Element('{http://netflint.net/protocol/test}test', {'failed':'3', 'passed':'24'}) + pl2 = ET.Element('{http://netflint.net/protocol/test-other}test', {'total':'27', 'failed':'3'}) + item2['payload'] = pl2 + retract = pubsub.EventRetract() + retract['id'] = 'aabbcc' + item['payload'] = pl + item['id'] = 'abc123' + item2['id'] = '123abc' + msg['pubsub_event']['items'].append(item) + msg['pubsub_event']['items'].append(retract) + msg['pubsub_event']['items'].append(item2) + msg['pubsub_event']['items']['node'] = 'cheese' + msg['type'] = 'normal' + self.checkMessage(msg, """ + + + + + + + + + + + + """) + + def testCollectionAssociate(self): + """Testing message/pubsub_event/collection/associate""" + msg = self.Message() + msg['pubsub_event']['collection']['associate']['node'] = 'cheese' + msg['pubsub_event']['collection']['node'] = 'cheeseburger' + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + + + """) + + def testCollectionDisassociate(self): + """Testing message/pubsub_event/collection/disassociate""" + msg = self.Message() + msg['pubsub_event']['collection']['disassociate']['node'] = 'cheese' + msg['pubsub_event']['collection']['node'] = 'cheeseburger' + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + + + """) + + def testEventConfiguration(self): + """Testing message/pubsub_event/configuration/config""" + msg = self.Message() + msg['pubsub_event']['configuration']['node'] = 'cheese' + msg['pubsub_event']['configuration']['form'].addField('pubsub#title', + ftype='text-single', + value='This thing is awesome') + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + + This thing is awesome + + + + + """) + + def testEventPurge(self): + """Testing message/pubsub_event/purge""" + msg = self.Message() + msg['pubsub_event']['purge']['node'] = 'pickles' + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + """) + + def testEventSubscription(self): + """Testing message/pubsub_event/subscription""" + msg = self.Message() + msg['pubsub_event']['subscription']['node'] = 'pickles' + msg['pubsub_event']['subscription']['jid'] = 'fritzy@netflint.net/test' + msg['pubsub_event']['subscription']['subid'] = 'aabb1122' + msg['pubsub_event']['subscription']['subscription'] = 'subscribed' + msg['pubsub_event']['subscription']['expiry'] = 'presence' + msg['type'] = 'headline' + self.checkMessage(msg, """ + + + + + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestPubsubStanzas) diff --git a/tests/test_roster.py b/tests/test_roster.py new file mode 100644 index 0000000..6f9fa3d --- /dev/null +++ b/tests/test_roster.py @@ -0,0 +1,84 @@ +from . sleektest import * +from sleekxmpp.stanza.roster import Roster + + +class TestRosterStanzas(SleekTest): + + def testAddItems(self): + """Test adding items to a roster stanza.""" + iq = self.Iq() + iq['roster'].setItems({ + 'user@example.com': { + 'name': 'User', + 'subscription': 'both', + 'groups': ['Friends', 'Coworkers']}, + 'otheruser@example.com': { + 'name': 'Other User', + 'subscription': 'both', + 'groups': []}}) + self.checkIq(iq, """ + + + + Friends + Coworkers + + + + + """) + + def testGetItems(self): + """Test retrieving items from a roster stanza.""" + xml_string = """ + + + + Friends + Coworkers + + + + + """ + iq = self.Iq(ET.fromstring(xml_string)) + expected = { + 'user@example.com': { + 'name': 'User', + 'subscription': 'both', + 'groups': ['Friends', 'Coworkers']}, + 'otheruser@example.com': { + 'name': 'Other User', + 'subscription': 'both', + 'groups': []}} + debug = "Roster items don't match after retrieval." + debug += "\nReturned: %s" % str(iq['roster']['items']) + debug += "\nExpected: %s" % str(expected) + self.failUnless(iq['roster']['items'] == expected, debug) + + def testDelItems(self): + """Test clearing items from a roster stanza.""" + xml_string = """ + + + + Friends + Coworkers + + + + + """ + iq = self.Iq(ET.fromstring(xml_string)) + del iq['roster']['items'] + self.checkIq(iq, """ + + + + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestRosterStanzas) diff --git a/tests/test_stream.py b/tests/test_stream.py new file mode 100644 index 0000000..6e24074 --- /dev/null +++ b/tests/test_stream.py @@ -0,0 +1,34 @@ +from . sleektest import * +import sleekxmpp.plugins.xep_0033 as xep_0033 + + +class TestStreamTester(SleekTest): + """ + Test that we can simulate and test a stanza stream. + """ + + def setUp(self): + self.streamStart() + + def tearDown(self): + self.streamClose() + + def testEcho(self): + def echo(msg): + msg.reply('Thanks for sending: %(body)s' % msg).send() + + self.xmpp.add_event_handler('message', echo) + + self.streamRecv(""" + + Hi! + + """) + + self.streamSendMessage(""" + + Thanks for sending: Hi! + + """) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamTester) diff --git a/tests/test_tostring.py b/tests/test_tostring.py new file mode 100644 index 0000000..2999949 --- /dev/null +++ b/tests/test_tostring.py @@ -0,0 +1,104 @@ +from . sleektest import * +from sleekxmpp.stanza import Message +from sleekxmpp.xmlstream.stanzabase import ET +from sleekxmpp.xmlstream.tostring import tostring, xml_escape + + +class TestToString(SleekTest): + + """ + Test the implementation of sleekxmpp.xmlstream.tostring + """ + + def tryTostring(self, original='', expected=None, message='', **kwargs): + """ + Compare the result of calling tostring against an + expected result. + """ + if not expected: + expected=original + if isinstance(original, str): + xml = ET.fromstring(original) + else: + xml=original + result = tostring(xml, **kwargs) + self.failUnless(result == expected, "%s: %s" % (message, result)) + + def testXMLEscape(self): + """Test escaping XML special characters.""" + original = """'Hi & welcome!'""" + escaped = xml_escape(original) + desired = """<foo bar="baz">'Hi""" + desired += """ & welcome!'</foo>""" + + self.failUnless(escaped == desired, + "XML escaping did not work: %s." % escaped) + + def testEmptyElement(self): + """Test converting an empty element to a string.""" + self.tryTostring( + original='', + message="Empty element not serialized correctly") + + def testEmptyElementWrapped(self): + """Test converting an empty element inside another element.""" + self.tryTostring( + original='', + message="Wrapped empty element not serialized correctly") + + def testEmptyElementWrappedText(self): + """ + Test converting an empty element wrapped with text + inside another element. + """ + self.tryTostring( + original='Some text. More text.', + message="Text wrapped empty element serialized incorrectly") + + def testMultipleChildren(self): + """Test converting multiple child elements to a Unicode string.""" + self.tryTostring( + original='', + message="Multiple child elements not serialized correctly") + + def testXMLNS(self): + """ + Test using xmlns tostring parameter, which will prevent adding + an xmlns attribute to the serialized element if the element's + namespace is the same. + """ + self.tryTostring( + original='', + expected='', + message="The xmlns parameter was not used properly.", + xmlns='foo') + + def testStanzaNs(self): + """ + Test using the stanza_ns tostring parameter, which will prevent + adding an xmlns attribute to the serialized element if the + element's namespace is the same. + """ + self.tryTostring( + original='', + expected='', + message="The stanza_ns parameter was not used properly.", + stanza_ns='foo') + + def testStanzaStr(self): + """ + Test that stanza objects are serialized properly. + """ + utf8_message = '\xe0\xb2\xa0_\xe0\xb2\xa0' + if not hasattr(utf8_message, 'decode'): + # Python 3 + utf8_message = bytes(utf8_message, encoding='utf-8') + msg = Message() + msg['body'] = utf8_message.decode('utf-8') + expected = '\xe0\xb2\xa0_\xe0\xb2\xa0' + result = msg.__str__() + self.failUnless(result == expected, + "Stanza Unicode handling is incorrect: %s" % result) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestToString) diff --git a/tests/xmlcompare.py b/tests/xmlcompare.py deleted file mode 100644 index d97af97..0000000 --- a/tests/xmlcompare.py +++ /dev/null @@ -1,28 +0,0 @@ -from xml.etree import cElementTree as ET - -def comparemany(xmls): - xml1 = xmls[0] - if type(xml1) == type(''): - xml1 = ET.fromstring(xml1) - for xml in xmls[1:]: - xml2 = xml - if type(xml2) == type(''): - xml2 = ET.fromstring(xml2) - if not compare(xml1, xml2): return False - return True - -def compare(xml1, xml2): - if xml1.tag != xml2.tag: - return False - if xml1.attrib != xml2.attrib: - return False - for child in xml1: - child2s = xml2.findall("%s" % child.tag) - if child2s is None: - return False - found = False - for child2 in child2s: - found = compare(child, child2) - if found: break - if not found: return False - return True