diff options
-rw-r--r-- | discord/client.py | 91 | ||||
-rw-r--r-- | discord/flags.py | 2 | ||||
-rw-r--r-- | discord/intents.py | 40 | ||||
-rw-r--r-- | discord/premium_type.py | 2 | ||||
-rw-r--r-- | discord/utils/event_emitter.py | 9 | ||||
-rw-r--r-- | discord/utils/exceptions.py | 1 | ||||
-rw-r--r-- | discord/utils/rest.py | 37 |
7 files changed, 150 insertions, 32 deletions
diff --git a/discord/client.py b/discord/client.py index d6cfd50..5f5c3b0 100644 --- a/discord/client.py +++ b/discord/client.py @@ -3,42 +3,69 @@ from enum import IntEnum import json import sys import threading +import warnings from typing import Optional, Coroutine, Any, Callable import zlib import aiohttp import websockets -from .utils import EventEmitter -from .utils.rest import RESTClient +from .utils import EventEmitter, RESTClient from .intents import Intents, get_number from .user import User + class GatewayEvents(IntEnum): - DISPATCH = 0 - HEARTBEAT = 1 - IDENTIFY = 2 - PRESENCE = 3 - VOICE_STATE = 4 - VOICE_PING = 5 - RESUME = 6 - RECONNECT = 7 - REQUEST_MEMBERS = 8 + """ + Contains constants for the gateway opcodes. + """ + DISPATCH = 0 + """An event was dispatched.""" + HEARTBEAT = 1 + """Sent at regular intervals by the client to keep the gateway connection alive.""" + IDENTIFY = 2 + """Used to identify yourself with the token during the initial handshake.""" + PRESENCE = 3 + """Used to update the client's presence.""" + VOICE_STATE = 4 + """Used to join and leave voice channels.""" + VOICE_PING = 5 + RESUME = 6 + """Used to resume a disconnected session.""" + RECONNECT = 7 + """Used to reconnect to the session.""" + REQUEST_MEMBERS = 8 + """Used to request information about guild members when there are too many for """ INVALIDATE_SESSION = 9 - HELLO = 10 - HEARTBEAT_ACK = 11 - GUILD_SYNC = 12 + """Means that the session is invalid. When this is received, you must reconnect and re-identify.""" + HELLO = 10 + """Acknowledgement of gateway connection.""" + HEARTBEAT_ACK = 11 + """Acknowledgement of gateway heartbeat.""" + GUILD_SYNC = 12 + class Client: + """ + Represents a Discord client (i.e. a bot). + You need to initialise one of these and then use `run()` with a token to login. + """ _token: str @property async def user(self): + """The `discord.user.User` associated with the client.""" data = await self.rest_client.get('/users/@me') return User(data) def __init__(self, intents: list[Intents]): self.gateway = None self.loop = asyncio.get_event_loop() + if Intents.MESSAGE_CONTENT in intents: + warnings.warn("Message Content will become a privileged intent in August 2022. You must enable it in the " + "Discord developer portal.") + if Intents.GUILD_MEMBERS in intents or Intents.GUILD_PRESENCES in intents: + warnings.warn("You are using one or more privileged intent (Guild Members and/or Guild Presences). You " + "must enable them in the Discord developer portal.") self.code: int = get_number(intents) self.event_emitter = EventEmitter() self.buffer = bytearray() @@ -51,18 +78,29 @@ class Client: })) async def connect(self): + """ + Connects to the Discord gateway and begins sending heartbeats. + This should not be called manually. + + **Parameters:** + - token: Your bot token. + - intent_code: The number which represents the `discord.intents.Intents` being used. + """ async with websockets.connect("wss://gateway.discord.gg/?v=10&encoding=json") as gateway: self.gateway = gateway threading.Thread(target=self.loop.run_forever).start() while True: await self.poll_event() - + async def send(self, data: dict): """ Send data to the gateway. + + **Parameters:** + - data: The data to send to the gateway. """ await self.gateway.send(json.dumps(data)) - + async def recv(self, msg): """ Receive data from the gateway. @@ -91,7 +129,7 @@ class Client: if opcode == GatewayEvents.HEARTBEAT_ACK.value: return await self.heartbeat(self.heartbeat_interval) - + if opcode == GatewayEvents.HEARTBEAT.value: return await self.heartbeat(self.heartbeat_interval) @@ -102,7 +140,6 @@ class Client: self.event_emitter.emit('on_' + event.lower()) - async def close(self): """ Close the client. @@ -113,9 +150,16 @@ class Client: async def poll_event(self): msg = await self.gateway.recv() await self.recv(msg) - async def heartbeat(self, interval: int): + """ + Sends a heartbeat through the gateway to keep the connection active. + This should not be called manually. + + **Parameters:** + - gateway: The gateway to keep open. + - interval: How often to send a heartbeat. This is given by the gateway in a Hello packet. + """ await asyncio.sleep(interval / 1000) heartbeat = { "op": 1, @@ -141,9 +185,13 @@ class Client: } await self.send(identify) - def event(self, coro: Optional[Callable[..., Coroutine[Any, Any, Any]]]=None, /) -> Optional[Callable[..., Coroutine[Any, Any, Any]]]: + def event(self, coro: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None, /) -> Optional[ + Callable[..., Coroutine[Any, Any, Any]]]: """ Registers a coroutine to be called when an event is emitted. + + **Parameters:** + - coro: The coroutine to be registered. """ if not asyncio.iscoroutinefunction(coro): raise TypeError('event registered must be a coroutine function') @@ -153,6 +201,9 @@ class Client: def run(self, token: str): """ Run the client. + + **Parameters:** + - token: Your bot token. Do not share this with anyone! """ self._token = token asyncio.run(self.connect()) diff --git a/discord/flags.py b/discord/flags.py index 65a9253..6f2ace7 100644 --- a/discord/flags.py +++ b/discord/flags.py @@ -24,6 +24,7 @@ def get_number(flags: list[Flags]): number += i.value return number + def get_flags(number: int): flags = [] while number != 0: @@ -32,4 +33,3 @@ def get_flags(number: int): flags.append(i) number -= i.value return flags - diff --git a/discord/intents.py b/discord/intents.py index f538fb6..1f2fdf1 100644 --- a/discord/intents.py +++ b/discord/intents.py @@ -16,24 +16,53 @@ class Intents(Enum): See more at: https://discord.com/developers/docs/topics/gateway#gateway-intents """ GUILDS = 1 + """Events relating to the creation, removal and modification of guilds (servers).""" GUILD_MEMBERS = 2 + """ + Events relating to the joining, leaving and modification of a guild's members. + Events from this intent relating to the client are sent regardless of whether the intent is enabled. + This is a privileged intent that must be enabled in the Discord developer portal. + """ GUILD_BANS = 4 + """Events relating to the creation and removal of a guild's bans.""" GUILD_EMOJIS_AND_STICKERS = 8 + """Events relating to the modification of a guild's emojis and stickers.""" GUILD_INTEGRATIONS = 16 + """Events relating to the creation, removal and modification of a guild's integrations.""" GUILD_WEBHOOKS = 32 + """Events relating to the modification of a guild's webhooks.""" GUILD_INVITES = 64 + """Events relating to the creation and removal of a guild's invites.""" GUILD_VOICE_STATES = 128 + """Events relating to the modification of a guild's voice states.""" GUILD_PRESENCES = 256 + """ + Events relating to the modification of a guild's members' presences. + This is a privileged intent that must be enabled in the Discord developer portal. + """ GUILD_MESSAGES = 512 + """Events relating to the sending, editing and deleting of messages in a guild's channels.""" GUILD_MESSAGE_REACTIONS = 1024 + """Events relating to the addition and removal of reactions to messages in a guild's channels.""" GUILD_MESSAGE_TYPING = 2048 + """Events relating to when members start typing in a guild's channels.""" DIRECT_MESSAGES = 4096 + """Events relating to the sending, editing and deleting of messages in a DM channel.""" DIRECT_MESSAGE_REACTIONS = 8192 + """Events relating to the addition and removal of reactions to messages in a DM channel.""" DIRECT_MESSAGE_TYPING = 16384 + """Events relating to when users start typing in a DM channel.""" MESSAGE_CONTENT = 32768 + """ + The data relating to the content of messages from message events. + As of August 2022, this will be a privileged intent that must be enabled in the Discord developer portal. + """ GUILD_SCHEDULED_EVENTS = 65536 + """Events relating to the scheduling, modification and cancelling of a guild's events.""" AUTO_MODERATION_CONFIGURATION = 1048576 + """Events relating to Automod rules.""" AUTO_MODERATION_EXECUTION = 2097152 + """Events relating to Automod actions.""" def get_number(intents: list[Intents]): @@ -41,7 +70,7 @@ def get_number(intents: list[Intents]): Generates the number used to tell the gateway which intents are active. **Parameters:** - - intents (list[Intents]): A list of active intents + - intents: A list of active intents **Returns:** - int: The number used as an argument for the gateway connection. @@ -53,6 +82,15 @@ def get_number(intents: list[Intents]): def get_intents(number: int): + """ + Generates a list of intents from the number used to tell the gateway which are active. + + **Parameters:** + - number: The number which represents the intents. + + **Returns:** + - list[`discord.intents.Intents`]: The list of intents which the number represents. + """ intents = [] while number != 0: for i in Intents: diff --git a/discord/premium_type.py b/discord/premium_type.py index 3549f92..5a03d0c 100644 --- a/discord/premium_type.py +++ b/discord/premium_type.py @@ -5,4 +5,4 @@ from enum import Enum, unique class PremiumType(Enum): NONE = 0, NITRO_CLASSIC = 1, - NITRO = 2 \ No newline at end of file + NITRO = 2 diff --git a/discord/utils/event_emitter.py b/discord/utils/event_emitter.py index 08b6060..7911326 100644 --- a/discord/utils/event_emitter.py +++ b/discord/utils/event_emitter.py @@ -1,18 +1,19 @@ import asyncio from typing import Optional, Coroutine, Any, Callable, Dict -class EventEmitter(): - def __init__(self, loop: Optional[asyncio.AbstractEventLoop]=None): + +class EventEmitter: + def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None): self.listeners: Dict[str, Optional[Callable[..., Coroutine[Any, Any, Any]]]] = {} self.loop = loop if loop else asyncio.get_event_loop() - def add_listener(self, event_name: str, func: Optional[Callable[..., Coroutine[Any, Any, Any]]]=None): + def add_listener(self, event_name: str, func: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None): if not self.listeners.get(event_name, None): self.listeners[event_name] = {func} else: self.listeners[event_name].add(func) - def remove_listener(self, event_name: str, func: Optional[Callable[..., Coroutine[Any, Any, Any]]]=None): + def remove_listener(self, event_name: str, func: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None): self.listeners[event_name].remove(func) if len(self.listeners[event_name]) == 0: del self.listeners[event_name] diff --git a/discord/utils/exceptions.py b/discord/utils/exceptions.py index a7f035c..d3e5748 100644 --- a/discord/utils/exceptions.py +++ b/discord/utils/exceptions.py @@ -1,3 +1,2 @@ class APIException(Exception): """Raised when the Discord API returns an error.""" - diff --git a/discord/utils/rest.py b/discord/utils/rest.py index 919d54d..652de5d 100644 --- a/discord/utils/rest.py +++ b/discord/utils/rest.py @@ -2,12 +2,24 @@ import aiohttp from discord.utils.exceptions import APIException + class RESTClient: + """ + Utility class to make it easier to make HTTP requests to Discord's API. This should not be used manually, + as it only works with Discord's API and the library should cover anything that can be requested from it. Any + requests to other APIs should use `aiohttp`. + """ def __init__(self, token: str, session: aiohttp.ClientSession): self.token = token self.session = session async def get(self, url: str): + """ + Makes a GET request to Discord's API. + + **Parameters:** + - url: The part of the request URL that goes after `https://discord.com/api/v10` + """ async with self.session.get(url='https://discord.com/api/v10' + url) as r: data = await r.json() match r.status: @@ -16,18 +28,30 @@ class RESTClient: case other: raise APIException(data['message']) - async def post(self, url: str, data): + """ + Makes a POST request to Discord's API. + + **Parameters:** + - url: The part of the request URL that goes after `https://discord.com/api/v10` + - data: The data to post. + """ async with self.session.post(url='https://discord.com/api/v10' + url, data=data) as r: data = await r.json() match r.status: - case 200 | 204: + case 200 | 204 | 201: return data case other: raise APIException(data['message']) - async def patch(self, url, data): + """ + Makes a PATCH request to Discord's API. + + **Parameters:** + - url: The part of the request URL that goes after `https://discord.com/api/v10` + - data: The data to patch. + """ async with self.session.patch(url='https://discord.com/api/v10' + url, data=data) as res: data = await res.json() match res.status: @@ -36,8 +60,13 @@ class RESTClient: case other: raise APIException(data['message']) - async def delete(self, url): + """ + Makes a POST request to Discord's API. + + **Parameters:** + - url: The part of the request URL that goes after `https://discord.com/api/v10` + """ async with self.session.delete(url='https://discord.com/api/v10' + url) as r: data = await r.json() match r.status: |