Passed
Pull Request — main (#75)
by
unknown
01:27
created

pincer.client   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 400
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 24
eloc 117
dl 0
loc 400
rs 10
c 0
b 0
f 0

1 Function

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

10 Methods

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