Passed
Push — main ( 45ca12...df1784 )
by
unknown
02:25
created

pincer.client.Client.handle_middleware()   B

Complexity

Conditions 5

Size

Total Lines 51
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 23
dl 0
loc 51
rs 8.8613
c 0
b 0
f 0
cc 5
nop 5

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
# -*- coding: utf-8 -*-
0 ignored issues
show
introduced by
Missing module docstring
Loading history...
2
# MIT License
3
#
4
# Copyright (c) 2021 Pincer
5
#
6
# Permission is hereby granted, free of charge, to any person obtaining
7
# a copy of this software and associated documentation files
8
# (the "Software"), to deal in the Software without restriction,
9
# including without limitation the rights to use, copy, modify, merge,
10
# publish, distribute, sublicense, and/or sell copies of the Software,
11
# and to permit persons to whom the Software is furnished to do so,
12
# subject to the following conditions:
13
#
14
# The above copyright notice and this permission notice shall be
15
# included in all copies or substantial portions of the Software.
16
#
17
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
19
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
20
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
21
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
22
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
23
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
24
25
from __future__ import annotations
26
27
import logging
28
from asyncio import iscoroutinefunction
29
from typing import Optional, Any, Union, Dict, Tuple, List
30
31
from . import __package__
32
from ._config import events
33
from .commands import ChatCommandHandler
0 ignored issues
show
introduced by
Cannot import 'commands' due to syntax error 'invalid syntax (<unknown>, line 80)'
Loading history...
34
from .core.dispatch import GatewayDispatch
35
from .core.gateway import Dispatcher
36
from .core.http import HTTPClient
37
from .exceptions import InvalidEventName
38
from .objects import User
39
from .objects.interactions import Interaction
40
from .utils import get_index, should_pass_cls, Coro
41
from .utils.extraction import get_params
42
43
_log = logging.getLogger(__package__)
44
45
MiddlewareType = Optional[Union[Coro, Tuple[str, List[Any], Dict[str, Any]]]]
46
47
_events: Dict[str, Optional[Union[str, Coro]]] = {}
48
49
for event in events:
50
    event_final_executor = f"on_{event}"
51
52
    # Event middleware for the library.
53
    # Function argument is a payload (GatewayDispatch).
54
55
    # The function must return a string which
56
    # contains the main event key.
57
58
    # As second value a list with arguments,
59
    # and thee third value value must be a dictionary.
60
    # The last two are passed on as *args and **kwargs.
61
62
    # NOTE: These return values must be passed as a tuple!
63
    _events[event] = event_final_executor
64
65
    # The registered event by the client. Do not manually overwrite.
66
    _events[event_final_executor] = None
67
68
69
def middleware(call: str, *, override: bool = False):
70
    """
71
    Middleware are methods which can be registered with this decorator.
72
    These methods are invoked before any ``on_`` event.
73
    As the ``on_`` event is the final call.
74
75
    A default call exists for all events, but some might already be in
76
    use by the library.
77
78
    If you know what you are doing, you can override these default
79
    middleware methods by passing the override parameter.
80
81
    The method to which this decorator is registered must be a coroutine,
82
    and it must return a tuple with the following format:
83
84
    .. code-block:: python
85
86
        tuple(
87
            key for next middleware or final event [str],
88
            args for next middleware/event which will be passed as *args
89
                [list(Any)],
90
            kwargs for next middleware/event which will be passed as
91
                **kwargs [dict(Any)]
92
        )
93
94
    Two parameters are passed to the middleware. The first parameter is
95
    the current socket connection with and the second one is the payload
96
    parameter which is of type :class:`~.core.dispatch.GatewayDispatch`.
97
    This contains the response from the discord API.
98
99
    :Implementation example:
100
101
    .. code-block:: pycon
102
103
        >>> @middleware("ready", override=True)
104
        >>> async def custom_ready(_, payload: GatewayDispatch):
105
        >>>     return "on_ready", [User.from_dict(payload.data.get("user"))]
106
107
        >>> @Client.event
108
        >>> async def on_ready(bot: User):
109
        >>>     print(f"Signed in as {bot}")
110
111
112
    :param call:
113
        The call where the method should be registered.
114
115
    Keyword Arguments:
116
117
    :param override:
118
        Setting this to True will allow you to override existing
119
        middleware. Usage of this is discouraged, but can help you out
120
        of some situations.
121
    """
122
123
    def decorator(func: Coro):
124
        if override:
125
            _log.warning(
126
                "Middleware overriding has been enabled for `%s`."
127
                " This might cause unexpected behavior.", call
128
            )
129
130
        if not override and callable(_events.get(call)):
131
            raise RuntimeError(
132
                f"Middleware event with call `{call}` has "
133
                "already been registered"
134
            )
135
136
        async def wrapper(cls, payload: GatewayDispatch):
137
            _log.debug("`%s` middleware has been invoked", call)
138
139
            return await (
140
                func(cls, payload)
141
                if should_pass_cls(func)
142
                else await func(payload)
143
            )
144
145
        _events[call] = wrapper
146
        return wrapper
147
148
    return decorator
149
150
151
class Client(Dispatcher):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
152
    def __init__(self, token: str):
153
        """
154
        The client is the main instance which is between the programmer
155
            and the discord API.
156
157
        This client represents your bot.
158
159
        :param token:
160
            The secret bot token which can be found in
161
            `<https://discord.com/developers/applications/\<bot_id\>/bot>`_
162
        """
163
        # TODO: Implement intents
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
164
        super().__init__(
165
            token,
166
            handlers={
167
                # Use this event handler for opcode 0.
168
                0: self.event_handler
169
            }
170
        )
171
172
        self.bot: Optional[User] = None
173
        self.__token = token
174
175
    @property
176
    def http(self):
177
        """
178
        Returns a http client with the current client its
179
        authentication credentials.
180
181
        :Usage example:
182
183
        .. code-block:: pycon
184
185
            >>> async with self.http as client:
186
            >>>     await client.post(
187
            ...         '<endpoint>',
188
            ...         {
189
            ...             "foo": "bar",
190
            ...             "bar": "baz",
191
            ...             "baz": "foo"
192
            ...         }
193
            ...    )
194
195
        """
196
        return HTTPClient(self.__token)
197
198
    @property
199
    def chat_commands(self):
200
        """
201
        Get a list of chat command calls which have been registered in
202
        the ChatCommandHandler.
203
        """
204
        return [cmd.app.name for cmd in ChatCommandHandler.register.values()]
205
206
    @staticmethod
207
    def event(coroutine: Coro):
208
        """
209
        Register a Discord gateway event listener. This event will get
210
        called when the client receives a new event update from Discord
211
        which matches the event name.
212
213
        The event name gets pulled from your method name, and this must
214
        start with ``on_``. This forces you to write clean and consistent
215
        code.
216
217
        This decorator can be used in and out of a class, and all
218
        event methods must be coroutines. *(async)*
219
220
        :Example usage:
221
222
        .. code-block:: pycon
223
224
            >>> # Function based
225
            >>> from pincer import Client
226
            >>>
227
            >>> client = Client("token")
228
            >>>
229
            >>> @client.event
230
            >>> async def on_ready():
231
            ...     print(f"Signed in as {client.bot}")
232
            >>>
233
            >>> if __name__ == "__main__":
234
            ...     client.run()
235
236
        .. code-block :: pycon
237
238
            >>> # Class based
239
            >>> from pincer import Client
240
            >>>
241
            >>> class BotClient(Client):
242
            ...     @Client.event
243
            ...     async def on_ready(self):
244
            ...         print(f"Signed in as {self.bot}")
245
            >>>
246
            >>> if __name__ == "__main__":
247
            ...     BotClient("token").run()
248
249
250
        :param coroutine: # TODO: add info
251
252
        :raises TypeError:
253
            If the method is not a coroutine.
254
255
        :raises InvalidEventName:
256
            If the event name does not start with ``on_``, has already
257
            been registered or is not a valid event name.
258
        """
259
260
        if not iscoroutinefunction(coroutine):
261
            raise TypeError(
262
                "Any event which is registered must be a coroutine function"
263
            )
264
265
        name: str = coroutine.__name__.lower()
266
267
        if not name.startswith("on_"):
268
            raise InvalidEventName(
269
                f"The event `{name}` its name must start with `on_`"
270
            )
271
272
        if _events.get(name) is not None:
273
            raise InvalidEventName(
274
                f"The event `{name}` has already been registered or is not "
275
                f"a valid event name."
276
            )
277
278
        _events[name] = coroutine
279
        return coroutine
280
281
    async def handle_middleware(
282
            self,
283
            payload: GatewayDispatch,
284
            key: str,
285
            *args,
286
            **kwargs
287
    ) -> Tuple[Optional[Coro], List[Any], Dict[str, Any]]:
288
        """
289
        Handles all middleware recursively. Stops when it has found an
290
        event name which starts with ``on_``.
291
292
        :param payload:
293
            The original payload for the event.
294
295
        :param key:
296
            The index of the middleware in ``_events``.
297
298
        :param \*args:
299
            The arguments which will be passed to the middleware.
300
301
        :param \*\*kwargs:
302
            The named arguments which will be passed to the middleware.
303
304
        :return:
305
            A tuple where the first element is the final executor
306
            (so the event) its index in ``_events``. The second and third
307
            element are the ``*args`` and ``**kwargs`` for the event.
308
        """
309
        ware: MiddlewareType = _events.get(key)
310
        next_call, arguments, params = ware, [], {}
311
312
        if iscoroutinefunction(ware):
313
            extractable = await ware(self, payload, *args, **kwargs)
314
315
            if not isinstance(extractable, tuple):
316
                raise RuntimeError(
317
                    f"Return type from `{key}` middleware must be tuple. "
318
                )
319
320
            next_call = get_index(extractable, 0, "")
321
            arguments = get_index(extractable, 1, [])
322
            params = get_index(extractable, 2, {})
323
324
        if next_call is None:
325
            raise RuntimeError(f"Middleware `{key}` has not been registered.")
326
327
        return (
328
            (next_call, arguments, params)
329
            if next_call.startswith("on_")
330
            else await self.handle_middleware(
331
                payload, next_call, *arguments, **params
332
            )
333
        )
334
335
    async def event_handler(self, _, payload: GatewayDispatch):
336
        """
337
        Handles all payload events with opcode 0.
338
339
        :param _:
340
            Socket param, but this isn't required for this handler. So
341
            its just a filler parameter, doesn't matter what is passed.
342
343
        :param payload:
344
            The payload sent from the Discord gateway, this contains the
345
            required data for the client to know what event it is and
346
            what specifically happened.
347
        """
348
        event_name = payload.event_name.lower()
349
350
        key, args, kwargs = await self.handle_middleware(payload, event_name)
351
352
        call = _events.get(key)
353
354
        if iscoroutinefunction(call):
355
            if should_pass_cls(call):
356
                await call(self, *args, **kwargs)
357
            else:
358
                await call(*args, **kwargs)
359
360
    @middleware("ready")
361
    async def on_ready_middleware(self, payload: GatewayDispatch):
362
        """
363
        Middleware for ``on_ready`` event.
364
365
        :param payload:
366
            The data received from the ready event.
367
        """
368
        self.bot = User.from_dict(payload.data.get("user"))
369
        await ChatCommandHandler(self).initialize()
370
        return "on_ready",
371
372
    @middleware("interaction_create")
373
    async def on_interaction_middleware(self, payload: GatewayDispatch):
374
        """
375
        Middleware for ``on_interaction``, which handles command
376
        execution.
377
378
        :param payload:
379
            The data received from the interaction event.
380
        """
381
        interaction: Interaction = Interaction.from_dict(payload.data)
382
        command = ChatCommandHandler.register.get(interaction.data.name)
383
384
        if command:
385
            defaults = {param: None for param in get_params(command.call)}
386
            params = {opt.name: opt.value for opt in interaction.data.options}
387
            kwargs = {**defaults, **params}
388
389
            if should_pass_cls(command.call):
390
                kwargs["self"] = self
391
392
            res = await command.call(**kwargs)
393
394
            if res:
395
                async with self.http as http:
396
                    await http.post(
397
                        f"interactions/{interaction.id}/{interaction.token}/callback",
398
                        {
399
                            "type": 4,
400
                            "data": {
401
                                "content": str(res)
402
                            }
403
                        })
404
405
        return "on_interaction_create", [interaction]
406
407
408
Bot = Client
409