diff options
28 files changed, 779 insertions, 35 deletions
@@ -60,6 +60,7 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0009', 'sleekxmpp/plugins/xep_0009/stanza', 'sleekxmpp/plugins/xep_0012', + 'sleekxmpp/plugins/xep_0013', 'sleekxmpp/plugins/xep_0016', 'sleekxmpp/plugins/xep_0027', 'sleekxmpp/plugins/xep_0030', @@ -80,6 +81,7 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0084', 'sleekxmpp/plugins/xep_0085', 'sleekxmpp/plugins/xep_0086', + 'sleekxmpp/plugins/xep_0091', 'sleekxmpp/plugins/xep_0092', 'sleekxmpp/plugins/xep_0107', 'sleekxmpp/plugins/xep_0108', @@ -105,6 +107,8 @@ packages = [ 'sleekxmpp', 'sleekxmpp/plugins/xep_0279', 'sleekxmpp/plugins/xep_0280', 'sleekxmpp/plugins/xep_0297', + 'sleekxmpp/plugins/xep_0308', + 'sleekxmpp/plugins/xep_0313', 'sleekxmpp/features', 'sleekxmpp/features/feature_mechanisms', 'sleekxmpp/features/feature_mechanisms/stanza', diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 244d19c5..55250751 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -114,6 +114,17 @@ class BaseXMPP(XMLStream): #: ``'to'`` and ``'from'`` JIDs of stanzas. self.is_component = False + #: Messages may optionally be tagged with ID values. Setting + #: :attr:`use_message_ids` to `True` will assign all outgoing + #: messages an ID. Some plugin features require enabling + #: this option. + self.use_message_ids = False + + #: Presence updates may optionally be tagged with ID values. + #: Setting :attr:`use_message_ids` to `True` will assign all + #: outgoing messages an ID. + self.use_presence_ids = False + #: The API registry is a way to process callbacks based on #: JID+node combinations. Each callback in the registry is #: marked with: diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index 18b618ac..77e19e61 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -18,6 +18,7 @@ __all__ = [ 'xep_0004', # Data Forms 'xep_0009', # Jabber-RPC 'xep_0012', # Last Activity + 'xep_0013', # Flexible Offline Message Retrieval 'xep_0016', # Privacy Lists 'xep_0027', # Current Jabber OpenPGP Usage 'xep_0030', # Service Discovery @@ -38,6 +39,7 @@ __all__ = [ 'xep_0084', # User Avatar 'xep_0085', # Chat State Notifications 'xep_0086', # Legacy Error Codes + 'xep_0091', # Legacy Delayed Delivery 'xep_0092', # Software Version 'xep_0106', # JID Escaping 'xep_0107', # User Mood @@ -72,4 +74,6 @@ __all__ = [ 'xep_0280', # Message Carbons 'xep_0297', # Stanza Forwarding 'xep_0302', # XMPP Compliance Suites 2012 + 'xep_0308', # Last Message Correction + 'xep_0313', # Message Archive Management ] diff --git a/sleekxmpp/plugins/xep_0013/__init__.py b/sleekxmpp/plugins/xep_0013/__init__.py new file mode 100644 index 00000000..ad400949 --- /dev/null +++ b/sleekxmpp/plugins/xep_0013/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0013.stanza import Offline +from sleekxmpp.plugins.xep_0013.offline import XEP_0013 + + +register_plugin(XEP_0013) diff --git a/sleekxmpp/plugins/xep_0013/offline.py b/sleekxmpp/plugins/xep_0013/offline.py new file mode 100644 index 00000000..a0d992a7 --- /dev/null +++ b/sleekxmpp/plugins/xep_0013/offline.py @@ -0,0 +1,134 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import logging + +import sleekxmpp +from sleekxmpp.stanza import Message, Iq +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream.handler import Collector +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0013 import stanza + + +log = logging.getLogger(__name__) + + +class XEP_0013(BasePlugin): + + """ + XEP-0013 Flexible Offline Message Retrieval + """ + + name = 'xep_0013' + description = 'XEP-0013: Flexible Offline Message Retrieval' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, stanza.Offline) + register_stanza_plugin(Message, stanza.Offline) + + def get_count(self, **kwargs): + return self.xmpp['xep_0030'].get_info( + node='http://jabber.org/protocol/offline', + local=False, + **kwargs) + + def get_headers(self, **kwargs): + return self.xmpp['xep_0030'].get_items( + node='http://jabber.org/protocol/offline', + local=False, + **kwargs) + + def view(self, nodes, ifrom=None, block=True, timeout=None, callback=None): + if not isinstance(nodes, (list, set)): + nodes = [nodes] + + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['from'] = ifrom + offline = iq['offline'] + for node in nodes: + item = stanza.Item() + item['node'] = node + item['action'] = 'view' + offline.append(item) + + collector = Collector( + 'Offline_Results_%s' % iq['id'], + StanzaPath('message/offline')) + self.xmpp.register_handler(collector) + + if not block and callback is not None: + def wrapped_cb(iq): + results = collector.stop() + if iq['type'] == 'result': + iq['offline']['results'] = results + callback(iq) + return iq.send(block=block, timeout=timeout, callback=wrapped_cb) + else: + try: + resp = iq.send(block=block, timeout=timeout, callback=callback) + resp['offline']['results'] = collector.stop() + return resp + except XMPPError as e: + collector.stop() + raise e + + def remove(self, nodes, ifrom=None, block=True, timeout=None, callback=None): + if not isinstance(nodes, (list, set)): + nodes = [nodes] + + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + offline = iq['offline'] + for node in nodes: + item = stanza.Item() + item['node'] = node + item['action'] = 'remove' + offline.append(item) + + return iq.send(block=block, timeout=timeout, callback=callback) + + def fetch(self, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + iq['offline']['fetch'] = True + + collector = Collector( + 'Offline_Results_%s' % iq['id'], + StanzaPath('message/offline')) + self.xmpp.register_handler(collector) + + if not block and callback is not None: + def wrapped_cb(iq): + results = collector.stop() + if iq['type'] == 'result': + iq['offline']['results'] = results + callback(iq) + return iq.send(block=block, timeout=timeout, callback=wrapped_cb) + else: + try: + resp = iq.send(block=block, timeout=timeout, callback=callback) + resp['offline']['results'] = collector.stop() + return resp + except XMPPError as e: + collector.stop() + raise e + + def purge(self, ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['from'] = ifrom + iq['offline']['purge'] = True + return iq.send(block=block, timeout=timeout, callback=callback) diff --git a/sleekxmpp/plugins/xep_0013/stanza.py b/sleekxmpp/plugins/xep_0013/stanza.py new file mode 100644 index 00000000..c9c69786 --- /dev/null +++ b/sleekxmpp/plugins/xep_0013/stanza.py @@ -0,0 +1,53 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.jid import JID +from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin + + +class Offline(ElementBase): + name = 'offline' + namespace = 'http://jabber.org/protocol/offline' + plugin_attrib = 'offline' + interfaces = set(['fetch', 'purge', 'results']) + bool_interfaces = interfaces + + def setup(self, xml=None): + ElementBase.setup(self, xml) + self._results = [] + + # The results interface is meant only as an easy + # way to access the set of collected message responses + # from the query. + + def get_results(self): + return self._results + + def set_results(self, values): + self._results = values + + def del_results(self): + self._results = [] + + +class Item(ElementBase): + name = 'item' + namespace = 'http://jabber.org/protocol/offline' + plugin_attrib = 'item' + interfaces = set(['action', 'node', 'jid']) + + actions = set(['view', 'remove']) + + def get_jid(self): + return JID(self._get_attr('jid')) + + def set_jid(self, value): + self._set_attr('jid', str(value)) + + +register_stanza_plugin(Offline, Item, iterable=True) diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index be66b6fd..278b4a34 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -288,7 +288,7 @@ class XEP_0030(BasePlugin): 'cached': cached} return self.api['has_identity'](jid, node, ifrom, data) - def get_info(self, jid=None, node=None, local=False, + def get_info(self, jid=None, node=None, local=None, cached=None, **kwargs): """ Retrieve the disco#info results from a given JID/node combination. @@ -325,17 +325,18 @@ class XEP_0030(BasePlugin): received instead of blocking and waiting for the reply. """ - if jid is not None and not isinstance(jid, JID): - jid = JID(jid) - if self.xmpp.is_component: - if jid.domain == self.xmpp.boundjid.domain: - local = True - else: - if str(jid) == str(self.xmpp.boundjid): - local = True - jid = jid.full - elif jid in (None, ''): - local = True + if local is None: + if jid is not None and not isinstance(jid, JID): + jid = JID(jid) + if self.xmpp.is_component: + if jid.domain == self.xmpp.boundjid.domain: + local = True + else: + if str(jid) == str(self.xmpp.boundjid): + local = True + jid = jid.full + elif jid in (None, ''): + local = True if local: log.debug("Looking up local disco#info data " + \ @@ -405,7 +406,7 @@ class XEP_0030(BasePlugin): the XEP-0059 plugin, if the plugin is loaded. Otherwise the parameter is ignored. """ - if local or jid is None: + if local or local is None and jid is None: items = self.api['get_items'](jid, node, kwargs.get('ifrom', None), kwargs) diff --git a/sleekxmpp/plugins/xep_0059/rsm.py b/sleekxmpp/plugins/xep_0059/rsm.py index 59cfc10b..d73b45bc 100644 --- a/sleekxmpp/plugins/xep_0059/rsm.py +++ b/sleekxmpp/plugins/xep_0059/rsm.py @@ -25,11 +25,14 @@ class ResultIterator(): An iterator for Result Set Managment """ - def __init__(self, query, interface, amount=10, start=None, reverse=False): + def __init__(self, query, interface, results='substanzas', amount=10, + start=None, reverse=False): """ Arguments: query -- The template query interface -- The substanza of the query, for example disco_items + results -- The query stanza's interface which provides a + countable list of query results. 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 @@ -46,6 +49,7 @@ class ResultIterator(): self.amount = amount self.start = start self.interface = interface + self.results = results self.reverse = reverse self._stop = False @@ -85,7 +89,7 @@ class ResultIterator(): r[self.interface]['rsm']['first_index']: count = int(r[self.interface]['rsm']['count']) first = int(r[self.interface]['rsm']['first_index']) - num_items = len(r[self.interface]['substanzas']) + num_items = len(r[self.interface][self.results]) if first + num_items == count: self._stop = True @@ -123,7 +127,7 @@ class XEP_0059(BasePlugin): def session_bind(self, jid): self.xmpp['xep_0030'].add_feature(Set.namespace) - def iterate(self, stanza, interface): + def iterate(self, stanza, interface, results='substanzas'): """ Create a new result set iterator for a given stanza query. @@ -135,5 +139,7 @@ class XEP_0059(BasePlugin): result set management stanza should be appended. For example, for disco#items queries the interface 'disco_items' should be used. + results -- The name of the interface containing the + query results (typically just 'substanzas'). """ - return ResultIterator(stanza, interface) + return ResultIterator(stanza, interface, results) diff --git a/sleekxmpp/plugins/xep_0091/__init__.py b/sleekxmpp/plugins/xep_0091/__init__.py new file mode 100644 index 00000000..04f21ef5 --- /dev/null +++ b/sleekxmpp/plugins/xep_0091/__init__.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0091 import stanza +from sleekxmpp.plugins.xep_0091.stanza import LegacyDelay +from sleekxmpp.plugins.xep_0091.legacy_delay import XEP_0091 + + +register_plugin(XEP_0091) diff --git a/sleekxmpp/plugins/xep_0091/legacy_delay.py b/sleekxmpp/plugins/xep_0091/legacy_delay.py new file mode 100644 index 00000000..7323d468 --- /dev/null +++ b/sleekxmpp/plugins/xep_0091/legacy_delay.py @@ -0,0 +1,29 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.stanza import Message, Presence +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0091 import stanza + + +class XEP_0091(BasePlugin): + + """ + XEP-0091: Legacy Delayed Delivery + """ + + name = 'xep_0091' + description = 'XEP-0091: Legacy Delayed Delivery' + dependencies = set() + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Message, stanza.LegacyDelay) + register_stanza_plugin(Presence, stanza.LegacyDelay) diff --git a/sleekxmpp/plugins/xep_0091/stanza.py b/sleekxmpp/plugins/xep_0091/stanza.py new file mode 100644 index 00000000..0b70ff63 --- /dev/null +++ b/sleekxmpp/plugins/xep_0091/stanza.py @@ -0,0 +1,46 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import datetime as dt + +from sleekxmpp.jid import JID +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 + + +class LegacyDelay(ElementBase): + + name = 'x' + namespace = 'jabber:x:delay' + plugin_attrib = 'legacy_delay' + interfaces = set(('from', 'stamp', 'text')) + + def get_from(self): + return JID(self._get_attr('from')) + + def set_from(self, value): + self._set_attr('from', str(value)) + + def get_stamp(self): + timestamp = self._get_attr('stamp') + return xep_0082.parse('%sZ' % timestamp) + + def set_stamp(self, value): + if isinstance(value, dt.datetime): + value = value.astimezone(xep_0082.tzutc) + value = xep_0082.format_datetime(value) + self._set_attr('stamp', value[0:19].replace('-', '')) + + def get_text(self): + return self.xml.text + + def set_text(self, value): + self.xml.text = value + + def del_text(self): + self.xml.text = '' diff --git a/sleekxmpp/plugins/xep_0203/stanza.py b/sleekxmpp/plugins/xep_0203/stanza.py index baae4cd3..9a11cae9 100644 --- a/sleekxmpp/plugins/xep_0203/stanza.py +++ b/sleekxmpp/plugins/xep_0203/stanza.py @@ -14,14 +14,17 @@ from sleekxmpp.plugins import xep_0082 class Delay(ElementBase): - """ - """ - name = 'delay' namespace = 'urn:xmpp:delay' plugin_attrib = 'delay' interfaces = set(('from', 'stamp', 'text')) + def get_from(self): + return JID(self._get_attr('from')) + + def set_from(self, value): + self._set_attr('from', str(value)) + def get_stamp(self): timestamp = self._get_attr('stamp') return xep_0082.parse(timestamp) diff --git a/sleekxmpp/plugins/xep_0280/__init__.py b/sleekxmpp/plugins/xep_0280/__init__.py index 8ed65346..929321af 100644 --- a/sleekxmpp/plugins/xep_0280/__init__.py +++ b/sleekxmpp/plugins/xep_0280/__init__.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permissio diff --git a/sleekxmpp/plugins/xep_0280/carbons.py b/sleekxmpp/plugins/xep_0280/carbons.py index 7eec7acd..fe2cdbb2 100644 --- a/sleekxmpp/plugins/xep_0280/carbons.py +++ b/sleekxmpp/plugins/xep_0280/carbons.py @@ -1,6 +1,6 @@ """ SleekXMPP: The Sleek XMPP Library - Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout This file is part of SleekXMPP. See the file LICENSE for copying permissio diff --git a/sleekxmpp/plugins/xep_0297/forwarded.py b/sleekxmpp/plugins/xep_0297/forwarded.py index 7876967c..95703a2d 100644 --- a/sleekxmpp/plugins/xep_0297/forwarded.py +++ b/sleekxmpp/plugins/xep_0297/forwarded.py @@ -26,9 +26,14 @@ class XEP_0297(BasePlugin): def plugin_init(self): register_stanza_plugin(Message, Forwarded) - register_stanza_plugin(Forwarded, Message) - register_stanza_plugin(Forwarded, Presence) - register_stanza_plugin(Forwarded, Iq) + + # While these are marked as iterable, that is just for + # making it easier to extract the forwarded stanza. There + # still can be only a single forwarded stanza. + register_stanza_plugin(Forwarded, Message, iterable=True) + register_stanza_plugin(Forwarded, Presence, iterable=True) + register_stanza_plugin(Forwarded, Iq, iterable=True) + register_stanza_plugin(Forwarded, self.xmpp['xep_0203'].stanza.Delay) self.xmpp.register_handler( diff --git a/sleekxmpp/plugins/xep_0297/stanza.py b/sleekxmpp/plugins/xep_0297/stanza.py index 1cf02f74..8b97accc 100644 --- a/sleekxmpp/plugins/xep_0297/stanza.py +++ b/sleekxmpp/plugins/xep_0297/stanza.py @@ -6,6 +6,7 @@ See the file LICENSE for copying permission. """ +from sleekxmpp.stanza import Message, Presence, Iq from sleekxmpp.xmlstream import ElementBase @@ -16,12 +17,9 @@ class Forwarded(ElementBase): interfaces = set(['stanza']) def get_stanza(self): - if self.xml.find('{jabber:client}message') is not None: - return self['message'] - elif self.xml.find('{jabber:client}presence') is not None: - return self['presence'] - elif self.xml.find('{jabber:client}iq') is not None: - return self['iq'] + for stanza in self: + if isinstance(stanza, (Message, Presence, Iq)): + return stanza return '' def set_stanza(self, value): @@ -29,6 +27,10 @@ class Forwarded(ElementBase): self.append(value) def del_stanza(self): - del self['message'] - del self['presence'] - del self['iq'] + found_stanzas = [] + for stanza in self: + if isinstance(stanza, (Message, Presence, Iq)): + found_stanzas.append(stanza) + for stanza in found_stanzas: + self.iterables.remove(stanza) + self.xml.remove(stanza.xml) diff --git a/sleekxmpp/plugins/xep_0308/__init__.py b/sleekxmpp/plugins/xep_0308/__init__.py new file mode 100644 index 00000000..a6a100ee --- /dev/null +++ b/sleekxmpp/plugins/xep_0308/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0308.stanza import Replace +from sleekxmpp.plugins.xep_0308.correction import XEP_0308 + + +register_plugin(XEP_0308) diff --git a/sleekxmpp/plugins/xep_0308/correction.py b/sleekxmpp/plugins/xep_0308/correction.py new file mode 100644 index 00000000..d32b4bc4 --- /dev/null +++ b/sleekxmpp/plugins/xep_0308/correction.py @@ -0,0 +1,52 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import logging + +import sleekxmpp +from sleekxmpp.stanza import Message +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0308 import stanza, Replace + + +log = logging.getLogger(__name__) + + +class XEP_0308(BasePlugin): + + """ + XEP-0308 Last Message Correction + """ + + name = 'xep_0308' + description = 'XEP-0308: Last Message Correction' + dependencies = set(['xep_0030']) + stanza = stanza + + def plugin_init(self): + self.xmpp.register_handler( + Callback('Message Correction', + StanzaPath('message/replace'), + self._handle_correction)) + + register_stanza_plugin(Message, Replace) + + self.xmpp.use_message_ids = True + + def plugin_end(self): + self.xmpp.remove_handler('Message Correction') + self.xmpp.plugin['xep_0030'].del_feature(feature=Replace.namespace) + + def session_bind(self, jid): + self.xmpp.plugin['xep_0030'].add_feature(Replace.namespace) + + def _handle_correction(self, msg): + self.xmpp.event('message_correction', msg) diff --git a/sleekxmpp/plugins/xep_0308/stanza.py b/sleekxmpp/plugins/xep_0308/stanza.py new file mode 100644 index 00000000..8f88cbc0 --- /dev/null +++ b/sleekxmpp/plugins/xep_0308/stanza.py @@ -0,0 +1,16 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.xmlstream import ElementBase + + +class Replace(ElementBase): + name = 'replace' + namespace = 'urn:xmpp:message-correct:0' + plugin_attrib = 'replace' + interfaces = set(['id']) diff --git a/sleekxmpp/plugins/xep_0313/__init__.py b/sleekxmpp/plugins/xep_0313/__init__.py new file mode 100644 index 00000000..8b6ed97d --- /dev/null +++ b/sleekxmpp/plugins/xep_0313/__init__.py @@ -0,0 +1,15 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +from sleekxmpp.plugins.base import register_plugin + +from sleekxmpp.plugins.xep_0313.stanza import Result, MAM, Preferences +from sleekxmpp.plugins.xep_0313.mam import XEP_0313 + + +register_plugin(XEP_0313) diff --git a/sleekxmpp/plugins/xep_0313/mam.py b/sleekxmpp/plugins/xep_0313/mam.py new file mode 100644 index 00000000..15aee828 --- /dev/null +++ b/sleekxmpp/plugins/xep_0313/mam.py @@ -0,0 +1,92 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import logging + +import sleekxmpp +from sleekxmpp.stanza import Message, Iq +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.xmlstream.handler import Collector +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin +from sleekxmpp.plugins import BasePlugin +from sleekxmpp.plugins.xep_0313 import stanza + + +log = logging.getLogger(__name__) + + +class XEP_0313(BasePlugin): + + """ + XEP-0313 Message Archive Management + """ + + name = 'xep_0313' + description = 'XEP-0313: Message Archive Management' + dependencies = set(['xep_0030', 'xep_0050', 'xep_0059', 'xep_0297']) + stanza = stanza + + def plugin_init(self): + register_stanza_plugin(Iq, stanza.MAM) + register_stanza_plugin(Iq, stanza.Preferences) + register_stanza_plugin(Message, stanza.Result) + register_stanza_plugin(stanza.MAM, self.xmpp['xep_0059'].stanza.Set) + + def retrieve(self, jid=None, start=None, end=None, with_jid=None, ifrom=None, + block=True, timeout=None, callback=None, iterator=False): + iq = self.xmpp.Iq() + query_id = iq['id'] + + iq['to'] = jid + iq['from'] = ifrom + iq['type'] = 'get' + iq['mam']['queryid'] = query_id + iq['mam']['start'] = start + iq['mam']['end'] = end + iq['mam']['with'] = with_jid + + collector = Collector( + 'MAM_Results_%s' % query_id, + StanzaPath('message/mam_result@queryid=%s' % query_id)) + self.xmpp.register_handler(collector) + + if iterator: + return self.xmpp['xep_0059'].iterate(iq, 'mam', 'results') + elif not block and callback is not None: + def wrapped_cb(iq): + results = collector.stop() + if iq['type'] == 'result': + iq['mam']['results'] = results + callback(iq) + return iq.send(block=block, timeout=timeout, callback=wrapped_cb) + else: + try: + resp = iq.send(block=block, timeout=timeout, callback=callback) + resp['mam']['results'] = collector.stop() + return resp + except XMPPError as e: + collector.stop() + raise e + + def set_preferences(self, jid=None, default=None, always=None, never=None, + ifrom=None, block=True, timeout=None, callback=None): + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = jid + iq['from'] = ifrom + iq['mam_prefs']['default'] = default + iq['mam_prefs']['always'] = always + iq['mam_prefs']['never'] = never + return iq.send(block=block, timeout=timeout, callback=callback) + + def get_configuration_commands(self, jid, **kwargs): + return self.xmpp['xep_0030'].get_items( + jid=jid, + node='urn:xmpp:mam#configure', + **kwargs) diff --git a/sleekxmpp/plugins/xep_0313/stanza.py b/sleekxmpp/plugins/xep_0313/stanza.py new file mode 100644 index 00000000..a33c2e35 --- /dev/null +++ b/sleekxmpp/plugins/xep_0313/stanza.py @@ -0,0 +1,131 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2012 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permissio +""" + +import datetime as dt + +from sleekxmpp.jid import JID +from sleekxmpp.xmlstream import ElementBase, ET +from sleekxmpp.plugins import xep_0082 + + +class MAM(ElementBase): + name = 'query' + namespace = 'urn:xmpp:mam:tmp' + plugin_attrib = 'mam' + interfaces = set(['queryid', 'start', 'end', 'with', 'results']) + sub_interfaces = set(['start', 'end', 'with']) + + def setup(self, xml=None): + ElementBase.setup(self, xml) + self._results = [] + + def get_start(self): + timestamp = self._get_attr('start') + return xep_0082.parse(timestamp) + + def set_start(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('start', value) + + def get_end(self): + timestamp = self._get_sub_text('end') + return xep_0082.parse(timestamp) + + def set_end(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_sub_text('end', value) + + def get_with(self): + return JID(self._get_sub_text('with')) + + def set_with(self, value): + self._set_sub_text('with', str(value)) + + # The results interface is meant only as an easy + # way to access the set of collected message responses + # from the query. + + def get_results(self): + return self._results + + def set_results(self, values): + self._results = values + + def del_results(self): + self._results = [] + + +class Preferences(ElementBase): + name = 'prefs' + namespace = 'urn:xmpp:mam:tmp' + plugin_attrib = 'mam_prefs' + interfaces = set(['default', 'always', 'never']) + sub_interfaces = set(['always', 'never']) + + def get_always(self): + results = set() + + jids = self.xml.findall('{%s}always/{%s}jid' % ( + self.namespace, self.namespace)) + + for jid in jids: + results.add(JID(jid.text)) + + return results + + def set_always(self, value): + self._set_sub_text('always', '', keep=True) + always = self.xml.find('{%s}always' % self.namespace) + always.clear() + + if not isinstance(value, (list, set)): + value = [value] + + for jid in value: + jid_xml = ET.Element('{%s}jid' % self.namespace) + jid_xml.text = str(jid) + always.append(jid_xml) + + def get_never(self): + results = set() + + jids = self.xml.findall('{%s}never/{%s}jid' % ( + self.namespace, self.namespace)) + + for jid in jids: + results.add(JID(jid.text)) + + return results + + def set_never(self, value): + self._set_sub_text('never', '', keep=True) + never = self.xml.find('{%s}never' % self.namespace) + never.clear() + + if not isinstance(value, (list, set)): + value = [value] + + for jid in value: + jid_xml = ET.Element('{%s}jid' % self.namespace) + jid_xml.text = str(jid) + never.append(jid_xml) + + +class Result(ElementBase): + name = 'result' + namespace = 'urn:xmpp:mam:tmp' + plugin_attrib = 'mam_result' + interfaces = set(['forwarded', 'queryid', 'id']) + + def get_forwarded(self): + return self.parent()['forwarded'] + + def del_forwarded(self): + del self.parent()['forwarded'] diff --git a/sleekxmpp/stanza/message.py b/sleekxmpp/stanza/message.py index 02133682..0bb6e587 100644 --- a/sleekxmpp/stanza/message.py +++ b/sleekxmpp/stanza/message.py @@ -63,6 +63,17 @@ class Message(RootStanza): lang_interfaces = sub_interfaces types = set(['normal', 'chat', 'headline', 'error', 'groupchat']) + def __init__(self, *args, **kwargs): + """ + Initialize a new <message /> stanza with an optional 'id' value. + + Overrides StanzaBase.__init__. + """ + StanzaBase.__init__(self, *args, **kwargs) + if self['id'] == '': + if self.stream is not None and self.stream.use_message_ids: + self['id'] = self.stream.new_id() + def get_type(self): """ Return the message type. diff --git a/sleekxmpp/stanza/presence.py b/sleekxmpp/stanza/presence.py index 7951f861..84bcd122 100644 --- a/sleekxmpp/stanza/presence.py +++ b/sleekxmpp/stanza/presence.py @@ -72,6 +72,17 @@ class Presence(RootStanza): 'subscribed', 'unsubscribe', 'unsubscribed']) showtypes = set(['dnd', 'chat', 'xa', 'away']) + def __init__(self, *args, **kwargs): + """ + Initialize a new <presence /> stanza with an optional 'id' value. + + Overrides StanzaBase.__init__. + """ + StanzaBase.__init__(self, *args, **kwargs) + if self['id'] == '': + if self.stream is not None and self.stream.use_presence_ids: + self['id'] = self.stream.new_id() + def exception(self, e): """ Override exception passback for presence. diff --git a/sleekxmpp/test/sleektest.py b/sleekxmpp/test/sleektest.py index 47af86cf..901c3a56 100644 --- a/sleekxmpp/test/sleektest.py +++ b/sleekxmpp/test/sleektest.py @@ -368,6 +368,11 @@ class SleekTest(unittest.TestCase): else: for plugin in plugins: self.xmpp.register_plugin(plugin) + + # Some plugins require messages to have ID values. Set + # this to True in tests related to those plugins. + self.xmpp.use_message_ids = False + self.xmpp.process(threaded=True) if skip: if socket != 'live': diff --git a/sleekxmpp/xmlstream/handler/__init__.py b/sleekxmpp/xmlstream/handler/__init__.py index 7bcf0b71..83c87f01 100644 --- a/sleekxmpp/xmlstream/handler/__init__.py +++ b/sleekxmpp/xmlstream/handler/__init__.py @@ -7,6 +7,7 @@ """ from sleekxmpp.xmlstream.handler.callback import Callback +from sleekxmpp.xmlstream.handler.collector import Collector from sleekxmpp.xmlstream.handler.waiter import Waiter from sleekxmpp.xmlstream.handler.xmlcallback import XMLCallback from sleekxmpp.xmlstream.handler.xmlwaiter import XMLWaiter diff --git a/sleekxmpp/xmlstream/handler/collector.py b/sleekxmpp/xmlstream/handler/collector.py new file mode 100644 index 00000000..8f02f8c3 --- /dev/null +++ b/sleekxmpp/xmlstream/handler/collector.py @@ -0,0 +1,66 @@ +# -*- coding: utf-8 -*- +""" + sleekxmpp.xmlstream.handler.collector + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2012 Nathanael C. Fritz, Lance J.T. Stout + :license: MIT, see LICENSE for more details +""" + +import logging + +from sleekxmpp.util import Queue, QueueEmpty +from sleekxmpp.xmlstream.handler.base import BaseHandler + + +log = logging.getLogger(__name__) + + +class Collector(BaseHandler): + + """ + The Collector handler allows for collecting a set of stanzas + that match a given pattern. Unlike the Waiter handler, a + Collector does not block execution, and will continue to + accumulate matching stanzas until told to stop. + + :param string name: The name of the handler. + :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase` + derived object for matching stanza objects. + :param stream: The :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` + instance this handler should monitor. + """ + + def __init__(self, name, matcher, stream=None): + BaseHandler.__init__(self, name, matcher, stream=stream) + self._payload = Queue() + + def prerun(self, payload): + """Store the matched stanza when received during processing. + + :param payload: The matched + :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object. + """ + self._payload.put(payload) + + def run(self, payload): + """Do not process this handler during the main event loop.""" + pass + + def stop(self): + """ + Stop collection of matching stanzas, and return the ones that + have been stored so far. + """ + self._destroy = True + results = [] + try: + while True: + results.append(self._payload.get(False)) + except QueueEmpty: + pass + + self.stream().remove_handler(self.name) + return results diff --git a/tests/test_stream_xep_0059.py b/tests/test_stream_xep_0059.py index 3a99842b..249ecf66 100644 --- a/tests/test_stream_xep_0059.py +++ b/tests/test_stream_xep_0059.py @@ -17,7 +17,7 @@ class TestStreamSet(SleekTest): def iter(self, rev=False): q = self.xmpp.Iq() q['type'] = 'get' - it = ResultIterator(q, 'disco_items', '1', reverse=rev) + it = ResultIterator(q, 'disco_items', amount='1', reverse=rev) for i in it: for j in i['disco_items']['items']: self.items.append(j[0]) |