summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xconn_tests/testpubsub.py2
-rw-r--r--setup.py4
-rw-r--r--sleekxmpp/basexmpp.py32
-rw-r--r--sleekxmpp/clientxmpp.py13
-rw-r--r--sleekxmpp/plugins/old_0004.py2
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py94
-rw-r--r--sleekxmpp/plugins/xep_0030/static.py5
-rw-r--r--sleekxmpp/plugins/xep_0045.py1
-rw-r--r--sleekxmpp/plugins/xep_0059/__init__.py10
-rw-r--r--sleekxmpp/plugins/xep_0059/rsm.py119
-rw-r--r--sleekxmpp/plugins/xep_0059/stanza.py108
-rw-r--r--sleekxmpp/plugins/xep_0078.py2
-rw-r--r--sleekxmpp/plugins/xep_0092.py56
-rw-r--r--sleekxmpp/plugins/xep_0092/__init__.py11
-rw-r--r--sleekxmpp/plugins/xep_0092/stanza.py42
-rw-r--r--sleekxmpp/plugins/xep_0092/version.py88
-rw-r--r--sleekxmpp/plugins/xep_0199.py2
-rw-r--r--sleekxmpp/plugins/xep_0202.py8
-rw-r--r--sleekxmpp/plugins/xep_0249/__init__.py2
-rw-r--r--sleekxmpp/plugins/xep_0249/invite.py75
-rw-r--r--sleekxmpp/plugins/xep_0249/stanza.py29
-rw-r--r--sleekxmpp/stanza/__init__.py1
-rw-r--r--sleekxmpp/stanza/iq.py25
-rw-r--r--sleekxmpp/stanza/nick.py2
-rw-r--r--sleekxmpp/stanza/stream_error.py69
-rw-r--r--sleekxmpp/test/sleektest.py23
-rw-r--r--sleekxmpp/thirdparty/__init__.py4
-rw-r--r--sleekxmpp/xmlstream/jid.py8
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py119
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py14
-rw-r--r--tests/test_stanza_message.py2
-rw-r--r--tests/test_stanza_presence.py2
-rw-r--r--tests/test_stanza_xep_0059.py106
-rw-r--r--tests/test_stream_xep_0030.py60
-rw-r--r--tests/test_stream_xep_0059.py162
-rw-r--r--tests/test_stream_xep_0092.py69
-rw-r--r--todo1.0185
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
diff --git a/setup.py b/setup.py
index b030f4a9..d6d8d6d0 100644
--- a/setup.py
+++ b/setup.py
@@ -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)
diff --git a/todo1.0 b/todo1.0
index 191c0e2d..3212a31a 100644
--- a/todo1.0
+++ b/todo1.0
@@ -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