818 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			818 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
 | |
| # Slixmpp: The Slick XMPP Library
 | |
| # Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout
 | |
| # This file is part of Slixmpp.
 | |
| # See the file LICENSE for copying permission.
 | |
| import asyncio
 | |
| import logging
 | |
| 
 | |
| from asyncio import Future
 | |
| from typing import (
 | |
|     Optional,
 | |
|     Callable,
 | |
|     List,
 | |
|     Union,
 | |
| )
 | |
| 
 | |
| from slixmpp import JID
 | |
| from slixmpp.stanza import Iq
 | |
| from slixmpp.types import OptJid
 | |
| from slixmpp.plugins import BasePlugin
 | |
| from slixmpp.xmlstream.handler import CoroutineCallback
 | |
| from slixmpp.xmlstream.matcher import StanzaPath
 | |
| from slixmpp.xmlstream import register_stanza_plugin
 | |
| from slixmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems
 | |
| from slixmpp.plugins.xep_0030 import StaticDisco
 | |
| 
 | |
| 
 | |
| log = logging.getLogger(__name__)
 | |
| 
 | |
| 
 | |
| class XEP_0030(BasePlugin):
 | |
| 
 | |
|     """
 | |
|     XEP-0030: Service Discovery
 | |
| 
 | |
|     Service discovery in XMPP allows entities to discover information about
 | |
|     other agents in the network, such as the feature sets supported by a
 | |
|     client, or signposts to other, related entities.
 | |
| 
 | |
|     Also see <http://www.xmpp.org/extensions/xep-0030.html>.
 | |
| 
 | |
|     The XEP-0030 plugin works using a hierarchy of dynamic
 | |
|     node handlers, ranging from global handlers to specific
 | |
|     JID+node handlers. The default set of handlers operate
 | |
|     in a static manner, storing disco information in memory.
 | |
|     However, custom handlers may use any available backend
 | |
|     storage mechanism desired, such as SQLite or Redis.
 | |
| 
 | |
|     Node handler hierarchy:
 | |
| 
 | |
|     ====== ======= ============================
 | |
|     JID     Node    Level
 | |
|     ====== ======= ============================
 | |
|     None    None    Global
 | |
|     Given   None    All nodes for the JID
 | |
|     None    Given   Node on self.xmpp.boundjid
 | |
|     Given   Given   A single node
 | |
|     ====== ======= ============================
 | |
| 
 | |
|     Stream Handlers:
 | |
| 
 | |
|     ::
 | |
| 
 | |
|         Disco Info  -- Any Iq stanze that includes a query with the
 | |
|                        namespace http://jabber.org/protocol/disco#info.
 | |
|         Disco Items -- Any Iq stanze that includes a query with the
 | |
|                        namespace http://jabber.org/protocol/disco#items.
 | |
| 
 | |
|     Events:
 | |
| 
 | |
|     - :term:`disco_info`         -- Received a disco#info Iq query result.
 | |
|     - :term:`disco_items`        -- Received a disco#items Iq query result.
 | |
| 
 | |
|     Attributes:
 | |
| 
 | |
|     :var static: Object containing the default set of
 | |
|                  static node handlers.
 | |
|     """
 | |
| 
 | |
|     name = 'xep_0030'
 | |
|     description = 'XEP-0030: Service Discovery'
 | |
|     dependencies = set()
 | |
|     stanza = stanza
 | |
|     default_config = {
 | |
|         'use_cache': True,
 | |
|         'wrap_results': False
 | |
|     }
 | |
|     static: StaticDisco
 | |
| 
 | |
|     def plugin_init(self):
 | |
|         """
 | |
|         Start the XEP-0030 plugin.
 | |
|         """
 | |
|         self.xmpp.register_handler(CoroutineCallback(
 | |
|             'Disco Info',
 | |
|             StanzaPath('iq/disco_info'),
 | |
|             self._handle_disco_info
 | |
|         ))
 | |
| 
 | |
|         self.xmpp.register_handler(CoroutineCallback(
 | |
|             '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)
 | |
| 
 | |
|         self._disco_ops = [
 | |
|                 'get_info', 'set_info', 'set_identities', 'set_features',
 | |
|                 'get_items', 'set_items', 'del_items', 'add_identity',
 | |
|                 'del_identity', 'add_feature', 'del_feature', 'add_item',
 | |
|                 'del_item', 'del_identities', 'del_features', 'cache_info',
 | |
|                 'get_cached_info', 'supports', 'has_identity']
 | |
| 
 | |
|         for op in self._disco_ops:
 | |
|             self.api.register(getattr(self.static, op), op, default=True)
 | |
| 
 | |
|         self.domain_infos = {}
 | |
| 
 | |
|     def session_bind(self, jid):
 | |
|         self.add_feature('http://jabber.org/protocol/disco#info')
 | |
| 
 | |
|     def plugin_end(self):
 | |
|         self.del_feature('http://jabber.org/protocol/disco#info')
 | |
| 
 | |
|     def _add_disco_op(self, op, default_handler):
 | |
|         self.api.register(default_handler, op)
 | |
|         self.api.register_default(default_handler, op)
 | |
| 
 | |
|     def set_node_handler(self, htype: str, jid: OptJid = None,
 | |
|                          node: Optional[str] = None,
 | |
|                          handler: Optional[Callable] = None):
 | |
|         """
 | |
|         Add a node handler for the given hierarchy level and
 | |
|         handler type.
 | |
| 
 | |
|         Node handlers are ordered in a hierarchy where the
 | |
|         most specific handler is executed. Thus, a fallback,
 | |
|         global handler can be used for the majority of cases
 | |
|         with a few node specific handler that override the
 | |
|         global behavior.
 | |
| 
 | |
|         Node handler hierarchy:
 | |
| 
 | |
|         ====== ======= ============================
 | |
|         JID     Node    Level
 | |
|         ====== ======= ============================
 | |
|         None    None    Global
 | |
|         Given   None    All nodes for the JID
 | |
|         None    Given   Node on self.xmpp.boundjid
 | |
|         Given   Given   A single node
 | |
|         ====== ======= ============================
 | |
| 
 | |
|         Handler types:
 | |
| 
 | |
|         ::
 | |
| 
 | |
|             get_info
 | |
|             get_items
 | |
|             set_identities
 | |
|             set_features
 | |
|             set_items
 | |
|             del_items
 | |
|             del_identities
 | |
|             del_identity
 | |
|             del_feature
 | |
|             del_features
 | |
|             del_item
 | |
|             add_identity
 | |
|             add_feature
 | |
|             add_item
 | |
| 
 | |
|         :param htype: The operation provided by the handler.
 | |
|         :param jid: The JID the handler applies to. May be narrowed
 | |
|                     further if a node is given.
 | |
|         :param node: The particular node the handler is for. If no JID
 | |
|                      is given, then the self.xmpp.boundjid.full is
 | |
|                      assumed.
 | |
|         :param handler: The handler function to use.
 | |
|         """
 | |
|         self.api.register(handler, htype, jid, node)
 | |
| 
 | |
|     def del_node_handler(self, htype: str, jid: OptJid, node: Optional[str]):
 | |
|         """
 | |
|         Remove a handler type for a JID and node combination.
 | |
| 
 | |
|         The next handler in the hierarchy will be used if one
 | |
|         exists. If removing the global handler, make sure that
 | |
|         other handlers exist to process existing nodes.
 | |
| 
 | |
|         Node handler hierarchy:
 | |
| 
 | |
|         ====== ======= ============================
 | |
|         JID     Node    Level
 | |
|         ====== ======= ============================
 | |
|         None    None    Global
 | |
|         Given   None    All nodes for the JID
 | |
|         None    Given   Node on self.xmpp.boundjid
 | |
|         Given   Given   A single node
 | |
|         ====== ======= ============================
 | |
| 
 | |
|         :param htype: The type of handler to remove.
 | |
|         :param jid: The JID from which to remove the handler.
 | |
|         :param node: The node from which to remove the handler.
 | |
|         """
 | |
|         self.api.unregister(htype, jid, node)
 | |
| 
 | |
|     def restore_defaults(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                          handlers: Optional[List[Callable]] = None):
 | |
|         """
 | |
|         Change all or some of a node's handlers to the default
 | |
|         handlers. Useful for manually overriding the contents
 | |
|         of a node that would otherwise be handled by a JID level
 | |
|         or global level dynamic handler.
 | |
| 
 | |
|         The default is to use the built-in static handlers, but that
 | |
|         may be changed by modifying self.default_handlers.
 | |
| 
 | |
|         :param jid: The JID owning the node to modify.
 | |
|         :param node: The node to change to using static handlers.
 | |
|         :param handlers: Optional list of handlers to change to the
 | |
|                          default version. If provided, only these
 | |
|                          handlers will be changed. Otherwise, all
 | |
|                          handlers will use the default version.
 | |
|         """
 | |
|         if handlers is None:
 | |
|             handlers = self._disco_ops
 | |
|         for op in handlers:
 | |
|             self.api.restore_default(op, jid, node)
 | |
| 
 | |
|     def supports(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                  feature: Optional[str] = None, local: bool = False,
 | |
|                  cached: bool = True, ifrom: OptJid = None) -> Future:
 | |
|         """
 | |
|         Check if a JID supports a given feature.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param jid: Request info from this JID.
 | |
|         :param node: The particular node to query.
 | |
|         :param feature: The name of the feature to check.
 | |
|         :param local: If true, then the query is for a JID/node
 | |
|                       combination handled by this Slixmpp instance and
 | |
|                       no stanzas need to be sent.
 | |
|                       Otherwise, a disco stanza must be sent to the
 | |
|                       remove JID to retrieve the info.
 | |
|         :param cached: If true, then look for the disco info data from
 | |
|                        the local cache system. If no results are found,
 | |
|                        send the query as usual. The self.use_cache
 | |
|                        setting must be set to true for this option to
 | |
|                        be useful. If set to false, then the cache will
 | |
|                        be skipped, even if a result has already been
 | |
|                        cached. Defaults to false.
 | |
| 
 | |
|         :returns True: The feature is supported
 | |
|         :returns False: The feature is not listed as supported
 | |
|         :returns None: Nothing could be found due to a timeout
 | |
|         """
 | |
|         data = {'feature': feature,
 | |
|                 'local': local,
 | |
|                 'cached': cached}
 | |
|         return self.api['supports'](jid, node, ifrom, data)
 | |
| 
 | |
|     def has_identity(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                      category: Optional[str] = None,
 | |
|                      itype: Optional[str] = None, lang: Optional[str] = None,
 | |
|                      local: bool = False, cached: bool = True,
 | |
|                      ifrom: OptJid = None) -> Future:
 | |
|         """
 | |
|         Check if a JID provides a given identity.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
| 
 | |
|         :param jid: Request info from this JID.
 | |
|         :param node: The particular node to query.
 | |
|         :param category: The category of the identity to check.
 | |
|         :param itype: The type of the identity to check.
 | |
|         :param lang: The language of the identity to check.
 | |
|         :param local: If true, then the query is for a JID/node
 | |
|                         combination handled by this Slixmpp instance and
 | |
|                         no stanzas need to be sent.
 | |
|                         Otherwise, a disco stanza must be sent to the
 | |
|                         remove JID to retrieve the info.
 | |
|         :param cached: If true, then look for the disco info data from
 | |
|                         the local cache system. If no results are found,
 | |
|                         send the query as usual. The self.use_cache
 | |
|                         setting must be set to true for this option to
 | |
|                         be useful. If set to false, then the cache will
 | |
|                         be skipped, even if a result has already been
 | |
|                         cached. Defaults to false.
 | |
| 
 | |
|         :returns True: The identity is provided
 | |
|         :returns False: The identity is not listed
 | |
|         :returns None: Nothing could be found due to a timeout
 | |
|         """
 | |
|         data = {'category': category,
 | |
|                 'itype': itype,
 | |
|                 'lang': lang,
 | |
|                 'local': local,
 | |
|                 'cached': cached}
 | |
|         return self.api['has_identity'](jid, node, ifrom, data)
 | |
| 
 | |
|     async def get_info_from_domain(self, domain=None, timeout=None,
 | |
|                                    cached=True, callback=None):
 | |
|         """Fetch disco#info of specified domain and one disco#items level below
 | |
|         """
 | |
| 
 | |
|         if domain is None:
 | |
|             domain = self.xmpp.boundjid.domain
 | |
| 
 | |
|         if not cached or domain not in self.domain_infos:
 | |
|             infos = [self.get_info(
 | |
|                 domain, timeout=timeout)]
 | |
|             iq_items = await self.get_items(
 | |
|                 domain, timeout=timeout)
 | |
|             items = iq_items['disco_items']['items']
 | |
|             infos += [
 | |
|                 self.get_info(item[0], timeout=timeout)
 | |
|                 for item in items]
 | |
|             info_futures, _ = await asyncio.wait(
 | |
|                 infos,
 | |
|                 timeout=timeout,
 | |
|                 loop=self.xmpp.loop
 | |
|             )
 | |
| 
 | |
|             self.domain_infos[domain] = [
 | |
|                 future.result()
 | |
|                 for future in info_futures if not future.exception()
 | |
|             ]
 | |
| 
 | |
|         results = self.domain_infos[domain]
 | |
| 
 | |
|         if callback is not None:
 | |
|             callback(results)
 | |
|         return results
 | |
| 
 | |
|     async def get_info(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                        local: Optional[bool] = None,
 | |
|                        cached: Optional[bool] = None, **kwargs) -> Iq:
 | |
|         """
 | |
|         Retrieve the disco#info results from a given JID/node combination.
 | |
| 
 | |
|         Info may be retrieved from both local resources and remote agents;
 | |
|         the local parameter indicates if the information should be gathered
 | |
|         by executing the local node handlers, or if a disco#info stanza
 | |
|         must be generated and sent.
 | |
| 
 | |
|         If requesting items from a local JID/node, then only a DiscoInfo
 | |
|         stanza will be returned. Otherwise, an Iq stanza will be returned.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function is now a coroutine.
 | |
| 
 | |
|         :param jid: Request info from this JID.
 | |
|         :param node: The particular node to query.
 | |
|         :param local: If true, then the query is for a JID/node
 | |
|                       combination handled by this Slixmpp instance and
 | |
|                       no stanzas need to be sent.
 | |
|                       Otherwise, a disco stanza must be sent to the
 | |
|                       remote JID to retrieve the info.
 | |
|         :param cached: If true, then look for the disco info data from
 | |
|                        the local cache system. If no results are found,
 | |
|                        send the query as usual. The self.use_cache
 | |
|                        setting must be set to true for this option to
 | |
|                        be useful. If set to false, then the cache will
 | |
|                        be skipped, even if a result has already been
 | |
|                        cached. Defaults to false.
 | |
|         """
 | |
|         if local is None:
 | |
|             if jid is not None and not isinstance(jid, JID):
 | |
|                 jid = JID(jid)
 | |
|                 if self.xmpp.is_component:
 | |
|                     if jid.domain == self.xmpp.boundjid.domain:
 | |
|                         local = True
 | |
|                 else:
 | |
|                     if str(jid) == str(self.xmpp.boundjid):
 | |
|                         local = True
 | |
|                 jid = jid.full
 | |
|             elif jid in (None, ''):
 | |
|                 local = True
 | |
| 
 | |
|         if local:
 | |
|             log.debug("Looking up local disco#info data "
 | |
|                       "for %s, node %s.", jid, node)
 | |
|             info = await self.api['get_info'](
 | |
|                 jid, node, kwargs.get('ifrom', None),
 | |
|                 kwargs
 | |
|             )
 | |
|             info = self._fix_default_info(info)
 | |
|             return self._wrap(kwargs.get('ifrom', None), jid, info)
 | |
| 
 | |
|         if cached:
 | |
|             log.debug("Looking up cached disco#info data "
 | |
|                       "for %s, node %s.", jid, node)
 | |
|             info = await self.api['get_cached_info'](
 | |
|                 jid, node,
 | |
|                 kwargs.get('ifrom', None),
 | |
|                 kwargs
 | |
|             )
 | |
|             if info is not None:
 | |
|                 return self._wrap(kwargs.get('ifrom', None), jid, info)
 | |
| 
 | |
|         iq = self.xmpp.Iq()
 | |
|         # Check dfrom parameter for backwards compatibility
 | |
|         iq['from'] = kwargs.get('ifrom', kwargs.get('dfrom', ''))
 | |
|         iq['to'] = jid
 | |
|         iq['type'] = 'get'
 | |
|         iq['disco_info']['node'] = node if node else ''
 | |
|         return await iq.send(**kwargs)
 | |
| 
 | |
|     def set_info(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                  info: Optional[Union[Iq, DiscoInfo]] = None) -> Future:
 | |
|         """
 | |
|         Set the disco#info data for a JID/node based on an existing
 | |
|         disco#info stanza.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         """
 | |
|         if isinstance(info, Iq):
 | |
|             info = info['disco_info']
 | |
|         return self.api['set_info'](jid, node, None, info)
 | |
| 
 | |
|     async def get_items(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                         local: bool = False, ifrom: OptJid = None,
 | |
|                         **kwargs) -> Iq:
 | |
|         """
 | |
|         Retrieve the disco#items results from a given JID/node combination.
 | |
| 
 | |
|         Items may be retrieved from both local resources and remote agents;
 | |
|         the local parameter indicates if the items should be gathered by
 | |
|         executing the local node handlers, or if a disco#items stanza must
 | |
|         be generated and sent.
 | |
| 
 | |
|         If requesting items from a local JID/node, then only a DiscoItems
 | |
|         stanza will be returned. Otherwise, an Iq stanza will be returned.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function is now a coroutine.
 | |
| 
 | |
|         :param jid: Request info from this JID.
 | |
|         :param node: The particular node to query.
 | |
|         :param local: If true, then the query is for a JID/node
 | |
|                       combination handled by this Slixmpp instance and
 | |
|                       no stanzas need to be sent.
 | |
|                       Otherwise, a disco stanza must be sent to the
 | |
|                       remove JID to retrieve the items.
 | |
|         :param iterator: If True, return a result set iterator using
 | |
|                          the XEP-0059 plugin, if the plugin is loaded.
 | |
|                          Otherwise the parameter is ignored.
 | |
|         """
 | |
|         if local or local is None and jid is None:
 | |
|             items = await self.api['get_items'](jid, node, ifrom, kwargs)
 | |
|             return self._wrap(kwargs.get('ifrom', None), jid, items)
 | |
| 
 | |
|         iq = self.xmpp.Iq()
 | |
|         # Check dfrom parameter for backwards compatibility
 | |
|         iq['from'] = ifrom or kwargs.get('dfrom', '')
 | |
|         iq['to'] = jid
 | |
|         iq['type'] = 'get'
 | |
|         iq['disco_items']['node'] = node if node else ''
 | |
|         if kwargs.get('iterator', False) and self.xmpp['xep_0059']:
 | |
|             return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
 | |
|         else:
 | |
|             return await iq.send(**kwargs)
 | |
| 
 | |
|     def set_items(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                   **kwargs) -> Future:
 | |
|         """
 | |
|         Set or replace all items for the specified JID/node combination.
 | |
| 
 | |
|         The given items must be in a list or set where each item is a
 | |
|         tuple of the form: (jid, node, name).
 | |
| 
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param jid: The JID to modify.
 | |
|         :param node: Optional node to modify.
 | |
|         :param items: A series of items in tuple format.
 | |
|         """
 | |
|         return self.api['set_items'](jid, node, None, kwargs)
 | |
| 
 | |
|     def del_items(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                   **kwargs) -> Future:
 | |
|         """
 | |
|         Remove all items from the given JID/node combination.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         Arguments:
 | |
|         :param jid: The JID to modify.
 | |
|         :param node: Optional node to modify.
 | |
|         """
 | |
|         return self.api['del_items'](jid, node, None, kwargs)
 | |
| 
 | |
|     def add_item(self, jid: str = '', name: str = '',
 | |
|                  node: Optional[str] = None, subnode: str = '',
 | |
|                  ijid: OptJid = None) -> Future:
 | |
|         """
 | |
|         Add a new item element to the given JID/node combination.
 | |
| 
 | |
|         Each item is required to have a JID, but may also specify
 | |
|         a node value to reference non-addressable entities.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param jid: The JID for the item.
 | |
|         :param name: Optional name for the item.
 | |
|         :param node: The node to modify.
 | |
|         :param subnode: Optional node for the item.
 | |
|         :param ijid: The JID to modify.
 | |
|         """
 | |
|         if not jid:
 | |
|             jid = self.xmpp.boundjid.full
 | |
|         kwargs = {'ijid': jid,
 | |
|                   'name': name,
 | |
|                   'inode': subnode}
 | |
|         return self.api['add_item'](ijid, node, None, kwargs)
 | |
| 
 | |
|     def del_item(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                  **kwargs) -> Future:
 | |
|         """
 | |
|         Remove a single item from the given JID/node combination.
 | |
| 
 | |
|         :param jid: The JID to modify.
 | |
|         :param node: The node to modify.
 | |
|         :param ijid: The item's JID.
 | |
|         :param inode: The item's node.
 | |
|         """
 | |
|         return self.api['del_item'](jid, node, None, kwargs)
 | |
| 
 | |
|     def add_identity(self, category: str = '', itype: str = '', name: str = '',
 | |
|                      node: Optional[str] = None, jid: OptJid = None,
 | |
|                      lang: Optional[str] = None) -> Future:
 | |
|         """
 | |
|         Add a new identity to the given JID/node combination.
 | |
| 
 | |
|         Each identity must be unique in terms of all four identity
 | |
|         components: category, type, name, and language.
 | |
| 
 | |
|         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. A category and type is always required.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param category: The identity's category.
 | |
|         :param itype: The identity's type.
 | |
|         :param name: Optional name for the identity.
 | |
|         :param lang: Optional two-letter language code.
 | |
|         :param node: The node to modify.
 | |
|         :param jid: The JID to modify.
 | |
|         """
 | |
|         kwargs = {'category': category,
 | |
|                   'itype': itype,
 | |
|                   'name': name,
 | |
|                   'lang': lang}
 | |
|         return self.api['add_identity'](jid, node, None, kwargs)
 | |
| 
 | |
|     def add_feature(self, feature: str, node: Optional[str] = None,
 | |
|                     jid: OptJid = None) -> Future:
 | |
|         """
 | |
|         Add a feature to a JID/node combination.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param feature: The namespace of the supported feature.
 | |
|         :param node: The node to modify.
 | |
|         :param jid: The JID to modify.
 | |
|         """
 | |
|         kwargs = {'feature': feature}
 | |
|         return self.api['add_feature'](jid, node, None, kwargs)
 | |
| 
 | |
|     def del_identity(self, jid: OptJid = None,
 | |
|                      node: Optional[str] = None, **kwargs) -> Future:
 | |
|         """
 | |
|         Remove an identity from the given JID/node combination.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param jid: The JID to modify.
 | |
|         :param node: The node to modify.
 | |
|         :param category: The identity's category.
 | |
|         :param itype: The identity's type value.
 | |
|         :param name: Optional, human readable name for the identity.
 | |
|         :param lang: Optional, the identity's xml:lang value.
 | |
|         """
 | |
|         return self.api['del_identity'](jid, node, None, kwargs)
 | |
| 
 | |
|     def del_feature(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                     **kwargs) -> Future:
 | |
|         """
 | |
|         Remove a feature from a given JID/node combination.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param jid: The JID to modify.
 | |
|         :param node: The node to modify.
 | |
|         :param feature: The feature's namespace.
 | |
|         """
 | |
|         return self.api['del_feature'](jid, node, None, kwargs)
 | |
| 
 | |
|     def set_identities(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                        **kwargs) -> Future:
 | |
|         """
 | |
|         Add or replace all identities for the given JID/node combination.
 | |
| 
 | |
|         The identities must be in a set where each identity is a tuple
 | |
|         of the form: (category, type, lang, name)
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param jid: The JID to modify.
 | |
|         :param node: The node to modify.
 | |
|         :param identities: A set of identities in tuple form.
 | |
|         :param lang: Optional, xml:lang value.
 | |
|         """
 | |
|         return self.api['set_identities'](jid, node, None, kwargs)
 | |
| 
 | |
|     def del_identities(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                        **kwargs) -> Future:
 | |
|         """
 | |
|         Remove all identities for a JID/node combination.
 | |
| 
 | |
|         If a language is specified, only identities using that
 | |
|         language will be removed.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param jid: The JID to modify.
 | |
|         :param node: The node to modify.
 | |
|         :param lang: Optional. If given, only remove identities
 | |
|                     using this xml:lang value.
 | |
|         """
 | |
|         return self.api['del_identities'](jid, node, None, kwargs)
 | |
| 
 | |
|     def set_features(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                      **kwargs) -> Future:
 | |
|         """
 | |
|         Add or replace the set of supported features
 | |
|         for a JID/node combination.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param jid: The JID to modify.
 | |
|         :param node: The node to modify.
 | |
|         :param features: The new set of supported features.
 | |
|         """
 | |
|         return self.api['set_features'](jid, node, None, kwargs)
 | |
| 
 | |
|     def del_features(self, jid: OptJid = None, node: Optional[str] = None,
 | |
|                      **kwargs) -> Future:
 | |
|         """
 | |
|         Remove all features from a JID/node combination.
 | |
| 
 | |
|         .. versionchanged:: 1.8.0
 | |
|             This function now returns a Future.
 | |
| 
 | |
|         :param jid: The JID to modify.
 | |
|         :param node: The node to modify.
 | |
|         """
 | |
|         return self.api['del_features'](jid, node, None, kwargs)
 | |
| 
 | |
|     async def _run_node_handler(self, htype, jid, node: Optional[str] = None,
 | |
|                                 ifrom: OptJid = None, data=None):
 | |
|         """
 | |
|         Execute the most specific node handler for the given
 | |
|         JID/node combination.
 | |
| 
 | |
|         :param htype: The handler type to execute.
 | |
|         :param jid: The JID requested.
 | |
|         :param node: The node requested.
 | |
|         :param data: Optional, custom data to pass to the handler.
 | |
|         """
 | |
|         if not data:
 | |
|             data = {}
 | |
| 
 | |
|         return await self.api[htype](jid, node, ifrom, data)
 | |
| 
 | |
|     async def _handle_disco_info(self, iq: 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.
 | |
| 
 | |
|         :param 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 = await self.api['get_info'](iq['to'],
 | |
|                                               iq['disco_info']['node'],
 | |
|                                               iq['from'],
 | |
|                                               iq)
 | |
|             if isinstance(info, Iq):
 | |
|                 info['id'] = iq['id']
 | |
|                 info.send()
 | |
|             else:
 | |
|                 node = iq['disco_info']['node']
 | |
|                 iq = iq.reply()
 | |
|                 if info:
 | |
|                     info = self._fix_default_info(info)
 | |
|                     info['node'] = node
 | |
|                     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'])
 | |
|             if self.use_cache:
 | |
|                 log.debug("Caching disco info result from "
 | |
|                           "<%s> to <%s>.", iq['from'], iq['to'])
 | |
|                 if self.xmpp.is_component:
 | |
|                     ito = iq['to'].full
 | |
|                 else:
 | |
|                     ito = None
 | |
|                 await self.api['cache_info'](iq['from'],
 | |
|                                              iq['disco_info']['node'],
 | |
|                                              ito,
 | |
|                                              iq)
 | |
|             self.xmpp.event('disco_info', iq)
 | |
| 
 | |
|     async def _handle_disco_items(self, iq: 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.
 | |
| 
 | |
|         :param 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 = await self.api['get_items'](iq['to'],
 | |
|                                                 iq['disco_items']['node'],
 | |
|                                                 iq['from'],
 | |
|                                                 iq)
 | |
|             if isinstance(items, Iq):
 | |
|                 items.send()
 | |
|             else:
 | |
|                 iq = 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: DiscoInfo):
 | |
|         """
 | |
|         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, Slixmpp 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.
 | |
| 
 | |
|         :param info: The disco#info quest (not the full Iq stanza) to modify.
 | |
|         """
 | |
|         result = info
 | |
|         if isinstance(info, Iq):
 | |
|             info = info['disco_info']
 | |
|         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 result
 | |
| 
 | |
|     def _wrap(self, ito: OptJid, ifrom: OptJid, payload, force=False) -> Iq:
 | |
|         """
 | |
|         Ensure that results are wrapped in an Iq stanza
 | |
|         if self.wrap_results has been set to True.
 | |
| 
 | |
|         :param ito: The JID to use as the 'to' value
 | |
|         :param ifrom: The JID to use as the 'from' value
 | |
|         :param payload: The disco data to wrap
 | |
|         :param force: Force wrapping, regardless of self.wrap_results
 | |
|         """
 | |
|         if (force or self.wrap_results) and not isinstance(payload, Iq):
 | |
|             iq = self.xmpp.Iq()
 | |
|             # Since we're simulating a result, we have to treat
 | |
|             # the 'from' and 'to' values opposite the normal way.
 | |
|             iq['to'] = self.xmpp.boundjid if ito is None else ito
 | |
|             iq['from'] = self.xmpp.boundjid if ifrom is None else ifrom
 | |
|             iq['type'] = 'result'
 | |
|             iq.append(payload)
 | |
|             return iq
 | |
|         return payload
 | 
