452 lines
15 KiB
Python
452 lines
15 KiB
Python
########################################################################
|
|
# 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
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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))
|
|
|
|
@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
|
|
)
|
|
|
|
|
|
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)
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
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
|
|
|
|
|
|
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
|
|
|
|
|
|
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
|
|
|
|
|
|
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
|