Source code for aioxmpp.dispatcher

########################################################################
# File name: dispatcher.py
# This file is part of: aioxmpp
#
# LICENSE
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this program.  If not, see
# <http://www.gnu.org/licenses/>.
#
########################################################################
"""
:mod:`~aioxmpp.dispatcher` --- Dispatch stanzas to callbacks
############################################################

.. versionadded:: 0.9

   The whole module was added in 0.9.

Stanza Dispatchers for Messages and Presences
=============================================

.. autoclass:: SimpleMessageDispatcher

.. autoclass:: SimplePresenceDispatcher


Decorators for :class:`aioxmpp.service.Service` Methods
=======================================================

.. autodecorator:: message_handler

.. autodecorator:: presence_handler

Test Functions
--------------

.. autofunction:: is_message_handler

.. autofunction:: is_presence_handler

Base Class for Stanza Dispatchers
=================================

.. autoclass:: SimpleStanzaDispatcher
"""
import abc
import asyncio
import contextlib

import aioxmpp.service
import aioxmpp.stream


[docs]class SimpleStanzaDispatcher(metaclass=abc.ABCMeta): """ Dispatch stanzas based on their sender and type. This is a service base class (not a service you should summon) which can be used to implement simple, pre-0.9 presence and message dispatching. For users, the following methods are relevant: .. automethod:: register_callback .. automethod:: unregister_callback .. automethod:: handler_context For deriving classes, the following methods are relevant: .. automethod:: _feed Subclasses must also provide the following property: .. autoattribute:: local_jid """ def __init__(self, **kwargs): super().__init__(**kwargs) self._map = {} @abc.abstractproperty def local_jid(self): """ The bare JID of the client for which this dispatcher is used. This is required to map missing ``@from`` attributes to this JID. The attribute must be provided by implementing subclasses. """
[docs] def _feed(self, stanza): """ Dispatch the given `stanza`. :param stanza: Stanza to dispatch :type stanza: :class:`~.StanzaBase` :rtype: :class:`bool` :return: true if the stanza was dispatched, false otherwise. Dispatch the stanza to up to one handler registered on the dispatcher. If no handler is found for the stanza, :data:`False` is returned. Otherwise, :data:`True` is returned. """ from_ = stanza.from_ if from_ is None: from_ = self.local_jid keys = [ (stanza.type_, from_, False), (stanza.type_, from_.bare(), True), (None, from_, False), (None, from_.bare(), True), (stanza.type_, None, False), (None, from_, False), (None, None, False), ] for key in keys: try: cb = self._map[key] except KeyError: continue cb(stanza) return
[docs] def register_callback(self, type_, from_, cb, *, wildcard_resource=True): """ Register a callback function. :param type_: Stanza type to listen for, or :data:`None` for a wildcard match. :param from_: Sender to listen for, or :data:`None` for a full wildcard match. :type from_: :class:`aioxmpp.JID` or :data:`None` :param cb: Callback function to register :param wildcard_resource: Whether to wildcard the resourcepart of the JID. :type wildcard_resource: :class:`bool` :raises ValueError: if another function is already registered for the callback slot. `cb` will be called whenever a stanza with the matching `type_` and `from_` is processed. The following wildcarding rules apply: 1. If the :attr:`~aioxmpp.stanza.StanzaBase.from_` attribute of the stanza has a resourcepart, the following lookup order for callbacks is used: +---------------------------+----------------------------------+----------------------+ |``type_`` |``from_`` |``wildcard_resource`` | +===========================+==================================+======================+ |:attr:`~.StanzaBase.type_` |:attr:`~.StanzaBase.from_` |*any* | +---------------------------+----------------------------------+----------------------+ |:attr:`~.StanzaBase.type_` |*bare* :attr:`~.StanzaBase.from_` |:data:`True` | +---------------------------+----------------------------------+----------------------+ |:data:`None` |:attr:`~.StanzaBase.from_` |*any* | +---------------------------+----------------------------------+----------------------+ |:data:`None` |*bare* :attr:`~.StanzaBase.from_` |:data:`True` | +---------------------------+----------------------------------+----------------------+ |:attr:`~.StanzaBase.type_` |:data:`None` |*any* | +---------------------------+----------------------------------+----------------------+ |:data:`None` |:data:`None` |*any* | +---------------------------+----------------------------------+----------------------+ 2. If the :attr:`~aioxmpp.stanza.StanzaBase.from_` attribute of the stanza does *not* have a resourcepart, the following lookup order for callbacks is used: +---------------------------+---------------------------+----------------------+ |``type_`` |``from_`` |``wildcard_resource`` | +===========================+===========================+======================+ |:attr:`~.StanzaBase.type_` |:attr:`~.StanzaBase.from_` |:data:`False` | +---------------------------+---------------------------+----------------------+ |:data:`None` |:attr:`~.StanzaBase.from_` |:data:`False` | +---------------------------+---------------------------+----------------------+ |:attr:`~.StanzaBase.type_` |:data:`None` |*any* | +---------------------------+---------------------------+----------------------+ |:data:`None` |:data:`None` |*any* | +---------------------------+---------------------------+----------------------+ Only the first callback which matches is called. `wildcard_resource` is ignored if `from_` is a full JID or :data:`None`. .. note:: When the server sends a stanza without from attribute, it is replaced with the bare :attr:`local_jid`, as per :rfc:`6120`. """ # NOQA: E501 if from_ is None or not from_.is_bare: wildcard_resource = False key = (type_, from_, wildcard_resource) if key in self._map: raise ValueError( "only one listener allowed per matcher" ) self._map[type_, from_, wildcard_resource] = cb
[docs] def unregister_callback(self, type_, from_, *, wildcard_resource=True): """ Unregister a callback function. :param type_: Stanza type to listen for, or :data:`None` for a wildcard match. :param from_: Sender to listen for, or :data:`None` for a full wildcard match. :type from_: :class:`aioxmpp.JID` or :data:`None` :param wildcard_resource: Whether to wildcard the resourcepart of the JID. :type wildcard_resource: :class:`bool` The callback must be disconnected with the same arguments as were used to connect it. """ if from_ is None or not from_.is_bare: wildcard_resource = False self._map.pop((type_, from_, wildcard_resource))
[docs] @contextlib.contextmanager def handler_context(self, type_, from_, cb, *, wildcard_resource=True): """ Context manager which temporarily registers a callback. The arguments are the same as for :meth:`register_callback`. When the context is entered, the callback `cb` is registered. When the context is exited, no matter if an exception is raised or not, the callback is unregistered. """ self.register_callback( type_, from_, cb, wildcard_resource=wildcard_resource ) try: yield finally: self.unregister_callback( type_, from_, wildcard_resource=wildcard_resource )
[docs]class SimpleMessageDispatcher(aioxmpp.service.Service, SimpleStanzaDispatcher): """ Dispatch messages to callbacks. This :class:`~aioxmpp.service.Service` dispatches :class:`~aioxmpp.Message` stanzas to callbacks. Callbacks registrations are managed with the :meth:`.SimpleStanzaDispatcher.register_callback` and :meth:`.SimpleStanzaDispatcher.unregister_callback` methods of the base class. The `type_` argument to these methods must be a :class:`aioxmpp.MessageType` or :data:`None` to make any sense. .. note:: It is not recommended to mix the use of a :class:`SimpleMessageDispatcher` with the modern Instant Messaging features provided by the :mod:`aioxmpp.im` module. Both will receive the messages and this may thus lead to duplicate messages. """ @property def local_jid(self): return self.client.local_jid @aioxmpp.service.depsignal(aioxmpp.stream.StanzaStream, "on_message_received") def _feed(self, stanza): super()._feed(stanza)
[docs]class SimplePresenceDispatcher(aioxmpp.service.Service, SimpleStanzaDispatcher): """ Dispatch presences to callbacks. This :class:`~aioxmpp.service.Service` dispatches :class:`~aioxmpp.Presence` stanzas to callbacks. Callbacks registrations are managed with the :meth:`.SimpleStanzaDispatcher.register_callback` and :meth:`.SimpleStanzaDispatcher.unregister_callback` methods of the base class. The `type_` argument to these methods must be a :class:`aioxmpp.MessageType` or :data:`None` to make any sense. .. warning:: It is not recommended to mix the use of a :class:`SimplePresenceDispatcher` with :class:`aioxmpp.RosterClient` and :class:`aioxmpp.PresenceClient`. Both of these register callbacks at the :class:`SimplePresenceDispatcher`. Registering callbacks for different slots will either make those callbacks not be called at all or will make the services miss stanzas. """ @property def local_jid(self): return self.client.local_jid @aioxmpp.service.depsignal(aioxmpp.stream.StanzaStream, "on_presence_received") def _feed(self, stanza): super()._feed(stanza)
def _apply_message_handler(instance, stream, func, type_, from_): return instance.dependencies[SimpleMessageDispatcher].handler_context( type_, from_, func, ) def _apply_presence_handler(instance, stream, func, type_, from_): return instance.dependencies[SimplePresenceDispatcher].handler_context( type_, from_, func, )
[docs]def message_handler(type_, from_): """ Register the decorated function as message handler. :param type_: Message type to listen for :type type_: :class:`~.MessageType` :param from_: Sender JIDs to listen for :type from_: :class:`aioxmpp.JID` or :data:`None` :raise TypeError: if the decorated object is a coroutine function .. seealso:: :meth:`~.StanzaStream.register_message_callback` for more details on the `type_` and `from_` arguments .. versionchanged:: 0.9 This is now based on :class:`aioxmpp.dispatcher.SimpleMessageDispatcher`. """ def decorator(f): if asyncio.iscoroutinefunction(f): raise TypeError("message_handler must not be a coroutine function") aioxmpp.service.add_handler_spec( f, aioxmpp.service.HandlerSpec( (_apply_message_handler, (type_, from_)), require_deps=( SimpleMessageDispatcher, ) ) ) return f return decorator
[docs]def presence_handler(type_, from_): """ Register the decorated function as presence stanza handler. :param type_: Presence type to listen for :type type_: :class:`~.PresenceType` :param from_: Sender JIDs to listen for :type from_: :class:`aioxmpp.JID` or :data:`None` :raise TypeError: if the decorated object is a coroutine function .. seealso:: :meth:`~.StanzaStream.register_presence_callback` for more details on the `type_` and `from_` arguments .. versionchanged:: 0.9 This is now based on :class:`aioxmpp.dispatcher.SimplePresenceDispatcher`. """ def decorator(f): if asyncio.iscoroutinefunction(f): raise TypeError( "presence_handler must not be a coroutine function" ) aioxmpp.service.add_handler_spec( f, aioxmpp.service.HandlerSpec( (_apply_presence_handler, (type_, from_)), require_deps=( SimplePresenceDispatcher, ) ) ) return f return decorator
[docs]def is_message_handler(type_, from_, cb): """ Return true if `cb` has been decorated with :func:`message_handler` for the given `type_` and `from_`. """ try: handlers = aioxmpp.service.get_magic_attr(cb) except AttributeError: return False return aioxmpp.service.HandlerSpec( (_apply_message_handler, (type_, from_)), require_deps=( SimpleMessageDispatcher, ) ) in handlers
[docs]def is_presence_handler(type_, from_, cb): """ Return true if `cb` has been decorated with :func:`presence_handler` for the given `type_` and `from_`. """ try: handlers = aioxmpp.service.get_magic_attr(cb) except AttributeError: return False return aioxmpp.service.HandlerSpec( (_apply_presence_handler, (type_, from_)), require_deps=( SimplePresenceDispatcher, ) ) in handlers