Passed
Push — main ( 0c164a...90232b )
by
unknown
01:54 queued 10s
created

pincer.core.gateway.Dispatcher.__init__()   B

Complexity

Conditions 2

Size

Total Lines 72
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 37
dl 0
loc 72
rs 8.9919
c 0
b 0
f 0
cc 2
nop 2

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 get_event_loop, AbstractEventLoop, ensure_future
29
from platform import system
30
from typing import Dict, Callable, Awaitable
31
32
from websockets import connect
33
from websockets.exceptions import ConnectionClosedError
34
from websockets.legacy.client import WebSocketClientProtocol
35
36
from pincer import __package__
1 ignored issue
show
Bug Best Practice introduced by
This seems to re-define the built-in __package__.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
37
from pincer._config import GatewayConfig
38
from pincer.core.dispatch import GatewayDispatch
39
from pincer.core.handlers.heartbeat import (
40
    handle_hello, handle_heartbeat, update_sequence
41
)
42
43
from pincer.exceptions import (
44
    PincerError, InvalidTokenError, UnhandledException,
45
    _InternalPerformReconnectError, DisallowedIntentsError
46
)
47
48
Handler = Callable[[WebSocketClientProtocol, GatewayDispatch], Awaitable[None]]
49
log = logging.getLogger(__package__)
50
51
52
class Dispatcher:
53
    """
54
    The Dispatcher handles all interactions with discord websocket API.
55
    This also contains the main event loop, and handles the heartbeat.
56
57
    Running the dispatcher will create a connection with the
58
    Discord WebSocket API on behalf of the provided token.
59
60
    This token must be a bot token.
61
    (Which can be found on `/developers/applications/<bot_id>/bot`)
62
    """
63
64
    # TODO: Add intents argument
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
65
    # TODO: Add handlers argument
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
66
    def __init__(self, token: str) -> None:
67
        """
68
        :param token: Bot token for discord's API.
69
        """
70
71
        if len(token) != 59:
72
            raise InvalidTokenError(
73
                "Discord Token must have exactly 59 characters."
74
            )
75
76
        self.__token = token
77
        self.__keep_alive = True
78
79
        async def identify_and_handle_hello(
80
            socket: WebSocketClientProtocol,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
81
            payload: GatewayDispatch
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
82
        ):
83
            """
84
            Identifies the client to the Discord Websocket API, this
85
            gets done when the client receives the `hello` (opcode 10)
86
            message from discord. Right after we send our identification
87
            the heartbeat starts.
88
89
            :param socket:
90
                The current socket, which can be used to interact
91
                with the Discord API.
92
93
            :param payload: The received payload from Discord.
94
            """
95
            log.debug("Sending authentication/identification message.")
96
97
            await socket.send(
98
                str(
99
                    GatewayDispatch(
100
                        2, {
101
                            "token": token,
102
                            "intents": 0,
103
                            "properties": {
104
                                "$os": system(),
105
                                "$browser": __package__,
106
                                "$device": __package__
107
                            }
108
                        }
109
                    )
110
                )
111
            )
112
113
            await handle_hello(socket, payload)
114
115
        async def handle_reconnect(_, payload: GatewayDispatch):
116
            """
117
            Closes the client and then reconnects it.
118
            """
119
            log.debug("Reconnecting client...")
120
            self.close()
121
122
            update_sequence(payload.seq)
123
            self.run()
124
125
        self.__dispatch_handlers: Dict[int, Handler] = {
126
            7: handle_reconnect,
127
            9: handle_reconnect,
128
            10: identify_and_handle_hello,
129
            11: handle_heartbeat
130
        }
131
132
        self.__dispatch_errors: Dict[int, PincerError] = {
133
            4000: _InternalPerformReconnectError(),
134
            4004: InvalidTokenError(),
135
            4007: _InternalPerformReconnectError(),
136
            4009: _InternalPerformReconnectError(),
137
            4014: DisallowedIntentsError()
138
        }
139
140
    async def handler_manager(
141
        self,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
142
        socket: WebSocketClientProtocol,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
143
        payload: GatewayDispatch,
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
144
        loop: AbstractEventLoop
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation before block (add 4 spaces).
Loading history...
145
    ):
146
        """
147
        This manages all handles for given OP codes.
148
        This method gets invoked for every message that is received from
149
        Discord.
150
151
        :param socket:
152
            The current socket, which can be used to interact
153
            with the Discord API.
154
        :param payload: The received payload from Discord.
155
        :param loop: The current async loop on which the future is bound.
156
        """
157
        log.debug(
0 ignored issues
show
Coding Style Best Practice introduced by
Use lazy % formatting in logging functions
Loading history...
158
            f"New event received, checking if handler exists for opcode: "
0 ignored issues
show
introduced by
Using an f-string that does not have any interpolated variables
Loading history...
159
            + str(payload.op)
160
        )
161
162
        handler: Handler = self.__dispatch_handlers.get(payload.op)
163
164
        if not handler:
165
            log.error(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
166
                f"No handler was found for opcode {payload.op}, "
167
                "please report this to the pincer dev team!"
168
            )
169
170
            raise UnhandledException(f"Unhandled payload: {payload}")
171
172
        log.debug("Event handler found, ensuring async future in current loop.")
173
        ensure_future(handler(socket, payload), loop=loop)
174
175
    async def __dispatcher(self, loop: AbstractEventLoop):
176
        """
177
        The main event loop.
178
        This handles all interactions with the websocket API.
179
        """
180
        log.debug(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
181
            f"Establishing websocket connection with `{GatewayConfig.uri()}`"
182
        )
183
184
        async with connect(GatewayConfig.uri()) as socket:
185
            log.debug(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
186
                "Successfully established websocket connection with "
187
                f"`{GatewayConfig.uri()}`"
188
            )
189
190
            while self.__keep_alive:
191
                try:
192
                    log.debug("Waiting for new event.")
193
                    await self.handler_manager(
194
                        socket,
195
                        GatewayDispatch.from_string(await socket.recv()),
196
                        loop
197
                    )
198
199
                except ConnectionClosedError as exc:
200
                    log.debug(
0 ignored issues
show
introduced by
Use lazy % formatting in logging functions
Loading history...
201
                        f"The connection with `{GatewayConfig.uri()}` "
202
                        f"has been broken unexpectedly. "
203
                        f"({exc.code}, {exc.reason})"
204
                    )
205
206
                    self.close()
207
                    exception = self.__dispatch_errors.get(exc.code)
208
209
                    if isinstance(exception, _InternalPerformReconnectError):
210
                        update_sequence(0)
211
                        self.close()
212
                        return self.run()
213
214
                    raise exception or UnhandledException(
215
                        f"Dispatch error ({exc.code}): {exc.reason}"
216
                    )
217
218
    def run(self):
219
        """
220
        Instantiate the dispatcher, this will create a connection to the
221
        Discord websocket API on behalf of the client who's token has
222
        been passed.
223
        """
224
        log.debug("Starting GatewayDispatcher")
225
        loop = get_event_loop()
226
        loop.run_until_complete(self.__dispatcher(loop))
227
        loop.close()
228
229
        # Prevent client from disconnecting
230
        if self.__keep_alive:
231
            log.debug("Reconnecting client!")
232
            self.run()
233
234
    def close(self):
235
        """
236
        Stop the dispatcher from listening and responding to gateway
237
        events. This should let the client close on itself.
238
        """
239
        log.debug(
240
            "Setting keep_alive to False, "
241
            "this will terminate the heartbeat."
242
        )
243
244
        self.__keep_alive = False
245