Passed
Push — main ( d0b3ac...149404 )
by
unknown
01:45
created

pincer.client   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 360
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 20
eloc 109
dl 0
loc 360
rs 10
c 0
b 0
f 0

1 Function

Rating   Name   Duplication   Size   Complexity  
A event_middleware() 0 80 5

6 Methods

Rating   Name   Duplication   Size   Complexity  
A Client.event_handler() 0 24 3
A Client.event() 0 74 4
A Client.run() 0 4 1
B Client.handle_middleware() 0 51 5
A Client.chat_commands() 0 7 1
A Client.__init__() 0 35 1
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, run
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 89)'
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 .middleware import middleware
39
from .objects import User, Intents
40
from .utils import get_index, should_pass_cls, Coro
41
42
_log = logging.getLogger(__package__)
43
44
MiddlewareType = Optional[Union[Coro, Tuple[str, List[Any], Dict[str, Any]]]]
45
46
_events: Dict[str, Optional[Union[str, Coro]]] = {}
47
48
for event in events:
49
    event_final_executor = f"on_{event}"
50
51
    # Event middleware for the library.
52
    # Function argument is a payload (GatewayDispatch).
53
54
    # The function must return a string which
55
    # contains the main event key.
56
57
    # As second value a list with arguments,
58
    # and the third value value must be a dictionary.
59
    # The last two are passed on as *args and **kwargs.
60
61
    # NOTE: These return values must be passed as a tuple!
62
    _events[event] = event_final_executor
63
64
    # The registered event by the client. Do not manually overwrite.
65
    _events[event_final_executor] = None
66
67
68
def event_middleware(call: str, *, override: bool = False):
69
    """
70
    Middleware are methods which can be registered with this decorator.
71
    These methods are invoked before any ``on_`` event.
72
    As the ``on_`` event is the final call.
73
74
    A default call exists for all events, but some might already be in
75
    use by the library.
76
77
    If you know what you are doing, you can override these default
78
    middleware methods by passing the override parameter.
79
80
    The method to which this decorator is registered must be a coroutine,
81
    and it must return a tuple with the following format:
82
83
    .. code-block:: python
84
85
        tuple(
86
            key for next middleware or final event [str],
87
            args for next middleware/event which will be passed as *args
88
                [list(Any)],
89
            kwargs for next middleware/event which will be passed as
90
                **kwargs [dict(Any)]
91
        )
92
93
    Two parameters are passed to the middleware. The first parameter is
94
    the current socket connection with and the second one is the payload
95
    parameter which is of type :class:`~.core.dispatch.GatewayDispatch`.
96
    This contains the response from the discord API.
97
98
    :Implementation example:
99
100
    .. code-block:: pycon
101
102
        >>> @event_middleware("ready", override=True)
103
        >>> async def custom_ready(_, payload: GatewayDispatch):
104
        >>>     return "on_ready", [User.from_dict(payload.data.get("user"))]
105
106
        >>> @Client.event
107
        >>> async def on_ready(bot: User):
108
        >>>     print(f"Signed in as {bot}")
109
110
111
    :param call:
112
        The call where the method should be registered.
113
114
    Keyword Arguments:
115
116
    :param override:
117
        Setting this to True will allow you to override existing
118
        middleware. Usage of this is discouraged, but can help you out
119
        of some situations.
120
    """
121
122
    def decorator(func: Coro):
123
        if override:
124
            _log.warning(
125
                "Middleware overriding has been enabled for `%s`."
126
                " This might cause unexpected behavior.", call
127
            )
128
129
        if not override and callable(_events.get(call)):
130
            raise RuntimeError(
131
                f"Middleware event with call `{call}` has "
132
                "already been registered"
133
            )
134
135
        async def wrapper(cls, payload: GatewayDispatch):
136
            _log.debug("`%s` middleware has been invoked", call)
137
138
            return await (
139
                func(cls, payload)
140
                if should_pass_cls(func)
141
                else await func(payload)
142
            )
143
144
        _events[call] = wrapper
145
        return wrapper
146
147
    return decorator
148
149
150
for event, middleware in middleware.items():
151
    event_middleware(event)(middleware)
152
153
154
class Client(Dispatcher):
0 ignored issues
show
introduced by
Missing class docstring
Loading history...
155
    def __init__(
156
            self,
157
            token: str, *,
158
            received: str = None,
159
            intents: Intents = None
160
    ):
161
        """
162
        The client is the main instance which is between the programmer
163
            and the discord API.
164
165
        This client represents your bot.
166
167
        :param token:
168
            The secret bot token which can be found in
169
            `<https://discord.com/developers/applications/<bot_id>/bot>`_
170
171
        :param received:
172
            The default message which will be sent when no response is
173
            given.
174
175
        :param intents:
176
            The discord intents for your client.
177
        """
178
        super().__init__(
179
            token,
180
            handlers={
181
                # Use this event handler for opcode 0.
182
                0: self.event_handler
183
            },
184
            intents=intents or Intents.NONE
185
        )
186
187
        self.bot: Optional[User] = None
188
        self.__received = received or "Command arrived successfully!"
189
        self.http = HTTPClient(token)
190
191
    @property
192
    def chat_commands(self):
193
        """
194
        Get a list of chat command calls which have been registered in
195
        the ChatCommandHandler.
196
        """
197
        return [cmd.app.name for cmd in ChatCommandHandler.register.values()]
198
199
    @staticmethod
200
    def event(coroutine: Coro):
201
        """
202
        Register a Discord gateway event listener. This event will get
203
        called when the client receives a new event update from Discord
204
        which matches the event name.
205
206
        The event name gets pulled from your method name, and this must
207
        start with ``on_``. This forces you to write clean and consistent
208
        code.
209
210
        This decorator can be used in and out of a class, and all
211
        event methods must be coroutines. *(async)*
212
213
        :Example usage:
214
215
        .. code-block:: pycon
216
217
            >>> # Function based
218
            >>> from pincer import Client
219
            >>>
220
            >>> client = Client("token")
221
            >>>
222
            >>> @client.event
223
            >>> async def on_ready():
224
            ...     print(f"Signed in as {client.bot}")
225
            >>>
226
            >>> if __name__ == "__main__":
227
            ...     client.run()
228
229
        .. code-block :: pycon
230
231
            >>> # Class based
232
            >>> from pincer import Client
233
            >>>
234
            >>> class BotClient(Client):
235
            ...     @Client.event
236
            ...     async def on_ready(self):
237
            ...         print(f"Signed in as {self.bot}")
238
            >>>
239
            >>> if __name__ == "__main__":
240
            ...     BotClient("token").run()
241
242
243
        :param coroutine: # TODO: add info
244
245
        :raises TypeError:
246
            If the method is not a coroutine.
247
248
        :raises InvalidEventName:
249
            If the event name does not start with ``on_``, has already
250
            been registered or is not a valid event name.
251
        """
252
253
        if not iscoroutinefunction(coroutine):
254
            raise TypeError(
255
                "Any event which is registered must be a coroutine function"
256
            )
257
258
        name: str = coroutine.__name__.lower()
259
260
        if not name.startswith("on_"):
261
            raise InvalidEventName(
262
                f"The event named `{name}` must start with `on_`"
263
            )
264
265
        if _events.get(name) is not None:
266
            raise InvalidEventName(
267
                f"The event `{name}` has already been registered or is not "
268
                f"a valid event name."
269
            )
270
271
        _events[name] = coroutine
272
        return coroutine
273
274
    def run(self):
275
        """Start the event listener"""
276
        self.start_loop()
277
        run(self.http.close())
278
279
    async def handle_middleware(
280
            self,
281
            payload: GatewayDispatch,
282
            key: str,
283
            *args,
284
            **kwargs
285
    ) -> Tuple[Optional[Coro], List[Any], Dict[str, Any]]:
286
        """
287
        Handles all middleware recursively. Stops when it has found an
288
        event name which starts with ``on_``.
289
290
        :param payload:
291
            The original payload for the event.
292
293
        :param key:
294
            The index of the middleware in ``_events``.
295
296
        :param \\*args:
297
            The arguments which will be passed to the middleware.
298
299
        :param \\*\\*kwargs:
300
            The named arguments which will be passed to the middleware.
301
302
        :return:
303
            A tuple where the first element is the final executor
304
            (so the event) its index in ``_events``. The second and third
305
            element are the ``*args`` and ``**kwargs`` for the event.
306
        """
307
        ware: MiddlewareType = _events.get(key)
308
        next_call, arguments, params = ware, [], {}
309
310
        if iscoroutinefunction(ware):
311
            extractable = await ware(self, payload, *args, **kwargs)
312
313
            if not isinstance(extractable, tuple):
314
                raise RuntimeError(
315
                    f"Return type from `{key}` middleware must be tuple. "
316
                )
317
318
            next_call = get_index(extractable, 0, "")
319
            arguments = get_index(extractable, 1, [])
320
            params = get_index(extractable, 2, {})
321
322
        if next_call is None:
323
            raise RuntimeError(f"Middleware `{key}` has not been registered.")
324
325
        return (
326
            (next_call, arguments, params)
327
            if next_call.startswith("on_")
328
            else await self.handle_middleware(
329
                payload, next_call, *arguments, **params
330
            )
331
        )
332
333
    async def event_handler(self, _, payload: GatewayDispatch):
334
        """
335
        Handles all payload events with opcode 0.
336
337
        :param _:
338
            Socket param, but this isn't required for this handler. So
339
            its just a filler parameter, doesn't matter what is passed.
340
341
        :param payload:
342
            The payload sent from the Discord gateway, this contains the
343
            required data for the client to know what event it is and
344
            what specifically happened.
345
        """
346
        event_name = payload.event_name.lower()
347
348
        key, args, kwargs = await self.handle_middleware(payload, event_name)
349
350
        call = _events.get(key)
351
352
        if iscoroutinefunction(call):
353
            if should_pass_cls(call):
354
                await call(self, *args, **kwargs)
355
            else:
356
                await call(*args, **kwargs)
357
358
359
Bot = Client
360