api.py 8.68 KB
Newer Older
mathieui's avatar
mathieui committed
1
from typing import Any, Optional, Callable
2
from asyncio import iscoroutinefunction, Future
louiz’'s avatar
louiz’ committed
3
from slixmpp.xmlstream import JID
4

mathieui's avatar
mathieui committed
5 6 7 8
APIHandler = Callable[
    [Optional[JID], Optional[str], Optional[JID], Any],
    Any
]
9 10

class APIWrapper(object):
mathieui's avatar
mathieui committed
11 12 13 14 15 16
    """Slixmpp API wrapper.

    This class provide a shortened binding to access ``self.api`` from
    plugins without having to specify the plugin name or the global
    :class:`~.APIRegistry`.
    """
17 18 19 20

    def __init__(self, api, name):
        self.api = api
        self.name = name
21 22
        if name not in self.api.settings:
            self.api.settings[name] = {}
23

mathieui's avatar
mathieui committed
24
    def __getattr__(self, attr: str):
25 26 27 28 29 30
        """Curry API management commands with the API name."""
        if attr == 'name':
            return self.name
        elif attr == 'settings':
            return self.api.settings[self.name]
        elif attr == 'register':
Lance Stout's avatar
Lance Stout committed
31
            def partial(handler, op, jid=None, node=None, default=False):
32
                register = getattr(self.api, attr)
33
                return register(handler, self.name, op, jid, node, default)
Lance Stout's avatar
Lance Stout committed
34
            return partial
35
        elif attr == 'register_default':
mathieui's avatar
mathieui committed
36
            def partial1(handler, op, jid=None, node=None):
37
                return getattr(self.api, attr)(handler, self.name, op)
mathieui's avatar
mathieui committed
38
            return partial1
39
        elif attr in ('run', 'restore_default', 'unregister'):
mathieui's avatar
mathieui committed
40
            def partial2(*args, **kwargs):
41
                return getattr(self.api, attr)(self.name, *args, **kwargs)
mathieui's avatar
mathieui committed
42
            return partial2
43 44 45
        return None

    def __getitem__(self, attr):
Lance Stout's avatar
Lance Stout committed
46
        def partial(jid=None, node=None, ifrom=None, args=None):
47
            return self.api.run(self.name, attr, jid, node, ifrom, args)
Lance Stout's avatar
Lance Stout committed
48
        return partial
49 50 51


class APIRegistry(object):
mathieui's avatar
mathieui committed
52 53 54 55 56
    """API Registry.

    This class is the global Slixmpp API registry, on which any handler will
    be registed.
    """
57 58 59 60 61

    def __init__(self, xmpp):
        self._handlers = {}
        self._handler_defaults = {}
        self.xmpp = xmpp
Lance Stout's avatar
Lance Stout committed
62
        self.settings = {}
63

64
    def _setup(self, ctype: str, op: str):
65 66
        """Initialize the API callback dictionaries.

mathieui's avatar
mathieui committed
67 68
        :param ctype: The name of the API to initialize.
        :param op: The API operation to initialize.
69 70 71 72 73 74 75 76 77 78 79 80
        """
        if ctype not in self.settings:
            self.settings[ctype] = {}
        if ctype not in self._handler_defaults:
            self._handler_defaults[ctype] = {}
        if ctype not in self._handlers:
            self._handlers[ctype] = {}
        if op not in self._handlers[ctype]:
            self._handlers[ctype][op] = {'global': None,
                                         'jid': {},
                                         'node': {}}

81
    def wrap(self, ctype: str) -> APIWrapper:
82 83 84
        """Return a wrapper object that targets a specific API."""
        return APIWrapper(self, ctype)

mathieui's avatar
mathieui committed
85
    def purge(self, ctype: str) -> None:
86 87 88 89 90
        """Remove all information for a given API."""
        del self.settings[ctype]
        del self._handler_defaults[ctype]
        del self._handlers[ctype]

91 92 93
    def run(self, ctype: str, op: str, jid: Optional[JID] = None,
            node: Optional[str] = None, ifrom: Optional[JID] = None,
            args: Any = None) -> Future:
94 95 96 97 98
        """Execute an API callback, based on specificity.

        The API callback that is executed is chosen based on the combination
        of the provided JID and node:

mathieui's avatar
mathieui committed
99 100 101 102 103 104 105 106
        ====== ======= ===================
        JID     node    Handler
        ====== ======= ===================
        Given   Given   Node + JID handler
        Given   None    JID handler
        None    Given   Node handler
        None    None    Global handler
        ====== ======= ===================
107 108 109 110 111 112 113 114

        A node handler is responsible for servicing a single node at a single
        JID, while a JID handler may respond for any node at a given JID, and
        the global handler will answer to any JID+node combination.

        Handlers should check that the JID ``ifrom`` is authorized to perform
        the desired action.

115 116 117 118 119 120 121 122 123 124
        .. versionchanged:: 1.8.0
            ``run()`` always returns a future, if the handler is a coroutine
            the future should be awaited on.

        :param ctype: The name of the API to use.
        :param op: The API operation to perform.
        :param jid: Optionally provide specific JID.
        :param node: Optionally provide specific node.
        :param ifrom: Optionally provide the requesting JID.
        :param args: Optional arguments to the handler.
125 126 127
        """
        self._setup(ctype, op)

128
        if not jid:
129
            jid = self.xmpp.boundjid
130
        elif jid and not isinstance(jid, JID):
131
            jid = JID(jid)
132 133
        elif jid == JID(''):
            jid = self.xmpp.boundjid
mathieui's avatar
mathieui committed
134
        assert jid is not None
135 136 137 138 139 140

        if node is None:
            node = ''

        if self.xmpp.is_component:
            if self.settings[ctype].get('component_bare', False):
mathieui's avatar
mathieui committed
141
                jid_str = jid.bare
142
            else:
mathieui's avatar
mathieui committed
143
                jid_str = jid.full
144
        else:
145
            if self.settings[ctype].get('client_bare', False):
mathieui's avatar
mathieui committed
146
                jid_str = jid.bare
147
            else:
mathieui's avatar
mathieui committed
148
                jid_str = jid.full
149

mathieui's avatar
mathieui committed
150
        jid = JID(jid_str)
151

152 153 154 155 156 157 158 159
        handler = self._handlers[ctype][op]['node'].get((jid, node), None)
        if handler is None:
            handler = self._handlers[ctype][op]['jid'].get(jid, None)
        if handler is None:
            handler = self._handlers[ctype][op].get('global', None)

        if handler:
            try:
160 161 162
                if iscoroutinefunction(handler):
                    return self.xmpp.wrap(handler(jid, node, ifrom, args))
                else:
mathieui's avatar
mathieui committed
163
                    future: Future = Future()
164 165 166
                    result = handler(jid, node, ifrom, args)
                    future.set_result(result)
                    return future
167 168 169 170
            except TypeError:
                # To preserve backward compatibility, drop the ifrom
                # parameter for existing handlers that don't understand it.
                return handler(jid, node, args)
mathieui's avatar
mathieui committed
171 172 173
        future = Future()
        future.set_result(None)
        return future
174

mathieui's avatar
mathieui committed
175
    def register(self, handler: Optional[APIHandler], ctype: str, op: str,
mathieui's avatar
mathieui committed
176 177
                 jid: Optional[JID] = None, node: Optional[str] = None,
                 default: bool = False):
178 179 180
        """Register an API callback, with JID+node specificity.

        The API callback can later be executed based on the
Lance Stout's avatar
Lance Stout committed
181 182
        specificity of the provided JID+node combination.

mathieui's avatar
mathieui committed
183
        See :meth:`~.APIRegistry.run` for more details.
184

mathieui's avatar
mathieui committed
185 186 187 188
        :param ctype: The name of the API to use.
        :param op: The API operation to perform.
        :param jid: Optionally provide specific JID.
        :param node: Optionally provide specific node.
189 190 191 192 193 194 195 196 197 198 199
        """
        self._setup(ctype, op)
        if jid is None and node is None:
            if handler is None:
                handler = self._handler_defaults[op]
            self._handlers[ctype][op]['global'] = handler
        elif jid is not None and node is None:
            self._handlers[ctype][op]['jid'][jid] = handler
        else:
            self._handlers[ctype][op]['node'][(jid, node)] = handler

200 201 202
        if default:
            self.register_default(handler, ctype, op)

mathieui's avatar
mathieui committed
203
    def register_default(self, handler, ctype: str, op: str):
204 205
        """Register a default, global handler for an operation.

mathieui's avatar
mathieui committed
206 207 208
        :param handler: The default, global handler for the operation.
        :param ctype: The name of the API to modify.
        :param op: The API operation to use.
209 210 211 212
        """
        self._setup(ctype, op)
        self._handler_defaults[ctype][op] = handler

mathieui's avatar
mathieui committed
213 214
    def unregister(self, ctype: str, op: str, jid: Optional[JID] = None,
                   node: Optional[str] = None):
215 216 217 218 219 220 221
        """Remove an API callback.

        The API callback chosen for removal is based on the
        specificity of the provided JID+node combination.

        See :meth:`~ApiRegistry.run` for more details.

mathieui's avatar
mathieui committed
222 223 224 225
        :param ctype: The name of the API to use.
        :param op: The API operation to perform.
        :param jid: Optionally provide specific JID.
        :param node: Optionally provide specific node.
226 227 228 229
        """
        self._setup(ctype, op)
        self.register(None, ctype, op, jid, node)

mathieui's avatar
mathieui committed
230 231
    def restore_default(self, ctype: str, op: str, jid: Optional[JID] = None,
                        node: Optional[str] = None):
232 233
        """Reset an API callback to use a default handler.

mathieui's avatar
mathieui committed
234 235 236 237
        :param ctype: The name of the API to use.
        :param op: The API operation to perform.
        :param jid: Optionally provide specific JID.
        :param node: Optionally provide specific node.
238 239 240
        """
        self.unregister(ctype, op, jid, node)
        self.register(self._handler_defaults[ctype][op], ctype, op, jid, node)