Merge branch 'develop' into roster

This commit is contained in:
Lance Stout 2010-12-21 11:33:40 -05:00
commit 3657bf6636
5 changed files with 191 additions and 48 deletions

View file

@ -108,7 +108,8 @@ class xep_0030(base_plugin):
self._disco_ops = ['get_info', 'set_identities', 'set_features', self._disco_ops = ['get_info', 'set_identities', 'set_features',
'get_items', 'set_items', 'del_items', 'get_items', 'set_items', 'del_items',
'add_identity', 'del_identity', 'add_feature', 'add_identity', 'del_identity', 'add_feature',
'del_feature', 'add_item', 'del_item'] 'del_feature', 'add_item', 'del_item',
'del_identities', 'del_features']
self._handlers = {} self._handlers = {}
for op in self._disco_ops: for op in self._disco_ops:
self._handlers[op] = {'global': getattr(self.static, op), self._handlers[op] = {'global': getattr(self.static, op),
@ -141,8 +142,10 @@ class xep_0030(base_plugin):
set_features set_features
set_items set_items
del_items del_items
del_identities
del_identity del_identity
del_feature del_feature
del_features
del_item del_item
add_identity add_identity
add_feature add_feature
@ -230,7 +233,7 @@ class xep_0030(base_plugin):
no stanzas need to be sent. no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the Otherwise, a disco stanza must be sent to the
remove JID to retrieve the info. remove JID to retrieve the info.
dfrom -- Specifiy the sender's JID. ifrom -- Specifiy the sender's JID.
block -- If true, block and wait for the stanzas' reply. block -- If true, block and wait for the stanzas' reply.
timeout -- The time in seconds to block while waiting for timeout -- The time in seconds to block while waiting for
a reply. If None, then wait indefinitely. a reply. If None, then wait indefinitely.
@ -245,7 +248,7 @@ class xep_0030(base_plugin):
return self._fix_default_info(info) return self._fix_default_info(info)
iq = self.xmpp.Iq() iq = self.xmpp.Iq()
iq['from'] = kwargs.get('dfrom', '') iq['from'] = kwargs.get('ifrom', '')
iq['to'] = jid iq['to'] = jid
iq['type'] = 'get' iq['type'] = 'get'
iq['disco_info']['node'] = node if node else '' iq['disco_info']['node'] = node if node else ''
@ -270,7 +273,7 @@ class xep_0030(base_plugin):
no stanzas need to be sent. no stanzas need to be sent.
Otherwise, a disco stanza must be sent to the Otherwise, a disco stanza must be sent to the
remove JID to retrieve the items. remove JID to retrieve the items.
dfrom -- Specifiy the sender's JID. ifrom -- Specifiy the sender's JID.
block -- If true, block and wait for the stanzas' reply. block -- If true, block and wait for the stanzas' reply.
timeout -- The time in seconds to block while waiting for timeout -- The time in seconds to block while waiting for
a reply. If None, then wait indefinitely. a reply. If None, then wait indefinitely.
@ -282,7 +285,7 @@ class xep_0030(base_plugin):
return self._run_node_handler('get_items', jid, node, kwargs) return self._run_node_handler('get_items', jid, node, kwargs)
iq = self.xmpp.Iq() iq = self.xmpp.Iq()
iq['from'] = kwargs.get('dfrom', '') iq['from'] = kwargs.get('ifrom', '')
iq['to'] = jid iq['to'] = jid
iq['type'] = 'get' iq['type'] = 'get'
iq['disco_items']['node'] = node if node else '' iq['disco_items']['node'] = node if node else ''

View file

@ -31,6 +31,11 @@ class StaticDisco(object):
StaticDisco provides a set of node handlers that will store StaticDisco provides a set of node handlers that will store
static sets of disco info and items in memory. static sets of disco info and items in memory.
Attributes:
nodes -- A dictionary mapping (JID, node) tuples to a dict
containing a disco#info and a disco#items stanza.
xmpp -- The main SleekXMPP object.
""" """
def __init__(self, xmpp): def __init__(self, xmpp):
@ -47,6 +52,14 @@ class StaticDisco(object):
self.xmpp = xmpp self.xmpp = xmpp
def add_node(self, jid=None, node=None): def add_node(self, jid=None, node=None):
"""
Create a new set of stanzas for the provided
JID and node combination.
Arguments:
jid -- The JID that will own the new stanzas.
node -- The node that will own the new stanzas.
"""
if jid is None: if jid is None:
jid = self.xmpp.boundjid.full jid = self.xmpp.boundjid.full
if node is None: if node is None:
@ -57,7 +70,21 @@ class StaticDisco(object):
self.nodes[(jid, node)]['info']['node'] = node self.nodes[(jid, node)]['info']['node'] = node
self.nodes[(jid, node)]['items']['node'] = node self.nodes[(jid, node)]['items']['node'] = node
# =================================================================
# Node Handlers
#
# Each handler accepts three arguments: jid, node, and data.
# The jid and node parameters together determine the set of
# info and items stanzas that will be retrieved or added.
# The data parameter is a dictionary with additional paramters
# that will be passed to other calls.
def get_info(self, jid, node, data): def get_info(self, jid, node, data):
"""
Return the stored info data for the requested JID/node combination.
The data parameter is not used.
"""
if (jid, node) not in self.nodes: if (jid, node) not in self.nodes:
if not node: if not node:
return DiscoInfo() return DiscoInfo()
@ -67,10 +94,20 @@ class StaticDisco(object):
return self.nodes[(jid, node)]['info'] return self.nodes[(jid, node)]['info']
def del_info(self, jid, node, data): def del_info(self, jid, node, data):
"""
Reset the info stanza for a given JID/node combination.
The data parameter is not used.
"""
if (jid, node) in self.nodes: if (jid, node) in self.nodes:
self.nodes[(jid, node)]['info'] = DiscoInfo() self.nodes[(jid, node)]['info'] = DiscoInfo()
def get_items(self, jid, node, data): def get_items(self, jid, node, data):
"""
Return the stored items data for the requested JID/node combination.
The data parameter is not used.
"""
if (jid, node) not in self.nodes: if (jid, node) not in self.nodes:
if not node: if not node:
return DiscoInfo() return DiscoInfo()
@ -80,15 +117,34 @@ class StaticDisco(object):
return self.nodes[(jid, node)]['items'] return self.nodes[(jid, node)]['items']
def set_items(self, jid, node, data): def set_items(self, jid, node, data):
"""
Replace the stored items data for a JID/node combination.
The data parameter is not used.
"""
items = data.get('items', set()) items = data.get('items', set())
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['items']['items'] = items self.nodes[(jid, node)]['items']['items'] = items
def del_items(self, jid, node, data): def del_items(self, jid, node, data):
"""
Reset the items stanza for a given JID/node combination.
The data parameter is not used.
"""
if (jid, node) in self.nodes: if (jid, node) in self.nodes:
self.nodes[(jid, node)]['items'] = DiscoItems() self.nodes[(jid, node)]['items'] = DiscoItems()
def add_identity(self, jid, node, data): def add_identity(self, jid, node, data):
"""
Add a new identity to te JID/node combination.
The data parameter may provide:
category -- The general category to which the agent belongs.
itype -- A more specific designation with the category.
name -- Optional human readable name for this identity.
lang -- Optional standard xml:lang value.
"""
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_identity( self.nodes[(jid, node)]['info'].add_identity(
data.get('category', ''), data.get('category', ''),
@ -97,11 +153,27 @@ class StaticDisco(object):
data.get('lang', None)) data.get('lang', None))
def set_identities(self, jid, node, data): def set_identities(self, jid, node, data):
"""
Add or replace all identities for a JID/node combination.
The data parameter should include:
identities -- A list of identities in tuple form:
(category, type, name, lang)
"""
identities = data.get('identities', set()) identities = data.get('identities', set())
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['info']['identities'] = identities self.nodes[(jid, node)]['info']['identities'] = identities
def del_identity(self, jid, node, data): def del_identity(self, jid, node, data):
"""
Remove an identity from a JID/node combination.
The data parameter may provide:
category -- The general category to which the agent belonged.
itype -- A more specific designation with the category.
name -- Optional human readable name for this identity.
lang -- Optional, standard xml:lang value.
"""
if (jid, node) not in self.nodes: if (jid, node) not in self.nodes:
return return
self.nodes[(jid, node)]['info'].del_identity( self.nodes[(jid, node)]['info'].del_identity(
@ -110,21 +182,68 @@ class StaticDisco(object):
data.get('name', None), data.get('name', None),
data.get('lang', None)) data.get('lang', None))
def del_identities(self, jid, node, data):
"""
Remove all identities from a JID/node combination.
The data parameter is not used.
"""
if (jid, node) not in self.nodes:
return
del self.nodes[(jid, node)]['info']['identities']
def add_feature(self, jid, node, data): def add_feature(self, jid, node, data):
"""
Add a feature to a JID/node combination.
The data parameter should include:
feature -- The namespace of the supported feature.
"""
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['info'].add_feature(data.get('feature', '')) self.nodes[(jid, node)]['info'].add_feature(data.get('feature', ''))
def set_features(self, jid, node, data): def set_features(self, jid, node, data):
"""
Add or replace all features for a JID/node combination.
The data parameter should include:
features -- The new set of supported features.
"""
features = data.get('features', set()) features = data.get('features', set())
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['info']['features'] = features self.nodes[(jid, node)]['info']['features'] = features
def del_feature(self, jid, node, data): def del_feature(self, jid, node, data):
"""
Remove a feature from a JID/node combination.
The data parameter should include:
feature -- The namespace of the removed feature.
"""
if (jid, node) not in self.nodes: if (jid, node) not in self.nodes:
return return
self.nodes[(jid, node)]['info'].del_feature(data.get('feature', '')) self.nodes[(jid, node)]['info'].del_feature(data.get('feature', ''))
def del_features(self, jid, node, data):
"""
Remove all features from a JID/node combination.
The data parameter is not used.
"""
if (jid, node) not in self.nodes:
return
del self.nodes[(jid, node)]['info']['features']
def add_item(self, jid, node, data): def add_item(self, jid, node, data):
"""
Add an item to a JID/node combination.
The data parameter may include:
ijid -- The JID for the item.
inode -- Optional additional information to reference
non-addressable items.
name -- Optional human readable name for the item.
"""
self.add_node(jid, node) self.add_node(jid, node)
self.nodes[(jid, node)]['items'].add_item( self.nodes[(jid, node)]['items'].add_item(
data.get('ijid', ''), data.get('ijid', ''),
@ -132,6 +251,13 @@ class StaticDisco(object):
name=data.get('name', None)) name=data.get('name', None))
def del_item(self, jid, node, data): def del_item(self, jid, node, data):
"""
Remove an item from a JID/node combination.
The data parameter may include:
ijid -- JID of the item to remove.
inode -- Optional extra identifying information.
"""
if (jid, node) in self.nodes: if (jid, node) in self.nodes:
self.nodes[(jid, node)]['items'].del_item( self.nodes[(jid, node)]['items'].del_item(
data.get('ijid', ''), data.get('ijid', ''),

View file

@ -54,16 +54,17 @@ class RootStanza(StanzaBase):
e.extension_args) e.extension_args)
self['error'].append(extxml) self['error'].append(extxml)
self['error']['type'] = e.etype self['error']['type'] = e.etype
self.send()
else: else:
# We probably didn't raise this on purpose, so send a traceback # We probably didn't raise this on purpose, so send an error stanza
self['error']['condition'] = 'undefined-condition' self['error']['condition'] = 'undefined-condition'
if sys.version_info < (3, 0): self['error']['text'] = "SleekXMPP got into trouble."
self['error']['text'] = "SleekXMPP got into trouble." self.send()
else: # log the error
self['error']['text'] = traceback.format_tb(e.__traceback__) log.exception('Error handling {%s}%s stanza' %
log.exception('Error handling {%s}%s stanza' % (self.namespace, self.name))
(self.namespace, self.name)) # Finally raise the exception, so it can be handled (or not)
self.send() # at a higher level by using sys.excepthook.
raise e
register_stanza_plugin(RootStanza, Error) register_stanza_plugin(RootStanza, Error)

View file

@ -16,7 +16,6 @@ import sys
import threading import threading
import time import time
import types import types
import signal
try: try:
import queue import queue
except ImportError: except ImportError:
@ -209,24 +208,6 @@ class XMLStream(object):
self.auto_reconnect = True self.auto_reconnect = True
self.is_client = False self.is_client = False
try:
if hasattr(signal, 'SIGHUP'):
signal.signal(signal.SIGHUP, self._handle_kill)
if hasattr(signal, 'SIGTERM'):
# Used in Windows
signal.signal(signal.SIGTERM, self._handle_kill)
except:
log.debug("Can not set interrupt signal handlers. " + \
"SleekXMPP is not running from a main thread.")
def _handle_kill(self, signum, frame):
"""
Capture kill event and disconnect cleanly after first
spawning the "killed" event.
"""
self.event("killed", direct=True)
self.disconnect()
def new_id(self): def new_id(self):
""" """
Generate and return a new stream ID in hexadecimal form. Generate and return a new stream ID in hexadecimal form.
@ -701,10 +682,12 @@ class XMLStream(object):
Event handlers and the send queue will be threaded Event handlers and the send queue will be threaded
regardless of this parameter's value. regardless of this parameter's value.
""" """
self._thread_excepthook()
self.scheduler.process(threaded=True) self.scheduler.process(threaded=True)
def start_thread(name, target): def start_thread(name, target):
self.__thread[name] = threading.Thread(name=name, target=target) self.__thread[name] = threading.Thread(name=name, target=target)
self.__thread[name].daemon = True
self.__thread[name].start() self.__thread[name].start()
for t in range(0, HANDLER_THREADS): for t in range(0, HANDLER_THREADS):
@ -972,3 +955,26 @@ class XMLStream(object):
self.disconnect() self.disconnect()
self.event_queue.put(('quit', None, None)) self.event_queue.put(('quit', None, None))
return return
def _thread_excepthook(self):
"""
If a threaded event handler raises an exception, there is no way to
catch it except with an excepthook. Currently, each thread has its own
excepthook, but ideally we could use the main sys.excepthook.
Modifies threading.Thread to use sys.excepthook when an exception
is not caught.
"""
init_old = threading.Thread.__init__
def init(self, *args, **kwargs):
init_old(self, *args, **kwargs)
run_old = self.run
def run_with_except_hook(*args, **kw):
try:
run_old(*args, **kw)
except (KeyboardInterrupt, SystemExit):
raise
except:
sys.excepthook(*sys.exc_info())
self.run = run_with_except_hook
threading.Thread.__init__ = init

View file

@ -10,6 +10,7 @@ class TestStreamExceptions(SleekTest):
""" """
def tearDown(self): def tearDown(self):
sys.excepthook = sys.__excepthook__
self.stream_close() self.stream_close()
def testXMPPErrorException(self): def testXMPPErrorException(self):
@ -78,9 +79,16 @@ class TestStreamExceptions(SleekTest):
def testUnknownException(self): def testUnknownException(self):
"""Test raising an generic exception in a threaded handler.""" """Test raising an generic exception in a threaded handler."""
raised_errors = []
def message(msg): def message(msg):
raise ValueError("Did something wrong") raise ValueError("Did something wrong")
def catch_error(*args, **kwargs):
raised_errors.append(True)
sys.excepthook = catch_error
self.stream_start() self.stream_start()
self.xmpp.add_event_handler('message', message) self.xmpp.add_event_handler('message', message)
@ -90,21 +98,20 @@ class TestStreamExceptions(SleekTest):
</message> </message>
""") """)
if sys.version_info < (3, 0): self.send("""
self.send(""" <message type="error">
<message type="error"> <error type="cancel">
<error type="cancel"> <undefined-condition
<undefined-condition xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" />
xmlns="urn:ietf:params:xml:ns:xmpp-stanzas" /> <text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas">
<text xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"> SleekXMPP got into trouble.
SleekXMPP got into trouble. </text>
</text> </error>
</error> </message>
</message> """)
""")
else: self.assertEqual(raised_errors, [True], "Exception was not raised: %s" % raised_errors)
# Unfortunately, tracebacks do not make for very portable tests.
pass
suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExceptions) suite = unittest.TestLoader().loadTestsFromTestCase(TestStreamExceptions)