From f4451fe6b72f7cfb9680ead7a608d5ca1bc7e753 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Thu, 9 Dec 2010 18:57:27 -0500 Subject: First pass at a new XEP-0030 plugin. Now with dynamic node handling goodness. Some things are not quite working yet, in particular: set_items set_info set_identities set_features And still need more unit tests to round things out. --- sleekxmpp/plugins/xep_0030.py | 356 -------------------------- sleekxmpp/plugins/xep_0030/__init__.py | 12 + sleekxmpp/plugins/xep_0030/disco.py | 314 +++++++++++++++++++++++ sleekxmpp/plugins/xep_0030/stanza/__init__.py | 10 + sleekxmpp/plugins/xep_0030/stanza/disco.py | 0 sleekxmpp/plugins/xep_0030/stanza/info.py | 262 +++++++++++++++++++ sleekxmpp/plugins/xep_0030/stanza/items.py | 138 ++++++++++ sleekxmpp/plugins/xep_0030/static.py | 127 +++++++++ 8 files changed, 863 insertions(+), 356 deletions(-) delete mode 100644 sleekxmpp/plugins/xep_0030.py create mode 100644 sleekxmpp/plugins/xep_0030/__init__.py create mode 100644 sleekxmpp/plugins/xep_0030/disco.py create mode 100644 sleekxmpp/plugins/xep_0030/stanza/__init__.py create mode 100644 sleekxmpp/plugins/xep_0030/stanza/disco.py create mode 100644 sleekxmpp/plugins/xep_0030/stanza/info.py create mode 100644 sleekxmpp/plugins/xep_0030/stanza/items.py create mode 100644 sleekxmpp/plugins/xep_0030/static.py (limited to 'sleekxmpp/plugins') diff --git a/sleekxmpp/plugins/xep_0030.py b/sleekxmpp/plugins/xep_0030.py deleted file mode 100644 index 3253bb68..00000000 --- a/sleekxmpp/plugins/xep_0030.py +++ /dev/null @@ -1,356 +0,0 @@ -""" - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout - This file is part of SleekXMPP. - - See the file LICENSE for copying permission. -""" - -import logging -from . import base -from .. xmlstream.handler.callback import Callback -from .. xmlstream.matcher.xpath import MatchXPath -from .. xmlstream.stanzabase import registerStanzaPlugin, ElementBase, ET, JID -from .. stanza.iq import Iq - - -log = logging.getLogger(__name__) - - -class DiscoInfo(ElementBase): - namespace = 'http://jabber.org/protocol/disco#info' - name = 'query' - plugin_attrib = 'disco_info' - interfaces = set(('node', 'features', 'identities')) - - def getFeatures(self): - features = [] - featuresXML = self.xml.findall('{%s}feature' % self.namespace) - for feature in featuresXML: - features.append(feature.attrib['var']) - return features - - def setFeatures(self, features): - self.delFeatures() - for name in features: - self.addFeature(name) - - def delFeatures(self): - featuresXML = self.xml.findall('{%s}feature' % self.namespace) - for feature in featuresXML: - self.xml.remove(feature) - - def addFeature(self, feature): - featureXML = ET.Element('{%s}feature' % self.namespace, - {'var': feature}) - self.xml.append(featureXML) - - def delFeature(self, feature): - featuresXML = self.xml.findall('{%s}feature' % self.namespace) - for featureXML in featuresXML: - if featureXML.attrib['var'] == feature: - self.xml.remove(featureXML) - - def getIdentities(self): - ids = [] - idsXML = self.xml.findall('{%s}identity' % self.namespace) - for idXML in idsXML: - idData = (idXML.attrib['category'], - idXML.attrib['type'], - idXML.attrib.get('name', '')) - ids.append(idData) - return ids - - def setIdentities(self, ids): - self.delIdentities() - for idData in ids: - self.addIdentity(*idData) - - def delIdentities(self): - idsXML = self.xml.findall('{%s}identity' % self.namespace) - for idXML in idsXML: - self.xml.remove(idXML) - - def addIdentity(self, category, itype, name=''): - idXML = ET.Element('{%s}identity' % self.namespace) - idXML.attrib['category'] = category - idXML.attrib['type'] = itype - if name: - idXML.attrib['name'] = name - self.xml.append(idXML) - - def delIdentity(self, category, id_type, name=''): - idsXML = self.xml.findall('{%s}identity' % self.namespace) - for idXML in idsXML: - idData = (idXML.attrib['category'], - idXML.attrib['type']) - delId = (category, id_type) - if idData == delId: - self.xml.remove(idXML) - - -class DiscoItems(ElementBase): - namespace = 'http://jabber.org/protocol/disco#items' - name = 'query' - plugin_attrib = 'disco_items' - interfaces = set(('node', 'items')) - - def getItems(self): - items = [] - itemsXML = self.xml.findall('{%s}item' % self.namespace) - for item in itemsXML: - itemData = (item.attrib['jid'], - item.attrib.get('node'), - item.attrib.get('name')) - items.append(itemData) - return items - - def setItems(self, items): - self.delItems() - for item in items: - self.addItem(*item) - - def delItems(self): - itemsXML = self.xml.findall('{%s}item' % self.namespace) - for item in itemsXML: - self.xml.remove(item) - - def addItem(self, jid, node='', name=''): - itemXML = ET.Element('{%s}item' % self.namespace, {'jid': jid}) - if name: - itemXML.attrib['name'] = name - if node: - itemXML.attrib['node'] = node - self.xml.append(itemXML) - - def delItem(self, jid, node=''): - itemsXML = self.xml.findall('{%s}item' % self.namespace) - for itemXML in itemsXML: - itemData = (itemXML.attrib['jid'], - itemXML.attrib.get('node', '')) - itemDel = (jid, node) - if itemData == itemDel: - self.xml.remove(itemXML) - - -class DiscoNode(object): - """ - Collection object for grouping info and item information - into nodes. - """ - def __init__(self, name): - self.name = name - self.info = DiscoInfo() - self.items = DiscoItems() - - self.info['node'] = name - self.items['node'] = name - - # This is a bit like poor man's inheritance, but - # to simplify adding information to the node we - # map node functions to either the info or items - # stanza objects. - # - # We don't want to make DiscoNode inherit from - # DiscoInfo and DiscoItems because DiscoNode is - # not an actual stanza, and doing so would create - # confusion and potential bugs. - - self._map(self.items, 'items', ['get', 'set', 'del']) - self._map(self.items, 'item', ['add', 'del']) - self._map(self.info, 'identities', ['get', 'set', 'del']) - self._map(self.info, 'identity', ['add', 'del']) - self._map(self.info, 'features', ['get', 'set', 'del']) - self._map(self.info, 'feature', ['add', 'del']) - - def isEmpty(self): - """ - Test if the node contains any information. Useful for - determining if a node can be deleted. - """ - ids = self.getIdentities() - features = self.getFeatures() - items = self.getItems() - - if not ids and not features and not items: - return True - return False - - def _map(self, obj, interface, access): - """ - Map functions of the form obj.accessInterface - to self.accessInterface for each given access type. - """ - interface = interface.title() - for access_type in access: - method = access_type + interface - if hasattr(obj, method): - setattr(self, method, getattr(obj, method)) - - -class xep_0030(base.base_plugin): - """ - XEP-0030 Service Discovery - """ - - def plugin_init(self): - self.xep = '0030' - self.description = 'Service Discovery' - - self.xmpp.registerHandler( - Callback('Disco Items', - MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns, - DiscoItems.namespace)), - self.handle_item_query)) - - self.xmpp.registerHandler( - Callback('Disco Info', - MatchXPath('{%s}iq/{%s}query' % (self.xmpp.default_ns, - DiscoInfo.namespace)), - self.handle_info_query)) - - registerStanzaPlugin(Iq, DiscoInfo) - registerStanzaPlugin(Iq, DiscoItems) - - self.xmpp.add_event_handler('disco_items_request', self.handle_disco_items) - self.xmpp.add_event_handler('disco_info_request', self.handle_disco_info) - - self.nodes = {} - - self.add_node('') - self.add_feature('http://jabber.org/protocol/disco#info', node='') - - def add_node(self, node): - if node not in self.nodes: - self.nodes[node] = DiscoNode(node) - - def del_node(self, node): - if node in self.nodes: - del self.nodes[node] - - def rename_node(self, node, new_name): - if new_name not in self.nodes and node in self.nodes: - self.nodes[new_name] = self.nodes[node] - self.nodes[new_name].name = new_name - self.nodes[new_name].info['node'] = new_name - self.nodes[new_name].items['node'] = new_name - self.del_node(node) - - def handle_item_query(self, iq): - if iq['type'] == 'get': - log.debug("Items requested by %s" % iq['from']) - self.xmpp.event('disco_items_request', iq) - elif iq['type'] == 'result': - log.debug("Items result from %s" % iq['from']) - self.xmpp.event('disco_items', iq) - - def handle_info_query(self, iq): - if iq['type'] == 'get': - log.debug("Info requested by %s" % iq['from']) - self.xmpp.event('disco_info_request', iq) - elif iq['type'] == 'result': - log.debug("Info result from %s" % iq['from']) - self.xmpp.event('disco_info', iq) - - def handle_disco_info(self, iq, forwarded=False): - """ - A default handler for disco#info requests. If another - handler is registered, this one will defer and not run. - """ - if not forwarded and \ - self.xmpp.event_handled('disco_info_request') > 1: - return - - node_name = iq['disco_info']['node'] - log.debug("Using default handler for disco#info on node '%s'." % node_name) - - if node_name in self.nodes: - node = self.nodes[node_name] - iq.reply() - iq['disco_info']['node'] = node_name - - identities = node.info['identities'] - if identities: - iq['disco_info']['identities'] = identities - else: - if self.xmpp.is_component: - iq['disco_info'].addIdentity( - category='component', - itype='generic') - else: - iq['disco_info'].addIdentity( - category='client', - itype='bot') - log.info("No identity found for node '%'," + \ - "using default, generic identity") - - iq['disco_info']['features'] = node.info['features'] - iq.send() - else: - log.debug("Node %s requested, but does not exist." % node_name) - iq.reply().error().setPayload(iq['disco_info'].xml) - iq['error']['code'] = '404' - iq['error']['type'] = 'cancel' - iq['error']['condition'] = 'item-not-found' - iq.send() - - def handle_disco_items(self, iq, forwarded=False): - """ - A default handler for disco#items requests. If another - handler is registered, this one will defer and not run. - - If this handler is called by your own custom handler with - forwarded set to True, then it will run as normal. - """ - if not forwarded and \ - self.xmpp.event_handled('disco_items_request') > 1: - return - - node_name = iq['disco_items']['node'] - log.debug("Using default handler for disco#items on node: '%s'." % node_name) - - if node_name in self.nodes: - node = self.nodes[node_name] - iq.reply().setPayload(node.items.xml).send() - else: - log.debug("Node %s requested, but does not exist." % node_name) - iq.reply().error().setPayload(iq['disco_items'].xml) - iq['error']['code'] = '404' - iq['error']['type'] = 'cancel' - iq['error']['condition'] = 'item-not-found' - iq.send() - - # Older interface methods for backwards compatibility - - def getInfo(self, jid, node='', dfrom=None): - iq = self.xmpp.Iq() - iq['type'] = 'get' - iq['to'] = jid - iq['from'] = dfrom - iq['disco_info']['node'] = node - return iq.send() - - def getItems(self, jid, node='', dfrom=None): - iq = self.xmpp.Iq() - iq['type'] = 'get' - iq['to'] = jid - iq['from'] = dfrom - iq['disco_items']['node'] = node - return iq.send() - - def add_feature(self, feature, node=''): - self.add_node(node) - self.nodes[node].addFeature(feature) - - def add_identity(self, category='', itype='', name='', node=''): - self.add_node(node) - self.nodes[node].addIdentity(category=category, - itype=itype, - name=name) - - def add_item(self, jid=None, name='', node='', subnode=''): - self.add_node(node) - self.add_node(subnode) - if jid is None: - jid = self.xmpp.fulljid - self.nodes[node].addItem(jid=jid, name=name, node=subnode) diff --git a/sleekxmpp/plugins/xep_0030/__init__.py b/sleekxmpp/plugins/xep_0030/__init__.py new file mode 100644 index 00000000..2e183852 --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/__init__.py @@ -0,0 +1,12 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0030 import stanza +from sleekxmpp.plugins.xep_0030.stanza import DiscoInfo, DiscoItems +from sleekxmpp.plugins.xep_0030.static import StaticDisco +from sleekxmpp.plugins.xep_0030.disco import xep_0030 diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py new file mode 100644 index 00000000..c323ba7c --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -0,0 +1,314 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +import sleekxmpp +from sleekxmpp import Iq +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems, StaticDisco + + +log = logging.getLogger(__name__) + + +class xep_0030(base_plugin): + + """ + XEP-0030: Service Discovery + + Stream Handlers: + Disco Info -- + Disco Items -- + + Events: + disco_info -- + disco_items -- + disco_info_query -- + disco_items_query -- + + Methods: + set_node_handler -- + del_node_handler -- + add_identity -- + del_identity -- + add_feature -- + del_feature -- + add_item -- + del_item -- + get_info -- + get_items -- + """ + + def plugin_init(self): + self.xep = '0030' + self.description = 'Service Discovery' + self.stanza = sleekxmpp.plugins.xep_0030.stanza + + self.xmpp.register_handler( + Callback('Disco Info', + StanzaPath('iq/disco_info'), + self._handle_disco_info)) + + self.xmpp.register_handler( + Callback('Disco Items', + StanzaPath('iq/disco_items'), + self._handle_disco_items)) + + register_stanza_plugin(Iq, DiscoInfo) + register_stanza_plugin(Iq, DiscoItems) + + self.static = StaticDisco(self.xmpp) + + self._disco_ops = ['get_info', 'set_identities', 'set_features', + 'del_info', 'get_items', 'set_items', 'del_items', + 'add_identity', 'del_identity', 'add_feature', + 'del_feature', 'add_item', 'del_item'] + self.handlers = {} + for op in self._disco_ops: + self.handlers[op] = {'global': getattr(self.static, op), + 'jid': {}, + 'node': {}} + + + def set_node_handler(self, htype, jid=None, node=None, handler=None): + """ + Arguments: + htype + jid + node + handler + """ + if htype not in self._disco_ops: + return + if jid is None and node is None: + self.handlers[htype]['global'] = handler + elif node is None: + self.handlers[htype]['jid'][jid] = handler + elif jid is None: + jid = self.xmpp.boundjid.full + self.handlers[htype]['node'][(jid, node)] = handler + else: + self.handlers[htype]['node'][(jid, node)] = handler + + def del_node_handler(self, htype, jid, node): + """ + Arguments: + htype + jid + node + """ + self.set_node_handler(htype, jid, node, None) + + def make_static(self, jid=None, node=None, handlers=None): + """ + Change all of a node's handlers to the default static + handlers. Useful for manually overriding the contents + of a node that would otherwise be handled by a JID level + or global level dynamic handler. + + Arguments: + jid -- The JID owning the node to modify. + node -- The node to change to using static handlers. + handlers -- Optional list of handlers to change to the + static version. If provided, only these + handlers will be changed. Otherwise, all + handlers will use the static version. + """ + if handlers is None: + handlers = self._disco_ops + for op in handlers: + self.del_node_handler(op, jid, node) + self.set_node_handler(op, jid, node, getattr(self.static, op)) + + def get_info(self, jid=None, node=None, local=False, **kwargs): + """ + Arguments: + jid -- + node -- + local -- + dfrom -- + block -- + timeout -- + callback -- + """ + if local or jid is None: + log.debug("Looking up local disco#info data " + \ + "for %s, node %s." % (jid, node)) + info = self._run_node_handler('get_info', jid, node, kwargs) + return self._fix_default_info(info) + + iq = self.xmpp.Iq() + iq['from'] = kwargs.get('dfrom', '') + iq['to'] = jid + iq['type'] = 'get' + iq['disco_info']['node'] = node if node else '' + return iq.send(timeout=kwargs.get('timeout', None), + block=kwargs.get('block', None), + callback=kwargs.get('callback', None)) + + def get_items(self, jid=None, node=None, local=False, **kwargs): + """ + Arguments: + jid -- + node -- + local -- + dfrom -- + block -- + timeout -- + callback -- + """ + if local or jid is None: + return self._run_node_handler('get_items', jid, node, kwargs) + + iq = self.xmpp.Iq() + iq['from'] = kwargs.get('dfrom', '') + iq['to'] = jid + iq['type'] = 'get' + iq['disco_items']['node'] = node if node else '' + return iq.send(timeout=kwargs.get('timeout', None), + block=kwargs.get('block', None), + callback=kwargs.get('callback', None)) + + def set_info(self, jid=None, node=None, **kwargs): + self._run_node_handler('set_info', jid, node, kwargs) + + def del_info(self, jid=None, node=None, **kwargs): + self._run_node_handler('del_info', jid, node, kwargs) + + def set_items(self, jid=None, node=None, **kwargs): + self._run_node_handler('set_items', jid, node, kwargs) + + def del_items(self, jid=None, node=None, **kwargs): + self._run_node_handler('del_items', jid, node, kwargs) + + def add_identity(self, jid=None, node=None, **kwargs): + self._run_node_handler('add_identity', jid, node, kwargs) + + def add_feature(self, jid=None, node=None, **kwargs): + self._run_node_handler('add_feature', jid, node, kwargs) + + def del_identity(self, jid=None, node=None, **kwargs): + self._run_node_handler('del_identity', jid, node, kwargs) + + def del_feature(self, jid=None, node=None, **kwargs): + self._run_node_handler('del_feature', jid, node, kwargs) + + def add_item(self, jid=None, node=None, **kwargs): + self._run_node_handler('add_item', jid, node, kwargs) + + def del_item(self, jid=None, node=None, **kwargs): + self._run_node_handler('del_item', jid, node, kwargs) + + def _run_node_handler(self, htype, jid, node, data=None): + """ + Execute the most specific node handler for the given + JID/node combination. + + Arguments: + htype -- The handler type to execute. + jid -- The JID requested. + node -- The node requested. + dat -- Optional, custom data to pass to the handler. + """ + if jid is None: + jid = self.xmpp.boundjid.full + if node is None: + node = '' + + if self.handlers[htype]['node'].get((jid, node), False): + return self.handlers[htype]['node'][(jid, node)](jid, node, data) + elif self.handlers[htype]['jid'].get(jid, False): + return self.handlers[htype]['jid'][jid](jid, node, data) + elif self.handlers[htype]['global']: + return self.handlers[htype]['global'](jid, node, data) + else: + return None + + def _handle_disco_info(self, iq): + """ + Process an incoming disco#info stanza. If it is a get + request, find and return the appropriate identities + and features. If it is an info result, fire the + disco_info event. + + Arguments: + iq -- The incoming disco#items stanza. + """ + if iq['type'] == 'get': + log.debug("Received disco info query from " + \ + "<%s> to <%s>." % (iq['from'], iq['to'])) + info = self._run_node_handler('get_info', + iq['to'].full, + iq['disco_info']['node'], + iq) + iq.reply() + if info: + info = self._fix_default_info(info) + iq.set_payload(info.xml) + iq.send() + elif iq['type'] == 'result': + log.debug("Received disco info result from" + \ + "%s to %s." % (iq['from'], iq['to'])) + self.xmpp.event('disco_info', iq) + + def _handle_disco_items(self, iq): + """ + Process an incoming disco#items stanza. If it is a get + request, find and return the appropriate items. If it + is an items result, fire the disco_items event. + + Arguments: + iq -- The incoming disco#items stanza. + """ + if iq['type'] == 'get': + log.debug("Received disco items query from " + \ + "<%s> to <%s>." % (iq['from'], iq['to'])) + items = self._run_node_handler('get_items', + iq['to'].full, + iq['disco_items']['node']) + iq.reply() + if items: + iq.set_payload(items.xml) + iq.send() + elif iq['type'] == 'result': + log.debug("Received disco items result from" + \ + "%s to %s." % (iq['from'], iq['to'])) + self.xmpp.event('disco_items', iq) + + def _fix_default_info(self, info): + """ + Disco#info results for a JID are required to include at least + one identity and feature. As a default, if no other identity is + provided, SleekXMPP will use either the generic component or the + bot client identity. A the standard disco#info feature will also be + added if no features are provided. + + Arguments: + info -- The disco#info quest (not the full Iq stanza) to modify. + """ + if not info['node']: + if not info['identities']: + if self.xmpp.is_component: + log.debug("No identity found for this entity." + \ + "Using default component identity.") + info.add_identity('component', 'generic') + else: + log.debug("No identity found for this entity." + \ + "Using default client identity.") + info.add_identity('client', 'bot') + if not info['features']: + log.debug("No features found for this entity." + \ + "Using default disco#info feature.") + info.add_feature(info.namespace) + return info + diff --git a/sleekxmpp/plugins/xep_0030/stanza/__init__.py b/sleekxmpp/plugins/xep_0030/stanza/__init__.py new file mode 100644 index 00000000..0d97cf3d --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/stanza/__init__.py @@ -0,0 +1,10 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.plugins.xep_0030.stanza.info import DiscoInfo +from sleekxmpp.plugins.xep_0030.stanza.items import DiscoItems diff --git a/sleekxmpp/plugins/xep_0030/stanza/disco.py b/sleekxmpp/plugins/xep_0030/stanza/disco.py new file mode 100644 index 00000000..e69de29b diff --git a/sleekxmpp/plugins/xep_0030/stanza/info.py b/sleekxmpp/plugins/xep_0030/stanza/info.py new file mode 100644 index 00000000..6764acbb --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/stanza/info.py @@ -0,0 +1,262 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class DiscoInfo(ElementBase): + + """ + XMPP allows for users and agents to find the identities and features + supported by other entities in the XMPP network through service discovery, + or "disco". In particular, the "disco#info" query type for stanzas is + used to request the list of identities and features offered by a JID. + + An identity is a combination of a category and type, such as the 'client' + category with a type of 'pc' to indicate the agent is a human operated + client with a GUI, or a category of 'gateway' with a type of 'aim' to + identify the agent as a gateway for the legacy AIM protocol. See + for a full list of + accepted category and type combinations. + + Features are simply a set of the namespaces that identify the supported + features. For example, a client that supports service discovery will + include the feature 'http://jabber.org/protocol/disco#info'. + + Since clients and components may operate in several roles at once, identity + and feature information may be grouped into "nodes". If one were to write + all of the identities and features used by a client, then node names would + be like section headings. + + Example disco#info stanzas: + + + + + + + + + + + + + + Stanza Interface: + node -- The name of the node to either + query or return info from. + identities -- A set of 4-tuples, where each tuple contains + the category, type, xml:lang, and name + of an identity. + features -- A set of namespaces for features. + + Methods: + add_identity -- Add a new, single identity. + del_identity -- Remove a single identity. + get_identities -- Return all identities in tuple form. + set_identities -- Use multiple identities, each given in tuple form. + del_identities -- Remove all identities. + add_feature -- Add a single feature. + del_feature -- Remove a single feature. + get_features -- Return a list of all features. + set_features -- Use a given list of features. + del_features -- Remove all features. + """ + + name = 'query' + namespace = 'http://jabber.org/protocol/disco#info' + plugin_attrib = 'disco_info' + interfaces = set(('node', 'features', 'identities')) + lang_interfaces = set(('identities',)) + + # Cache identities and features + _identities = set() + _features = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches identity and feature information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + + self._identities = set([id[0:3] for id in self['identities']]) + self._features = self['features'] + + def add_identity(self, category, itype, name=None, lang=None): + """ + Add a new identity element. Each identity must be unique + in terms of all four identity components. + + Multiple, identical category/type pairs are allowed only + if the xml:lang values are different. Likewise, multiple + category/type/xml:lang pairs are allowed so long as the names + are different. In any case, a category and type are required. + + Arguments: + category -- The general category to which the agent belongs. + itype -- A more specific designation with the category. + name -- Optional human readable name for this identity. + lang -- Optional standard xml:lang value. + """ + identity = (category, itype, lang) + if identity not in self._identities: + self._identities.add(identity) + id_xml = ET.Element('{%s}identity' % self.namespace) + id_xml.attrib['category'] = category + id_xml.attrib['type'] = itype + if lang: + id_xml.attrib['{%s}lang' % self.xml_ns] = lang + if name: + id_xml.attrib['name'] = name + self.xml.append(id_xml) + return True + return False + + def del_identity(self, category, itype, name=None, lang=None): + """ + Remove a given identity. + + Arguments: + category -- The general category to which the agent belonged. + itype -- A more specific designation with the category. + name -- Optional human readable name for this identity. + lang -- Optional, standard xml:lang value. + """ + identity = (category, itype, lang) + if identity in self._identities: + self._identities.remove(identity) + for id_xml in self.findall('{%s}identity' % self.namespace): + id = (id_xml.attrib['category'], + id_xml.attrib['type'], + id_xml.attrib.get('{%s}lang' % self.xml_ns, None)) + if id == identity: + self.xml.remove(id_xml) + return True + return False + + def get_identities(self, lang=None): + """ + Return a set of all identities in tuple form as so: + (category, type, lang, name) + + If a language was specified, only return identities using + that language. + + Arguments: + lang -- Optional, standard xml:lang value. + """ + identities = set() + for id_xml in self.findall('{%s}identity' % self.namespace): + xml_lang = id_xml.attrib.get('{%s}lang' % self.xml_ns, None) + if lang is None or xml_lang == lang: + identities.add(( + id_xml.attrib['category'], + id_xml.attrib['type'], + id_xml.attrib.get('{%s}lang' % self.xml_ns, None), + id_xml.attrib.get('name', None))) + return identities + + def set_identities(self, identities, lang=None): + """ + Add or replace all identities. The identities must be a in set + where each identity is a tuple of the form: + (category, type, lang, name) + + If a language is specifified, any identities using that language + will be removed to be replaced with the given identities. + + NOTE: An identity's language will not be changed regardless of + the value of lang. + + Arguments: + identities -- A set of identities in tuple form. + lang -- Optional, standard xml:lang value. + """ + self.del_identities(lang) + for identity in identities: + category, itype, lang, name = identity + self.add_identity(category, itype, name, lang) + + def del_identities(self, lang=None): + """ + Remove all identities. If a language was specified, only + remove identities using that language. + + Arguments: + lang -- Optional, standard xml:lang value. + """ + for id_xml in self.findall('{%s}identity' % self.namespace): + if lang is None: + self.xml.remove(id_xml) + elif id_xml.attrib.get('{%s}lang' % self.xml_ns, None) == lang: + self._identities.remove(( + id_xml.attrib['category'], + id_xml.attrib['type'], + id_xml.attrib.get('{%s}lang' % self.xml_ns, None))) + self.xml.remove(id_xml) + + def add_feature(self, feature): + """ + Add a single, new feature. + + Arguments: + feature -- The namespace of the supported feature. + """ + if feature not in self._features: + self._features.add(feature) + feature_xml = ET.Element('{%s}feature' % self.namespace) + feature_xml.attrib['var'] = feature + self.xml.append(feature_xml) + return True + return False + + def del_feature(self, feature): + """ + Remove a single feature. + + Arguments: + feature -- The namespace of the removed feature. + """ + if feature in self._features: + self._features.remove(feature) + for feature_xml in self.findall('{%s}feature' % self.namespace): + if feature_xml.attrib['var'] == feature: + self.xml.remove(feature_xml) + return True + return False + + def get_features(self): + """Return the set of all supported features.""" + features = set() + for feature_xml in self.findall('{%s}feature' % self.namespace): + features.add(feature_xml.attrib['var']) + return features + + def set_features(self, features): + """ + Add or replace the set of supported features. + + Arguments: + features -- The new set of supported features. + """ + self.del_features() + for feature in features: + self.add_feature(feature) + + def del_features(self): + """Remove all features.""" + self._features = set() + for feature_xml in self.findall('{%s}feature' % self.namespace): + self.xml.remove(feature_xml) diff --git a/sleekxmpp/plugins/xep_0030/stanza/items.py b/sleekxmpp/plugins/xep_0030/stanza/items.py new file mode 100644 index 00000000..319e666f --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/stanza/items.py @@ -0,0 +1,138 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +from sleekxmpp.xmlstream import ElementBase, ET + + +class DiscoItems(ElementBase): + + """ + + + Example disco#items stanzas: + + + + + + + + + + + + Stanza Interface: + node -- The name of the node to either + query or return info from. + items -- A list of 3-tuples, where each tuple contains + the JID, node, and name of an item. + + Methods: + add_item -- Add a single new item. + del_item -- Remove a single item. + get_items -- Return all items. + set_items -- Set or replace all items. + del_items -- Remove all items. + """ + + name = 'query' + namespace = 'http://jabber.org/protocol/disco#items' + plugin_attrib = 'disco_items' + interfaces = set(('node', 'items')) + + # Cache items + _items = set() + + def setup(self, xml=None): + """ + Populate the stanza object using an optional XML object. + + Overrides ElementBase.setup + + Caches item information. + + Arguments: + xml -- Use an existing XML object for the stanza's values. + """ + ElementBase.setup(self, xml) + self._items = set([item[0:2] for item in self['items']]) + + def add_item(self, jid, node=None, name=None): + """ + Add a new item element. Each item is required to have a + JID, but may also specify a node value to reference + non-addressable entitities. + + Arguments: + jid -- The JID for the item. + node -- Optional additional information to reference + non-addressable items. + name -- Optional human readable name for the item. + """ + if (jid, node) not in self._items: + self._items.add((jid, node)) + item_xml = ET.Element('{%s}item' % self.namespace) + item_xml.attrib['jid'] = jid + if name: + item_xml.attrib['name'] = name + if node: + item_xml.attrib['node'] = node + self.xml.append(item_xml) + return True + return False + + def del_item(self, jid, node=None): + """ + Remove a single item. + + Arguments: + jid -- JID of the item to remove. + node -- Optional extra identifying information. + """ + if (jid, node) in self._items: + for item_xml in self.findall('{%s}item' % self.namespace): + item = (item_xml.attrib['jid'], + item_xml.attrib.get('node', None)) + if item == (jid, node): + self.xml.remove(item_xml) + return True + return False + + def get_items(self): + """Return all items.""" + items = set() + for item_xml in self.findall('{%s}item' % self.namespace): + item = (item_xml.attrib['jid'], + item_xml.attrib.get('node'), + item_xml.attrib.get('name')) + items.add(item) + return items + + def set_items(self, items): + """ + Set or replace all items. The given items must be in a + list or set where each item is a tuple of the form: + (jid, node, name) + + Arguments: + items -- A series of items in tuple format. + """ + self.del_items() + for item in items: + jid, node, name = item + self.add_item(jid, node, name) + + def del_items(self): + """Remove all items.""" + self._items = set() + for item_xml in self.findall('{%s}item' % self.namespace): + self.xml.remove(item_xml) diff --git a/sleekxmpp/plugins/xep_0030/static.py b/sleekxmpp/plugins/xep_0030/static.py new file mode 100644 index 00000000..f3693228 --- /dev/null +++ b/sleekxmpp/plugins/xep_0030/static.py @@ -0,0 +1,127 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file LICENSE for copying permission. +""" + +import logging + +import sleekxmpp +from sleekxmpp import Iq +from sleekxmpp.exceptions import XMPPError +from sleekxmpp.plugins.base import base_plugin +from sleekxmpp.xmlstream.handler import Callback +from sleekxmpp.xmlstream.matcher import StanzaPath +from sleekxmpp.xmlstream import register_stanza_plugin, ElementBase, ET, JID +from sleekxmpp.plugins.xep_0030 import DiscoInfo, DiscoItems + + +log = logging.getLogger(__name__) + + +class StaticDisco(object): + + """ + While components will likely require fully dynamic handling + of service discovery information, most clients and simple bots + only need to manage a few disco nodes that will remain mostly + static. + + StaticDisco provides a set of node handlers that will store + static sets of disco info and items in memory. + """ + + def __init__(self, xmpp): + """ + Arguments: + xmpp -- The main SleekXMPP object. + """ + self.nodes = {} + self.xmpp = xmpp + + def add_node(self, jid=None, node=None): + if jid is None: + jid = self.xmpp.boundjid.full + if node is None: + node = '' + if (jid, node) not in self.nodes: + self.nodes[(jid, node)] = {'info': DiscoInfo(), + 'items': DiscoItems()} + self.nodes[(jid, node)]['info']['node'] = node + self.nodes[(jid, node)]['items']['node'] = node + + def get_info(self, jid, node, data=None): + if (jid, node) not in self.nodes: + if not node: + return DiscoInfo() + else: + raise XMPPError(condition='item-not-found') + else: + return self.nodes[(jid, node)]['info'] + + def del_info(self, jid, node, data=None): + if (jid, node) in self.nodes: + self.nodes[(jid, node)]['info'] = DiscoInfo() + + def get_items(self, jid, node, data=None): + if (jid, node) not in self.nodes: + if not node: + return DiscoInfo() + else: + raise XMPPError(condition='item-not-found') + else: + return self.nodes[(jid, node)]['items'] + + def set_items(self, jid, node, data=None): + pass + + def del_items(self, jid, node, data=None): + if (jid, node) in self.nodes: + self.nodes[(jid, node)]['items'] = DiscoItems() + + def add_identity(self, jid, node, data={}): + self.add_node(jid, node) + self.nodes[(jid, node)]['info'].add_identity( + data.get('category', ''), + data.get('itype', ''), + data.get('name', None), + data.get('lang', None)) + + def set_identities(self, jid, node, data=None): + pass + + def del_identity(self, jid, node, data=None): + if (jid, node) not in self.nodes: + return + self.nodes[(jid, node)]['info'].del_identity( + data.get('category', ''), + data.get('itype', ''), + data.get('name', None), + data.get('lang', None)) + + + def add_feature(self, jid, node, data=None): + self.add_node(jid, node) + self.nodes[(jid, node)]['info'].add_feature(data.get('feature', '')) + + def set_features(self, jid, node, data=None): + pass + + def del_feature(self, jid, node, data=None): + if (jid, node) not in self.nodes: + return + self.nodes[(jid, node)]['info'].del_feature(data.get('feature', '')) + + def add_item(self, jid, node, data=None): + self.add_node(jid, node) + self.nodes[(jid, node)]['items'].add_item( + data.get('ijid', ''), + node=data.get('inode', None), + name=data.get('name', None)) + + def del_item(self, jid, node, data=None): + if (jid, node) in self.nodes: + self.nodes[(jid, node)]['items'].del_item(**data) + -- cgit v1.2.3