diff options
37 files changed, 1267 insertions, 289 deletions
diff --git a/conn_tests/testpubsub.py b/conn_tests/testpubsub.py index 24855c90..3aa7200e 100755 --- a/conn_tests/testpubsub.py +++ b/conn_tests/testpubsub.py @@ -33,7 +33,7 @@ class testps(sleekxmpp.ClientXMPP): self.node = "pstestnode_%s" self.pshost = pshost if pshost is None: - self.pshost = self.server + self.pshost = self.boundjid.host self.nodenum = int(nodenum) self.leafnode = self.nodenum + 1 self.collectnode = self.nodenum + 2 @@ -48,7 +48,9 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0009',
'sleekxmpp/plugins/xep_0009/stanza',
'sleekxmpp/plugins/xep_0030',
- 'sleekxmpp/plugins/xep_0030/stanza'
+ 'sleekxmpp/plugins/xep_0030/stanza',
+ 'sleekxmpp/plugins/xep_0059',
+ 'sleekxmpp/plugins/xep_0092',
]
if sys.version_info < (3, 0):
diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index e3c7bc5a..3cf949a7 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -15,7 +15,7 @@ import logging import sleekxmpp from sleekxmpp import plugins -from sleekxmpp.stanza import Message, Presence, Iq, Error +from sleekxmpp.stanza import Message, Presence, Iq, Error, StreamError from sleekxmpp.stanza.roster import Roster from sleekxmpp.stanza.nick import Nick from sleekxmpp.stanza.htmlim import HTMLIM @@ -128,6 +128,10 @@ class BaseXMPP(XMLStream): Callback('Presence', MatchXPath("{%s}presence" % self.default_ns), self._handle_presence)) + self.register_handler( + Callback('Stream Error', + MatchXPath("{%s}error" % self.stream_ns), + self._handle_stream_error)) self.add_event_handler('presence_subscribe', self._handle_subscribe) @@ -135,9 +139,10 @@ class BaseXMPP(XMLStream): self._handle_disconnected) # Set up the XML stream with XMPP's root stanzas. - self.registerStanza(Message) - self.registerStanza(Iq) - self.registerStanza(Presence) + self.register_stanza(Message) + self.register_stanza(Iq) + self.register_stanza(Presence) + self.register_stanza(StreamError) # Initialize a few default stanza plugins. register_stanza_plugin(Iq, Roster) @@ -245,24 +250,24 @@ class BaseXMPP(XMLStream): """Create a Presence stanza associated with this stream.""" return Presence(self, *args, **kwargs) - def make_iq(self, id=0, ifrom=None, ito=None, itype=None, query=None): + def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None): """ Create a new Iq stanza with a given Id and from JID. Arguments: - id -- An ideally unique ID value for this stanza thread. - Defaults to 0. - ifrom -- The from JID to use for this stanza. - ito -- The destination JID for this stanza. - type -- The Iq's type, one of: get, set, result, or error. - query -- Optional namespace for adding a query element. + id -- An ideally unique ID value for this stanza thread. + Defaults to 0. + ifrom -- The from JID to use for this stanza. + ito -- The destination JID for this stanza. + itype -- The Iq's type, one of: get, set, result, or error. + iquery -- Optional namespace for adding a query element. """ iq = self.Iq() iq['id'] = str(id) iq['to'] = ito iq['from'] = ifrom iq['type'] = itype - iq['query'] = query + iq['query'] = iquery return iq def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None): @@ -579,6 +584,9 @@ class BaseXMPP(XMLStream): """When disconnected, reset the roster""" self.roster = {} + def _handle_stream_error(self, error): + self.event('stream_error', error) + def _handle_message(self, msg): """Process incoming message stanzas.""" self.event('message', msg) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index 32795e4b..a1813985 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -139,7 +139,7 @@ class ClientXMPP(BaseXMPP): log.debug("Session start has taken more than 15 seconds") self.disconnect(reconnect=self.auto_reconnect) - def connect(self, address=tuple()): + def connect(self, address=tuple(), reattempt=True): """ Connect to the XMPP server. @@ -148,7 +148,9 @@ class ClientXMPP(BaseXMPP): will be used. Arguments: - address -- A tuple containing the server's host and port. + address -- A tuple containing the server's host and port. + reattempt -- If True, reattempt the connection if an + error occurs. """ self.session_started_event.clear() if not address or len(address) < 2: @@ -162,11 +164,13 @@ class ClientXMPP(BaseXMPP): log.debug("Since no address is supplied," + \ "attempting SRV lookup.") try: - xmpp_srv = "_xmpp-client._tcp.%s" % self.server + xmpp_srv = "_xmpp-client._tcp.%s" % self.boundjid.host answers = dns.resolver.query(xmpp_srv, dns.rdatatype.SRV) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): log.debug("No appropriate SRV record found." + \ " Using JID server name.") + except (dns.exception.Timeout,): + log.debug("DNS resolution timed out.") else: # Pick a random server, weighted by priority. @@ -190,7 +194,8 @@ class ClientXMPP(BaseXMPP): # If all else fails, use the server from the JID. address = (self.boundjid.host, 5222) - return XMLStream.connect(self, address[0], address[1], use_tls=True) + return XMLStream.connect(self, address[0], address[1], + use_tls=True, reattempt=reattempt) def register_feature(self, mask, pointer, breaker=False): """ diff --git a/sleekxmpp/plugins/old_0004.py b/sleekxmpp/plugins/old_0004.py index ade3d682..7f086866 100644 --- a/sleekxmpp/plugins/old_0004.py +++ b/sleekxmpp/plugins/old_0004.py @@ -6,7 +6,7 @@ See the file LICENSE for copying permission. """ from . import base -import log +import logging from xml.etree import cElementTree as ET import copy import logging diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 92ee5ec6..a976b988 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -90,6 +90,10 @@ class xep_0030(base_plugin): self.description = 'Service Discovery' self.stanza = sleekxmpp.plugins.xep_0030.stanza + # Retain some backwards compatibility + self.getInfo = self.get_info + self.getItems = self.get_items + self.xmpp.register_handler( Callback('Disco Info', StanzaPath('iq/disco_info'), @@ -116,6 +120,12 @@ class xep_0030(base_plugin): 'jid': {}, 'node': {}} + def post_init(self): + """Handle cross-plugin dependencies.""" + base_plugin.post_init(self) + if self.xmpp['xep_0059']: + register_stanza_plugin(DiscoItems, self.xmpp['xep_0059'].stanza.Set) + def set_node_handler(self, htype, jid=None, node=None, handler=None): """ Add a node handler for the given hierarchy level and @@ -167,7 +177,10 @@ class xep_0030(base_plugin): elif node is None: self._handlers[htype]['jid'][jid] = handler elif jid is None: - jid = self.xmpp.boundjid.full + if self.xmpp.is_component: + jid = self.xmpp.boundjid.full + else: + jid = self.xmpp.boundjid.bare self._handlers[htype]['node'][(jid, node)] = handler else: self._handlers[htype]['node'][(jid, node)] = handler @@ -225,6 +238,9 @@ class xep_0030(base_plugin): by executing the local node handlers, or if a disco#info stanza must be generated and sent. + If requesting items from a local JID/node, then only a DiscoInfo + stanza will be returned. Otherwise, an Iq stanza will be returned. + Arguments: jid -- Request info from this JID. node -- The particular node to query. @@ -236,7 +252,8 @@ class xep_0030(base_plugin): ifrom -- Specifiy the sender's JID. block -- If true, block and wait for the stanzas' reply. timeout -- The time in seconds to block while waiting for - a reply. If None, then wait indefinitely. + a reply. If None, then wait indefinitely. The + timeout value is only used when block=True. callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. @@ -248,7 +265,8 @@ class xep_0030(base_plugin): return self._fix_default_info(info) iq = self.xmpp.Iq() - iq['from'] = kwargs.get('ifrom', '') + # Check dfrom parameter for backwards compatibility + iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', '')) iq['to'] = jid iq['type'] = 'get' iq['disco_info']['node'] = node if node else '' @@ -265,6 +283,9 @@ class xep_0030(base_plugin): executing the local node handlers, or if a disco#items stanza must be generated and sent. + If requesting items from a local JID/node, then only a DiscoItems + stanza will be returned. Otherwise, an Iq stanza will be returned. + Arguments: jid -- Request info from this JID. node -- The particular node to query. @@ -280,18 +301,25 @@ class xep_0030(base_plugin): callback -- Optional callback to execute when a reply is received instead of blocking and waiting for the reply. + iterator -- If True, return a result set iterator using + the XEP-0059 plugin, if the plugin is loaded. + Otherwise the parameter is ignored. """ if local or jid is None: return self._run_node_handler('get_items', jid, node, kwargs) iq = self.xmpp.Iq() - iq['from'] = kwargs.get('ifrom', '') + # Check dfrom parameter for backwards compatibility + iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', '')) iq['to'] = jid iq['type'] = 'get' iq['disco_items']['node'] = node if node else '' - return iq.send(timeout=kwargs.get('timeout', None), - block=kwargs.get('block', None), - callback=kwargs.get('callback', None)) + if kwargs.get('iterator', False) and self.xmpp['xep_0059']: + return self.xmpp['xep_0059'].iterate(iq, 'disco_items') + else: + return iq.send(timeout=kwargs.get('timeout', None), + block=kwargs.get('block', None), + callback=kwargs.get('callback', None)) def set_items(self, jid=None, node=None, **kwargs): """ @@ -317,7 +345,7 @@ class xep_0030(base_plugin): """ self._run_node_handler('del_items', jid, node, kwargs) - def add_item(self, jid=None, node=None, **kwargs): + def add_item(self, jid='', name='', node=None, subnode='', ijid=None): """ Add a new item element to the given JID/node combination. @@ -325,13 +353,18 @@ class xep_0030(base_plugin): a node value to reference non-addressable entities. Arguments: - jid -- The JID to modify. - node -- The node to modify. - ijid -- The JID for the item. - inode -- Optional node for the item. + jid -- The JID for the item. name -- Optional name for the item. + node -- The node to modify. + subnode -- Optional node for the item. + ijid -- The JID to modify. """ - self._run_node_handler('add_item', jid, node, kwargs) + if not jid: + jid = self.xmpp.boundjid.full + kwargs = {'ijid': jid, + 'name': name, + 'inode': subnode} + self._run_node_handler('add_item', ijid, node, kwargs) def del_item(self, jid=None, node=None, **kwargs): """ @@ -345,7 +378,7 @@ class xep_0030(base_plugin): """ self._run_node_handler('del_item', jid, node, kwargs) - def add_identity(self, jid=None, node=None, **kwargs): + def add_identity(self, category='', itype='', name='', node=None, jid=None, lang=None): """ Add a new identity to the given JID/node combination. @@ -358,24 +391,29 @@ class xep_0030(base_plugin): names are different. A category and type is always required. Arguments: - jid -- The JID to modify. - node -- The node to modify. category -- The identity's category. itype -- The identity's type. name -- Optional name for the identity. lang -- Optional two-letter language code. + node -- The node to modify. + jid -- The JID to modify. """ + kwargs = {'category': category, + 'itype': itype, + 'name': name, + 'lang': lang} self._run_node_handler('add_identity', jid, node, kwargs) - def add_feature(self, jid=None, node=None, **kwargs): + def add_feature(self, feature, node=None, jid=None): """ Add a feature to a JID/node combination. Arguments: - jid -- The JID to modify. - node -- The node to modify. feature -- The namespace of the supported feature. + node -- The node to modify. + jid -- The JID to modify. """ + kwargs = {'feature': feature} self._run_node_handler('add_feature', jid, node, kwargs) def del_identity(self, jid=None, node=None, **kwargs): @@ -467,7 +505,10 @@ class xep_0030(base_plugin): data -- Optional, custom data to pass to the handler. """ if jid is None: - jid = self.xmpp.boundjid.full + if self.xmpp.is_component: + jid = self.xmpp.boundjid.full + else: + jid = self.xmpp.boundjid.bare if node is None: node = '' @@ -493,8 +534,12 @@ class xep_0030(base_plugin): if iq['type'] == 'get': log.debug("Received disco info query from " + \ "<%s> to <%s>." % (iq['from'], iq['to'])) + if self.xmpp.is_component: + jid = iq['to'].full + else: + jid = iq['to'].bare info = self._run_node_handler('get_info', - iq['to'].full, + jid, iq['disco_info']['node'], iq) iq.reply() @@ -519,8 +564,12 @@ class xep_0030(base_plugin): if iq['type'] == 'get': log.debug("Received disco items query from " + \ "<%s> to <%s>." % (iq['from'], iq['to'])) + if self.xmpp.is_component: + jid = iq['to'].full + else: + jid = iq['to'].bare items = self._run_node_handler('get_items', - iq['to'].full, + jid, iq['disco_items']['node']) iq.reply() if items: @@ -557,3 +606,4 @@ class xep_0030(base_plugin): "Using default disco#info feature.") info.add_feature(info.namespace) return info + diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py index eff67f02..f957c84c 100644 --- a/sleekxmpp/plugins/xep_0030/static.py +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -247,8 +247,8 @@ class StaticDisco(object): self.add_node(jid, node) self.nodes[(jid, node)]['items'].add_item( data.get('ijid', ''), - node=data.get('inode', None), - name=data.get('name', None)) + node=data.get('inode', ''), + name=data.get('name', '')) def del_item(self, jid, node, data): """ @@ -262,3 +262,4 @@ class StaticDisco(object): self.nodes[(jid, node)]['items'].del_item( data.get('ijid', ''), node=data.get('inode', None)) + diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index feec70db..364fbbd9 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -316,6 +316,7 @@ class xep_0045(base.base_plugin): x = ET.Element('{jabber:x:data}x', type='cancel') query.append(x) iq = self.xmpp.makeIqSet(query) + iq['to'] = room iq.send() def setRoomConfig(self, room, config, ifrom=''): diff --git a/sleekxmpp/plugins/xep_0059/__init__.py b/sleekxmpp/plugins/xep_0059/__init__.py new file mode 100644 index 00000000..3a9b8edf --- /dev/null +++ b/sleekxmpp/plugins/xep_0059/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0059.stanza import Set +from sleekxmpp.plugins.xep_0059.rsm import ResultIterator, xep_0059 diff --git a/sleekxmpp/plugins/xep_0059/rsm.py b/sleekxmpp/plugins/xep_0059/rsm.py new file mode 100644 index 00000000..35908473 --- /dev/null +++ b/sleekxmpp/plugins/xep_0059/rsm.py @@ -0,0 +1,119 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +import sleekxmpp +from sleekxmpp import Iq +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0059 import Set + + +log = logging.getLogger(__name__) + + +class ResultIterator(): + + """ + An iterator for Result Set Managment + """ + + def __init__(self, query, interface, amount=10, start=None, reverse=False): + """ + Arguments: + query -- The template query + interface -- The substanza of the query, for example disco_items + amount -- The max amounts of items to request per iteration + start -- From which item id to start + reverse -- If True, page backwards through the results + + Example: + q = Iq() + q['to'] = 'pubsub.example.com' + q['disco_items']['node'] = 'blog' + for i in ResultIterator(q, 'disco_items', '10'): + print i['disco_items']['items'] + + """ + self.query = query + self.amount = amount + self.start = start + self.interface = interface + self.reverse = reverse + + def __iter__(self): + return self + + def __next__(self): + return self.next() + + def next(self): + """ + Return the next page of results from a query. + + Note: If using backwards paging, then the next page of + results will be the items before the current page + of items. + """ + self.query[self.interface]['rsm']['before'] = self.reverse + self.query['id'] = self.query.stream.new_id() + self.query[self.interface]['rsm']['max'] = str(self.amount) + + if self.start and self.reverse: + self.query[self.interface]['rsm']['before'] = self.start + elif self.start: + self.query[self.interface]['rsm']['after'] = self.start + + r = self.query.send(block=True) + + if not r or not r[self.interface]['rsm']['first'] and \ + not r[self.interface]['rsm']['last']: + raise StopIteration + + if self.reverse: + self.start = r[self.interface]['rsm']['first'] + else: + self.start = r[self.interface]['rsm']['last'] + + return r + + +class xep_0059(base_plugin): + + """ + XEP-0050: Result Set Management + """ + + def plugin_init(self): + """ + Start the XEP-0059 plugin. + """ + self.xep = '0059' + self.description = 'Result Set Management' + self.stanza = sleekxmpp.plugins.xep_0059.stanza + + def post_init(self): + """Handle inter-plugin dependencies.""" + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature(Set.namespace) + + def iterate(self, stanza, interface): + """ + Create a new result set iterator for a given stanza query. + + Arguments: + stanza -- A stanza object to serve as a template for + queries made each iteration. For example, a + basic disco#items query. + interface -- The name of the substanza to which the + result set management stanza should be + appended. For example, for disco#items queries + the interface 'disco_items' should be used. + """ + return ResultIterator(stanza, interface) diff --git a/sleekxmpp/plugins/xep_0059/stanza.py b/sleekxmpp/plugins/xep_0059/stanza.py new file mode 100644 index 00000000..7c637d0b --- /dev/null +++ b/sleekxmpp/plugins/xep_0059/stanza.py @@ -0,0 +1,108 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Erik Reuterborg Larsson + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET +from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems + + +class Set(ElementBase): + + """ + XEP-0059 (Result Set Managment) can be used to manage the + results of queries. For example, limiting the number of items + per response or starting at certain positions. + + Example set stanzas: + <iq type="get"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <set xmlns="http://jabber.org/protocol/rsm"> + <max>2</max> + </set> + </query> + </iq> + + <iq type="result"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <item jid="conference.example.com" /> + <item jid="pubsub.example.com" /> + <set xmlns="http://jabber.org/protocol/rsm"> + <first>conference.example.com</first> + <last>pubsub.example.com</last> + </set> + </query> + </iq> + + Stanza Interface: + first_index -- The index attribute of <first> + after -- The id defining from which item to start + before -- The id defining from which item to + start when browsing backwards + max -- Max amount per response + first -- Id for the first item in the response + last -- Id for the last item in the response + index -- Used to set an index to start from + count -- The number of remote items available + + Methods: + set_first_index -- Sets the index attribute for <first> and + creates the element if it doesn't exist + get_first_index -- Returns the value of the index + attribute for <first> + del_first_index -- Removes the index attribute for <first> + but keeps the element + set_before -- Sets the value of <before>, if the value is True + then the element will be created without a value + get_before -- Returns the value of <before>, if it is + empty it will return True + + """ + namespace = 'http://jabber.org/protocol/rsm' + name = 'set' + plugin_attrib = 'rsm' + sub_interfaces = set(('first', 'after', 'before', 'count', + 'index', 'last', 'max')) + interfaces = set(('first_index', 'first', 'after', 'before', + 'count', 'index', 'last', 'max')) + + def set_first_index(self, val): + fi = self.find("{%s}first" % (self.namespace)) + if fi is not None: + if val: + fi.attrib['index'] = val + else: + del fi.attrib['index'] + elif val: + fi = ET.Element("{%s}first" % (self.namespace)) + fi.attrib['index'] = val + self.xml.append(fi) + + def get_first_index(self): + fi = self.find("{%s}first" % (self.namespace)) + if fi is not None: + return fi.attrib.get('index', '') + + def del_first_index(self): + fi = self.xml.find("{%s}first" % (self.namespace)) + if fi is not None: + del fi.attrib['index'] + + def set_before(self, val): + b = self.xml.find("{%s}before" % (self.namespace)) + if b is None and val == True: + self._set_sub_text('{%s}before' % self.namespace, '', True) + else: + self._set_sub_text('{%s}before' % self.namespace, val) + + def get_before(self): + b = self.xml.find("{%s}before" % (self.namespace)) + if b is not None and not b.text: + return True + elif b is not None: + return b.text + else: + return None diff --git a/sleekxmpp/plugins/xep_0078.py b/sleekxmpp/plugins/xep_0078.py index d2c81b16..bb6a4632 100644 --- a/sleekxmpp/plugins/xep_0078.py +++ b/sleekxmpp/plugins/xep_0078.py @@ -36,7 +36,7 @@ class xep_0078(base.base_plugin): log.debug("Starting jabber:iq:auth Authentication") auth_request = self.xmpp.makeIqGet() auth_request_query = ET.Element('{jabber:iq:auth}query') - auth_request.attrib['to'] = self.xmpp.server + auth_request.attrib['to'] = self.xmpp.boundjid.host username = ET.Element('username') username.text = self.xmpp.username auth_request_query.append(username) diff --git a/sleekxmpp/plugins/xep_0092.py b/sleekxmpp/plugins/xep_0092.py deleted file mode 100644 index c9b418ff..00000000 --- a/sleekxmpp/plugins/xep_0092.py +++ /dev/null @@ -1,56 +0,0 @@ -""" - 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 -from .. xmlstream.handler.xmlwaiter import XMLWaiter - -class xep_0092(base.base_plugin): - """ - XEP-0092 Software Version - """ - def plugin_init(self): - self.description = "Software Version" - self.xep = "0092" - self.name = self.config.get('name', 'SleekXMPP') - self.version = self.config.get('version', '0.1-dev') - self.xmpp.add_handler("<iq type='get' xmlns='%s'><query xmlns='jabber:iq:version' /></iq>" % self.xmpp.default_ns, self.report_version, name='Sofware Version') - - def post_init(self): - base.base_plugin.post_init(self) - self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version') - - def report_version(self, xml): - iq = self.xmpp.makeIqResult(xml.get('id', 'unknown')) - iq.attrib['to'] = xml.get('from', self.xmpp.server) - query = ET.Element('{jabber:iq:version}query') - name = ET.Element('name') - name.text = self.name - version = ET.Element('version') - version.text = self.version - query.append(name) - query.append(version) - iq.append(query) - self.xmpp.send(iq) - - def getVersion(self, jid): - iq = self.xmpp.makeIqGet() - query = ET.Element('{jabber:iq:version}query') - iq.append(query) - iq.attrib['to'] = jid - iq.attrib['from'] = self.xmpp.boundjid.full - id = iq.get('id') - result = iq.send() - if result and result is not None and result.get('type', 'error') != 'error': - qry = result.find('{jabber:iq:version}query') - version = {} - for child in qry.getchildren(): - version[child.tag.split('}')[-1]] = child.text - return version - else: - return False - diff --git a/sleekxmpp/plugins/xep_0092/__init__.py b/sleekxmpp/plugins/xep_0092/__init__.py new file mode 100644 index 00000000..7c5bdb76 --- /dev/null +++ b/sleekxmpp/plugins/xep_0092/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0092 import stanza +from sleekxmpp.plugins.xep_0092.stanza import Version +from sleekxmpp.plugins.xep_0092.version import xep_0092 diff --git a/sleekxmpp/plugins/xep_0092/stanza.py b/sleekxmpp/plugins/xep_0092/stanza.py new file mode 100644 index 00000000..77654e37 --- /dev/null +++ b/sleekxmpp/plugins/xep_0092/stanza.py @@ -0,0 +1,42 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class Version(ElementBase): + + """ + XMPP allows for an agent to advertise the name and version of the + underlying software libraries, as well as the operating system + that the agent is running on. + + Example version stanzas: + <iq type="get"> + <query xmlns="jabber:iq:version" /> + </iq> + + <iq type="result"> + <query xmlns="jabber:iq:version"> + <name>SleekXMPP</name> + <version>1.0</version> + <os>Linux</os> + </query> + </iq> + + Stanza Interface: + name -- The human readable name of the software. + version -- The specific version of the software. + os -- The name of the operating system running the program. + """ + + name = 'query' + namespace = 'jabber:iq:version' + plugin_attrib = 'software_version' + interfaces = set(('name', 'version', 'os')) + sub_interfaces = interfaces diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py new file mode 100644 index 00000000..fb3671e4 --- /dev/null +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -0,0 +1,88 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +import sleekxmpp +from sleekxmpp import Iq +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins.xep_0092 import Version + + +log = logging.getLogger(__name__) + + +class xep_0092(base_plugin): + + """ + XEP-0092: Software Version + """ + + def plugin_init(self): + """ + Start the XEP-0092 plugin. + """ + self.xep = "0092" + self.description = "Software Version" + self.stanza = sleekxmpp.plugins.xep_0092.stanza + + self.name = self.config.get('name', 'SleekXMPP') + self.version = self.config.get('version', '0.1-dev') + self.os = self.config.get('os', '') + + self.getVersion = self.get_version + + self.xmpp.register_handler( + Callback('Software Version', + StanzaPath('iq/software_version'), + self._handle_version)) + + register_stanza_plugin(Iq, Version) + + def post_init(self): + """ + Handle cross-plugin dependencies. + """ + base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature('jabber:iq:version') + + def _handle_version(self, iq): + """ + Respond to a software version query. + + Arguments: + iq -- The Iq stanza containing the software version query. + """ + iq.reply() + iq['software_version']['name'] = self.name + iq['software_version']['version'] = self.version + iq['software_version']['os'] = self.os + iq.send() + + def get_version(self, jid, ifrom=None): + """ + Retrieve the software version of a remote agent. + + Arguments: + jid -- The JID of the entity to query. + """ + iq = self.xmpp.Iq() + iq['to'] = jid + if ifrom: + iq['from'] = ifrom + iq['type'] = 'get' + iq['query'] = Version.namespace + + result = iq.send() + + if result and result['type'] != 'error': + return result['software_version'].values + return False diff --git a/sleekxmpp/plugins/xep_0199.py b/sleekxmpp/plugins/xep_0199.py index 2e99ae76..16e79e26 100644 --- a/sleekxmpp/plugins/xep_0199.py +++ b/sleekxmpp/plugins/xep_0199.py @@ -33,7 +33,7 @@ class xep_0199(base.base_plugin): def scheduled_ping(self): log.debug("pinging...") - if self.sendPing(self.xmpp.server, self.config.get('timeout', 30)) is False: + if self.sendPing(self.xmpp.boundjid.host, self.config.get('timeout', 30)) is False: log.debug("Did not recieve ping back in time. Requesting Reconnect.") self.xmpp.reconnect() diff --git a/sleekxmpp/plugins/xep_0202.py b/sleekxmpp/plugins/xep_0202.py index fe1191ea..3b31c97a 100644 --- a/sleekxmpp/plugins/xep_0202.py +++ b/sleekxmpp/plugins/xep_0202.py @@ -27,10 +27,12 @@ class EntityTime(ElementBase): interfaces = set(('tzo', 'utc'))
sub_interfaces = set(('tzo', 'utc'))
- #def get_utc(self): # TODO: return a datetime.tzinfo object?
+ #def get_tzo(self):
+ # TODO: Right now it returns a string but maybe it should
+ # return a datetime.tzinfo object or maybe a datetime.timedelta?
#pass
- def set_tzo(self, tzo): # TODO: support datetime.tzinfo objects?
+ def set_tzo(self, tzo):
if isinstance(tzo, tzinfo):
td = datetime.now(tzo).utcoffset() # What if we are faking the time? datetime.now() shouldn't be used here'
seconds = td.seconds + td.days * 24 * 3600
@@ -45,7 +47,7 @@ class EntityTime(ElementBase): # Returns a datetime object instead the string. Is this a good idea?
value = self._get_sub_text('utc')
if '.' in value:
- return datetime.strptime(value, '%Y-%m-%d.%fT%H:%M:%SZ')
+ return datetime.strptime(value, '%Y-%m-%dT%H:%M:%S.%fZ')
else:
return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
diff --git a/sleekxmpp/plugins/xep_0249/__init__.py b/sleekxmpp/plugins/xep_0249/__init__.py new file mode 100644 index 00000000..4af19a2b --- /dev/null +++ b/sleekxmpp/plugins/xep_0249/__init__.py @@ -0,0 +1,2 @@ +from sleekxmpp.plugins.xep_0249.stanza import Invite +from sleekxmpp.plugins.xep_0249.invite import xep_0249 diff --git a/sleekxmpp/plugins/xep_0249/invite.py b/sleekxmpp/plugins/xep_0249/invite.py new file mode 100644 index 00000000..7c966f15 --- /dev/null +++ b/sleekxmpp/plugins/xep_0249/invite.py @@ -0,0 +1,75 @@ +"""Direct MUC Invitation.""" + + +import logging + +import sleekxmpp +from sleekxmpp import Message +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.plugins.xep_0249 import Invite + +log = logging.getLogger(__name__) + + +class xep_0249(base_plugin): + + """ + XEP-0249: Direct MUC Invitations + """ + + def plugin_init(self): + self.xep = "0249" + self.description = "Direct MUC Invitations" + self.stanza = sleekxmpp.plugins.xep_0249.stanza + + self.xmpp.register_handler( + Callback('Direct MUC Invitations', + StanzaPath('message/groupchat_invite'), + self._handle_invite)) + + register_stanza_plugin(Message, Invite) + + def post_init(self): + base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature(Invite.namespace) + + def _handle_invite(self, message): + """ + Raise an event for all invitations received. + + """ + log.debug("Received direct muc invitation from %s to room %s", + message['from'], message['groupchat_invite']['jid']) + + self.xmpp.event('groupchat_direct_invite', message) + + def send_invitation(self, jid, roomjid, password=None, + reason=None, ifrom=None): + """ + Send a direct MUC invitation to an XMPP entity. + + Arguments: + jid -- The jid of the entity to which the inviation + is sent + roomjid -- the address of the groupchat room to be joined + password -- a password needed for entry into a + password-protected room (OPTIONAL). + reason -- a human-readable purpose for the invitation + (OPTIONAL). + + """ + + message = self.xmpp.Message() + message['to'] = jid + if ifrom is not None: + message['from'] = ifrom + message['groupchat_invite']['jid'] = roomjid + if password is not None: + message['groupchat_invite']['password'] = password + if reason is not None: + message['groupchat_invite']['reason'] = reason + + return message.send() diff --git a/sleekxmpp/plugins/xep_0249/stanza.py b/sleekxmpp/plugins/xep_0249/stanza.py new file mode 100644 index 00000000..43bb65d0 --- /dev/null +++ b/sleekxmpp/plugins/xep_0249/stanza.py @@ -0,0 +1,29 @@ +from sleekxmpp.xmlstream import ElementBase + + +class Invite(ElementBase): + """ + XMPP allows for an agent in an MUC room to directly invite another + user to join the chat room (as opposed to a mediated invitation + done through the server). + + Example invite stanza: + <message from='crone1@shakespeare.lit/desktop' + to='hecate@shakespeare.lit'> + <x xmlns='jabber:x:conference' + jid='darkcave@macbeth.shakespeare.lit' + password='cauldronburn' + reason='Hey Hecate, this is the place for all good witches!'/> + </message> + + Stanza Interface: + jid -- The JID of the groupchat room + password -- The password used to gain entry in the room + (optional) + reason -- The reason for the invitation (optional) + + """ + name = "x" + namespace = "jabber:x:conference" + plugin_attrib = "groupchat_invite" + interfaces = ("jid", "password", "reason")
\ No newline at end of file diff --git a/sleekxmpp/stanza/__init__.py b/sleekxmpp/stanza/__init__.py index 8302c43d..dbf7b86f 100644 --- a/sleekxmpp/stanza/__init__.py +++ b/sleekxmpp/stanza/__init__.py @@ -8,6 +8,7 @@ from sleekxmpp.stanza.error import Error +from sleekxmpp.stanza.stream_error import StreamError from sleekxmpp.stanza.iq import Iq from sleekxmpp.stanza.message import Message from sleekxmpp.stanza.presence import Presence diff --git a/sleekxmpp/stanza/iq.py b/sleekxmpp/stanza/iq.py index 906e6648..c6aa64d0 100644 --- a/sleekxmpp/stanza/iq.py +++ b/sleekxmpp/stanza/iq.py @@ -199,3 +199,28 @@ class Iq(RootStanza): return waitfor.wait(timeout) else: return StanzaBase.send(self) + + def _set_stanza_values(self, values): + """ + Set multiple stanza interface values using a dictionary. + + Stanza plugin values may be set usind nested dictionaries. + + If the interface 'query' is given, then it will be set + last to avoid duplication of the <query /> element. + + Overrides ElementBase._set_stanza_values. + + Arguments: + values -- A dictionary mapping stanza interface with values. + Plugin interfaces may accept a nested dictionary that + will be used recursively. + """ + query = values.get('query', '') + if query: + del values['query'] + StanzaBase._set_stanza_values(self, values) + self['query'] = query + else: + StanzaBase._set_stanza_values(self, values) + return self diff --git a/sleekxmpp/stanza/nick.py b/sleekxmpp/stanza/nick.py index a9243d1a..dce41d14 100644 --- a/sleekxmpp/stanza/nick.py +++ b/sleekxmpp/stanza/nick.py @@ -44,7 +44,7 @@ class Nick(ElementBase): del_nick -- Remove the <nick> element. """ - namespace = 'http://jabber.org/nick/nick' + namespace = 'http://jabber.org/protocol/nick' name = 'nick' plugin_attrib = name interfaces = set(('nick',)) diff --git a/sleekxmpp/stanza/stream_error.py b/sleekxmpp/stanza/stream_error.py new file mode 100644 index 00000000..cf59a7fa --- /dev/null +++ b/sleekxmpp/stanza/stream_error.py @@ -0,0 +1,69 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.stanza.error import Error +from sleekxmpp.xmlstream import StanzaBase, ElementBase, ET +from sleekxmpp.xmlstream import register_stanza_plugin + + +class StreamError(Error, StanzaBase): + + """ + XMPP stanzas of type 'error' should include an <error> stanza that + describes the nature of the error and how it should be handled. + + Use the 'XEP-0086: Error Condition Mappings' plugin to include error + codes used in older XMPP versions. + + The stream:error stanza is used to provide more information for + error that occur with the underlying XML stream itself, and not + a particular stanza. + + Note: The StreamError stanza is mostly the same as the normal + Error stanza, but with different namespaces and + condition names. + + Example error stanza: + <stream:error> + <not-well-formed xmlns="urn:ietf:params:xml:ns:xmpp-streams" /> + <text xmlns="urn:ietf:params:xml:ns:xmpp-streams"> + XML was not well-formed. + </text> + </stream:error> + + Stanza Interface: + condition -- The name of the condition element. + text -- Human readable description of the error. + + Attributes: + conditions -- The set of allowable error condition elements. + condition_ns -- The namespace for the condition element. + + Methods: + setup -- Overrides ElementBase.setup. + get_condition -- Retrieve the name of the condition element. + set_condition -- Add a condition element. + del_condition -- Remove the condition element. + get_text -- Retrieve the contents of the <text> element. + set_text -- Set the contents of the <text> element. + del_text -- Remove the <text> element. + """ + + namespace = 'http://etherx.jabber.org/streams' + interfaces = set(('condition', 'text')) + conditions = set(( + 'bad-format', 'bad-namespace-prefix', 'conflict', + 'connection-timeout', 'host-gone', 'host-unknown', + 'improper-addressing', 'internal-server-error', 'invalid-from', + 'invalid-namespace', 'invalid-xml', 'not-authorized', + 'not-well-formed', 'policy-violation', 'remote-connection-failed', + 'reset', 'resource-constraint', 'restricted-xml', 'see-other-host', + 'system-shutdown', 'undefined-condition', 'unsupported-encoding', + 'unsupported-feature', 'unsupported-stanza-type', + 'unsupported-version')) + condition_ns = 'urn:ietf:params:xml:ns:xmpp-streams' diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index aa411cd7..b5c28fde 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -157,8 +157,7 @@ class SleekTest(unittest.TestCase): """ 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. + If use_values is False, tests using stanza.values will not be used. Some stanzas provide default values for some interfaces, but these defaults can be problematic for testing since they can easily @@ -181,9 +180,8 @@ class SleekTest(unittest.TestCase): 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. + use_values -- Indicates if testing using stanza.values should + be used. Defaults to True. """ if method is None and hasattr(self, 'match_method'): method = getattr(self, 'match_method') @@ -216,10 +214,10 @@ class SleekTest(unittest.TestCase): 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. + # Using stanza.values will add XML for any interface that + # has a default value. We need to set those defaults on + # the existing stanzas and XML so that they will compare + # correctly. default_stanza = stanza_class() if defaults is None: known_defaults = { @@ -238,9 +236,9 @@ class SleekTest(unittest.TestCase): value = default_stanza.xml.attrib[interface] xml.attrib[interface] = value - values = stanza2.getStanzaValues() + values = stanza2.values stanza3 = stanza_class() - stanza3.setStanzaValues(values) + stanza3.values = values debug = "Three methods for creating stanzas do not match.\n" debug += "Given XML:\n%s\n" % tostring(xml) @@ -390,8 +388,7 @@ class SleekTest(unittest.TestCase): 'id', 'stanzapath', 'xpath', and 'mask'. Defaults to the value of self.match_method. use_values -- Indicates if stanza comparisons should test using - getStanzaValues() and setStanzaValues(). - Defaults to True. + stanza.values. Defaults to True. timeout -- Time to wait in seconds for data to be received by a live connection. """ diff --git a/sleekxmpp/thirdparty/__init__.py b/sleekxmpp/thirdparty/__init__.py index e69de29b..276ac3cc 100644 --- a/sleekxmpp/thirdparty/__init__.py +++ b/sleekxmpp/thirdparty/__init__.py @@ -0,0 +1,4 @@ +try: + from collections import OrderedDict +except: + from sleekxmpp.thirdparty.ordereddict import OrderedDict diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py index d8f45b92..5019a25e 100644 --- a/sleekxmpp/xmlstream/jid.py +++ b/sleekxmpp/xmlstream/jid.py @@ -71,7 +71,7 @@ class JID(object): if self._domain is None: self._domain = self._jid.split('@', 1)[-1].split('/', 1)[0] return self._domain or "" - elif name == 'full': + elif name in ('full', 'jid'): return self._jid or "" elif name == 'bare': if self._bare is None: @@ -124,3 +124,9 @@ class JID(object): def __repr__(self): return str(self) + + def __eq__(self, other): + """ + Two JIDs are considered equal if they have the same full JID value. + """ + return str(other) == str(self) diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 5551d439..3937a7a9 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -14,6 +14,7 @@ from xml.etree import cElementTree as ET from sleekxmpp.xmlstream import JID from sleekxmpp.xmlstream.tostring import tostring +from sleekxmpp.thirdparty import OrderedDict log = logging.getLogger(__name__) @@ -23,17 +24,23 @@ log = logging.getLogger(__name__) XML_TYPE = type(ET.Element('xml')) -def register_stanza_plugin(stanza, plugin): +def register_stanza_plugin(stanza, plugin, iterable=False): """ Associate a stanza object as a plugin for another stanza. Arguments: - stanza -- The class of the parent stanza. - plugin -- The class of the plugin stanza. + stanza -- The class of the parent stanza. + plugin -- The class of the plugin stanza. + iterable -- Indicates if the plugin stanza + should be included in the parent + stanza's iterable 'substanzas' + interface results. """ tag = "{%s}%s" % (plugin.namespace, plugin.name) stanza.plugin_attrib_map[plugin.plugin_attrib] = plugin stanza.plugin_tag_map[tag] = plugin + if iterable: + stanza.plugin_iterables.add(plugin) # To maintain backwards compatibility for now, preserve the camel case name. @@ -95,10 +102,22 @@ class ElementBase(object): >>> message['custom']['useful_thing'] = 'foo' If a plugin provides an interface that is the same as the plugin's - plugin_attrib value, then the plugin's interface may be accessed - directly from the parent stanza, as so: + plugin_attrib value, then the plugin's interface may be assigned + directly from the parent stanza, as shown below, but retrieving + information will require all interfaces to be used, as so: >>> message['custom'] = 'bar' # Same as using message['custom']['custom'] + >>> message['custom']['custom'] # Must use all interfaces + 'bar' + + If the plugin sets the value is_extension = True, then both setting + and getting an interface value that is the same as the plugin's + plugin_attrib value will work, as so: + + >>> message['custom'] = 'bar' # Using is_extension=True + >>> message['custom'] + 'bar' + Class Attributes: name -- The name of the stanza's main element. @@ -108,14 +127,23 @@ class ElementBase(object): sub_interfaces -- A subset of the set of interfaces which map to subelements instead of attributes. subitem -- A set of stanza classes which are allowed to - be added as substanzas. + be added as substanzas. Deprecated version + of plugin_iterables. types -- A set of generic type attribute values. + tag -- The namespaced name of the stanza's root + element. Example: "{foo_ns}bar" plugin_attrib -- The interface name that the stanza uses to be accessed as a plugin from another stanza. plugin_attrib_map -- A mapping of plugin attribute names with the associated plugin stanza classes. + plugin_iterables -- A set of stanza classes which are allowed to + be added as substanzas. plugin_tag_map -- A mapping of plugin stanza tag names with the associated plugin stanza classes. + is_extension -- When True, allows the stanza to provide one + additional interface to the parent stanza, + extending the interfaces supported by the + parent. Defaults to False. xml_ns -- The XML namespace, http://www.w3.org/XML/1998/namespace, for use with xml:lang values. @@ -128,6 +156,10 @@ class ElementBase(object): values -- A dictionary of the stanza's interfaces and interface values, including plugins. + Class Methods + tag_name -- Return the namespaced version of the stanza's + root element's name. + Methods: setup -- Initialize the stanza's XML contents. enable -- Instantiate a stanza plugin. @@ -160,6 +192,7 @@ class ElementBase(object): appendxml -- Add XML content to the stanza. pop -- Remove a substanza. next -- Return the next iterable substanza. + clear -- Reset the stanza's XML contents. _fix_ns -- Apply the stanza's namespace to non-namespaced elements in an XPath expression. """ @@ -171,8 +204,10 @@ class ElementBase(object): types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) sub_interfaces = tuple() plugin_attrib_map = {} + plugin_iterables = set() plugin_tag_map = {} subitem = None + is_extension = False xml_ns = 'http://www.w3.org/XML/1998/namespace' def __init__(self, xml=None, parent=None): @@ -196,9 +231,10 @@ class ElementBase(object): self.setStanzaValues = self._set_stanza_values self.xml = xml - self.plugins = {} + self.plugins = OrderedDict() self.iterables = [] self._index = 0 + self.tag = self.tag_name() if parent is None: self.parent = None else: @@ -218,9 +254,11 @@ class ElementBase(object): 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 + self.plugin_iterables.add(sub) + for sub in self.plugin_iterables: + if child.tag == "{%s}%s" % (sub.namespace, sub.name): + self.iterables.append(sub(child, self)) + break def setup(self, xml=None): """ @@ -287,14 +325,12 @@ class ElementBase(object): for interface in self.interfaces: values[interface] = self[interface] for plugin, stanza in self.plugins.items(): - values[plugin] = stanza._get_stanza_values() + values[plugin] = stanza.values if self.iterables: iterables = [] for stanza in self.iterables: - iterables.append(stanza._get_stanza_values()) - iterables[-1].update({ - '__childtag__': "{%s}%s" % (stanza.namespace, - stanza.name)}) + iterables.append(stanza.values) + iterables[-1]['__childtag__'] = stanza.tag values['substanzas'] = iterables return values @@ -318,7 +354,7 @@ class ElementBase(object): subclass.name) if subdict['__childtag__'] == child_tag: sub = subclass(parent=self) - sub._set_stanza_values(subdict) + sub.values = subdict self.iterables.append(sub) break elif interface in self.interfaces: @@ -326,7 +362,7 @@ class ElementBase(object): elif interface in self.plugin_attrib_map: if interface not in self.plugins: self.init_plugin(interface) - self.plugins[interface]._set_stanza_values(value) + self.plugins[interface].values = value return self def __getitem__(self, attrib): @@ -371,6 +407,8 @@ class ElementBase(object): elif attrib in self.plugin_attrib_map: if attrib not in self.plugins: self.init_plugin(attrib) + if self.plugins[attrib].is_extension: + return self.plugins[attrib][attrib] return self.plugins[attrib] else: return '' @@ -467,8 +505,13 @@ class ElementBase(object): elif attrib in self.plugin_attrib_map: if attrib in self.plugins: xml = self.plugins[attrib].xml + if self.plugins[attrib].is_extension: + del self.plugins[attrib][attrib] del self.plugins[attrib] - self.xml.remove(xml) + try: + self.xml.remove(xml) + except: + pass return self def _set_attr(self, name, value): @@ -790,6 +833,28 @@ class ElementBase(object): """ return self.__next__() + def clear(self): + """ + Remove all XML element contents and plugins. + + Any attribute values will be preserved. + """ + for child in self.xml.getchildren(): + self.xml.remove(child) + for plugin in list(self.plugins.keys()): + del self.plugins[plugin] + return self + + @classmethod + def tag_name(cls): + """ + Return the namespaced name of the stanza's root element. + + For example, for the stanza <foo xmlns="bar" />, + stanza.tag would return "{bar}foo". + """ + return "{%s}%s" % (cls.namespace, cls.name) + @property def attrib(self): """ @@ -862,13 +927,13 @@ class ElementBase(object): return False # Check that this stanza is a superset of the other stanza. - values = self._get_stanza_values() + values = self.values for key in other.keys(): if key not in values or values[key] != other[key]: return False # Check that the other stanza is a superset of this stanza. - values = other._get_stanza_values() + values = other.values for key in self.keys(): if key not in values or values[key] != self[key]: return False @@ -972,7 +1037,6 @@ class StanzaBase(ElementBase): Attributes: stream -- The XMLStream instance that will handle sending this stanza. - tag -- The namespaced version of the stanza's name. Methods: set_type -- Set the type of the stanza. @@ -983,7 +1047,6 @@ class StanzaBase(ElementBase): get_payload -- Return the stanza's XML contents. set_payload -- Append to the stanza's XML contents. del_payload -- Remove the stanza's XML contents. - clear -- Reset the stanza's XML contents. reply -- Reset the stanza and modify the 'to' and 'from' attributes to prepare for sending a reply. error -- Set the stanza's type to 'error'. @@ -1098,18 +1161,6 @@ class StanzaBase(ElementBase): self.clear() return self - def clear(self): - """ - Remove all XML element contents and plugins. - - Any attribute values will be preserved. - """ - for child in self.xml.getchildren(): - self.xml.remove(child) - for plugin in list(self.plugins.keys()): - del self.plugins[plugin] - return self - def reply(self): """ Reset the stanza and swap its 'from' and 'to' attributes to prepare diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index d5c1043b..1cd23fba 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -292,6 +292,7 @@ class XMLStream(object): return True except Socket.error as serr: error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" + self.event('socket_error', serr) log.error(error_msg % (self.address[0], self.address[1], serr.errno, serr.strerror)) time.sleep(1) @@ -327,7 +328,7 @@ class XMLStream(object): self.filesocket.close() self.socket.shutdown(Socket.SHUT_RDWR) except Socket.error as serr: - pass + self.event('socket_error', serr) finally: #clear your application state self.event("disconnected", direct=True) @@ -734,7 +735,8 @@ class XMLStream(object): except SystemExit: log.debug("SystemExit in _process") self.stop.set() - except Socket.error: + except Socket.error as serr: + self.event('socket_error', serr) log.exception('Socket Error') except: if not self.stop.isSet(): @@ -800,7 +802,8 @@ class XMLStream(object): default_ns = self.default_ns stanza_type = StanzaBase for stanza_class in self.__root_stanza: - if xml.tag == "{%s}%s" % (default_ns, stanza_class.name): + if xml.tag == "{%s}%s" % (default_ns, stanza_class.name) or \ + xml.tag == stanza_class.tag_name(): stanza_type = stanza_class break stanza = stanza_type(self, xml) @@ -825,7 +828,8 @@ class XMLStream(object): # stanza type applies, a generic StanzaBase stanza will be used. stanza_type = StanzaBase for stanza_class in self.__root_stanza: - if xml.tag == "{%s}%s" % (self.default_ns, stanza_class.name): + if xml.tag == "{%s}%s" % (self.default_ns, stanza_class.name) or \ + xml.tag == stanza_class.tag_name(): stanza_type = stanza_class break stanza = stanza_type(self, xml) @@ -899,7 +903,7 @@ class XMLStream(object): args[0].exception(e) elif etype == 'schedule': try: - log.debug(args) + log.debug('Scheduled event: %s' % args) handler(*args[0]) except: log.exception('Error processing scheduled task') diff --git a/tests/test_stanza_message.py b/tests/test_stanza_message.py index f06b0253..e55971df 100644 --- a/tests/test_stanza_message.py +++ b/tests/test_stanza_message.py @@ -49,7 +49,7 @@ class TestMessageStanzas(SleekTest): msg['nick']['nick'] = 'A nickname!' self.check(msg, """ <message> - <nick xmlns="http://jabber.org/nick/nick">A nickname!</nick> + <nick xmlns="http://jabber.org/protocol/nick">A nickname!</nick> </message> """) diff --git a/tests/test_stanza_presence.py b/tests/test_stanza_presence.py index 8d043d5d..f9305a31 100644 --- a/tests/test_stanza_presence.py +++ b/tests/test_stanza_presence.py @@ -58,7 +58,7 @@ class TestPresenceStanzas(SleekTest): p['nick']['nick'] = 'A nickname!' self.check(p, """ <presence> - <nick xmlns="http://jabber.org/nick/nick">A nickname!</nick> + <nick xmlns="http://jabber.org/protocol/nick">A nickname!</nick> </presence> """) diff --git a/tests/test_stanza_xep_0059.py b/tests/test_stanza_xep_0059.py new file mode 100644 index 00000000..913436a6 --- /dev/null +++ b/tests/test_stanza_xep_0059.py @@ -0,0 +1,106 @@ +from sleekxmpp.test import * +from sleekxmpp.plugins.xep_0059 import Set + + +class TestSetStanzas(SleekTest): + + def testSetFirstIndex(self): + s = Set() + s['first'] = 'id' + s.set_first_index('10') + self.check(s, """ + <set xmlns="http://jabber.org/protocol/rsm"> + <first index="10">id</first> + </set> + """) + + def testGetFirstIndex(self): + xml_string = """ + <set xmlns="http://jabber.org/protocol/rsm"> + <first index="10">id</first> + </set> + """ + s = Set(ET.fromstring(xml_string)) + expected = '10' + self.failUnless(s['first_index'] == expected) + + def testDelFirstIndex(self): + xml_string = """ + <set xmlns="http://jabber.org/protocol/rsm"> + <first index="10">id</first> + </set> + """ + s = Set(ET.fromstring(xml_string)) + del s['first_index'] + self.check(s, """ + <set xmlns="http://jabber.org/protocol/rsm"> + <first>id</first> + </set> + """) + + def testSetBefore(self): + s = Set() + s['before'] = True + self.check(s, """ + <set xmlns="http://jabber.org/protocol/rsm"> + <before /> + </set> + """) + + def testGetBefore(self): + xml_string = """ + <set xmlns="http://jabber.org/protocol/rsm"> + <before /> + </set> + """ + s = Set(ET.fromstring(xml_string)) + expected = True + self.failUnless(s['before'] == expected) + + def testGetBefore(self): + xml_string = """ + <set xmlns="http://jabber.org/protocol/rsm"> + <before /> + </set> + """ + s = Set(ET.fromstring(xml_string)) + del s['before'] + self.check(s, """ + <set xmlns="http://jabber.org/protocol/rsm"> + </set> + """) + + def testSetBeforeVal(self): + s = Set() + s['before'] = 'id' + self.check(s, """ + <set xmlns="http://jabber.org/protocol/rsm"> + <before>id</before> + </set> + """) + + def testGetBeforeVal(self): + xml_string = """ + <set xmlns="http://jabber.org/protocol/rsm"> + <before>id</before> + </set> + """ + s = Set(ET.fromstring(xml_string)) + expected = 'id' + self.failUnless(s['before'] == expected) + + def testGetBeforeVal(self): + xml_string = """ + <set xmlns="http://jabber.org/protocol/rsm"> + <before>id</before> + </set> + """ + s = Set(ET.fromstring(xml_string)) + del s['before'] + self.check(s, """ + <set xmlns="http://jabber.org/protocol/rsm"> + </set> + """) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestSetStanzas) diff --git a/tests/test_stream_xep_0030.py b/tests/test_stream_xep_0030.py index 1f989745..c960fc7a 100644 --- a/tests/test_stream_xep_0030.py +++ b/tests/test_stream_xep_0030.py @@ -1,3 +1,4 @@ +import sys import time import threading @@ -11,6 +12,7 @@ class TestStreamDisco(SleekTest): """ def tearDown(self): + sys.excepthook = sys.__excepthook__ self.stream_close() def testInfoEmptyDefaultNode(self): @@ -406,10 +408,10 @@ class TestStreamDisco(SleekTest): self.xmpp['xep_0030'].make_static(jid='tester@localhost', node='testing') - self.xmpp['xep_0030'].add_item(jid='tester@localhost', + self.xmpp['xep_0030'].add_item(ijid='tester@localhost', node='testing', - ijid='tester@localhost', - inode='foo', + jid='tester@localhost', + subnode='foo', name='Test') self.recv(""" @@ -446,10 +448,10 @@ class TestStreamDisco(SleekTest): self.xmpp['xep_0030'].make_static(jid='user@tester.localhost', node='testing') - self.xmpp['xep_0030'].add_item(jid='user@tester.localhost', + self.xmpp['xep_0030'].add_item(ijid='user@tester.localhost', node='testing', - ijid='user@tester.localhost', - inode='foo', + jid='user@tester.localhost', + subnode='foo', name='Test') self.recv(""" @@ -524,5 +526,51 @@ class TestStreamDisco(SleekTest): self.assertEqual(results, items, "Unexpected items: %s" % results) + def testGetItemsIterator(self): + """Test interaction between XEP-0030 and XEP-0059 plugins.""" + + raised_exceptions = [] + + def catch_exception(*args, **kwargs): + raised_exceptions.append(True) + + sys.excepthook = catch_exception + + self.stream_start(mode='client', + plugins=['xep_0030', 'xep_0059']) + + results = self.xmpp['xep_0030'].get_items(jid='foo@localhost', + node='bar', + iterator=True) + results.amount = 10 + + t = threading.Thread(name="get_items_iterator", + target=results.next) + t.start() + + self.send(""" + <iq id="2" type="get" to="foo@localhost"> + <query xmlns="http://jabber.org/protocol/disco#items" + node="bar"> + <set xmlns="http://jabber.org/protocol/rsm"> + <max>10</max> + </set> + </query> + </iq> + """) + self.recv(""" + <iq id="2" type="result" to="tester@localhost"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <set xmlns="http://jabber.org/protocol/rsm"> + </set> + </query> + </iq> + """) + + t.join() + + self.assertEqual(raised_exceptions, [True], + "StopIteration was not raised: %s" % raised_exceptions) + suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamDisco) diff --git a/tests/test_stream_xep_0059.py b/tests/test_stream_xep_0059.py new file mode 100644 index 00000000..3a99842b --- /dev/null +++ b/tests/test_stream_xep_0059.py @@ -0,0 +1,162 @@ +import threading + +from sleekxmpp.test import * +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins.xep_0030 import DiscoItems +from sleekxmpp.plugins.xep_0059 import ResultIterator, Set + + +class TestStreamSet(SleekTest): + + def setUp(self): + register_stanza_plugin(DiscoItems, Set) + + def tearDown(self): + self.stream_close() + + def iter(self, rev=False): + q = self.xmpp.Iq() + q['type'] = 'get' + it = ResultIterator(q, 'disco_items', '1', reverse=rev) + for i in it: + for j in i['disco_items']['items']: + self.items.append(j[0]) + + def testResultIterator(self): + self.items = [] + self.stream_start(mode='client') + t = threading.Thread(target=self.iter) + t.start() + self.send(""" + <iq type="get" id="2"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <set xmlns="http://jabber.org/protocol/rsm"> + <max>1</max> + </set> + </query> + </iq> + """) + self.recv(""" + <iq type="result" id="2"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <item jid="item1" /> + <set xmlns="http://jabber.org/protocol/rsm"> + <last>item1</last> + </set> + </query> + </iq> + """) + self.send(""" + <iq type="get" id="3"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <set xmlns="http://jabber.org/protocol/rsm"> + <max>1</max> + <after>item1</after> + </set> + </query> + </iq> + """) + self.recv(""" + <iq type="result" id="3"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <item jid="item2" /> + <set xmlns="http://jabber.org/protocol/rsm"> + <last>item2</last> + </set> + </query> + </iq> + """) + self.send(""" + <iq type="get" id="4"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <set xmlns="http://jabber.org/protocol/rsm"> + <max>1</max> + <after>item2</after> + </set> + </query> + </iq> + """) + self.recv(""" + <iq type="result" id="4"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <item jid="item2" /> + <set xmlns="http://jabber.org/protocol/rsm"> + </set> + </query> + </iq> + """) + t.join() + self.failUnless(self.items == ['item1', 'item2']) + + def testResultIteratorReverse(self): + self.items = [] + self.stream_start(mode='client') + + t = threading.Thread(target=self.iter, args=(True,)) + t.start() + + self.send(""" + <iq type="get" id="2"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <set xmlns="http://jabber.org/protocol/rsm"> + <max>1</max> + <before /> + </set> + </query> + </iq> + """) + self.recv(""" + <iq type="result" id="2"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <item jid="item2" /> + <set xmlns="http://jabber.org/protocol/rsm"> + <first>item2</first> + </set> + </query> + </iq> + """) + self.send(""" + <iq type="get" id="3"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <set xmlns="http://jabber.org/protocol/rsm"> + <max>1</max> + <before>item2</before> + </set> + </query> + </iq> + """) + self.recv(""" + <iq type="result" id="3"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <item jid="item1" /> + <set xmlns="http://jabber.org/protocol/rsm"> + <first>item1</first> + </set> + </query> + </iq> + """) + self.send(""" + <iq type="get" id="4"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <set xmlns="http://jabber.org/protocol/rsm"> + <max>1</max> + <before>item1</before> + </set> + </query> + </iq> + """) + self.recv(""" + <iq type="result" id="4"> + <query xmlns="http://jabber.org/protocol/disco#items"> + <item jid="item1" /> + <set xmlns="http://jabber.org/protocol/rsm"> + </set> + </query> + </iq> + """) + + t.join() + self.failUnless(self.items == ['item2', 'item1']) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamSet) diff --git a/tests/test_stream_xep_0092.py b/tests/test_stream_xep_0092.py new file mode 100644 index 00000000..4a038558 --- /dev/null +++ b/tests/test_stream_xep_0092.py @@ -0,0 +1,69 @@ +import threading + +from sleekxmpp.test import * + + +class TestStreamSet(SleekTest): + + def tearDown(self): + self.stream_close() + + def testHandleSoftwareVersionRequest(self): + self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092']) + + self.xmpp['xep_0092'].name = 'SleekXMPP' + self.xmpp['xep_0092'].version = 'dev' + self.xmpp['xep_0092'].os = 'Linux' + + self.recv(""" + <iq type="get" id="1"> + <query xmlns="jabber:iq:version" /> + </iq> + """) + + self.send(""" + <iq type="result" id="1"> + <query xmlns="jabber:iq:version"> + <name>SleekXMPP</name> + <version>dev</version> + <os>Linux</os> + </query> + </iq> + """) + + def testMakeSoftwareVersionRequest(self): + results = [] + + def query(): + r = self.xmpp['xep_0092'].get_version('foo@bar') + results.append(r) + + self.stream_start(mode='client', plugins=['xep_0030', 'xep_0092']) + + t = threading.Thread(target=query) + t.start() + + self.send(""" + <iq type="get" id="1" to="foo@bar"> + <query xmlns="jabber:iq:version" /> + </iq> + """) + + self.recv(""" + <iq type="result" id="1" from="foo@bar" to="tester@localhost"> + <query xmlns="jabber:iq:version"> + <name>Foo</name> + <version>1.0</version> + <os>Linux</os> + </query> + </iq> + """) + + t.join() + + expected = [{'name': 'Foo', 'version': '1.0', 'os':'Linux'}] + self.assertEqual(results, expected, + "Did not receive expected results: %s" % results) + + +suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamSet) @@ -1,123 +1,62 @@ -ElementBase sub_items not subitem? - -*XMPP needs to use JID class instead of lots of fields. - -BaseXMPP set_jid, makeIqQuery, getjidresource, getjidbare not needed - -Why CamelCase and underscore_names? Document semantics. - -conn_tests and sleekxmpp/tests and sleekxmpp/xmlstresm/test.* -> convert to either unit tests, or at least put in same place - -Update setup.py - github url, version # - -scheduler needs unit tests - -ClientXMPP stream:features handler should use new state machine - -Write stream tests for startls, features, etc. - - - --- PEP8 - all files - -Need to use spaces - -Docstrings are lacking. Need to document attributes and return values. - -Organize imports - -Use absolute, not relative imports - -Fix one-liner if statements - -Line length limit of 79 characters - - - --- Plugins - ---- xep_0004 - -Need more unit tests - ---- xep_0009 - -Need stanza objects - -Need unit tests - ---- xep_0045 - -Need to use stanza objects - -A few TODO comments for checking roles and using defaults - -Need unit tests - ---- xep_0050 - -Need unit tests - -Need stanza objects - use new xep_0004 - ---- xep_0060 - -Need unit tests - -Need to use existing stanza objects - ---- xep_0078 - -Is it useful still? - -Need stanza objects/unit tests - ---- xep_0086 - -Is there a way to automate setting error codes? - -Seems like this should be part of the error stanza by default - -Use stanza objects - ---- xep_0092 - -Stanza objects - -Unit tests - ---- xep_0199 - -Stanza objects - -Unit tests - -Clean commented code - -Use the new scheduler - - - --- Documentation - -Document the Zen/Tao/Whatever of SleekXMPP to explain design goals and decisions - -Write architecture description - -XMPP:TDG needs to be rewritten. - -Need to update docs that reference old JID attributes of sleekxmpp objects - -Page describing new JID class - -Message page needs updating - -Iq page needs to be written - -Make guides to go with example.py and component_example.py - -Page on xmlstream.matchers - -Page on xmlstream.handlers, especially waiters - -Page on using xmlstream.scheduler +Plugins: + 0004 + PEP8 + Stream/Unit tests + Fix serialization issue + Use OrderedDict for fields/values + 0009 + Review contribution from dannmartens + 0012 + PEP8 + Documentation + Stream/Unit tests + 0030 + Done + 0033 + PEP8 + Documentation + Stream/Unit tests + 0045 + PEP8 + Documentation + Stream/Unit tests + 0050 + Review replacement in github.com/legastero/adhoc + 0059 + Done + 0060 + PEP8 + Documentation + Stream/Unit tests + 0078 + Will require new stream features handling, see stream_features branch. + PEP8 + Documentation + Stream/Unit tests + 0085 + PEP8 + Documentation + Stream/Unit tests + 0086 + PEP8 + Documentation + Consider any simplifications. + 0092 + Done + 0128 + Needs complete rewrite to work with new 0030 plugin. + 0199 + PEP8 + Documentation + Stream/Unit tests + Needs to use scheduler instead of its own thread. + 0202 + PEP8 + Documentation + Stream/Unit tests + 0249 + Review, minor cleanup + gmail_notify + PEP8 + Documentation + Stream/Unit tests |