api.py 7.84 KB
Newer Older
1 2
from typing import Any, Optional
from asyncio import iscoroutinefunction, Future
louiz’'s avatar
louiz’ committed
3
from slixmpp.xmlstream import JID
4 5 6 7 8 9 10


class APIWrapper(object):

    def __init__(self, api, name):
        self.api = api
        self.name = name
11 12
        if name not in self.api.settings:
            self.api.settings[name] = {}
13 14 15 16 17 18 19 20

    def __getattr__(self, attr):
        """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
21
            def partial(handler, op, jid=None, node=None, default=False):
22
                register = getattr(self.api, attr)
23
                return register(handler, self.name, op, jid, node, default)
Lance Stout's avatar
Lance Stout committed
24
            return partial
25
        elif attr == 'register_default':
Lance Stout's avatar
Lance Stout committed
26
            def partial(handler, op, jid=None, node=None):
27
                return getattr(self.api, attr)(handler, self.name, op)
Lance Stout's avatar
Lance Stout committed
28
            return partial
29
        elif attr in ('run', 'restore_default', 'unregister'):
Lance Stout's avatar
Lance Stout committed
30
            def partial(*args, **kwargs):
31
                return getattr(self.api, attr)(self.name, *args, **kwargs)
Lance Stout's avatar
Lance Stout committed
32
            return partial
33 34 35
        return None

    def __getitem__(self, attr):
Lance Stout's avatar
Lance Stout committed
36
        def partial(jid=None, node=None, ifrom=None, args=None):
37
            return self.api.run(self.name, attr, jid, node, ifrom, args)
Lance Stout's avatar
Lance Stout committed
38
        return partial
39 40 41 42 43 44 45 46


class APIRegistry(object):

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

49
    def _setup(self, ctype: str, op: str):
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
        """Initialize the API callback dictionaries.

        :param string ctype: The name of the API to initialize.
        :param string op: The API operation to initialize.
        """
        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': {}}

66
    def wrap(self, ctype: str) -> APIWrapper:
67 68 69
        """Return a wrapper object that targets a specific API."""
        return APIWrapper(self, ctype)

70
    def purge(self, ctype: str):
71 72 73 74 75
        """Remove all information for a given API."""
        del self.settings[ctype]
        del self._handler_defaults[ctype]
        del self._handlers[ctype]

76 77 78
    def run(self, ctype: str, op: str, jid: Optional[JID] = None,
            node: Optional[str] = None, ifrom: Optional[JID] = None,
            args: Any = None) -> Future:
79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96
        """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:

        JID   | node  | Handler
        ==============================
        Given | Given | Node handler
        Given | None  | JID handler
        None  | None  | Global handler

        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.

97 98 99 100 101 102 103 104 105 106
        .. 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.
107 108 109
        """
        self._setup(ctype, op)

110
        if not jid:
111
            jid = self.xmpp.boundjid
112
        elif jid and not isinstance(jid, JID):
113
            jid = JID(jid)
114 115
        elif jid == JID(''):
            jid = self.xmpp.boundjid
116 117 118 119 120 121 122 123 124 125

        if node is None:
            node = ''

        if self.xmpp.is_component:
            if self.settings[ctype].get('component_bare', False):
                jid = jid.bare
            else:
                jid = jid.full
        else:
126
            if self.settings[ctype].get('client_bare', False):
127 128 129 130
                jid = jid.bare
            else:
                jid = jid.full

131 132
        jid = JID(jid)

133 134 135 136 137 138 139 140
        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:
141 142 143 144 145 146 147
                if iscoroutinefunction(handler):
                    return self.xmpp.wrap(handler(jid, node, ifrom, args))
                else:
                    future = Future()
                    result = handler(jid, node, ifrom, args)
                    future.set_result(result)
                    return future
148 149 150 151 152 153 154 155 156
            except TypeError:
                # To preserve backward compatibility, drop the ifrom
                # parameter for existing handlers that don't understand it.
                return handler(jid, node, args)

    def register(self, handler, ctype, op, jid=None, node=None, default=False):
        """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
157 158
        specificity of the provided JID+node combination.

159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
        See :meth:`~ApiRegistry.run` for more details.

        :param string ctype: The name of the API to use.
        :param string op: The API operation to perform.
        :param JID jid: Optionally provide specific JID.
        :param string node: Optionally provide specific node.
        """
        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

176 177 178
        if default:
            self.register_default(handler, ctype, op)

179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
    def register_default(self, handler, ctype, op):
        """Register a default, global handler for an operation.

        :param func handler: The default, global handler for the operation.
        :param string ctype: The name of the API to modify.
        :param string op: The API operation to use.
        """
        self._setup(ctype, op)
        self._handler_defaults[ctype][op] = handler

    def unregister(self, ctype, op, jid=None, node=None):
        """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.

        :param string ctype: The name of the API to use.
        :param string op: The API operation to perform.
        :param JID jid: Optionally provide specific JID.
        :param string node: Optionally provide specific node.
        """
        self._setup(ctype, op)
        self.register(None, ctype, op, jid, node)

    def restore_default(self, ctype, op, jid=None, node=None):
        """Reset an API callback to use a default handler.

        :param string ctype: The name of the API to use.
        :param string op: The API operation to perform.
        :param JID jid: Optionally provide specific JID.
        :param string node: Optionally provide specific node.
        """
        self.unregister(ctype, op, jid, node)
        self.register(self._handler_defaults[ctype][op], ctype, op, jid, node)