XEP-0030: API changes

- ``supports``, ``has_identity``, ``get_info``, ``get_items`` are now coroutines
- ``set_info````set_items``, ``del_items``, ``add_item``, ``add_identity``,
  ``del_identity``, ``set_identities``, ``del_identities``, ``add_feature``,
  ``del_feature``, ``set_feature``, ``set_features``, ``del_features``
  now return a Future

also fix has_identity and supports which have been broken in forever
This commit is contained in:
mathieui 2021-02-14 12:06:05 +01:00
parent 7772e26a8c
commit 13de36baa1
3 changed files with 125 additions and 71 deletions

View File

@ -6,13 +6,13 @@
import asyncio import asyncio
import logging import logging
from asyncio import Future
from typing import Optional, Callable from typing import Optional, Callable
from slixmpp import Iq from slixmpp import Iq
from slixmpp import future_wrapper from slixmpp import future_wrapper
from slixmpp.plugins import BasePlugin from slixmpp.plugins import BasePlugin
from slixmpp.xmlstream.handler import Callback from slixmpp.xmlstream.handler import Callback, CoroutineCallback
from slixmpp.xmlstream.matcher import StanzaPath from slixmpp.xmlstream.matcher import StanzaPath
from slixmpp.xmlstream import register_stanza_plugin, JID from slixmpp.xmlstream import register_stanza_plugin, JID
from slixmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems from slixmpp.plugins.xep_0030 import stanza, DiscoInfo, DiscoItems
@ -91,12 +91,12 @@ class XEP_0030(BasePlugin):
Start the XEP-0030 plugin. Start the XEP-0030 plugin.
""" """
self.xmpp.register_handler( self.xmpp.register_handler(
Callback('Disco Info', CoroutineCallback('Disco Info',
StanzaPath('iq/disco_info'), StanzaPath('iq/disco_info'),
self._handle_disco_info)) self._handle_disco_info))
self.xmpp.register_handler( self.xmpp.register_handler(
Callback('Disco Items', CoroutineCallback('Disco Items',
StanzaPath('iq/disco_items'), StanzaPath('iq/disco_items'),
self._handle_disco_items)) self._handle_disco_items))
@ -228,10 +228,13 @@ class XEP_0030(BasePlugin):
self.api.restore_default(op, jid, node) self.api.restore_default(op, jid, node)
def supports(self, jid=None, node=None, feature=None, local=False, def supports(self, jid=None, node=None, feature=None, local=False,
cached=True, ifrom=None): cached=True, ifrom=None) -> Future:
""" """
Check if a JID supports a given feature. Check if a JID supports a given feature.
.. versionchanged:: 1.8.0
This function now returns a Future.
Return values: Return values:
:param True: The feature is supported :param True: The feature is supported
:param False: The feature is not listed as supported :param False: The feature is not listed as supported
@ -259,10 +262,13 @@ class XEP_0030(BasePlugin):
return self.api['supports'](jid, node, ifrom, data) return self.api['supports'](jid, node, ifrom, data)
def has_identity(self, jid=None, node=None, category=None, itype=None, def has_identity(self, jid=None, node=None, category=None, itype=None,
lang=None, local=False, cached=True, ifrom=None): lang=None, local=False, cached=True, ifrom=None) -> Future:
""" """
Check if a JID provides a given identity. Check if a JID provides a given identity.
.. versionchanged:: 1.8.0
This function now returns a Future.
Return values: Return values:
:param True: The identity is provided :param True: The identity is provided
:param False: The identity is not listed :param False: The identity is not listed
@ -324,8 +330,7 @@ class XEP_0030(BasePlugin):
callback(results) callback(results)
return results return results
@future_wrapper async def get_info(self, jid=None, node=None, local=None,
def get_info(self, jid=None, node=None, local=None,
cached=None, **kwargs): cached=None, **kwargs):
""" """
Retrieve the disco#info results from a given JID/node combination. Retrieve the disco#info results from a given JID/node combination.
@ -338,6 +343,9 @@ class XEP_0030(BasePlugin):
If requesting items from a local JID/node, then only a DiscoInfo If requesting items from a local JID/node, then only a DiscoInfo
stanza will be returned. Otherwise, an Iq stanza will be returned. 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 jid: Request info from this JID.
:param node: The particular node to query. :param node: The particular node to query.
:param local: If true, then the query is for a JID/node :param local: If true, then the query is for a JID/node
@ -369,18 +377,21 @@ class XEP_0030(BasePlugin):
if local: if local:
log.debug("Looking up local disco#info data " + \ log.debug("Looking up local disco#info data " + \
"for %s, node %s.", jid, node) "for %s, node %s.", jid, node)
info = self.api['get_info'](jid, node, info = await self.api['get_info'](
kwargs.get('ifrom', None), jid, node, kwargs.get('ifrom', None),
kwargs) kwargs
)
info = self._fix_default_info(info) info = self._fix_default_info(info)
return self._wrap(kwargs.get('ifrom', None), jid, info) return self._wrap(kwargs.get('ifrom', None), jid, info)
if cached: if cached:
log.debug("Looking up cached disco#info data " + \ log.debug("Looking up cached disco#info data " + \
"for %s, node %s.", jid, node) "for %s, node %s.", jid, node)
info = self.api['get_cached_info'](jid, node, info = await self.api['get_cached_info'](
jid, node,
kwargs.get('ifrom', None), kwargs.get('ifrom', None),
kwargs) kwargs
)
if info is not None: if info is not None:
return self._wrap(kwargs.get('ifrom', None), jid, info) return self._wrap(kwargs.get('ifrom', None), jid, info)
@ -390,21 +401,24 @@ class XEP_0030(BasePlugin):
iq['to'] = jid iq['to'] = jid
iq['type'] = 'get' iq['type'] = 'get'
iq['disco_info']['node'] = node if node else '' iq['disco_info']['node'] = node if node else ''
return iq.send(timeout=kwargs.get('timeout', None), return await iq.send(timeout=kwargs.get('timeout', None),
callback=kwargs.get('callback', None), callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)) timeout_callback=kwargs.get('timeout_callback', None))
def set_info(self, jid=None, node=None, info=None): def set_info(self, jid=None, node=None, info=None) -> Future:
""" """
Set the disco#info data for a JID/node based on an existing Set the disco#info data for a JID/node based on an existing
disco#info stanza. disco#info stanza.
.. versionchanged:: 1.8.0
This function now returns a Future.
""" """
if isinstance(info, Iq): if isinstance(info, Iq):
info = info['disco_info'] info = info['disco_info']
self.api['set_info'](jid, node, None, info) return self.api['set_info'](jid, node, None, info)
@future_wrapper async def get_items(self, jid=None, node=None, local=False, **kwargs):
def get_items(self, jid=None, node=None, local=False, **kwargs):
""" """
Retrieve the disco#items results from a given JID/node combination. Retrieve the disco#items results from a given JID/node combination.
@ -416,6 +430,9 @@ class XEP_0030(BasePlugin):
If requesting items from a local JID/node, then only a DiscoItems If requesting items from a local JID/node, then only a DiscoItems
stanza will be returned. Otherwise, an Iq stanza will be returned. 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 jid: Request info from this JID.
:param node: The particular node to query. :param node: The particular node to query.
:param local: If true, then the query is for a JID/node :param local: If true, then the query is for a JID/node
@ -428,7 +445,7 @@ class XEP_0030(BasePlugin):
Otherwise the parameter is ignored. Otherwise the parameter is ignored.
""" """
if local or local is None and jid is None: if local or local is None and jid is None:
items = self.api['get_items'](jid, node, items = await self.api['get_items'](jid, node,
kwargs.get('ifrom', None), kwargs.get('ifrom', None),
kwargs) kwargs)
return self._wrap(kwargs.get('ifrom', None), jid, items) return self._wrap(kwargs.get('ifrom', None), jid, items)
@ -440,43 +457,52 @@ class XEP_0030(BasePlugin):
iq['type'] = 'get' iq['type'] = 'get'
iq['disco_items']['node'] = node if node else '' iq['disco_items']['node'] = node if node else ''
if kwargs.get('iterator', False) and self.xmpp['xep_0059']: if kwargs.get('iterator', False) and self.xmpp['xep_0059']:
raise NotImplementedError("XEP 0059 has not yet been fixed")
return self.xmpp['xep_0059'].iterate(iq, 'disco_items') return self.xmpp['xep_0059'].iterate(iq, 'disco_items')
else: else:
return iq.send(timeout=kwargs.get('timeout', None), return await iq.send(timeout=kwargs.get('timeout', None),
callback=kwargs.get('callback', None), callback=kwargs.get('callback', None),
timeout_callback=kwargs.get('timeout_callback', None)) timeout_callback=kwargs.get('timeout_callback', None))
def set_items(self, jid=None, node=None, **kwargs): def set_items(self, jid=None, node=None, **kwargs) -> Future:
""" """
Set or replace all items for the specified JID/node combination. 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 The given items must be in a list or set where each item is a
tuple of the form: (jid, node, name). tuple of the form: (jid, node, name).
.. versionchanged:: 1.8.0
This function now returns a Future.
:param jid: The JID to modify. :param jid: The JID to modify.
:param node: Optional node to modify. :param node: Optional node to modify.
:param items: A series of items in tuple format. :param items: A series of items in tuple format.
""" """
self.api['set_items'](jid, node, None, kwargs) return self.api['set_items'](jid, node, None, kwargs)
def del_items(self, jid=None, node=None, **kwargs): def del_items(self, jid=None, node=None, **kwargs) -> Future:
""" """
Remove all items from the given JID/node combination. Remove all items from the given JID/node combination.
.. versionchanged:: 1.8.0
This function now returns a Future.
Arguments: Arguments:
:param jid: The JID to modify. :param jid: The JID to modify.
:param node: Optional node to modify. :param node: Optional node to modify.
""" """
self.api['del_items'](jid, node, None, kwargs) return self.api['del_items'](jid, node, None, kwargs)
def add_item(self, jid='', name='', node=None, subnode='', ijid=None): def add_item(self, jid='', name='', node=None, subnode='', ijid=None) -> Future:
""" """
Add a new item element to the given JID/node combination. Add a new item element to the given JID/node combination.
Each item is required to have a JID, but may also specify Each item is required to have a JID, but may also specify
a node value to reference non-addressable entities. 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 jid: The JID for the item.
:param name: Optional name for the item. :param name: Optional name for the item.
:param node: The node to modify. :param node: The node to modify.
@ -488,9 +514,9 @@ class XEP_0030(BasePlugin):
kwargs = {'ijid': jid, kwargs = {'ijid': jid,
'name': name, 'name': name,
'inode': subnode} 'inode': subnode}
self.api['add_item'](ijid, node, None, kwargs) return self.api['add_item'](ijid, node, None, kwargs)
def del_item(self, jid=None, node=None, **kwargs): def del_item(self, jid=None, node=None, **kwargs) -> Future:
""" """
Remove a single item from the given JID/node combination. Remove a single item from the given JID/node combination.
@ -499,10 +525,10 @@ class XEP_0030(BasePlugin):
:param ijid: The item's JID. :param ijid: The item's JID.
:param inode: The item's node. :param inode: The item's node.
""" """
self.api['del_item'](jid, node, None, kwargs) return self.api['del_item'](jid, node, None, kwargs)
def add_identity(self, category='', itype='', name='', def add_identity(self, category='', itype='', name='',
node=None, jid=None, lang=None): node=None, jid=None, lang=None) -> Future:
""" """
Add a new identity to the given JID/node combination. Add a new identity to the given JID/node combination.
@ -514,6 +540,9 @@ class XEP_0030(BasePlugin):
category/type/xml:lang pairs are allowed so long as the category/type/xml:lang pairs are allowed so long as the
names are different. A category and type is always required. 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 category: The identity's category.
:param itype: The identity's type. :param itype: The identity's type.
:param name: Optional name for the identity. :param name: Optional name for the identity.
@ -525,24 +554,31 @@ class XEP_0030(BasePlugin):
'itype': itype, 'itype': itype,
'name': name, 'name': name,
'lang': lang} 'lang': lang}
self.api['add_identity'](jid, node, None, kwargs) return self.api['add_identity'](jid, node, None, kwargs)
def add_feature(self, feature: str, node: Optional[str] = None, def add_feature(self, feature: str, node: Optional[str] = None,
jid: Optional[JID] = None): jid: Optional[JID] = None) -> Future:
""" """
Add a feature to a JID/node combination. 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 feature: The namespace of the supported feature.
:param node: The node to modify. :param node: The node to modify.
:param jid: The JID to modify. :param jid: The JID to modify.
""" """
kwargs = {'feature': feature} kwargs = {'feature': feature}
self.api['add_feature'](jid, node, None, kwargs) return self.api['add_feature'](jid, node, None, kwargs)
def del_identity(self, jid: Optional[JID] = None, node: Optional[str] = None, **kwargs): def del_identity(self, jid: Optional[JID] = None,
node: Optional[str] = None, **kwargs) -> Future:
""" """
Remove an identity from the given JID/node combination. 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 jid: The JID to modify.
:param node: The node to modify. :param node: The node to modify.
:param category: The identity's category. :param category: The identity's category.
@ -550,67 +586,82 @@ class XEP_0030(BasePlugin):
:param name: Optional, human readable name for the identity. :param name: Optional, human readable name for the identity.
:param lang: Optional, the identity's xml:lang value. :param lang: Optional, the identity's xml:lang value.
""" """
self.api['del_identity'](jid, node, None, kwargs) return self.api['del_identity'](jid, node, None, kwargs)
def del_feature(self, jid=None, node=None, **kwargs): def del_feature(self, jid=None, node=None, **kwargs) -> Future:
""" """
Remove a feature from a given JID/node combination. 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 jid: The JID to modify.
:param node: The node to modify. :param node: The node to modify.
:param feature: The feature's namespace. :param feature: The feature's namespace.
""" """
self.api['del_feature'](jid, node, None, kwargs) return self.api['del_feature'](jid, node, None, kwargs)
def set_identities(self, jid=None, node=None, **kwargs): def set_identities(self, jid=None, node=None, **kwargs) -> Future:
""" """
Add or replace all identities for the given JID/node combination. Add or replace all identities for the given JID/node combination.
The identities must be in a set where each identity is a tuple The identities must be in a set where each identity is a tuple
of the form: (category, type, lang, name) of the form: (category, type, lang, name)
.. versionchanged:: 1.8.0
This function now returns a Future.
:param jid: The JID to modify. :param jid: The JID to modify.
:param node: The node to modify. :param node: The node to modify.
:param identities: A set of identities in tuple form. :param identities: A set of identities in tuple form.
:param lang: Optional, xml:lang value. :param lang: Optional, xml:lang value.
""" """
self.api['set_identities'](jid, node, None, kwargs) return self.api['set_identities'](jid, node, None, kwargs)
def del_identities(self, jid=None, node=None, **kwargs): def del_identities(self, jid=None, node=None, **kwargs) -> Future:
""" """
Remove all identities for a JID/node combination. Remove all identities for a JID/node combination.
If a language is specified, only identities using that If a language is specified, only identities using that
language will be removed. language will be removed.
.. versionchanged:: 1.8.0
This function now returns a Future.
:param jid: The JID to modify. :param jid: The JID to modify.
:param node: The node to modify. :param node: The node to modify.
:param lang: Optional. If given, only remove identities :param lang: Optional. If given, only remove identities
using this xml:lang value. using this xml:lang value.
""" """
self.api['del_identities'](jid, node, None, kwargs) return self.api['del_identities'](jid, node, None, kwargs)
def set_features(self, jid=None, node=None, **kwargs): def set_features(self, jid=None, node=None, **kwargs) -> Future:
""" """
Add or replace the set of supported features Add or replace the set of supported features
for a JID/node combination. for a JID/node combination.
.. versionchanged:: 1.8.0
This function now returns a Future.
:param jid: The JID to modify. :param jid: The JID to modify.
:param node: The node to modify. :param node: The node to modify.
:param features: The new set of supported features. :param features: The new set of supported features.
""" """
self.api['set_features'](jid, node, None, kwargs) return self.api['set_features'](jid, node, None, kwargs)
def del_features(self, jid=None, node=None, **kwargs): def del_features(self, jid=None, node=None, **kwargs) -> Future:
""" """
Remove all features from a JID/node combination. 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 jid: The JID to modify.
:param node: The node to modify. :param node: The node to modify.
""" """
self.api['del_features'](jid, node, None, kwargs) return self.api['del_features'](jid, node, None, kwargs)
def _run_node_handler(self, htype, jid, node=None, ifrom=None, data=None): async def _run_node_handler(self, htype, jid, node=None, ifrom=None, data=None):
""" """
Execute the most specific node handler for the given Execute the most specific node handler for the given
JID/node combination. JID/node combination.
@ -623,9 +674,9 @@ class XEP_0030(BasePlugin):
if not data: if not data:
data = {} data = {}
return self.api[htype](jid, node, ifrom, data) return await self.api[htype](jid, node, ifrom, data)
def _handle_disco_info(self, iq): async def _handle_disco_info(self, iq):
""" """
Process an incoming disco#info stanza. If it is a get Process an incoming disco#info stanza. If it is a get
request, find and return the appropriate identities request, find and return the appropriate identities
@ -637,7 +688,7 @@ class XEP_0030(BasePlugin):
if iq['type'] == 'get': if iq['type'] == 'get':
log.debug("Received disco info query from " + \ log.debug("Received disco info query from " + \
"<%s> to <%s>.", iq['from'], iq['to']) "<%s> to <%s>.", iq['from'], iq['to'])
info = self.api['get_info'](iq['to'], info = await self.api['get_info'](iq['to'],
iq['disco_info']['node'], iq['disco_info']['node'],
iq['from'], iq['from'],
iq) iq)
@ -662,13 +713,13 @@ class XEP_0030(BasePlugin):
ito = iq['to'].full ito = iq['to'].full
else: else:
ito = None ito = None
self.api['cache_info'](iq['from'], await self.api['cache_info'](iq['from'],
iq['disco_info']['node'], iq['disco_info']['node'],
ito, ito,
iq) iq)
self.xmpp.event('disco_info', iq) self.xmpp.event('disco_info', iq)
def _handle_disco_items(self, iq): async def _handle_disco_items(self, iq):
""" """
Process an incoming disco#items stanza. If it is a get Process an incoming disco#items stanza. If it is a get
request, find and return the appropriate items. If it request, find and return the appropriate items. If it
@ -679,7 +730,7 @@ class XEP_0030(BasePlugin):
if iq['type'] == 'get': if iq['type'] == 'get':
log.debug("Received disco items query from " + \ log.debug("Received disco items query from " + \
"<%s> to <%s>.", iq['from'], iq['to']) "<%s> to <%s>.", iq['from'], iq['to'])
items = self.api['get_items'](iq['to'], items = await self.api['get_items'](iq['to'],
iq['disco_items']['node'], iq['disco_items']['node'],
iq['from'], iq['from'],
iq) iq)

View File

@ -109,7 +109,7 @@ class StaticDisco(object):
# the requester's JID, except for cached results. To do that, # the requester's JID, except for cached results. To do that,
# register a custom node handler. # register a custom node handler.
def supports(self, jid, node, ifrom, data): async def supports(self, jid, node, ifrom, data):
""" """
Check if a JID supports a given feature. Check if a JID supports a given feature.
@ -137,7 +137,7 @@ class StaticDisco(object):
return False return False
try: try:
info = self.disco.get_info(jid=jid, node=node, info = await self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data) ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True) info = self.disco._wrap(ifrom, jid, info, True)
features = info['disco_info']['features'] features = info['disco_info']['features']
@ -147,7 +147,7 @@ class StaticDisco(object):
except IqTimeout: except IqTimeout:
return None return None
def has_identity(self, jid, node, ifrom, data): async def has_identity(self, jid, node, ifrom, data):
""" """
Check if a JID has a given identity. Check if a JID has a given identity.
@ -176,7 +176,7 @@ class StaticDisco(object):
'cached': data.get('cached', True)} 'cached': data.get('cached', True)}
try: try:
info = self.disco.get_info(jid=jid, node=node, info = await self.disco.get_info(jid=jid, node=node,
ifrom=ifrom, **data) ifrom=ifrom, **data)
info = self.disco._wrap(ifrom, jid, info, True) info = self.disco._wrap(ifrom, jid, info, True)
trunc = lambda i: (i[0], i[1], i[2]) trunc = lambda i: (i[0], i[1], i[2])

View File

@ -1,5 +1,5 @@
import asyncio
import time import time
import threading
import unittest import unittest
from slixmpp.test import SlixTest from slixmpp.test import SlixTest
@ -288,7 +288,9 @@ class TestStreamDisco(SlixTest):
self.xmpp.add_event_handler('disco_info', handle_disco_info) self.xmpp.add_event_handler('disco_info', handle_disco_info)
self.xmpp['xep_0030'].get_info('user@localhost', 'foo')
self.xmpp.wrap(self.xmpp['xep_0030'].get_info('user@localhost', 'foo'))
self.wait_()
self.send(""" self.send("""
<iq type="get" to="user@localhost" id="1"> <iq type="get" to="user@localhost" id="1">
@ -483,7 +485,8 @@ class TestStreamDisco(SlixTest):
self.xmpp.add_event_handler('disco_items', handle_disco_items) self.xmpp.add_event_handler('disco_items', handle_disco_items)
self.xmpp['xep_0030'].get_items('user@localhost', 'foo') self.xmpp.wrap(self.xmpp['xep_0030'].get_items('user@localhost', 'foo'))
self.wait_()
self.send(""" self.send("""
<iq type="get" to="user@localhost" id="1"> <iq type="get" to="user@localhost" id="1">