From 5345e9a46ba6b862944bcddacd73fba448758109 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Thu, 14 Jan 2010 07:37:44 -0800 Subject: [PATCH 1/5] fixed bug from duplicate append methods in stanzabase --- sleekxmpp/xmlstream/stanzabase.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 277882e..00a1439 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -2,6 +2,8 @@ from xml.etree import cElementTree as ET import logging import traceback +xmltester = type(ET.Element('xml')) + class JID(object): def __init__(self, jid): self.jid = jid @@ -62,7 +64,10 @@ class ElementBase(object): def append(self, item): if not isinstance(item, ElementBase): - raise TypeError + if type(item) == xmltester: + return self.appendxml(item) + else: + raise TypeError self.xml.append(item.xml) self.iterables.append(item) return self From e39a2395d7d5b1490e66316cd46b3f31b787cb37 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Fri, 15 Jan 2010 21:07:28 -0800 Subject: [PATCH 2/5] xep 30 and 50 always reply from jid iq sent to --- sleekxmpp/plugins/stanza_pubsub.py | 5 ++-- sleekxmpp/plugins/xep_0030.py | 6 ++--- sleekxmpp/plugins/xep_0050.py | 3 +++ sleekxmpp/xmlstream/matcher/id.py | 2 +- sleekxmpp/xmlstream/matcher/stanzapath.py | 7 +++++ sleekxmpp/xmlstream/matcher/xmlmask.py | 1 + sleekxmpp/xmlstream/matcher/xpath.py | 1 + sleekxmpp/xmlstream/stanzabase.py | 26 ++++++++++++++---- sleekxmpp/xmlstream/xmlstream.py | 2 +- tests/test_pubsubstanzas.py | 32 +++++++++++++++++++++-- 10 files changed, 71 insertions(+), 14 deletions(-) create mode 100644 sleekxmpp/xmlstream/matcher/stanzapath.py diff --git a/sleekxmpp/plugins/stanza_pubsub.py b/sleekxmpp/plugins/stanza_pubsub.py index 900f3c8..58f9c89 100644 --- a/sleekxmpp/plugins/stanza_pubsub.py +++ b/sleekxmpp/plugins/stanza_pubsub.py @@ -58,7 +58,7 @@ class Subscription(ElementBase): namespace = 'http://jabber.org/protocol/pubsub' name = 'subscription' plugin_attrib = name - interfaces = set(('jid', 'node', 'subscription')) + interfaces = set(('jid', 'node', 'subscription', 'subid')) plugin_attrib_map = {} plugin_tag_map = {} @@ -207,9 +207,10 @@ class Publish(Items): namespace = 'http://jabber.org/protocol/pubsub' name = 'publish' plugin_attrib = name - interfaces = set(('node')) + interfaces = set(('node',)) plugin_attrib_map = {} plugin_tag_map = {} + subitem = Item stanzaPlugin(Pubsub, Publish) diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py index 791038c..bff0dc1 100644 --- a/sleekxmpp/plugins/xep_0030.py +++ b/sleekxmpp/plugins/xep_0030.py @@ -53,7 +53,7 @@ class xep_0030(base.base_plugin): def info_handler(self, xml): logging.debug("Info request from %s" % xml.get('from', '')) iq = self.xmpp.makeIqResult(xml.get('id', self.xmpp.getNewId())) - iq.attrib['from'] = self.xmpp.fulljid + iq.attrib['from'] = xml.get('to') iq.attrib['to'] = xml.get('from', self.xmpp.server) query = xml.find('{http://jabber.org/protocol/disco#info}query') node = query.get('node', 'main') @@ -74,7 +74,7 @@ class xep_0030(base.base_plugin): def item_handler(self, xml): logging.debug("Item request from %s" % xml.get('from', '')) iq = self.xmpp.makeIqResult(xml.get('id', self.xmpp.getNewId())) - iq.attrib['from'] = self.xmpp.fulljid + iq.attrib['from'] = xml.get('to') iq.attrib['to'] = xml.get('from', self.xmpp.server) query = self.xmpp.makeIqQuery(iq, 'http://jabber.org/protocol/disco#items').find('{http://jabber.org/protocol/disco#items}query') node = xml.find('{http://jabber.org/protocol/disco#items}query').get('node', 'main') @@ -82,7 +82,7 @@ class xep_0030(base.base_plugin): itemxml = ET.Element('item') itemxml.attrib = item if itemxml.attrib['jid'] is None: - itemxml.attrib['jid'] = self.xmpp.fulljid + itemxml.attrib['jid'] = xml.get('to') query.append(itemxml) self.xmpp.send(iq) diff --git a/sleekxmpp/plugins/xep_0050.py b/sleekxmpp/plugins/xep_0050.py index b75a675..0ca66dd 100644 --- a/sleekxmpp/plugins/xep_0050.py +++ b/sleekxmpp/plugins/xep_0050.py @@ -62,6 +62,7 @@ class xep_0050(base.base_plugin): name, form, pointer, multi = self.commands[node] self.sessions[sessionid] = {} self.sessions[sessionid]['jid'] = xml.get('from') + self.sessions[sessionid]['to'] = xml.get('to') self.sessions[sessionid]['past'] = [(form, None)] self.sessions[sessionid]['next'] = pointer npointer = pointer @@ -133,6 +134,8 @@ class xep_0050(base.base_plugin): command.append(xmlactions) if not sessionid: sessionid = self.getNewSession() + else: + iq.attrib['from'] = self.sessions[sessionid]['to'] command.attrib['sessionid'] = sessionid if form is not None: if hasattr(form,'getXML'): diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py index ec7597d..44fad15 100644 --- a/sleekxmpp/xmlstream/matcher/id.py +++ b/sleekxmpp/xmlstream/matcher/id.py @@ -3,4 +3,4 @@ from . import base class MatcherId(base.MatcherBase): def match(self, xml): - return xml.get('id') == self._criteria + return xml['id'] == self._criteria diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py new file mode 100644 index 0000000..d036d0b --- /dev/null +++ b/sleekxmpp/xmlstream/matcher/stanzapath.py @@ -0,0 +1,7 @@ +from . import base +from xml.etree import cElementTree + +class StanzaPath(base.MatcherBase): + + def match(self, stanza): + return stanza.match(self._criteria) diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index e8e4df0..1cd1964 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -16,6 +16,7 @@ class MatchXMLMask(base.MatcherBase): self.default_ns = ns def match(self, xml): + xml = xml.xml return self.maskcmp(xml, self._criteria, True) def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'): diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index 060d5df..5e0411e 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -6,6 +6,7 @@ ignore_ns = False class MatchXPath(base.MatcherBase): def match(self, xml): + xml = xml.xml x = cElementTree.Element('x') x.append(xml) if not ignore_ns: diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 00a1439..feb9b26 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -91,11 +91,27 @@ class ElementBase(object): out.append('substanzas') return tuple(out) - def find(self, item): - return self.iterables.find(item) - - def match(self, xml): - return xml.tag == self.tag + 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.name): return False + founditerable = False + for iterable in self.iterables: + 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) diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index 5178693..06011aa 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -277,7 +277,7 @@ class XMLStream(object): stanza = StanzaBase(self, xmlobj) unhandled = True for handler in self.__handlers: - if handler.match(xmlobj): + if handler.match(stanza): handler.prerun(stanza) self.eventqueue.put(('stanza', handler, stanza)) if handler.checkDelete(): self.__handlers.pop(self.__handlers.index(handler)) diff --git a/tests/test_pubsubstanzas.py b/tests/test_pubsubstanzas.py index b84a517..4c3737d 100644 --- a/tests/test_pubsubstanzas.py +++ b/tests/test_pubsubstanzas.py @@ -1,5 +1,6 @@ import unittest from xml.etree import cElementTree as ET +from sleekxmpp.xmlstream.matcher.stanzapath import StanzaPath class testpubsubstanzas(unittest.TestCase): @@ -23,7 +24,8 @@ class testpubsubstanzas(unittest.TestCase): iq3 = self.ps.Iq() values = iq2.getValues() iq3.setValues(values) - self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) + 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" @@ -34,9 +36,10 @@ class testpubsubstanzas(unittest.TestCase): 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 = """""" + xmlstring = """""" iq2 = self.ps.Iq(None, self.ps.ET.fromstring(xmlstring)) iq3 = self.ps.Iq() values = iq2.getValues() @@ -124,5 +127,30 @@ class testpubsubstanzas(unittest.TestCase): values = iq2.getValues() iq3.setValues(values) self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) + + def testPublish(self): + 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) + #print() + #print(xmlstring) + #print(iq) + #print(iq2) + #print(iq3) + self.failUnless(xmlstring == str(iq) == str(iq2) == str(iq3)) suite = unittest.TestLoader().loadTestsFromTestCase(testpubsubstanzas) From 7a9a86af3d2406ef921ae2d5f8ba7e42eeff973d Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Fri, 15 Jan 2010 21:36:53 -0800 Subject: [PATCH 3/5] fixed matcher bug introduced with stanza matching --- sleekxmpp/xmlstream/matcher/xmlmask.py | 3 ++- sleekxmpp/xmlstream/matcher/xpath.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index 1cd1964..e4a22fa 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -16,7 +16,8 @@ class MatchXMLMask(base.MatcherBase): self.default_ns = ns def match(self, xml): - xml = xml.xml + if hasattr(xml, 'xml'): + xml = xml.xml return self.maskcmp(xml, self._criteria, True) def maskcmp(self, source, maskobj, use_ns=False, default_ns='__no_ns__'): diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index 5e0411e..fe18a65 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -6,7 +6,8 @@ ignore_ns = False class MatchXPath(base.MatcherBase): def match(self, xml): - xml = xml.xml + if hasattr(xml, 'xml'): + xml = xml.xml x = cElementTree.Element('x') x.append(xml) if not ignore_ns: From 986255eefc890263f7a326c53d3094fd5858272a Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Wed, 20 Jan 2010 00:19:30 -0800 Subject: [PATCH 4/5] Added readme and install --- INSTALL | 8 ++++++++ README | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 INSTALL create mode 100644 README diff --git a/INSTALL b/INSTALL new file mode 100644 index 0000000..b73b5e5 --- /dev/null +++ b/INSTALL @@ -0,0 +1,8 @@ +Pre-requisites: +Python 3.1 or 2.6 + +Install: +python3 setup.py install + +Root install: +sudo python3 setup.py install diff --git a/README b/README new file mode 100644 index 0000000..a698909 --- /dev/null +++ b/README @@ -0,0 +1,37 @@ +SleekXMPP is an XMPP library written for Python 3.x (with 2.6 compatibility). +Featured in examples in XMPP: The Definitive Guide by Kevin Smith, Remko Tronçon, and Peter Saint-Andre + +SleekXMPP has several design goals/philosophies: +- Low number of dependencies. +- Every XEP as a plugin. +- Rewarding to work with. + +The goals for 1.0 include (and we're getting close): +- Nearly Full test coverage of stanzas. +- Wide range of functional tests. +- Stanza objects for all interaction with the stream +- Documentation on using and extending SleekXMPP. +- Complete documentation on all implemented stanza objects +- Documentation on all examples used in XMPP: The Definitive Guide + +1.1 will include: +- More functional and unit tests +- PEP-8 compliance +- XEP-225 support + +Since 0.2, here's the Changelog: +- MANY bugfixes +- Re-implementation of handlers/threading to greatly simplify and remove bugs (no more spawning threads in handlers) +- Stanza objects for jabber:client and all implemented XEPs +- Raising XMPPError for jabber:client and extended errors in handlers +- Robust error handling and better insurance of iq responses +- Stanza objects have made life a lot easier! +- Massive audit/cleanup. + +Credits +---------------- +Main Author: Nathan Fritz fritz@netflint.net +XEP-0045 original implementation: Kevin Smith +Patches: Remko Tronçon + +Feel free to add fritzy@netflint.net to your roster for direct support and comments. From 6b130eb94775da8675750a0a9aed75fa6f328137 Mon Sep 17 00:00:00 2001 From: Nathan Fritz Date: Wed, 20 Jan 2010 01:42:53 -0800 Subject: [PATCH 5/5] unhandled iq's should only respond to errors when type=get/set --- sleekxmpp/plugins/xep_0045.py | 7 +++++-- sleekxmpp/stanza/iq.py | 9 +++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index 4b181f9..8273742 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -223,12 +223,15 @@ class xep_0045(base.base_plugin): return False return True - def setAffiliation(self, room, jid, affiliation='member'): + def setAffiliation(self, room, jid=None, nick=None, affiliation='member'): """ Change room affiliation.""" if affiliation not in ('outcast', 'member', 'admin', 'owner', 'none'): raise TypeError query = ET.Element('{http://jabber.org/protocol/muc#admin}query') - item = ET.Element('item', {'affiliation':affiliation, 'jid':jid}) + if nick is not None: + item = ET.Element('item', {'affiliation':affiliation, 'nick':nick}) + else: + item = ET.Element('item', {'affiliation':affiliation, 'jid':jid}) query.append(item) iq = self.xmpp.makeIqSet(query) iq['to'] = room diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index e3eccfd..85165e1 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -20,10 +20,11 @@ class Iq(RootStanza): self['id'] = '0' def unhandled(self): - self.reply() - self['error']['condition'] = 'feature-not-implemented' - self['error']['text'] = 'No handlers registered for this request.' - self.send() + 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 result(self): self['type'] = 'result'