From 5058a47b28fa1f0a5982a65c2a65955ea158a0e4 Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 9 Jul 2022 18:55:43 +0100 Subject: docs: Document most of everything so far --- discord/client.py | 37 +++++++++++++++++++++++++++++++++++++ discord/intents.py | 40 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index a2716b0..1100c83 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2,6 +2,7 @@ import asyncio import json import sys import threading +import warnings from typing import Optional, Coroutine, Any, Callable import websockets @@ -12,20 +13,39 @@ from .user import User 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 get(self._token, '/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 = get_number(intents) self.event_emitter = EventEmitter() async def connect(self, token: str, intent_code: int): + """ + 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: hello = await gateway.recv() self.gateway = gateway @@ -51,6 +71,9 @@ class Client: 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)) @@ -71,6 +94,14 @@ class Client: pass async def heartbeat(self, gateway: websockets.WebSocketClientProtocol, 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. + """ while True: await asyncio.sleep(interval / 1000) heartbeat = { @@ -83,6 +114,9 @@ class Client: 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') @@ -92,6 +126,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(token, self.code)) 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: -- cgit 1.4.1-2-gfad0 From 5d1b2e09c330f39bdb98e7e906f8a72bcf9c90ab Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 9 Jul 2022 19:01:50 +0100 Subject: fix(client): { --- discord/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/client.py b/discord/client.py index b9abe1e..bd46bfc 100644 --- a/discord/client.py +++ b/discord/client.py @@ -169,6 +169,8 @@ class Client: "browser": "discobra", "device": "discobra" } + } + } def event(self, coro: Optional[Callable[..., Coroutine[Any, Any, Any]]]=None, /) -> Optional[Callable[..., Coroutine[Any, Any, Any]]]: """ -- cgit 1.4.1-2-gfad0 From fabbc3ac7a6a39dbb6c44512ec4bf17ac2745126 Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 9 Jul 2022 19:03:31 +0100 Subject: chore: Reformat code --- discord/client.py | 35 ++++++++++++++++++----------------- discord/flags.py | 2 +- discord/premium_type.py | 2 +- discord/utils/__init__.py | 2 +- discord/utils/event_emitter.py | 9 +++++---- discord/utils/exceptions.py | 1 - discord/utils/rest.py | 4 +--- 7 files changed, 27 insertions(+), 28 deletions(-) diff --git a/discord/client.py b/discord/client.py index bd46bfc..7ba1f96 100644 --- a/discord/client.py +++ b/discord/client.py @@ -14,20 +14,22 @@ from .utils.rest import 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 + DISPATCH = 0 + HEARTBEAT = 1 + IDENTIFY = 2 + PRESENCE = 3 + VOICE_STATE = 4 + VOICE_PING = 5 + RESUME = 6 + RECONNECT = 7 + REQUEST_MEMBERS = 8 INVALIDATE_SESSION = 9 - HELLO = 10 - HEARTBEAT_ACK = 11 - GUILD_SYNC = 12 + HELLO = 10 + HEARTBEAT_ACK = 11 + GUILD_SYNC = 12 + class Client: """ @@ -77,7 +79,7 @@ class Client: 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. @@ -86,7 +88,7 @@ class Client: - 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. @@ -126,7 +128,6 @@ class Client: self.event_emitter.emit('on_' + event.lower()) - async def close(self): """ Close the client. @@ -138,7 +139,6 @@ class Client: 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. @@ -172,7 +172,8 @@ class Client: } } - 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. 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/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/__init__.py b/discord/utils/__init__.py index 7fdb52b..337f2e6 100644 --- a/discord/utils/__init__.py +++ b/discord/utils/__init__.py @@ -1 +1 @@ -from .event_emitter import * \ No newline at end of file +from .event_emitter import * 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..53853ed 100644 --- a/discord/utils/rest.py +++ b/discord/utils/rest.py @@ -2,6 +2,7 @@ import aiohttp from discord.utils.exceptions import APIException + class RESTClient: def __init__(self, token: str, session: aiohttp.ClientSession): self.token = token @@ -16,7 +17,6 @@ class RESTClient: case other: raise APIException(data['message']) - async def post(self, url: str, data): async with self.session.post(url='https://discord.com/api/v10' + url, data=data) as r: data = await r.json() @@ -26,7 +26,6 @@ class RESTClient: case other: raise APIException(data['message']) - async def patch(self, url, data): async with self.session.patch(url='https://discord.com/api/v10' + url, data=data) as res: data = await res.json() @@ -36,7 +35,6 @@ class RESTClient: case other: raise APIException(data['message']) - async def delete(self, url): async with self.session.delete(url='https://discord.com/api/v10' + url) as r: data = await r.json() -- cgit 1.4.1-2-gfad0 From 6576d95f3601613fc03232b106f1cdeda1d99e06 Mon Sep 17 00:00:00 2001 From: Noah Date: Sat, 9 Jul 2022 19:14:04 +0100 Subject: docs: Add more documentation where possible. --- discord/client.py | 14 ++++++++++++++ discord/utils/rest.py | 33 ++++++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index 7ba1f96..fc9bcd5 100644 --- a/discord/client.py +++ b/discord/client.py @@ -16,18 +16,32 @@ from .user import User class GatewayEvents(IntEnum): + """ + 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 + """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 diff --git a/discord/utils/rest.py b/discord/utils/rest.py index 53853ed..652de5d 100644 --- a/discord/utils/rest.py +++ b/discord/utils/rest.py @@ -4,11 +4,22 @@ 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: @@ -18,15 +29,29 @@ class RESTClient: 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,6 +61,12 @@ class RESTClient: 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: -- cgit 1.4.1-2-gfad0