Passed
Push — main ( a47b7f...8b0789 )
by Yohann
01:13
created

pincer.core.gateway   A

Complexity

Total Complexity 11

Size/Duplication

Total Lines 169
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 11
eloc 63
dl 0
loc 169
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A Dispatcher.run() 0 10 1
A Dispatcher.__dispatcher() 0 21 5
A Dispatcher.close() 0 7 1
A Dispatcher.__init__() 0 46 2
A Dispatcher.handler_manager() 0 22 2
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 a copy
7
# of this software and associated documentation files (the "Software"), to deal
8
# in the Software without restriction, including without limitation the rights
9
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
# copies of the Software, and to permit persons to whom the Software is
11
# furnished to do so, subject to the following conditions:
12
#
13
# The above copyright notice and this permission notice shall be included in all
14
# copies or substantial portions of the Software.
15
#
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
# SOFTWARE.
23
from __future__ import annotations
24
25
from asyncio import get_event_loop, AbstractEventLoop, ensure_future
26
from platform import system
27
from typing import Dict, Callable, Awaitable
28
29
from websockets import connect
1 ignored issue
show
Bug introduced by
The name connect does not seem to exist in module websockets.
Loading history...
30
from websockets.exceptions import ConnectionClosedError
31
from websockets.legacy.client import WebSocketClientProtocol
32
33
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...
34
from pincer._config import GatewayConfig
35
# TODO: Implement logging
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
36
from pincer.core.dispatch import GatewayDispatch
37
from pincer.core.handlers.heartbeat import handle_hello, handle_heartbeat
38
from pincer.exceptions import PincerError, InvalidTokenError, \
39
    UnhandledException
40
41
Handler = Callable[[WebSocketClientProtocol, GatewayDispatch], Awaitable[None]]
42
43
44
class Dispatcher:
45
46
    """
47
    The Dispatcher handles all interactions with the discord websocket
48
    API. This also contains the main event loop, and handles the heartbeat.
49
50
    Running the dispatcher will create a connection with the
51
    Discord WebSocket API on behalf of the provided token. This token
52
    must be a bot token. (Which can be found on
53
    `/developers/applications/<bot_id>/bot`)
54
    """
55
56
    # TODO: Add intents argument
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
57
    # TODO: Add handlers argument
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
58
    def __init__(self, token: str) -> None:
59
        """
60
        :param token:
61
            Bot token for discord's API.
62
        """
63
64
        if len(token) != 59:
65
            raise InvalidTokenError(
66
                "Discord Token must have exactly 59 characters."
67
            )
68
69
        self.__token = token
70
        self.__keep_alive = True
71
72
        async def identify_and_handle_hello(socket: WebSocketClientProtocol,
73
                                            payload: GatewayDispatch):
74
            """
75
            Identifies the client to the Discord Websocket API, this
76
            gets done when the client receives the `hello` (opcode 10)
77
            message from discord. Right after we send our identification
78
            the heartbeat starts.
79
80
            :param socket:
81
                The current socket, which can be used to interact
82
                with the Discord API.
83
84
            :param payload: The received payload from Discord.
85
            """
86
            await socket.send(str(GatewayDispatch(2, {
87
                "token": token,
88
                "intents": 0,
89
                "properties": {
90
                    "$os": system(),
91
                    "$browser": __package__,
92
                    "$device": __package__
93
                }
94
            })))
95
            await handle_hello(socket, payload)
96
97
        self.__dispatch_handlers: Dict[int, Handler] = {
98
            10: identify_and_handle_hello,
99
            11: handle_heartbeat
100
        }
101
102
        self.__dispatch_errors: Dict[int, PincerError] = {
103
            4004: InvalidTokenError()
104
        }
105
106
    async def handler_manager(self, socket: WebSocketClientProtocol,
107
                              payload: GatewayDispatch,
108
                              loop: AbstractEventLoop):
109
        """
110
        This manages all handles for given OP codes.
111
        This method gets invoked for every message that is received from
112
        Discord.
113
114
        :param socket:
115
            The current socket, which can be used to interact
116
            with the Discord API.
117
        :param payload: The received payload from Discord.
118
        :param loop: The current async loop on which the future is bound.
119
        """
120
        # TODO: Implement given handlers.
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
121
        # TODO: Implement logging
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
122
        handler: Handler = self.__dispatch_handlers.get(payload.op)
123
124
        if not handler:
125
            raise UnhandledException(f"Unhandled payload: {payload}")
126
127
        ensure_future(handler(socket, payload), loop=loop)
128
129
    async def __dispatcher(self, loop: AbstractEventLoop):
130
        """
131
        The main event loop.
132
        This handles all interactions with the websocket API.
133
        """
134
        # TODO: Implement logging
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
135
        async with connect(GatewayConfig.uri()) as socket:
136
            while self.__keep_alive:
137
                try:
138
                    await self.handler_manager(
139
                        socket,
140
                        GatewayDispatch.from_string(await socket.recv()),
141
                        loop)
142
143
                except ConnectionClosedError as exc:
144
                    self.close()
145
146
                    exception = self.__dispatch_errors.get(exc.code)
147
148
                    raise exception if exception else UnhandledException(
149
                        f"Dispatch error ({exc.code}): {exc.reason}")
150
151
    def run(self):
152
        """
153
        Instantiate the dispatcher, this will create a connection to the
154
        Discord websocket API on behalf of the client who's token has
155
        been passed.
156
        """
157
        # TODO: Implement logging
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
158
        loop = get_event_loop()
159
        loop.run_until_complete(self.__dispatcher(loop))
160
        loop.close()
161
162
    def close(self):
163
        """
164
        Stop the dispatcher from listening and responding to gateway
165
        events. This should let the client close on itself.
166
        """
167
        # TODO: Implement logging
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
168
        self.__keep_alive = False
169