diff options
Diffstat (limited to 'sleekxmpp/plugins')
33 files changed, 1916 insertions, 753 deletions
diff --git a/sleekxmpp/plugins/__init__.py b/sleekxmpp/plugins/__init__.py index d27937ae..c0b1121b 100644 --- a/sleekxmpp/plugins/__init__.py +++ b/sleekxmpp/plugins/__init__.py @@ -6,5 +6,8 @@ See the file LICENSE for copying permission. """ __all__ = ['xep_0004', 'xep_0009', 'xep_0012', 'xep_0030', 'xep_0033', - 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0085', 'xep_0086', - 'xep_0092', 'xep_0128', 'xep_0199', 'xep_0202', 'gmail_notify'] + 'xep_0045', 'xep_0050', 'xep_0060', 'xep_0066', 'xep_0082', + 'xep_0085', 'xep_0086', 'xep_0092', 'xep_0128', 'xep_0199', + 'xep_0203', 'xep_0224', 'xep_0249', 'gmail_notify'] + +# Don't automatically load xep_0078 diff --git a/sleekxmpp/plugins/base.py b/sleekxmpp/plugins/base.py index 2dd68c8d..561421d8 100644 --- a/sleekxmpp/plugins/base.py +++ b/sleekxmpp/plugins/base.py @@ -66,7 +66,8 @@ class base_plugin(object): """ if config is None: config = {} - self.xep = 'base' + self.xep = None + self.rfc = None self.description = 'Base Plugin' self.xmpp = xmpp self.config = config diff --git a/sleekxmpp/plugins/xep_0060.py b/sleekxmpp/plugins/old_0060.py index 93124fca..93124fca 100644 --- a/sleekxmpp/plugins/xep_0060.py +++ b/sleekxmpp/plugins/old_0060.py diff --git a/sleekxmpp/plugins/stanza_pubsub.py b/sleekxmpp/plugins/stanza_pubsub.py deleted file mode 100644 index b5964537..00000000 --- a/sleekxmpp/plugins/stanza_pubsub.py +++ /dev/null @@ -1,557 +0,0 @@ -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID -from .. stanza.iq import Iq -from .. stanza.message import Message -from .. basexmpp import basexmpp -from .. xmlstream.xmlstream import XMLStream -import logging -from . import xep_0004 - - -class PubsubState(ElementBase): - namespace = 'http://jabber.org/protocol/psstate' - name = 'state' - plugin_attrib = 'psstate' - interfaces = set(('node', 'item', 'payload')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setPayload(self, value): - self.xml.append(value) - - def getPayload(self): - childs = self.xml.getchildren() - if len(childs) > 0: - return childs[0] - - def delPayload(self): - for child in self.xml.getchildren(): - self.xml.remove(child) - -registerStanzaPlugin(Iq, PubsubState) - -class PubsubStateEvent(ElementBase): - namespace = 'http://jabber.org/protocol/psstate#event' - name = 'event' - plugin_attrib = 'psstate_event' - intefaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Message, PubsubStateEvent) -registerStanzaPlugin(PubsubStateEvent, PubsubState) - -class Pubsub(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'pubsub' - plugin_attrib = 'pubsub' - interfaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Iq, Pubsub) - -class PubsubOwner(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'pubsub' - plugin_attrib = 'pubsub_owner' - interfaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Iq, PubsubOwner) - -class Affiliation(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'affiliation' - plugin_attrib = name - interfaces = set(('node', 'affiliation')) - plugin_attrib_map = {} - plugin_tag_map = {} - -class Affiliations(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'affiliations' - plugin_attrib = 'affiliations' - interfaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (Affiliation,) - - def append(self, affiliation): - if not isinstance(affiliation, Affiliation): - raise TypeError - self.xml.append(affiliation.xml) - return self.iterables.append(affiliation) - -registerStanzaPlugin(Pubsub, Affiliations) - - -class Subscription(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'subscription' - plugin_attrib = name - interfaces = set(('jid', 'node', 'subscription', 'subid')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setjid(self, value): - self._setattr('jid', str(value)) - - def getjid(self): - return jid(self._getattr('jid')) - -registerStanzaPlugin(Pubsub, Subscription) - -class Subscriptions(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'subscriptions' - plugin_attrib = 'subscriptions' - interfaces = set(tuple()) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (Subscription,) - -registerStanzaPlugin(Pubsub, Subscriptions) - -class OptionalSetting(object): - interfaces = set(('required',)) - - def setRequired(self, value): - value = bool(value) - if value and not self['required']: - self.xml.append(ET.Element("{%s}required" % self.namespace)) - elif not value and self['required']: - self.delRequired() - - def getRequired(self): - required = self.xml.find("{%s}required" % self.namespace) - if required is not None: - return True - else: - return False - - def delRequired(self): - required = self.xml.find("{%s}required" % self.namespace) - if required is not None: - self.xml.remove(required) - - -class SubscribeOptions(ElementBase, OptionalSetting): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'subscribe-options' - plugin_attrib = 'suboptions' - plugin_attrib_map = {} - plugin_tag_map = {} - interfaces = set(('required',)) - -registerStanzaPlugin(Subscription, SubscribeOptions) - -class Item(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'item' - plugin_attrib = name - interfaces = set(('id', 'payload')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setPayload(self, value): - self.xml.append(value) - - def getPayload(self): - childs = self.xml.getchildren() - if len(childs) > 0: - return childs[0] - - def delPayload(self): - for child in self.xml.getchildren(): - self.xml.remove(child) - -class Items(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'items' - plugin_attrib = 'items' - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (Item,) - -registerStanzaPlugin(Pubsub, Items) - -class Create(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'create' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Pubsub, Create) - -#class Default(ElementBase): -# namespace = 'http://jabber.org/protocol/pubsub' -# name = 'default' -# plugin_attrib = name -# interfaces = set(('node', 'type')) -# plugin_attrib_map = {} -# plugin_tag_map = {} -# -# def getType(self): -# t = self._getAttr('type') -# if not t: t == 'leaf' -# return t -# -#registerStanzaPlugin(Pubsub, Default) - -class Publish(Items): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'publish' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (Item,) - -registerStanzaPlugin(Pubsub, Publish) - -class Retract(Items): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'retract' - plugin_attrib = name - interfaces = set(('node', 'notify')) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Pubsub, Retract) - -class Unsubscribe(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'unsubscribe' - plugin_attrib = name - interfaces = set(('node', 'jid')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(Pubsub, Unsubscribe) - -class Subscribe(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'subscribe' - plugin_attrib = name - interfaces = set(('node', 'jid')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(Pubsub, Subscribe) - -class Configure(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'configure' - plugin_attrib = name - interfaces = set(('node', 'type')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def getType(self): - t = self._getAttr('type') - if not t: t == 'leaf' - return t - -registerStanzaPlugin(Pubsub, Configure) -registerStanzaPlugin(Configure, xep_0004.Form) - -class DefaultConfig(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'default' - plugin_attrib = 'default' - interfaces = set(('node', 'type', 'config')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def __init__(self, *args, **kwargs): - ElementBase.__init__(self, *args, **kwargs) - - def getType(self): - t = self._getAttr('type') - if not t: t = 'leaf' - return t - - def getConfig(self): - return self['form'] - - def setConfig(self, value): - self['form'].setStanzaValues(value.getStanzaValues()) - return self - -registerStanzaPlugin(PubsubOwner, DefaultConfig) -registerStanzaPlugin(DefaultConfig, xep_0004.Form) - -class Options(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub' - name = 'options' - plugin_attrib = 'options' - interfaces = set(('jid', 'node', 'options')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def __init__(self, *args, **kwargs): - ElementBase.__init__(self, *args, **kwargs) - - def getOptions(self): - config = self.xml.find('{jabber:x:data}x') - form = xep_0004.Form() - if config is not None: - form.fromXML(config) - return form - - def setOptions(self, value): - self.xml.append(value.getXML()) - return self - - def delOptions(self): - config = self.xml.find('{jabber:x:data}x') - self.xml.remove(config) - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(Pubsub, Options) -registerStanzaPlugin(Subscribe, Options) - -class OwnerAffiliations(Affiliations): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('node')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def append(self, affiliation): - if not isinstance(affiliation, OwnerAffiliation): - raise TypeError - self.xml.append(affiliation.xml) - return self.affiliations.append(affiliation) - -registerStanzaPlugin(PubsubOwner, OwnerAffiliations) - -class OwnerAffiliation(Affiliation): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('affiliation', 'jid')) - plugin_attrib_map = {} - plugin_tag_map = {} - -class OwnerConfigure(Configure): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('node', 'config')) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(PubsubOwner, OwnerConfigure) - -class OwnerDefault(OwnerConfigure): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('node', 'config')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def getConfig(self): - return self['form'] - - def setConfig(self, value): - self['form'].setStanzaValues(value.getStanzaValues()) - return self - -registerStanzaPlugin(PubsubOwner, OwnerDefault) -registerStanzaPlugin(OwnerDefault, xep_0004.Form) - -class OwnerDelete(ElementBase, OptionalSetting): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'delete' - plugin_attrib = 'delete' - plugin_attrib_map = {} - plugin_tag_map = {} - interfaces = set(('node',)) - -registerStanzaPlugin(PubsubOwner, OwnerDelete) - -class OwnerPurge(ElementBase, OptionalSetting): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'purge' - plugin_attrib = name - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(PubsubOwner, OwnerPurge) - -class OwnerRedirect(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'redirect' - plugin_attrib = name - interfaces = set(('node', 'jid')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(OwnerDelete, OwnerRedirect) - -class OwnerSubscriptions(Subscriptions): - namespace = 'http://jabber.org/protocol/pubsub#owner' - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - - def append(self, subscription): - if not isinstance(subscription, OwnerSubscription): - raise TypeError - self.xml.append(subscription.xml) - return self.subscriptions.append(subscription) - -registerStanzaPlugin(PubsubOwner, OwnerSubscriptions) - -class OwnerSubscription(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#owner' - name = 'subscription' - plugin_attrib = name - interfaces = set(('jid', 'subscription')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('from')) - -class Event(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'event' - plugin_attrib = 'pubsub_event' - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Message, Event) - -class EventItem(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'item' - plugin_attrib = 'item' - interfaces = set(('id', 'payload')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setPayload(self, value): - self.xml.append(value) - - def getPayload(self): - childs = self.xml.getchildren() - if len(childs) > 0: - return childs[0] - - def delPayload(self): - for child in self.xml.getchildren(): - self.xml.remove(child) - - -class EventRetract(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'retract' - plugin_attrib = 'retract' - interfaces = set(('id',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -class EventItems(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'items' - plugin_attrib = 'items' - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - subitem = (EventItem, EventRetract) - -registerStanzaPlugin(Event, EventItems) - -class EventCollection(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'collection' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Event, EventCollection) - -class EventAssociate(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'associate' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(EventCollection, EventAssociate) - -class EventDisassociate(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'disassociate' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(EventCollection, EventDisassociate) - -class EventConfiguration(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'configuration' - plugin_attrib = name - interfaces = set(('node', 'config')) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Event, EventConfiguration) -registerStanzaPlugin(EventConfiguration, xep_0004.Form) - -class EventPurge(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'purge' - plugin_attrib = name - interfaces = set(('node',)) - plugin_attrib_map = {} - plugin_tag_map = {} - -registerStanzaPlugin(Event, EventPurge) - -class EventSubscription(ElementBase): - namespace = 'http://jabber.org/protocol/pubsub#event' - name = 'subscription' - plugin_attrib = name - interfaces = set(('node','expiry', 'jid', 'subid', 'subscription')) - plugin_attrib_map = {} - plugin_tag_map = {} - - def setJid(self, value): - self._setAttr('jid', str(value)) - - def getJid(self): - return JID(self._getAttr('jid')) - -registerStanzaPlugin(Event, EventSubscription) diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py index 8c534118..b5d10b85 100644 --- a/sleekxmpp/plugins/xep_0009/remote.py +++ b/sleekxmpp/plugins/xep_0009/remote.py @@ -463,7 +463,7 @@ class RemoteSession(object): key = "%s.%s" % (endpoint, name) log.debug("Registering call handler for %s (%s)." % (key, method)) with self._lock: - if self._entries.has_key(key): + if key in self._entries: raise KeyError("A handler for %s has already been regisered!" % endpoint) self._entries[key] = JabberRPCEntry(endpoint, method) return key diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py index 72c6c513..dd1c88d6 100644 --- a/sleekxmpp/plugins/xep_0050/adhoc.py +++ b/sleekxmpp/plugins/xep_0050/adhoc.py @@ -589,5 +589,5 @@ class xep_0050(base_plugin): elif iq['type'] == 'error': self.terminate_command(session) - if iq['command']['status'] == 'completed': + if iq['command']['status'] == 'completed': self.terminate_command(session) diff --git a/sleekxmpp/plugins/xep_0060/__init__.py b/sleekxmpp/plugins/xep_0060/__init__.py new file mode 100644 index 00000000..026f7c2b --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/__init__.py @@ -0,0 +1,2 @@ +from sleekxmpp.plugins.xep_0060.pubsub import xep_0060 +from sleekxmpp.plugins.xep_0060 import stanza diff --git a/sleekxmpp/plugins/xep_0060/pubsub.py b/sleekxmpp/plugins/xep_0060/pubsub.py new file mode 100644 index 00000000..e199be07 --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/pubsub.py @@ -0,0 +1,313 @@ +from __future__ import with_statement +from sleekxmpp.plugins import base +import logging +#from xml.etree import cElementTree as ET +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET +from sleekxmpp.plugins.xep_0060 import stanza +from sleekxmpp.plugins.xep_0004 import Form + + +log = logging.getLogger(__name__) + + +class xep_0060(base.base_plugin): + """ + XEP-0060 Publish Subscribe + """ + + def plugin_init(self): + self.xep = '0060' + self.description = 'Publish-Subscribe' + + def create_node(self, jid, node, config=None, collection=False, ntype=None): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + create = ET.Element('create') + create.set('node', node) + pubsub.append(create) + configure = ET.Element('configure') + if collection: + ntype = 'collection' + #if config is None: + # submitform = self.xmpp.plugin['xep_0004'].makeForm('submit') + #else: + if config is not None: + submitform = config + if 'FORM_TYPE' in submitform.field: + submitform.field['FORM_TYPE'].setValue('http://jabber.org/protocol/pubsub#node_config') + else: + submitform.addField('FORM_TYPE', 'hidden', value='http://jabber.org/protocol/pubsub#node_config') + if ntype: + if 'pubsub#node_type' in submitform.field: + submitform.field['pubsub#node_type'].setValue(ntype) + else: + submitform.addField('pubsub#node_type', value=ntype) + else: + if 'pubsub#node_type' in submitform.field: + submitform.field['pubsub#node_type'].setValue('leaf') + else: + submitform.addField('pubsub#node_type', value='leaf') + submitform['type'] = 'submit' + configure.append(submitform.xml) + pubsub.append(configure) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is False or result is None or result['type'] == 'error': return False + return True + + def subscribe(self, jid, node, bare=True, subscribee=None): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + subscribe = ET.Element('subscribe') + subscribe.attrib['node'] = node + if subscribee is None: + if bare: + subscribe.attrib['jid'] = self.xmpp.boundjid.bare + else: + subscribe.attrib['jid'] = self.xmpp.boundjid.full + else: + subscribe.attrib['jid'] = subscribee + pubsub.append(subscribe) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is False or result is None or result['type'] == 'error': return False + return True + + def unsubscribe(self, jid, node, bare=True, subscribee=None): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + unsubscribe = ET.Element('unsubscribe') + unsubscribe.attrib['node'] = node + if subscribee is None: + if bare: + unsubscribe.attrib['jid'] = self.xmpp.boundjid.bare + else: + unsubscribe.attrib['jid'] = self.xmpp.boundjid.full + else: + unsubscribe.attrib['jid'] = subscribee + pubsub.append(unsubscribe) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is False or result is None or result['type'] == 'error': return False + return True + + def getNodeConfig(self, jid, node=None): # if no node, then grab default + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + if node is not None: + configure = ET.Element('configure') + configure.attrib['node'] = node + else: + configure = ET.Element('default') + pubsub.append(configure) + #TODO: Add configure support. + iq = self.xmpp.makeIqGet() + iq.append(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + #self.xmpp.add_handler("<iq id='%s'/>" % id, self.handlerCreateNodeResponse) + result = iq.send() + if result is None or result == False or result['type'] == 'error': + log.warning("got error instead of config") + return False + if node is not None: + form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}configure/{jabber:x:data}x') + else: + form = result.find('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}default/{jabber:x:data}x') + if not form or form is None: + log.error("No form found.") + return False + return Form(xml=form) + + def getNodeSubscriptions(self, jid, node): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + subscriptions = ET.Element('subscriptions') + subscriptions.attrib['node'] = node + pubsub.append(subscriptions) + iq = self.xmpp.makeIqGet() + iq.append(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result == False or result['type'] == 'error': + log.warning("got error instead of config") + return False + else: + results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}subscriptions/{http://jabber.org/protocol/pubsub#owner}subscription') + if results is None: + return False + subs = {} + for sub in results: + subs[sub.get('jid')] = sub.get('subscription') + return subs + + def getNodeAffiliations(self, jid, node): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + affiliations = ET.Element('affiliations') + affiliations.attrib['node'] = node + pubsub.append(affiliations) + iq = self.xmpp.makeIqGet() + iq.append(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result == False or result['type'] == 'error': + log.warning("got error instead of config") + return False + else: + results = result.findall('{http://jabber.org/protocol/pubsub#owner}pubsub/{http://jabber.org/protocol/pubsub#owner}affiliations/{http://jabber.org/protocol/pubsub#owner}affiliation') + if results is None: + return False + subs = {} + for sub in results: + subs[sub.get('jid')] = sub.get('affiliation') + return subs + + def deleteNode(self, jid, node): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + iq = self.xmpp.makeIqSet() + delete = ET.Element('delete') + delete.attrib['node'] = node + pubsub.append(delete) + iq.append(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + result = iq.send() + if result is not None and result is not False and result['type'] != 'error': + return True + else: + return False + + + def setNodeConfig(self, jid, node, config): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + configure = ET.Element('configure') + configure.attrib['node'] = node + config = config.getXML('submit') + configure.append(config) + pubsub.append(configure) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result['type'] == 'error': + return False + return True + + def setItem(self, jid, node, items=[]): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + publish = ET.Element('publish') + publish.attrib['node'] = node + for pub_item in items: + id, payload = pub_item + item = ET.Element('item') + if id is not None: + item.attrib['id'] = id + item.append(payload) + publish.append(item) + pubsub.append(publish) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result is False or result['type'] == 'error': return False + return True + + def addItem(self, jid, node, items=[]): + return self.setItem(jid, node, items) + + def deleteItem(self, jid, node, item): + pubsub = ET.Element('{http://jabber.org/protocol/pubsub}pubsub') + retract = ET.Element('retract') + retract.attrib['node'] = node + itemn = ET.Element('item') + itemn.attrib['id'] = item + retract.append(itemn) + pubsub.append(retract) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result is False or result['type'] == 'error': return False + return True + + def getNodes(self, jid): + response = self.xmpp.plugin['xep_0030'].getItems(jid) + items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') + nodes = {} + if items is not None and items is not False: + for item in items: + nodes[item.get('node')] = item.get('name') + return nodes + + def getItems(self, jid, node): + response = self.xmpp.plugin['xep_0030'].getItems(jid, node) + items = response.findall('{http://jabber.org/protocol/disco#items}query/{http://jabber.org/protocol/disco#items}item') + nodeitems = [] + if items is not None and items is not False: + for item in items: + nodeitems.append(item.get('node')) + return nodeitems + + def addNodeToCollection(self, jid, child, parent=''): + config = self.getNodeConfig(jid, child) + if not config or config is None: + self.lasterror = "Config Error" + return False + try: + config.field['pubsub#collection'].setValue(parent) + except KeyError: + log.warning("pubsub#collection doesn't exist in config, trying to add it") + config.addField('pubsub#collection', value=parent) + if not self.setNodeConfig(jid, child, config): + return False + return True + + def modifyAffiliation(self, ps_jid, node, user_jid, affiliation): + if affiliation not in ('owner', 'publisher', 'member', 'none', 'outcast'): + raise TypeError + pubsub = ET.Element('{http://jabber.org/protocol/pubsub#owner}pubsub') + affs = ET.Element('affiliations') + affs.attrib['node'] = node + aff = ET.Element('affiliation') + aff.attrib['jid'] = user_jid + aff.attrib['affiliation'] = affiliation + affs.append(aff) + pubsub.append(affs) + iq = self.xmpp.makeIqSet(pubsub) + iq.attrib['to'] = ps_jid + iq.attrib['from'] = self.xmpp.boundjid.full + id = iq['id'] + result = iq.send() + if result is None or result is False or result['type'] == 'error': + return False + return True + + def addNodeToCollection(self, jid, child, parent=''): + config = self.getNodeConfig(jid, child) + if not config or config is None: + self.lasterror = "Config Error" + return False + try: + config.field['pubsub#collection'].setValue(parent) + except KeyError: + log.warning("pubsub#collection doesn't exist in config, trying to add it") + config.addField('pubsub#collection', value=parent) + if not self.setNodeConfig(jid, child, config): + return False + return True + + def removeNodeFromCollection(self, jid, child): + self.addNodeToCollection(jid, child, '') + diff --git a/sleekxmpp/plugins/xep_0060/stanza/__init__.py b/sleekxmpp/plugins/xep_0060/stanza/__init__.py new file mode 100644 index 00000000..d7cd91a8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/__init__.py @@ -0,0 +1,3 @@ +from sleekxmpp.plugins.xep_0060.stanza.pubsub import Pubsub, Affiliation, Affiliations, Subscription, Subscriptions, SubscribeOptions, Item, Items, Create, Publish, Retract, Unsubscribe, Subscribe, Configure, Options, PubsubState, PubsubStateEvent +from sleekxmpp.plugins.xep_0060.stanza.pubsub_owner import PubsubOwner, DefaultConfig, OwnerAffiliations, OwnerAffiliation, OwnerConfigure, OwnerDefault, OwnerDelete, OwnerPurge, OwnerRedirect, OwnerSubscriptions, OwnerSubscription +from sleekxmpp.plugins.xep_0060.stanza.pubsub_event import Event, EventItem, EventRetract, EventItems, EventCollection, EventAssociate, EventDisassociate, EventConfiguration, EventPurge, EventSubscription diff --git a/sleekxmpp/plugins/xep_0060/stanza/base.py b/sleekxmpp/plugins/xep_0060/stanza/base.py new file mode 100644 index 00000000..9b1efe1b --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/base.py @@ -0,0 +1,24 @@ +from xml.etree import cElementTree as ET + +class OptionalSetting(object): + interfaces = set(('required',)) + + def setRequired(self, value): + value = bool(value) + if value and not self['required']: + self.xml.append(ET.Element("{%s}required" % self.namespace)) + elif not value and self['required']: + self.delRequired() + + def getRequired(self): + required = self.xml.find("{%s}required" % self.namespace) + if required is not None: + return True + else: + return False + + def delRequired(self): + required = self.xml.find("{%s}required" % self.namespace) + if required is not None: + self.xml.remove(required) + diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py new file mode 100644 index 00000000..96655942 --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub.py @@ -0,0 +1,277 @@ +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.stanza.message import Message +from sleekxmpp.basexmpp import basexmpp +from sleekxmpp.xmlstream.xmlstream import XMLStream +import logging +from sleekxmpp.plugins import xep_0004 +from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting + + +class Pubsub(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'pubsub' + plugin_attrib = 'pubsub' + interfaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Iq, Pubsub) + + +class Affiliation(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'affiliation' + plugin_attrib = name + interfaces = set(('node', 'affiliation')) + plugin_attrib_map = {} + plugin_tag_map = {} + +class Affiliations(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'affiliations' + plugin_attrib = 'affiliations' + interfaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (Affiliation,) + + def append(self, affiliation): + if not isinstance(affiliation, Affiliation): + raise TypeError + self.xml.append(affiliation.xml) + return self.iterables.append(affiliation) + +registerStanzaPlugin(Pubsub, Affiliations) + + +class Subscription(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'subscription' + plugin_attrib = name + interfaces = set(('jid', 'node', 'subscription', 'subid')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setjid(self, value): + self._setattr('jid', str(value)) + + def getjid(self): + return jid(self._getattr('jid')) + +registerStanzaPlugin(Pubsub, Subscription) + +class Subscriptions(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'subscriptions' + plugin_attrib = 'subscriptions' + interfaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (Subscription,) + +registerStanzaPlugin(Pubsub, Subscriptions) + + +class SubscribeOptions(ElementBase, OptionalSetting): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'subscribe-options' + plugin_attrib = 'suboptions' + plugin_attrib_map = {} + plugin_tag_map = {} + interfaces = set(('required',)) + +registerStanzaPlugin(Subscription, SubscribeOptions) + +class Item(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'item' + plugin_attrib = name + interfaces = set(('id', 'payload')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setPayload(self, value): + self.xml.append(value) + + def getPayload(self): + childs = self.xml.getchildren() + if len(childs) > 0: + return childs[0] + + def delPayload(self): + for child in self.xml.getchildren(): + self.xml.remove(child) + +class Items(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'items' + plugin_attrib = 'items' + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (Item,) + +registerStanzaPlugin(Pubsub, Items) + +class Create(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'create' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Pubsub, Create) + +#class Default(ElementBase): +# namespace = 'http://jabber.org/protocol/pubsub' +# name = 'default' +# plugin_attrib = name +# interfaces = set(('node', 'type')) +# plugin_attrib_map = {} +# plugin_tag_map = {} +# +# def getType(self): +# t = self._getAttr('type') +# if not t: t == 'leaf' +# return t +# +#registerStanzaPlugin(Pubsub, Default) + +class Publish(Items): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'publish' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (Item,) + +registerStanzaPlugin(Pubsub, Publish) + +class Retract(Items): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'retract' + plugin_attrib = name + interfaces = set(('node', 'notify')) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Pubsub, Retract) + +class Unsubscribe(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'unsubscribe' + plugin_attrib = name + interfaces = set(('node', 'jid')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(Pubsub, Unsubscribe) + +class Subscribe(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'subscribe' + plugin_attrib = name + interfaces = set(('node', 'jid')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(Pubsub, Subscribe) + +class Configure(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'configure' + plugin_attrib = name + interfaces = set(('node', 'type')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def getType(self): + t = self._getAttr('type') + if not t: t == 'leaf' + return t + +registerStanzaPlugin(Pubsub, Configure) +registerStanzaPlugin(Configure, xep_0004.Form) + +class Options(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub' + name = 'options' + plugin_attrib = 'options' + interfaces = set(('jid', 'node', 'options')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def __init__(self, *args, **kwargs): + ElementBase.__init__(self, *args, **kwargs) + + def getOptions(self): + config = self.xml.find('{jabber:x:data}x') + form = xep_0004.Form() + if config is not None: + form.fromXML(config) + return form + + def setOptions(self, value): + self.xml.append(value.getXML()) + return self + + def delOptions(self): + config = self.xml.find('{jabber:x:data}x') + self.xml.remove(config) + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(Pubsub, Options) +registerStanzaPlugin(Subscribe, Options) + +class PubsubState(ElementBase): + namespace = 'http://jabber.org/protocol/psstate' + name = 'state' + plugin_attrib = 'psstate' + interfaces = set(('node', 'item', 'payload')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setPayload(self, value): + self.xml.append(value) + + def getPayload(self): + childs = self.xml.getchildren() + if len(childs) > 0: + return childs[0] + + def delPayload(self): + for child in self.xml.getchildren(): + self.xml.remove(child) + +registerStanzaPlugin(Iq, PubsubState) + +class PubsubStateEvent(ElementBase): + namespace = 'http://jabber.org/protocol/psstate#event' + name = 'event' + plugin_attrib = 'psstate_event' + intefaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Message, PubsubStateEvent) +registerStanzaPlugin(PubsubStateEvent, PubsubState) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py new file mode 100644 index 00000000..2dfe6c4a --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_event.py @@ -0,0 +1,124 @@ +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.stanza.message import Message +from sleekxmpp.basexmpp import basexmpp +from sleekxmpp.xmlstream.xmlstream import XMLStream +import logging +from sleekxmpp.plugins import xep_0004 + +class Event(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'event' + plugin_attrib = 'pubsub_event' + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Message, Event) + +class EventItem(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'item' + plugin_attrib = 'item' + interfaces = set(('id', 'payload')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setPayload(self, value): + self.xml.append(value) + + def getPayload(self): + childs = self.xml.getchildren() + if len(childs) > 0: + return childs[0] + + def delPayload(self): + for child in self.xml.getchildren(): + self.xml.remove(child) + + +class EventRetract(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'retract' + plugin_attrib = 'retract' + interfaces = set(('id',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +class EventItems(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'items' + plugin_attrib = 'items' + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + subitem = (EventItem, EventRetract) + +registerStanzaPlugin(Event, EventItems) + +class EventCollection(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'collection' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Event, EventCollection) + +class EventAssociate(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'associate' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(EventCollection, EventAssociate) + +class EventDisassociate(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'disassociate' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(EventCollection, EventDisassociate) + +class EventConfiguration(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'configuration' + plugin_attrib = name + interfaces = set(('node', 'config')) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Event, EventConfiguration) +registerStanzaPlugin(EventConfiguration, xep_0004.Form) + +class EventPurge(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'purge' + plugin_attrib = name + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Event, EventPurge) + +class EventSubscription(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#event' + name = 'subscription' + plugin_attrib = name + interfaces = set(('node','expiry', 'jid', 'subid', 'subscription')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(Event, EventSubscription) diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py new file mode 100644 index 00000000..a90780cc --- /dev/null +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_owner.py @@ -0,0 +1,152 @@ +from sleekxmpp.xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID +from sleekxmpp.stanza.iq import Iq +from sleekxmpp.stanza.message import Message +from sleekxmpp.basexmpp import basexmpp +from sleekxmpp.xmlstream.xmlstream import XMLStream +import logging +from sleekxmpp.plugins import xep_0004 +from sleekxmpp.plugins.xep_0060.stanza.base import OptionalSetting +from sleekxmpp.plugins.xep_0060.stanza.pubsub import Affiliations, Affiliation, Configure, Subscriptions + +class PubsubOwner(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'pubsub' + plugin_attrib = 'pubsub_owner' + interfaces = set(tuple()) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(Iq, PubsubOwner) + +class DefaultConfig(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'default' + plugin_attrib = 'default' + interfaces = set(('node', 'type', 'config')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def __init__(self, *args, **kwargs): + ElementBase.__init__(self, *args, **kwargs) + + def getType(self): + t = self._getAttr('type') + if not t: t = 'leaf' + return t + + def getConfig(self): + return self['form'] + + def setConfig(self, value): + self['form'].setStanzaValues(value.getStanzaValues()) + return self + +registerStanzaPlugin(PubsubOwner, DefaultConfig) +registerStanzaPlugin(DefaultConfig, xep_0004.Form) + +class OwnerAffiliations(Affiliations): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('node')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def append(self, affiliation): + if not isinstance(affiliation, OwnerAffiliation): + raise TypeError + self.xml.append(affiliation.xml) + return self.affiliations.append(affiliation) + +registerStanzaPlugin(PubsubOwner, OwnerAffiliations) + +class OwnerAffiliation(Affiliation): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('affiliation', 'jid')) + plugin_attrib_map = {} + plugin_tag_map = {} + +class OwnerConfigure(Configure): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('node', 'config')) + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(PubsubOwner, OwnerConfigure) + +class OwnerDefault(OwnerConfigure): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('node', 'config')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def getConfig(self): + return self['form'] + + def setConfig(self, value): + self['form'].setStanzaValues(value.getStanzaValues()) + return self + +registerStanzaPlugin(PubsubOwner, OwnerDefault) +registerStanzaPlugin(OwnerDefault, xep_0004.Form) + +class OwnerDelete(ElementBase, OptionalSetting): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'delete' + plugin_attrib = 'delete' + plugin_attrib_map = {} + plugin_tag_map = {} + interfaces = set(('node',)) + +registerStanzaPlugin(PubsubOwner, OwnerDelete) + +class OwnerPurge(ElementBase, OptionalSetting): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'purge' + plugin_attrib = name + plugin_attrib_map = {} + plugin_tag_map = {} + +registerStanzaPlugin(PubsubOwner, OwnerPurge) + +class OwnerRedirect(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'redirect' + plugin_attrib = name + interfaces = set(('node', 'jid')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('jid')) + +registerStanzaPlugin(OwnerDelete, OwnerRedirect) + +class OwnerSubscriptions(Subscriptions): + namespace = 'http://jabber.org/protocol/pubsub#owner' + interfaces = set(('node',)) + plugin_attrib_map = {} + plugin_tag_map = {} + + def append(self, subscription): + if not isinstance(subscription, OwnerSubscription): + raise TypeError + self.xml.append(subscription.xml) + return self.subscriptions.append(subscription) + +registerStanzaPlugin(PubsubOwner, OwnerSubscriptions) + +class OwnerSubscription(ElementBase): + namespace = 'http://jabber.org/protocol/pubsub#owner' + name = 'subscription' + plugin_attrib = name + interfaces = set(('jid', 'subscription')) + plugin_attrib_map = {} + plugin_tag_map = {} + + def setJid(self, value): + self._setAttr('jid', str(value)) + + def getJid(self): + return JID(self._getAttr('from')) diff --git a/sleekxmpp/plugins/xep_0066/__init__.py b/sleekxmpp/plugins/xep_0066/__init__.py new file mode 100644 index 00000000..ebfbd0c2 --- /dev/null +++ b/sleekxmpp/plugins/xep_0066/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0066 import stanza +from sleekxmpp.plugins.xep_0066.stanza import OOB, OOBTransfer +from sleekxmpp.plugins.xep_0066.oob import xep_0066 diff --git a/sleekxmpp/plugins/xep_0066/oob.py b/sleekxmpp/plugins/xep_0066/oob.py new file mode 100644 index 00000000..98cb81cd --- /dev/null +++ b/sleekxmpp/plugins/xep_0066/oob.py @@ -0,0 +1,154 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import Message, Presence, Iq +from sleekxmpp.exceptions import XMPPError +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_0066 import stanza + + +log = logging.getLogger(__name__) + + +class xep_0066(base_plugin): + + """ + XEP-0066: Out-of-Band Data + + Out-of-Band Data is a basic method for transferring files between + XMPP agents. The URL of the resource in question is sent to the receiving + entity, which then downloads the resource before responding to the OOB + request. OOB is also used as a generic means to transmit URLs in other + stanzas to indicate where to find additional information. + + Also see <http://www.xmpp.org/extensions/xep-0066.html>. + + Events: + oob_transfer -- Raised when a request to download a resource + has been received. + + Methods: + send_oob -- Send a request to another entity to download a file + or other addressable resource. + """ + + def plugin_init(self): + """Start the XEP-0066 plugin.""" + self.xep = '0066' + self.description = 'Out-of-Band Transfer' + self.stanza = stanza + + self.url_handlers = {'global': self._default_handler, + 'jid': {}} + + register_stanza_plugin(Iq, stanza.OOBTransfer) + register_stanza_plugin(Message, stanza.OOB) + register_stanza_plugin(Presence, stanza.OOB) + + self.xmpp.register_handler( + Callback('OOB Transfer', + StanzaPath('iq@type=set/oob_transfer'), + self._handle_transfer)) + + def post_init(self): + """Handle cross-plugin dependencies.""" + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature(stanza.OOBTransfer.namespace) + self.xmpp['xep_0030'].add_feature(stanza.OOB.namespace) + + def register_url_handler(self, jid=None, handler=None): + """ + Register a handler to process download requests, either for all + JIDs or a single JID. + + Arguments: + jid -- If None, then set the handler as a global default. + handler -- If None, then remove the existing handler for the + given JID, or reset the global handler if the JID + is None. + """ + if jid is None: + if handler is not None: + self.url_handlers['global'] = handler + else: + self.url_handlers['global'] = self._default_handler + else: + if handler is not None: + self.url_handlers['jid'][jid] = handler + else: + del self.url_handlers['jid'][jid] + + def send_oob(self, to, url, desc=None, ifrom=None, **iqargs): + """ + Initiate a basic file transfer by sending the URL of + a file or other resource. + + Arguments: + url -- The URL of the resource to transfer. + desc -- An optional human readable description of the item + that is to be transferred. + 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. + callback -- Optional callback to execute when a reply is + received instead of blocking and waiting for + the reply. + """ + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['to'] = to + if ifrom: + iq['from'] = ifrom + iq['oob_transfer']['url'] = url + iq['oob_transfer']['desc'] = desc + return iq.send(**iqargs) + + def _run_url_handler(self, iq): + """ + Execute the appropriate handler for a transfer request. + + Arguments: + iq -- The Iq stanza containing the OOB transfer request. + """ + if iq['to'] in self.url_handlers['jid']: + return self.url_handlers['jid'][jid](iq) + else: + if self.url_handlers['global']: + self.url_handlers['global'](iq) + else: + raise XMPPError('service-unavailable') + + def _default_handler(self, iq): + """ + As a safe default, don't actually download files. + + Register a new handler using self.register_url_handler to + screen requests and download files. + + Arguments: + iq -- The Iq stanza containing the OOB transfer request. + """ + raise XMPPError('service-unavailable') + + def _handle_transfer(self, iq): + """ + Handle receiving an out-of-band transfer request. + + Arguments: + iq -- An Iq stanza containing an OOB transfer request. + """ + log.debug('Received out-of-band data request for %s from %s:' % ( + iq['oob_transfer']['url'], iq['from'])) + self._run_url_handler(iq) + iq.reply().send() diff --git a/sleekxmpp/plugins/xep_0066/stanza.py b/sleekxmpp/plugins/xep_0066/stanza.py new file mode 100644 index 00000000..21387485 --- /dev/null +++ b/sleekxmpp/plugins/xep_0066/stanza.py @@ -0,0 +1,33 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase + + +class OOBTransfer(ElementBase): + + """ + """ + + name = 'query' + namespace = 'jabber:iq:oob' + plugin_attrib = 'oob_transfer' + interfaces = set(('url', 'desc', 'sid')) + sub_interfaces = set(('url', 'desc')) + + +class OOB(ElementBase): + + """ + """ + + name = 'x' + namespace = 'jabber:x:oob' + plugin_attrib = 'oob' + interfaces = set(('url', 'desc')) + sub_interfaces = interfaces diff --git a/sleekxmpp/plugins/xep_0078.py b/sleekxmpp/plugins/xep_0078.py deleted file mode 100644 index bb6a4632..00000000 --- a/sleekxmpp/plugins/xep_0078.py +++ /dev/null @@ -1,72 +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 __future__ import with_statement -from xml.etree import cElementTree as ET -import logging -import hashlib -from . import base - - -log = logging.getLogger(__name__) - - -class xep_0078(base.base_plugin): - """ - XEP-0078 NON-SASL Authentication - """ - def plugin_init(self): - self.description = "Non-SASL Authentication (broken)" - self.xep = "0078" - self.xmpp.add_event_handler("session_start", self.check_stream) - #disabling until I fix conflict with PLAIN - #self.xmpp.registerFeature("<auth xmlns='http://jabber.org/features/iq-auth'/>", self.auth) - self.streamid = '' - - def check_stream(self, xml): - self.streamid = xml.attrib['id'] - if xml.get('version', '0') != '1.0': - self.auth() - - def auth(self, xml=None): - 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.boundjid.host - username = ET.Element('username') - username.text = self.xmpp.username - auth_request_query.append(username) - auth_request.append(auth_request_query) - result = auth_request.send() - rquery = result.find('{jabber:iq:auth}query') - attempt = self.xmpp.makeIqSet() - query = ET.Element('{jabber:iq:auth}query') - resource = ET.Element('resource') - resource.text = self.xmpp.resource - query.append(username) - query.append(resource) - if rquery.find('{jabber:iq:auth}digest') is None: - log.warning("Authenticating via jabber:iq:auth Plain.") - password = ET.Element('password') - password.text = self.xmpp.password - query.append(password) - else: - log.debug("Authenticating via jabber:iq:auth Digest") - digest = ET.Element('digest') - digest.text = hashlib.sha1(b"%s%s" % (self.streamid, self.xmpp.password)).hexdigest() - query.append(digest) - attempt.append(query) - result = attempt.send() - if result.attrib['type'] == 'result': - with self.xmpp.lock: - self.xmpp.authenticated = True - self.xmpp.sessionstarted = True - self.xmpp.event("session_start") - else: - log.info("Authentication failed") - self.xmpp.disconnect() - self.xmpp.event("failed_auth") diff --git a/sleekxmpp/plugins/xep_0078/__init__.py b/sleekxmpp/plugins/xep_0078/__init__.py new file mode 100644 index 00000000..5a2bda77 --- /dev/null +++ b/sleekxmpp/plugins/xep_0078/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0078 import stanza +from sleekxmpp.plugins.xep_0078.stanza import IqAuth, AuthFeature +from sleekxmpp.plugins.xep_0078.legacyauth import xep_0078 + diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py new file mode 100644 index 00000000..bdd2df67 --- /dev/null +++ b/sleekxmpp/plugins/xep_0078/legacyauth.py @@ -0,0 +1,108 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import hashlib +import random + +from sleekxmpp.stanza import Iq, StreamFeatures +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.plugins.xep_0078 import stanza + + +log = logging.getLogger(__name__) + + +class xep_0078(base_plugin): + + """ + XEP-0078 NON-SASL Authentication + + This XEP is OBSOLETE in favor of using SASL, so DO NOT use this plugin + unless you are forced to use an old XMPP server implementation. + """ + + def plugin_init(self): + self.xep = "0078" + self.description = "Non-SASL Authentication" + self.stanza = stanza + + self.xmpp.register_feature('auth', + self._handle_auth, + restart=False, + order=self.config.get('order', 15)) + + register_stanza_plugin(Iq, stanza.IqAuth) + register_stanza_plugin(StreamFeatures, stanza.AuthFeature) + + + def _handle_auth(self, features): + # If we can or have already authenticated with SASL, do nothing. + if 'mechanisms' in features['features']: + return False + if self.xmpp.authenticated: + return False + + log.debug("Starting jabber:iq:auth Authentication") + + # Step 1: Request the auth form + iq = self.xmpp.Iq() + iq['type'] = 'get' + iq['to'] = self.xmpp.boundjid.host + iq['auth']['username'] = self.xmpp.boundjid.user + resp = iq.send(now=True) + + if resp is None or resp['type'] != 'result': + log.info("Authentication failed: %s" % resp['error']['condition']) + self.xmpp.event('failed_auth', resp, direct=True) + self.xmpp.disconnect() + return True + + # Step 2: Fill out auth form for either password or digest auth + iq = self.xmpp.Iq() + iq['type'] = 'set' + iq['auth']['username'] = self.xmpp.boundjid.user + + # A resource is required, so create a random one if necessary + if self.xmpp.boundjid.resource: + iq['auth']['resource'] = self.xmpp.boundjid.resource + else: + iq['auth']['resource'] = '%s' % random.random() + + if 'digest' in resp['auth']['fields']: + log.debug('Authenticating via jabber:iq:auth Digest') + if sys.version_info < (3, 0): + stream_id = bytes(self.xmpp.stream_id) + password = bytes(self.xmpp.password) + else: + stream_id = bytes(self.xmpp.stream_id, encoding='utf-8') + password = bytes(self.xmpp.password, encoding='utf-8') + + digest = hashlib.sha1(b'%s%s' % (stream_id, password)).hexdigest() + iq['auth']['digest'] = digest + else: + log.warning('Authenticating via jabber:iq:auth Plain.') + iq['auth']['password'] = self.xmpp.password + + # Step 3: Send credentials + result = iq.send(now=True) + if result is not None and result.attrib['type'] == 'result': + self.xmpp.features.add('auth') + + self.xmpp.authenticated = True + log.debug("Established Session") + self.xmpp.sessionstarted = True + self.xmpp.session_started_event.set() + self.xmpp.event('session_start') + else: + log.info("Authentication failed") + self.xmpp.disconnect() + self.xmpp.event("failed_auth") + + return True diff --git a/sleekxmpp/plugins/xep_0078/stanza.py b/sleekxmpp/plugins/xep_0078/stanza.py new file mode 100644 index 00000000..86ba09ad --- /dev/null +++ b/sleekxmpp/plugins/xep_0078/stanza.py @@ -0,0 +1,43 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET, register_stanza_plugin + + +class IqAuth(ElementBase): + namespace = 'jabber:iq:auth' + name = 'query' + plugin_attrib = 'auth' + interfaces = set(('fields', 'username', 'password', 'resource', 'digest')) + sub_interfaces = set(('username', 'password', 'resource', 'digest')) + plugin_tag_map = {} + plugin_attrib_map = {} + + def get_fields(self): + fields = set() + for field in self.sub_interfaces: + if self.xml.find('{%s}%s' % (self.namespace, field)) is not None: + fields.add(field) + return fields + + def set_resource(self, value): + self._set_sub_text('resource', value, keep=True) + + def set_password(self, value): + self._set_sub_text('password', value, keep=True) + + +class AuthFeature(ElementBase): + namespace = 'http://jabber.org/features/iq-auth' + name = 'auth' + plugin_attrib = 'auth' + interfaces = set() + plugin_tag_map = {} + plugin_attrib_map = {} + + diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py new file mode 100644 index 00000000..d3c4cc56 --- /dev/null +++ b/sleekxmpp/plugins/xep_0082.py @@ -0,0 +1,206 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging +import datetime as dt + +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.thirdparty import tzutc, tzoffset, parse_iso + + +# ===================================================================== +# To make it easier for stanzas without direct access to plugin objects +# to use the XEP-0082 utility methods, we will define them as top-level +# functions and then just reference them in the plugin itself. + +def parse(time_str): + """ + Convert a string timestamp into a datetime object. + + Arguments: + time_str -- A formatted timestamp string. + """ + return parse_iso(time_str) + + +def format_date(time_obj): + """ + Return a formatted string version of a date object. + + Format: + YYYY-MM-DD + + Arguments: + time_obj -- A date or datetime object. + """ + if isinstance(time_obj, dt.datetime): + time_obj = time_obj.date() + return time_obj.isoformat() + +def format_time(time_obj): + """ + Return a formatted string version of a time object. + + format: + hh:mm:ss[.sss][TZD] + + arguments: + time_obj -- A time or datetime object. + """ + if isinstance(time_obj, dt.datetime): + time_obj = time_obj.timetz() + timestamp = time_obj.isoformat() + if time_obj.tzinfo == tzutc(): + timestamp = timestamp[:-6] + return '%sZ' % timestamp + return timestamp + +def format_datetime(time_obj): + """ + Return a formatted string version of a datetime object. + + Format: + YYYY-MM-DDThh:mm:ss[.sss]TZD + + arguments: + time_obj -- A datetime object. + """ + timestamp = time_obj.isoformat('T') + if time_obj.tzinfo == tzutc(): + timestamp = timestamp[:-6] + return '%sZ' % timestamp + return timestamp + +def date(year=None, month=None, day=None): + """ + Create a date only timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + year -- Integer value of the year (4 digits) + month -- Integer value of the month + day -- Integer value of the day of the month. + """ + today = dt.datetime.today() + if year is None: + year = today.year + if month is None: + month = today.month + if day is None: + day = today.day + return format_date(dt.date(year, month, day)) + +def time(hour=None, min=None, sec=None, micro=None, offset=None): + """ + Create a time only timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + hour -- Integer value of the hour. + min -- Integer value of the number of minutes. + sec -- Integer value of the number of seconds. + micro -- Integer value of the number of microseconds. + offset -- Either a positive or negative number of seconds + to offset from UTC to match a desired timezone, + or a tzinfo object. + """ + now = dt.datetime.utcnow() + if hour is None: + hour = now.hour + if min is None: + min = now.minute + if sec is None: + sec = now.second + if micro is None: + micro = now.microsecond + if offset is None: + offset = tzutc() + elif not isinstance(offset, dt.tzinfo): + offset = tzoffset(None, offset) + time = dt.time(hour, min, sec, micro, offset) + return format_time(time) + +def datetime(year=None, month=None, day=None, hour=None, + min=None, sec=None, micro=None, offset=None, + separators=True): + """ + Create a datetime timestamp for the given instant. + + Unspecified components default to their current counterparts. + + Arguments: + year -- Integer value of the year (4 digits) + month -- Integer value of the month + day -- Integer value of the day of the month. + hour -- Integer value of the hour. + min -- Integer value of the number of minutes. + sec -- Integer value of the number of seconds. + micro -- Integer value of the number of microseconds. + offset -- Either a positive or negative number of seconds + to offset from UTC to match a desired timezone, + or a tzinfo object. + """ + now = dt.datetime.utcnow() + if year is None: + year = now.year + if month is None: + month = now.month + if day is None: + day = now.day + if hour is None: + hour = now.hour + if min is None: + min = now.minute + if sec is None: + sec = now.second + if micro is None: + micro = now.microsecond + if offset is None: + offset = tzutc() + elif not isinstance(offset, dt.tzinfo): + offset = tzoffset(None, offset) + + date = dt.datetime(year, month, day, hour, + min, sec, micro, offset) + return format_datetime(date) + +class xep_0082(base_plugin): + + """ + XEP-0082: XMPP Date and Time Profiles + + XMPP uses a subset of the formats allowed by ISO 8601 as a matter of + pragmatism based on the relatively few formats historically used by + the XMPP. + + Also see <http://www.xmpp.org/extensions/xep-0082.html>. + + Methods: + date -- Create a time stamp using the Date profile. + datetime -- Create a time stamp using the DateTime profile. + time -- Create a time stamp using the Time profile. + format_date -- Format an existing date object. + format_datetime -- Format an existing datetime object. + format_time -- Format an existing time object. + parse -- Convert a time string into a Python datetime object. + """ + + def plugin_init(self): + """Start the XEP-0082 plugin.""" + self.xep = '0082' + self.description = 'XMPP Date and Time Profiles' + + self.date = date + self.datetime = datetime + self.time = time + self.format_date = format_date + self.format_datetime = format_datetime + self.format_time = format_time + self.parse = parse diff --git a/sleekxmpp/plugins/xep_0092/version.py b/sleekxmpp/plugins/xep_0092/version.py index 1ca6c15e..ac0924b8 100644 --- a/sleekxmpp/plugins/xep_0092/version.py +++ b/sleekxmpp/plugins/xep_0092/version.py @@ -35,7 +35,7 @@ class xep_0092(base_plugin): self.stanza = sleekxmpp.plugins.xep_0092.stanza self.name = self.config.get('name', 'SleekXMPP') - self.version = self.config.get('version', '0.1-dev') + self.version = self.config.get('version', sleekxmpp.__version__) self.os = self.config.get('os', '') self.getVersion = self.get_version diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py index d1e08e61..0fa22f8a 100644 --- a/sleekxmpp/plugins/xep_0199/ping.py +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -108,7 +108,7 @@ class xep_0199(base_plugin): iq -- The ping request. """ log.debug("Pinged by %s" % iq['from']) - iq.reply().enable('ping').send() + iq.reply().send() def send_ping(self, jid, timeout=None, errorfalse=False, ifrom=None, block=True, callback=None): diff --git a/sleekxmpp/plugins/xep_0202.py b/sleekxmpp/plugins/xep_0202.py deleted file mode 100644 index 3b31c97a..00000000 --- a/sleekxmpp/plugins/xep_0202.py +++ /dev/null @@ -1,117 +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 datetime import datetime, tzinfo
-import logging
-import time
-
-from . import base
-from .. stanza.iq import Iq
-from .. xmlstream.handler.callback import Callback
-from .. xmlstream.matcher.xpath import MatchXPath
-from .. xmlstream import ElementBase, ET, JID, register_stanza_plugin
-
-
-log = logging.getLogger(__name__)
-
-
-class EntityTime(ElementBase):
- name = 'time'
- namespace = 'urn:xmpp:time'
- plugin_attrib = 'entity_time'
- interfaces = set(('tzo', 'utc'))
- sub_interfaces = set(('tzo', 'utc'))
-
- #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):
- 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
- sign = ('+' if seconds >= 0 else '-')
- minutes = abs(seconds // 60)
- tzo = '{sign}{hours:02d}:{minutes:02d}'.format(sign=sign, hours=minutes//60, minutes=minutes%60)
- elif not isinstance(tzo, str):
- raise TypeError('The time should be a string or a datetime.tzinfo object.')
- self._set_sub_text('tzo', tzo)
-
- def get_utc(self):
- # 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-%dT%H:%M:%S.%fZ')
- else:
- return datetime.strptime(value, '%Y-%m-%dT%H:%M:%SZ')
-
- def set_utc(self, tim=None):
- if isinstance(tim, datetime):
- if tim.utcoffset():
- tim = tim - tim.utcoffset()
- tim = tim.strftime('%Y-%m-%dT%H:%M:%SZ')
- elif isinstance(tim, time.struct_time):
- tim = time.strftime('%Y-%m-%dT%H:%M:%SZ', tim)
- elif not isinstance(tim, str):
- raise TypeError('The time should be a string or a datetime.datetime or time.struct_time object.')
-
- self._set_sub_text('utc', tim)
-
-
-class xep_0202(base.base_plugin):
- """
- XEP-0202 Entity Time
- """
- def plugin_init(self):
- self.description = "Entity Time"
- self.xep = "0202"
-
- self.xmpp.registerHandler(
- Callback('Time Request',
- MatchXPath('{%s}iq/{%s}time' % (self.xmpp.default_ns,
- EntityTime.namespace)),
- self.handle_entity_time_query))
- register_stanza_plugin(Iq, EntityTime)
-
- self.xmpp.add_event_handler('entity_time_request', self.handle_entity_time)
-
-
- def post_init(self):
- base.base_plugin.post_init(self)
-
- self.xmpp.plugin['xep_0030'].add_feature('urn:xmpp:time')
-
- def handle_entity_time_query(self, iq):
- if iq['type'] == 'get':
- log.debug("Entity time requested by %s" % iq['from'])
- self.xmpp.event('entity_time_request', iq)
- elif iq['type'] == 'result':
- log.debug("Entity time result from %s" % iq['from'])
- self.xmpp.event('entity_time', iq)
-
- def handle_entity_time(self, iq):
- iq = iq.reply()
- iq.enable('entity_time')
- tzo = time.strftime('%z') # %z is not on all ANSI C libraries
- tzo = tzo[:3] + ':' + tzo[3:]
- iq['entity_time']['tzo'] = tzo
- iq['entity_time']['utc'] = datetime.utcnow()
- iq.send()
-
- def get_entity_time(self, jid):
- iq = self.xmpp.makeIqGet()
- iq.enable('entity_time')
- 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':
- return {'utc': result['entity_time']['utc'], 'tzo': result['entity_time']['tzo']}
- else:
- return False
diff --git a/sleekxmpp/plugins/xep_0202/__init__.py b/sleekxmpp/plugins/xep_0202/__init__.py new file mode 100644 index 00000000..a34b2376 --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + + +from sleekxmpp.plugins.xep_0202 import stanza +from sleekxmpp.plugins.xep_0202.stanza import EntityTime +from sleekxmpp.plugins.xep_0202.time import xep_0202 diff --git a/sleekxmpp/plugins/xep_0202/stanza.py b/sleekxmpp/plugins/xep_0202/stanza.py new file mode 100644 index 00000000..b6ccc960 --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/stanza.py @@ -0,0 +1,127 @@ +""" + 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 datetime as dt + +from sleekxmpp.xmlstream import ElementBase +from sleekxmpp.plugins import xep_0082 +from sleekxmpp.thirdparty import tzutc, tzoffset + + +class EntityTime(ElementBase): + + """ + The <time> element represents the local time for an XMPP agent. + The time is expressed in UTC to make synchronization easier + between entities, but the offset for the local timezone is also + included. + + Example <time> stanzas: + <iq type="result"> + <time xmlns="urn:xmpp:time"> + <utc>2011-07-03T11:37:12.234569</utc> + <tzo>-07:00</tzo> + </time> + </iq> + + Stanza Interface: + time -- The local time for the entity (updates utc and tzo). + utc -- The UTC equivalent to local time. + tzo -- The local timezone offset from UTC. + + Methods: + get_time -- Return local time datetime object. + set_time -- Set UTC and TZO fields. + del_time -- Remove both UTC and TZO fields. + get_utc -- Return datetime object of UTC time. + set_utc -- Set the UTC time. + get_tzo -- Return tzinfo object. + set_tzo -- Set the local timezone offset. + """ + + name = 'time' + namespace = 'urn:xmpp:time' + plugin_attrib = 'entity_time' + interfaces = set(('tzo', 'utc', 'time')) + sub_interfaces = interfaces + + def set_time(self, value): + """ + Set both the UTC and TZO fields given a time object. + + Arguments: + value -- A datetime object or properly formatted + string equivalent. + """ + date = value + if not isinstance(value, dt.datetime): + date = xep_0082.parse(value) + self['utc'] = date + self['tzo'] = date.tzinfo + + def get_time(self): + """ + Return the entity's local time based on the UTC and TZO data. + """ + date = self['utc'] + tz = self['tzo'] + return date.astimezone(tz) + + def del_time(self): + """Remove both the UTC and TZO fields.""" + del self['utc'] + del self['tzo'] + + def get_tzo(self): + """ + Return the timezone offset from UTC as a tzinfo object. + """ + tzo = self._get_sub_text('tzo') + if tzo == '': + tzo = 'Z' + time = xep_0082.parse('00:00:00%s' % tzo) + return time.tzinfo + + def set_tzo(self, value): + """ + Set the timezone offset from UTC. + + Arguments: + value -- Either a tzinfo object or the number of + seconds (positive or negative) to offset. + """ + time = xep_0082.time(offset=value) + if xep_0082.parse(time).tzinfo == tzutc(): + self._set_sub_text('tzo', 'Z') + else: + self._set_sub_text('tzo', time[-6:]) + + def get_utc(self): + """ + Return the time in UTC as a datetime object. + """ + value = self._get_sub_text('utc') + if value == '': + return xep_0082.parse(xep_0082.datetime()) + return xep_0082.parse('%sZ' % value) + + def set_utc(self, value): + """ + Set the time in UTC. + + Arguments: + value -- A datetime object or properly formatted + string equivalent. + """ + date = value + if not isinstance(value, dt.datetime): + date = xep_0082.parse(value) + date = date.astimezone(tzutc()) + value = xep_0082.format_datetime(date)[:-1] + self._set_sub_text('utc', value) diff --git a/sleekxmpp/plugins/xep_0202/time.py b/sleekxmpp/plugins/xep_0202/time.py new file mode 100644 index 00000000..bcad8bc8 --- /dev/null +++ b/sleekxmpp/plugins/xep_0202/time.py @@ -0,0 +1,92 @@ +"""
+ 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
+
+from sleekxmpp.stanza.iq 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 import xep_0082
+from sleekxmpp.plugins.xep_0202 import stanza
+
+
+log = logging.getLogger(__name__)
+
+
+class xep_0202(base_plugin):
+
+ """
+ XEP-0202: Entity Time
+ """
+
+ def plugin_init(self):
+ """Start the XEP-0203 plugin."""
+ self.xep = '0202'
+ self.description = 'Entity Time'
+ self.stanza = stanza
+
+ self.tz_offset = self.config.get('tz_offset', 0)
+
+ # As a default, respond to time requests with the
+ # local time returned by XEP-0082. However, a
+ # custom function can be supplied which accepts
+ # the JID of the entity to query for the time.
+ self.local_time = self.config.get('local_time', None)
+ if not self.local_time:
+ self.local_time = lambda x: xep_0082.datetime(offset=self.tz_offset)
+
+ self.xmpp.registerHandler(
+ Callback('Entity Time',
+ StanzaPath('iq/entity_time'),
+ self._handle_time_request))
+ register_stanza_plugin(Iq, stanza.EntityTime)
+
+ def post_init(self):
+ """Handle cross-plugin interactions."""
+ base_plugin.post_init(self)
+ self.xmpp['xep_0030'].add_feature('urn:xmpp:time')
+
+
+ def _handle_time_request(self, iq):
+ """
+ Respond to a request for the local time.
+
+ The time is taken from self.local_time(), which may be replaced
+ during plugin configuration with a function that maps JIDs to
+ times.
+
+ Arguments:
+ iq -- The Iq time request stanza.
+ """
+ iq.reply()
+ iq['entity_time']['time'] = self.local_time(iq['to'])
+ iq.send()
+
+ def get_entity_time(self, to, ifrom=None, **iqargs):
+ """
+ Request the time from another entity.
+
+ Arguments:
+ to -- JID of the entity to query.
+ 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.
+ callback -- Optional callback to execute when a reply is
+ received instead of blocking and waiting for
+ the reply.
+ """
+ iq = self.xmpp.Iq()
+ iq['type'] = 'get'
+ iq['to'] = 'to'
+ if ifrom:
+ iq['from'] = 'ifrom'
+ iq.enable('entity_time')
+ return iq.send(**iqargs)
diff --git a/sleekxmpp/plugins/xep_0203/__init__.py b/sleekxmpp/plugins/xep_0203/__init__.py new file mode 100644 index 00000000..445ccf37 --- /dev/null +++ b/sleekxmpp/plugins/xep_0203/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0203 import stanza +from sleekxmpp.plugins.xep_0203.stanza import Delay +from sleekxmpp.plugins.xep_0203.delay import xep_0203 + diff --git a/sleekxmpp/plugins/xep_0203/delay.py b/sleekxmpp/plugins/xep_0203/delay.py new file mode 100644 index 00000000..8ff14d18 --- /dev/null +++ b/sleekxmpp/plugins/xep_0203/delay.py @@ -0,0 +1,36 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 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.base import base_plugin +from sleekxmpp.plugins.xep_0203 import stanza + + +class xep_0203(base_plugin): + + """ + XEP-0203: Delayed Delivery + + XMPP stanzas are sometimes withheld for delivery due to the recipient + being offline, or are resent in order to establish recent history as + is the case with MUCS. In any case, it is important to know when the + stanza was originally sent, not just when it was last received. + + Also see <http://www.xmpp.org/extensions/xep-0203.html>. + """ + + def plugin_init(self): + """Start the XEP-0203 plugin.""" + self.xep = '0203' + self.description = 'Delayed Delivery' + self.stanza = stanza + + register_stanza_plugin(Message, stanza.Delay) + register_stanza_plugin(Presence, stanza.Delay) diff --git a/sleekxmpp/plugins/xep_0203/stanza.py b/sleekxmpp/plugins/xep_0203/stanza.py new file mode 100644 index 00000000..baae4cd3 --- /dev/null +++ b/sleekxmpp/plugins/xep_0203/stanza.py @@ -0,0 +1,41 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 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.xmlstream import ElementBase +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_stamp(self): + timestamp = self._get_attr('stamp') + return xep_0082.parse(timestamp) + + def set_stamp(self, value): + if isinstance(value, dt.datetime): + value = xep_0082.format_datetime(value) + self._set_attr('stamp', value) + + 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_0224/__init__.py b/sleekxmpp/plugins/xep_0224/__init__.py new file mode 100644 index 00000000..62f5bf82 --- /dev/null +++ b/sleekxmpp/plugins/xep_0224/__init__.py @@ -0,0 +1,11 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0224 import stanza +from sleekxmpp.plugins.xep_0224.stanza import Attention +from sleekxmpp.plugins.xep_0224.attention import xep_0224 diff --git a/sleekxmpp/plugins/xep_0224/attention.py b/sleekxmpp/plugins/xep_0224/attention.py new file mode 100644 index 00000000..41d7a0f1 --- /dev/null +++ b/sleekxmpp/plugins/xep_0224/attention.py @@ -0,0 +1,72 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +from sleekxmpp.stanza import Message +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_0224 import stanza + + +log = logging.getLogger(__name__) + + +class xep_0224(base_plugin): + + """ + XEP-0224: Attention + """ + + def plugin_init(self): + """Start the XEP-0224 plugin.""" + self.xep = '0224' + self.description = 'Attention' + self.stanza = stanza + + register_stanza_plugin(Message, stanza.Attention) + + self.xmpp.register_handler( + Callback('Attention', + StanzaPath('message/attention'), + self._handle_attention)) + + def post_init(self): + """Handle cross-plugin dependencies.""" + base_plugin.post_init(self) + self.xmpp['xep_0030'].add_feature(stanza.Attention.namespace) + + def request_attention(self, to, mfrom=None, mbody=''): + """ + Send an attention message with an optional body. + + Arguments: + to -- The attention request recipient's JID. + mfrom -- Optionally specify the sender of the attention request. + mbody -- An optional message body to include in the request. + """ + m = self.xmpp.Message() + m['to'] = to + m['type'] = 'headline' + m['attention'] = True + if mfrom: + m['from'] = mfrom + m['body'] = mbody + m.send() + + def _handle_attention(self, msg): + """ + Raise an event after receiving a message with an attention request. + + Arguments: + msg -- A message stanza with an attention element. + """ + log.debug("Received attention request from: %s" % msg['from']) + self.xmpp.event('attention', msg) diff --git a/sleekxmpp/plugins/xep_0224/stanza.py b/sleekxmpp/plugins/xep_0224/stanza.py new file mode 100644 index 00000000..f15172d9 --- /dev/null +++ b/sleekxmpp/plugins/xep_0224/stanza.py @@ -0,0 +1,40 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2011 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class Attention(ElementBase): + + """ + """ + + name = 'attention' + namespace = 'urn:xmpp:attention:0' + plugin_attrib = 'attention' + interfaces = set(('attention',)) + is_extension = True + + def setup(self, xml): + return True + + def set_attention(self, value): + if value: + xml = ET.Element(self.tag_name()) + self.parent().xml.append(xml) + else: + self.del_attention() + + def get_attention(self): + xml = self.parent().xml.find(self.tag_name()) + return xml is not None + + def del_attention(self): + xml = self.parent().xml.find(self.tag_name()) + if xml is not None: + self.parent().xml.remove(xml) |