From df64df1732b86529ee58af1e3f4a159d3bc84a93 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Thu, 11 Nov 2021 17:20:51 +0100 Subject: [PATCH 01/99] Start of converters --- discord/api/dataConverters.py | 31 +++++ discord/api/{handler.py => httphandler.py} | 2 +- discord/channels/guildchannel.py | 15 ++- discord/client.py | 16 ++- discord/guild/guild.py | 132 --------------------- discord/message.py | 7 +- 6 files changed, 59 insertions(+), 144 deletions(-) create mode 100644 discord/api/dataConverters.py rename discord/api/{handler.py => httphandler.py} (99%) delete mode 100644 discord/guild/guild.py diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py new file mode 100644 index 0000000..af04381 --- /dev/null +++ b/discord/api/dataConverters.py @@ -0,0 +1,31 @@ +from ..color import Color +from ..embeds import Embed +from ..guild import Guild +from ..message import Message +from ..role import Role +from ..activity.activity import Activity +from ..channels.dmchannel import DMChannel +from ..channels.guildchannel import TextChannel, VoiceChannel +from ..interactions.components import Component, View +from ..user.member import Member +from ..user.user import User + +import inspect +import typing + +class DataConverter: + def __init__(self, client): + self.client = client + self.converters = {} + for name, func in inspect.getmembers(self): + if name.startswith('convert_'): + self.converters[name[6:]] = func + + def convert(self, data): + func: typing.Callable = self.converters.get('convert_' + data['t']) + if not func: + raise NotImplementedError(f"No converter has been implemented for {data['t']}") + return func(data) + + def convert_message_create(self, data): + return Message(self.client, data) \ No newline at end of file diff --git a/discord/api/handler.py b/discord/api/httphandler.py similarity index 99% rename from discord/api/handler.py rename to discord/api/httphandler.py index dfeaa07..6225dbc 100644 --- a/discord/api/handler.py +++ b/discord/api/httphandler.py @@ -14,7 +14,7 @@ DiscordNotFound, DiscordServerError) -class Handler: +class HTTPHandler: def __init__(self): self.base_url: str = "https://discord.com/api/v9/" self.user_agent: str = "Disthon Discord API wrapper V0.0.1b" diff --git a/discord/channels/guildchannel.py b/discord/channels/guildchannel.py index 8326c96..800c7a3 100644 --- a/discord/channels/guildchannel.py +++ b/discord/channels/guildchannel.py @@ -1,11 +1,10 @@ from __future__ import annotations -from ..guild.guild import GuildChannel import ..abc from .basechannel import BaseChannel -class TextChannel(GuildChannel): +class TextChannel(BaseChannel): __slots__ = ( "name", "id", @@ -14,4 +13,14 @@ class TextChannel(GuildChannel): "category_id", "position" ) - \ No newline at end of file + +class VoiceChannel(BaseChannel): + __slots__ = ( + "name", + "id", + "guild", + "bitrate", + "user_limit", + "category_id", + "position" + ) \ No newline at end of file diff --git a/discord/client.py b/discord/client.py index f963cd4..a84d8a6 100644 --- a/discord/client.py +++ b/discord/client.py @@ -7,9 +7,10 @@ import typing from copy import deepcopy -from .api.handler import Handler +from .api.httphandler import HTTPHandler from .api.intents import Intents from .api.websocket import WebSocket +from .api.dataConverters import DataConverter class Client: @@ -25,21 +26,22 @@ def __init__( self.respond_self = respond_self self.stay_alive = True - self.handler = Handler() + self.httphandler = HTTPHandler() self.lock = asyncio.Lock() self.closed = False self.events = {} + self.converter = DataConverter(self) async def login(self, token: str) -> None: self.token = token async with self.lock: - self.info = await self.handler.login(token) + self.info = await self.httphandler.login(token) async def connect(self) -> None: while not self.closed: socket = WebSocket(self, self.token) async with self.lock: - g_url = await self.handler.gateway() + g_url = await self.httphandler.gateway() if not isinstance(self.intents, Intents): raise TypeError( f"Intents must be of type Intents, got {self.intents.__class__}" @@ -57,7 +59,7 @@ async def alive_loop(self, token: str) -> None: await self.close() async def close(self) -> None: - await self.handler.close() + await self.httphandler.close() def run(self, token: str): def stop_loop_on_completion(_): @@ -100,10 +102,12 @@ async def handle_event(self, msg): global_message = deepcopy(msg) global_message["t"] = "MESSAGE" await self.handle_event(global_message) + + args = self.converter.convert(msg) for coro in self.events.get(event, []): try: - await coro(msg) + await coro(*args) except Exception as error: print(f"Ignoring exception in event {coro.__name__}", file=sys.stderr) traceback.print_exception( diff --git a/discord/guild/guild.py b/discord/guild/guild.py deleted file mode 100644 index b66e101..0000000 --- a/discord/guild/guild.py +++ /dev/null @@ -1,132 +0,0 @@ -from typing import ( - NamedTuple, - Optional, - List, - MISSING -) - -from discord.abc.discordobject import DiscordObject -from discord.channels.guildchannel import GuildChannel -from discord.member.member import Member -from discord.role.role import Role -from discord.types.guildpayload import GuildPayload -from discord.types.snowflake import Snowflake -from discord.user.user import User - -from ..abc.discordobject import DiscordObject -from ..channels.guildchannel import CategoryChannel, GuildChannel -from ..role import Role -from ..user.member import Member - - -class BanEntry(NamedTuple): - user: User - reason: Optional[str] - - -class GuildLimit(NamedTuple): - filesize: int - emoji: int - channels: int - roles: int - categories: int - bitrate: int - stickers: int - - -class Guild(DiscordObject): - __slots__ = ( - "region" - "owner_id" - "mfa.level" - "name" - "id" - "_members" - "_channels" - "_vanity" - "_banner" - ) - - _roles: set[Role] - me: Member - - - def __init__(self, data: GuildPayload): - self._members: dict[Snowflake, Member] = {} - self._channels: dict[Snowflake, GuildChannel] = {} - self._roles = set() - - def _add_channel(self, channel: GuildChannel, /) -> None: - self._channels[channel.id] = channel - - def _delete_channel(self, channel: DiscordObject) -> None: - self._channels.pop(channel.id, None) - - def add_member(self, member: Member) -> None: - self._members[member.id] = member - - def add_roles(self, role: Role) -> None: - for p in self._roles.values: - p.postion += not p.is_default() - # checks if role is @everyone or not - - self._roles[role.id] = role - def remove_roles(self, role:Role) -> None: - - def remove_roles(self, role: Role) -> None: - role = self._roles.pop(role.id) - - for p in self._roles.values: - p.position -= p.position > role.position - - return role - - @property - async def channels(self) -> List[GuildChannel]: - return list(self._channels.values()) - - @property - async def roles(self) -> List[Role]: - return sorted(self._roles.values()) - - @property - async def owner(self) -> Optional[Member]: - return self.get_member(self.owner.id) - - @property - async def members(self) -> List[Member]: - return list(self._members.values()) - - def get_member(self, member_id: int) -> Optional[Member]: - return self._members.get(member_id) - - def get_channel(self, channel_id: int) -> Optional[GuildChannel]: - return self._channels(channel_id) - - async def create_channel( - self, - *, - name: str, - type: str = None, - reason: Optional[str] = None, - category: Optional[CategoryChannel] = None, - position: int = None, - slowmode_delay: int = None, - ): - return - - async def delete_channel( - self, *, channel: GuildChannel, reason: Optional[str] = None - ): - pass - - async def edit_channel( - self, - *, - name: Optional[str] = None, - position: Optional[int] = None, - slowmode_delay: Optional[int] = None, - category: Optional[CategoryChannel] = None, - ): - pass - \ No newline at end of file diff --git a/discord/message.py b/discord/message.py index 92a7f06..bf79941 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1,5 +1,8 @@ from __future__ import annotations +from pydantic import BaseModel -class Message: - pass +class Message(BaseModel): + def __init__(self, client, data): + self.client = client + From e330338b6a0c95bed4085cc4717b1f54853a5f87 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Thu, 11 Nov 2021 18:13:45 +0100 Subject: [PATCH 02/99] Fixed a lot of weird issues --- discord/__init__.py | 2 +- discord/abc/abstractuser.py | 4 +-- discord/activity/presenseassets.py | 6 ++-- discord/api/dataConverters.py | 18 ++++++++---- discord/api/websocket.py | 4 +-- .../channels/{___init__.py => __init__.py} | 0 discord/channels/guildchannel.py | 17 +++++++++-- discord/client.py | 3 ++ discord/guild.py | 28 ++++++++++--------- discord/message.py | 10 +++++++ discord/role.py | 6 ++-- discord/types/avatar.py | 4 +-- discord/types/enums/nsfwlevel.py | 2 +- discord/types/enums/verificationlevel.py | 2 +- discord/types/guildpayload.py | 8 +++--- discord/types/image.py | 5 +++- discord/types/rolepayload.py | 2 +- discord/types/userpayload.py | 6 ++-- discord/user/baseuser.py | 15 ++++++---- discord/user/member.py | 8 ++++-- setup.py | 4 +-- 21 files changed, 99 insertions(+), 55 deletions(-) rename discord/channels/{___init__.py => __init__.py} (100%) diff --git a/discord/__init__.py b/discord/__init__.py index d609e58..fa909cf 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -4,4 +4,4 @@ from .api.intents import Intents as Intents from .client import Client as Client -from .embeds import Embed as Embed +from .embeds import Embed as Embed \ No newline at end of file diff --git a/discord/abc/abstractuser.py b/discord/abc/abstractuser.py index 15eb8a6..5c39797 100644 --- a/discord/abc/abstractuser.py +++ b/discord/abc/abstractuser.py @@ -2,7 +2,7 @@ from typing import Optional -from discordobject import DiscordObject +from .discordobject import DiscordObject from ..message import Message from ..types.avatar import Avatar @@ -28,7 +28,7 @@ def discriminator(self): def mention(self): return f"<@!{self.id}>" - @propery + @property def name(self): return self.username diff --git a/discord/activity/presenseassets.py b/discord/activity/presenseassets.py index 078f356..e337b4c 100644 --- a/discord/activity/presenseassets.py +++ b/discord/activity/presenseassets.py @@ -1,8 +1,10 @@ from __future__ import annotations +from typing import TYPE_CHECKING from ..types.snowflake import Snowflake -from .activity import Activity -from .rawactivityassets import RawActivityAssets +if TYPE_CHECKING: + from .activity import Activity + from .rawactivityassets import RawActivityAssets class PresenceAssets(dict[Snowflake, str]): diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index af04381..7fe2bda 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -19,13 +19,19 @@ def __init__(self, client): self.converters = {} for name, func in inspect.getmembers(self): if name.startswith('convert_'): - self.converters[name[6:]] = func + self.converters[name[8:]] = func + + def convert_message(self, data): + return Message(self.client, data) + + def convert_ready(self, data): + return None + + def convert_guild_create(self, data): + return data def convert(self, data): - func: typing.Callable = self.converters.get('convert_' + data['t']) + func: typing.Callable = self.converters.get(data['t'].lower()) if not func: raise NotImplementedError(f"No converter has been implemented for {data['t']}") - return func(data) - - def convert_message_create(self, data): - return Message(self.client, data) \ No newline at end of file + return func(data) \ No newline at end of file diff --git a/discord/api/websocket.py b/discord/api/websocket.py index ca875c7..1677168 100644 --- a/discord/api/websocket.py +++ b/discord/api/websocket.py @@ -47,8 +47,8 @@ async def start( reconnect: typing.Optional[bool] = False ): if not url: - url = self.client.handler.gateway() - self.socket = await self.client.handler.connect(url) + url = self.client.httphandler.gateway() + self.socket = await self.client.httphandler.connect(url) await self.receive_events() await self.identify() if reconnect: diff --git a/discord/channels/___init__.py b/discord/channels/__init__.py similarity index 100% rename from discord/channels/___init__.py rename to discord/channels/__init__.py diff --git a/discord/channels/guildchannel.py b/discord/channels/guildchannel.py index 800c7a3..2d6fc7d 100644 --- a/discord/channels/guildchannel.py +++ b/discord/channels/guildchannel.py @@ -1,7 +1,5 @@ from __future__ import annotations -import ..abc - from .basechannel import BaseChannel class TextChannel(BaseChannel): @@ -11,7 +9,20 @@ class TextChannel(BaseChannel): "guild", "nsfw", "category_id", - "position" + "position", + "topic" + ) + +class ThreadChannel(BaseChannel): + __slots__ = ( + "name", + "id", + "guild", + "nsfw", + "category_id", + "position", + "topic", + "parent" ) class VoiceChannel(BaseChannel): diff --git a/discord/client.py b/discord/client.py index a84d8a6..d0d2347 100644 --- a/discord/client.py +++ b/discord/client.py @@ -95,6 +95,7 @@ def add_listener( self.events[event] = [func] async def handle_event(self, msg): + print("got event") event: str = "on_" + msg["t"].lower() # create a global on_message event for either guild or dm messages @@ -103,7 +104,9 @@ async def handle_event(self, msg): global_message["t"] = "MESSAGE" await self.handle_event(global_message) + print("converting") args = self.converter.convert(msg) + print(args) for coro in self.events.get(event, []): try: diff --git a/discord/guild.py b/discord/guild.py index f302a06..8661770 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1,15 +1,17 @@ from __future__ import annotations -from typing import NamedTuple, Optional +from typing import TYPE_CHECKING, NamedTuple, Optional, Text, Union from .abc.discordobject import DiscordObject -from .channels.guildchannel import GuildChannel -from .role import Role +from .channels.guildchannel import TextChannel, VoiceChannel from .types.guildpayload import GuildPayload from .types.snowflake import Snowflake from .user.member import Member from .user.user import User +if TYPE_CHECKING: + from .role import Role + class BanEntry(NamedTuple): user: User @@ -28,14 +30,14 @@ class GuildLimit(NamedTuple): class Guild(DiscordObject): __slots__ = ( - "region" - "owner_id" - "mfa.level" - "name" - "id" - "_members" - "_channels" - "_vanity" + "region", + "owner_id", + "mfa_level", + "name", + "id", + "_members", + "_channels", + "_vanity", "_banner" ) @@ -45,10 +47,10 @@ class Guild(DiscordObject): def __init__(self, data: GuildPayload): self._members: dict[Snowflake, Member] = {} - self._channels: dict[Snowflake, GuildChannel] = {} + self._channels: dict[Snowflake, Union[TextChannel, VoiceChannel]] = {} self._roles = set() - def _add_channel(self, channel: GuildChannel, /) -> None: + def _add_channel(self, channel: Union[TextChannel, VoiceChannel], /) -> None: self._channels[channel.id] = channel def _delete_channel(self, channel: DiscordObject) -> None: diff --git a/discord/message.py b/discord/message.py index bf79941..684557e 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1,8 +1,18 @@ from __future__ import annotations from pydantic import BaseModel +from typing import Optional, TYPE_CHECKING + +if TYPE_CHECKING: + from .client import Client class Message(BaseModel): + + client: Client + id: int + channel_id: int + content: str + def __init__(self, client, data): self.client = client diff --git a/discord/role.py b/discord/role.py index bb914b9..bde87b1 100644 --- a/discord/role.py +++ b/discord/role.py @@ -1,10 +1,12 @@ from __future__ import annotations -from typing import Optional, TypeVar +from typing import TYPE_CHECKING, Optional, TypeVar from .abc.discordobject import DiscordObject from .color import Color -from .guild import Guild + +if TYPE_CHECKING: + from .guild import Guild __all__ = ("RoleTags", "Role") diff --git a/discord/types/avatar.py b/discord/types/avatar.py index 0bd9c85..33cc452 100644 --- a/discord/types/avatar.py +++ b/discord/types/avatar.py @@ -3,8 +3,8 @@ from os.path import splitext from typing import Optional -from enums.validavatarformat import ValidAvatarFormat, ValidStaticAvatarFormat -from image import Image +from .enums.validavatarformat import ValidAvatarFormat, ValidStaticAvatarFormat +from .image import Image from yarl import URL from ..cache import LFUCache diff --git a/discord/types/enums/nsfwlevel.py b/discord/types/enums/nsfwlevel.py index a6a87b7..c24a108 100644 --- a/discord/types/enums/nsfwlevel.py +++ b/discord/types/enums/nsfwlevel.py @@ -1,7 +1,7 @@ from enum import IntEnum -class NSFWLevel(IntEnum, comparable=True): +class NSFWLevel(IntEnum): default = 0 explicit = 1 safe = 2 diff --git a/discord/types/enums/verificationlevel.py b/discord/types/enums/verificationlevel.py index e3b048f..5bdcdb5 100644 --- a/discord/types/enums/verificationlevel.py +++ b/discord/types/enums/verificationlevel.py @@ -1,7 +1,7 @@ from enum import IntEnum -class VerificationLevel(IntEnum, comparable=True): +class VerificationLevel(IntEnum): none = 0 low = 1 medium = 2 diff --git a/discord/types/guildpayload.py b/discord/types/guildpayload.py index fa07d5f..4f4f8cc 100644 --- a/discord/types/guildpayload.py +++ b/discord/types/guildpayload.py @@ -1,10 +1,10 @@ from __future__ import annotations -from typing import Optional +from typing import Optional, Union from pydantic import BaseModel -from ..channels.guildchannel import GuildChannel +from ..channels.guildchannel import TextChannel, VoiceChannel, ThreadChannel from ..role import Role from ..user.member import Member from ..user.user import User @@ -35,9 +35,9 @@ class GuildPayload(BaseModel): member_count: int voice_states: list[GuildVoiceState] members: list[Member] - channels: list[GuildChannel] + channels: list[Union[TextChannel, VoiceChannel]] presences: list[PartialPresenceUpdate] - threads: list[Thread] + threads: list[ThreadChannel] max_presences: Optional[int] max_members: int premium_subscription_count: int diff --git a/discord/types/image.py b/discord/types/image.py index d585d7a..016879c 100644 --- a/discord/types/image.py +++ b/discord/types/image.py @@ -4,7 +4,7 @@ from os import PathLike from typing import ClassVar, Optional, Union -from enums.imagetype import ImageType +from .enums.imagetype import ImageType from pydantic import BaseModel from ..cache import LFUCache @@ -13,6 +13,9 @@ class Image(BaseModel): + class Config: + arbitrary_types_allowed = True + url: str format: ImageType cache: Optional[LFUCache] diff --git a/discord/types/rolepayload.py b/discord/types/rolepayload.py index 8f6bacc..8de220c 100644 --- a/discord/types/rolepayload.py +++ b/discord/types/rolepayload.py @@ -2,7 +2,7 @@ from typing import TypedDict -from snowflake import Snowflake +from .snowflake import Snowflake from ..color import Color diff --git a/discord/types/userpayload.py b/discord/types/userpayload.py index 5f4fe01..e8c68c2 100644 --- a/discord/types/userpayload.py +++ b/discord/types/userpayload.py @@ -2,10 +2,10 @@ from typing import Optional, TypedDict -from enums.locale import Locale -from enums.userflags import UserFlags +from .enums.locale import Locale +from .enums.userflags import UserFlags from pydantic.main import BaseModel -from snowflake import Snowflake +from .snowflake import Snowflake from ..types.avatar import Avatar from ..types.banner import Banner diff --git a/discord/user/baseuser.py b/discord/user/baseuser.py index 4155eb9..6aaaf53 100644 --- a/discord/user/baseuser.py +++ b/discord/user/baseuser.py @@ -1,12 +1,12 @@ from __future__ import annotations -from abc.abstractuser import AbstractUser +from ..abc.abstractuser import AbstractUser from datetime import datetime -from types.avatar import Avatar -from types.banner import Banner -from types.enums.defaultavatar import DefaultAvatar -from types.enums.userflags import UserFlags -from types.userpayload import UserPayload +from ..types.avatar import Avatar +from ..types.banner import Banner +from ..types.enums.defaultavatar import DefaultAvatar +from ..types.enums.userflags import UserFlags +from ..types.userpayload import UserPayload from typing import Optional from ..cache import GuildCache, UserCache @@ -15,6 +15,9 @@ class BaseUser(AbstractUser): + class Config: + arbitrary_types_allowed = True + banner: Banner system: bool display_avatar: Avatar diff --git a/discord/user/member.py b/discord/user/member.py index ec5ef78..df2515c 100644 --- a/discord/user/member.py +++ b/discord/user/member.py @@ -1,9 +1,11 @@ from __future__ import annotations +from typing import TYPE_CHECKING -from user import User +from .user import User -from ..guild import Guild -from ..role import Role +if TYPE_CHECKING: + from ..guild import Guild + from ..role import Role class Member(User): diff --git a/setup.py b/setup.py index d478fdb..eaf53d4 100644 --- a/setup.py +++ b/setup.py @@ -1,11 +1,11 @@ -from setuptools import setup +from setuptools import setup, find_packages with open("README.md", "r") as file: long_des = file.read() setup( name="disthon", - packages=[], + packages=find_packages(), install_requires=["aiohttp", "yarl", "pydantic", "arrow"], description="An API wrapper for the discord API written in python", version="0.0.1", From 75780da33edf9f5f9b756f2b2abac8f302736889 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Thu, 11 Nov 2021 19:01:39 +0100 Subject: [PATCH 03/99] Messages now get converted! --- discord/api/dataConverters.py | 21 +++++++++++++++------ discord/cache.py | 4 ++-- discord/channels/guildchannel.py | 6 ++++++ discord/client.py | 13 ++++--------- discord/message.py | 17 +++++++++++++---- 5 files changed, 40 insertions(+), 21 deletions(-) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index 7fe2bda..26de5a3 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -21,17 +21,26 @@ def __init__(self, client): if name.startswith('convert_'): self.converters[name[8:]] = func + def _get_channel(self, id): + return None # TODO: get channel from cache + def convert_message(self, data): - return Message(self.client, data) + return [Message(self.client, data)] def convert_ready(self, data): - return None + return [] def convert_guild_create(self, data): - return data + return [data] + + def convert_presence_update(self, data): + return [data] - def convert(self, data): - func: typing.Callable = self.converters.get(data['t'].lower()) + def convert_typing_start(self, data): + return [data] + + def convert(self, event, data): + func: typing.Callable = self.converters.get(event.removeprefix('on_')) if not func: - raise NotImplementedError(f"No converter has been implemented for {data['t']}") + raise NotImplementedError(f"No converter has been implemented for {event}") return func(data) \ No newline at end of file diff --git a/discord/cache.py b/discord/cache.py index 9045488..5ad0785 100644 --- a/discord/cache.py +++ b/discord/cache.py @@ -4,7 +4,7 @@ if TYPE_CHECKING: - from .api.handler import Handler + from .api.httphandler import HTTPHandler from .guild import Guild from .message import Message from .role import Role @@ -18,7 +18,7 @@ class LFUCache: capacity: int _cache: OrderedDict[Snowflake, Any] _frequency: dict[Snowflake, int] - handler: Handler + httphandler: HTTPHandler def __init__(self, capacity: int) -> None: self.capacity = capacity diff --git a/discord/channels/guildchannel.py b/discord/channels/guildchannel.py index 2d6fc7d..9aa713c 100644 --- a/discord/channels/guildchannel.py +++ b/discord/channels/guildchannel.py @@ -1,6 +1,12 @@ from __future__ import annotations +from typing import Optional, Union, List, TYPE_CHECKING from .basechannel import BaseChannel +from ..message import Message + +if TYPE_CHECKING: + from ..embeds import Embed + from ..interactions.components import View class TextChannel(BaseChannel): __slots__ = ( diff --git a/discord/client.py b/discord/client.py index d0d2347..219c474 100644 --- a/discord/client.py +++ b/discord/client.py @@ -95,18 +95,13 @@ def add_listener( self.events[event] = [func] async def handle_event(self, msg): - print("got event") event: str = "on_" + msg["t"].lower() - # create a global on_message event for either guild or dm messages + # Convert all messages types to single message event if event in ("on_message_create", "on_dm_message_create"): - global_message = deepcopy(msg) - global_message["t"] = "MESSAGE" - await self.handle_event(global_message) - - print("converting") - args = self.converter.convert(msg) - print(args) + event = "on_message" + + args = self.converter.convert(event, msg['d']) for coro in self.events.get(event, []): try: diff --git a/discord/message.py b/discord/message.py index 684557e..02083b0 100644 --- a/discord/message.py +++ b/discord/message.py @@ -7,12 +7,21 @@ class Message(BaseModel): - - client: Client id: int channel_id: int content: str + + _client: Client def __init__(self, client, data): - self.client = client - + super().__init__(_client = client, **data) + + def __str__(self): + return self.content + + def __repr__(self): + return f"" + + @property + def channel(self): + return self._client.converter._get_channel(self.channel_id) \ No newline at end of file From 297125884c810b0088ea40485f4038e9061d4f31 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Thu, 11 Nov 2021 20:00:56 +0100 Subject: [PATCH 04/99] event decorator now has overwrite --- discord/client.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/discord/client.py b/discord/client.py index 219c474..a9123b1 100644 --- a/discord/client.py +++ b/discord/client.py @@ -73,15 +73,15 @@ def stop_loop_on_completion(_): if not future.cancelled(): return future.result() - def event(self, event: str = None): + def event(self, event: str = None, *, overwrite: bool = False): def wrapper(func): - self.add_listener(func, event) + self.add_listener(func, event, overwrite) return func return wrapper def add_listener( - self, func: typing.Callable, event: typing.Optional[str] = None + self, func: typing.Callable, event: typing.Optional[str] = None, overwrite: bool = False ) -> None: event = event or func.__name__ if not inspect.iscoroutinefunction(func): @@ -89,7 +89,7 @@ def add_listener( "The callback is not a valid coroutine function. Did you forget to add async before def?" ) - if event in self.events: + if event in self.events and not overwrite: self.events[event].append(func) else: self.events[event] = [func] @@ -97,10 +97,6 @@ def add_listener( async def handle_event(self, msg): event: str = "on_" + msg["t"].lower() - # Convert all messages types to single message event - if event in ("on_message_create", "on_dm_message_create"): - event = "on_message" - args = self.converter.convert(event, msg['d']) for coro in self.events.get(event, []): From 6d89f95164f8a39337f7b1b4461a372a821e4546 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Thu, 11 Nov 2021 20:11:31 +0100 Subject: [PATCH 05/99] ran black + isort --- discord/__init__.py | 2 +- discord/abc/abstractuser.py | 9 ++++----- discord/activity/presenseassets.py | 2 ++ discord/api/dataConverters.py | 27 ++++++++++++++------------- discord/channels/guildchannel.py | 24 ++++++++++-------------- discord/client.py | 9 ++++++--- discord/color.py | 1 + discord/ext/commands/context.py | 7 +++++-- discord/guild.py | 2 +- discord/message.py | 10 ++++++---- discord/types/avatar.py | 4 ++-- discord/types/guildpayload.py | 2 +- discord/types/image.py | 5 ++--- discord/types/rolepayload.py | 3 +-- discord/types/userpayload.py | 6 +++--- discord/user/baseuser.py | 14 +++++++------- discord/user/member.py | 1 + setup.py | 6 +++--- 18 files changed, 70 insertions(+), 64 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index fa909cf..d609e58 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -4,4 +4,4 @@ from .api.intents import Intents as Intents from .client import Client as Client -from .embeds import Embed as Embed \ No newline at end of file +from .embeds import Embed as Embed diff --git a/discord/abc/abstractuser.py b/discord/abc/abstractuser.py index 5c39797..8ef53c1 100644 --- a/discord/abc/abstractuser.py +++ b/discord/abc/abstractuser.py @@ -2,10 +2,9 @@ from typing import Optional -from .discordobject import DiscordObject - from ..message import Message from ..types.avatar import Avatar +from .discordobject import DiscordObject class AbstractUser(DiscordObject): @@ -19,7 +18,7 @@ class AbstractUser(DiscordObject): @property def tag(self): return f"{self.username}#{self.discriminator}" - + @property def discriminator(self): return self.discriminator @@ -27,11 +26,11 @@ def discriminator(self): @property def mention(self): return f"<@!{self.id}>" - + @property def name(self): return self.username - + @property def id(self): return self.id diff --git a/discord/activity/presenseassets.py b/discord/activity/presenseassets.py index e337b4c..c51c276 100644 --- a/discord/activity/presenseassets.py +++ b/discord/activity/presenseassets.py @@ -1,7 +1,9 @@ from __future__ import annotations + from typing import TYPE_CHECKING from ..types.snowflake import Snowflake + if TYPE_CHECKING: from .activity import Activity from .rawactivityassets import RawActivityAssets diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index 26de5a3..79a9479 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -1,35 +1,36 @@ +import inspect +import typing + +from ..activity.activity import Activity +from ..channels.dmchannel import DMChannel +from ..channels.guildchannel import TextChannel, VoiceChannel from ..color import Color from ..embeds import Embed from ..guild import Guild +from ..interactions.components import Component, View from ..message import Message from ..role import Role -from ..activity.activity import Activity -from ..channels.dmchannel import DMChannel -from ..channels.guildchannel import TextChannel, VoiceChannel -from ..interactions.components import Component, View from ..user.member import Member from ..user.user import User -import inspect -import typing class DataConverter: def __init__(self, client): self.client = client self.converters = {} for name, func in inspect.getmembers(self): - if name.startswith('convert_'): + if name.startswith("convert_"): self.converters[name[8:]] = func def _get_channel(self, id): - return None # TODO: get channel from cache + return None # TODO: get channel from cache def convert_message(self, data): return [Message(self.client, data)] - + def convert_ready(self, data): return [] - + def convert_guild_create(self, data): return [data] @@ -38,9 +39,9 @@ def convert_presence_update(self, data): def convert_typing_start(self, data): return [data] - + def convert(self, event, data): - func: typing.Callable = self.converters.get(event.removeprefix('on_')) + func: typing.Callable = self.converters.get(event.removeprefix("on_")) if not func: raise NotImplementedError(f"No converter has been implemented for {event}") - return func(data) \ No newline at end of file + return func(data) diff --git a/discord/channels/guildchannel.py b/discord/channels/guildchannel.py index 9aa713c..23640ee 100644 --- a/discord/channels/guildchannel.py +++ b/discord/channels/guildchannel.py @@ -1,23 +1,18 @@ from __future__ import annotations -from typing import Optional, Union, List, TYPE_CHECKING -from .basechannel import BaseChannel +from typing import TYPE_CHECKING, List, Optional, Union + from ..message import Message +from .basechannel import BaseChannel if TYPE_CHECKING: from ..embeds import Embed from ..interactions.components import View + class TextChannel(BaseChannel): - __slots__ = ( - "name", - "id", - "guild", - "nsfw", - "category_id", - "position", - "topic" - ) + __slots__ = ("name", "id", "guild", "nsfw", "category_id", "position", "topic") + class ThreadChannel(BaseChannel): __slots__ = ( @@ -28,9 +23,10 @@ class ThreadChannel(BaseChannel): "category_id", "position", "topic", - "parent" + "parent", ) + class VoiceChannel(BaseChannel): __slots__ = ( "name", @@ -39,5 +35,5 @@ class VoiceChannel(BaseChannel): "bitrate", "user_limit", "category_id", - "position" - ) \ No newline at end of file + "position", + ) diff --git a/discord/client.py b/discord/client.py index a9123b1..f2eca64 100644 --- a/discord/client.py +++ b/discord/client.py @@ -7,10 +7,10 @@ import typing from copy import deepcopy +from .api.dataConverters import DataConverter from .api.httphandler import HTTPHandler from .api.intents import Intents from .api.websocket import WebSocket -from .api.dataConverters import DataConverter class Client: @@ -81,7 +81,10 @@ def wrapper(func): return wrapper def add_listener( - self, func: typing.Callable, event: typing.Optional[str] = None, overwrite: bool = False + self, + func: typing.Callable, + event: typing.Optional[str] = None, + overwrite: bool = False, ) -> None: event = event or func.__name__ if not inspect.iscoroutinefunction(func): @@ -97,7 +100,7 @@ def add_listener( async def handle_event(self, msg): event: str = "on_" + msg["t"].lower() - args = self.converter.convert(event, msg['d']) + args = self.converter.convert(event, msg["d"]) for coro in self.events.get(event, []): try: diff --git a/discord/color.py b/discord/color.py index 47d1db8..7734367 100644 --- a/discord/color.py +++ b/discord/color.py @@ -4,6 +4,7 @@ import random import re from typing import Optional, Union + from pydantic import BaseModel from .exceptions import InvalidColor diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py index bbca144..77ba7ca 100644 --- a/discord/ext/commands/context.py +++ b/discord/ext/commands/context.py @@ -1,6 +1,9 @@ -from typing import Optional, List, Any, Dict +from typing import Any, Dict, List, Optional + from discord.user.user import User + from ...abc.discordobject import DiscordObject + class Context: - pass + pass diff --git a/discord/guild.py b/discord/guild.py index 8661770..9fc401d 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -38,7 +38,7 @@ class Guild(DiscordObject): "_members", "_channels", "_vanity", - "_banner" + "_banner", ) _roles: set[Role] diff --git a/discord/message.py b/discord/message.py index 02083b0..ba8c4a7 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1,6 +1,8 @@ from __future__ import annotations + +from typing import TYPE_CHECKING, Optional + from pydantic import BaseModel -from typing import Optional, TYPE_CHECKING if TYPE_CHECKING: from .client import Client @@ -12,9 +14,9 @@ class Message(BaseModel): content: str _client: Client - + def __init__(self, client, data): - super().__init__(_client = client, **data) + super().__init__(_client=client, **data) def __str__(self): return self.content @@ -24,4 +26,4 @@ def __repr__(self): @property def channel(self): - return self._client.converter._get_channel(self.channel_id) \ No newline at end of file + return self._client.converter._get_channel(self.channel_id) diff --git a/discord/types/avatar.py b/discord/types/avatar.py index 33cc452..431f4b2 100644 --- a/discord/types/avatar.py +++ b/discord/types/avatar.py @@ -3,12 +3,12 @@ from os.path import splitext from typing import Optional -from .enums.validavatarformat import ValidAvatarFormat, ValidStaticAvatarFormat -from .image import Image from yarl import URL from ..cache import LFUCache from ..exceptions import DiscordInvalidArgument +from .enums.validavatarformat import ValidAvatarFormat, ValidStaticAvatarFormat +from .image import Image class Avatar(Image): diff --git a/discord/types/guildpayload.py b/discord/types/guildpayload.py index 4f4f8cc..c6e09d0 100644 --- a/discord/types/guildpayload.py +++ b/discord/types/guildpayload.py @@ -4,7 +4,7 @@ from pydantic import BaseModel -from ..channels.guildchannel import TextChannel, VoiceChannel, ThreadChannel +from ..channels.guildchannel import TextChannel, ThreadChannel, VoiceChannel from ..role import Role from ..user.member import Member from ..user.user import User diff --git a/discord/types/image.py b/discord/types/image.py index 016879c..f1cd77d 100644 --- a/discord/types/image.py +++ b/discord/types/image.py @@ -4,18 +4,17 @@ from os import PathLike from typing import ClassVar, Optional, Union -from .enums.imagetype import ImageType from pydantic import BaseModel from ..cache import LFUCache from ..exceptions import DiscordException, DiscordNotFound +from .enums.imagetype import ImageType class Image(BaseModel): - class Config: arbitrary_types_allowed = True - + url: str format: ImageType cache: Optional[LFUCache] diff --git a/discord/types/rolepayload.py b/discord/types/rolepayload.py index 8de220c..ec664b5 100644 --- a/discord/types/rolepayload.py +++ b/discord/types/rolepayload.py @@ -2,9 +2,8 @@ from typing import TypedDict -from .snowflake import Snowflake - from ..color import Color +from .snowflake import Snowflake class RoleTagsPayload(TypedDict, total=False): diff --git a/discord/types/userpayload.py b/discord/types/userpayload.py index e8c68c2..e9e1f53 100644 --- a/discord/types/userpayload.py +++ b/discord/types/userpayload.py @@ -2,13 +2,13 @@ from typing import Optional, TypedDict -from .enums.locale import Locale -from .enums.userflags import UserFlags from pydantic.main import BaseModel -from .snowflake import Snowflake from ..types.avatar import Avatar from ..types.banner import Banner +from .enums.locale import Locale +from .enums.userflags import UserFlags +from .snowflake import Snowflake class UserPayload(BaseModel): diff --git a/discord/user/baseuser.py b/discord/user/baseuser.py index 6aaaf53..9c82d6c 100644 --- a/discord/user/baseuser.py +++ b/discord/user/baseuser.py @@ -1,23 +1,23 @@ from __future__ import annotations -from ..abc.abstractuser import AbstractUser from datetime import datetime -from ..types.avatar import Avatar -from ..types.banner import Banner -from ..types.enums.defaultavatar import DefaultAvatar -from ..types.enums.userflags import UserFlags -from ..types.userpayload import UserPayload from typing import Optional +from ..abc.abstractuser import AbstractUser from ..cache import GuildCache, UserCache from ..color import Color from ..message import Message +from ..types.avatar import Avatar +from ..types.banner import Banner +from ..types.enums.defaultavatar import DefaultAvatar +from ..types.enums.userflags import UserFlags +from ..types.userpayload import UserPayload class BaseUser(AbstractUser): class Config: arbitrary_types_allowed = True - + banner: Banner system: bool display_avatar: Avatar diff --git a/discord/user/member.py b/discord/user/member.py index df2515c..9e070d6 100644 --- a/discord/user/member.py +++ b/discord/user/member.py @@ -1,4 +1,5 @@ from __future__ import annotations + from typing import TYPE_CHECKING from .user import User diff --git a/setup.py b/setup.py index eaf53d4..6c15733 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,4 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup with open("README.md", "r") as file: long_des = file.read() @@ -16,11 +16,11 @@ author_email="arshia.aghaei@gmail.com", url="https://github.com/AA1999/Disthon", keywords=["API", "discord"], - classifiers = [ + classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Development Status :: 2 - Pre-Alpha", "Intended Audience :: Developers", - ] + ], ) From 0f4a521a77d22a851f62ff7660485af95d2a6ca5 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Thu, 11 Nov 2021 20:57:40 +0100 Subject: [PATCH 06/99] some init goofyness --- discord/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index d609e58..6160e43 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -2,6 +2,6 @@ A work in progress discord wrapper built from scratch """ -from .api.intents import Intents as Intents -from .client import Client as Client -from .embeds import Embed as Embed +from .api.intents import Intents +from .client import Client +from .embeds import Embed From f926733d45148ebc8dafc0398c5fbb0e22296838 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Mon, 15 Nov 2021 15:04:10 +0100 Subject: [PATCH 07/99] Changed event decorators to on and once --- discord/api/dataConverters.py | 2 +- discord/client.py | 30 +++++++++++++++++++++++------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index 79a9479..5f2b5d2 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -41,7 +41,7 @@ def convert_typing_start(self, data): return [data] def convert(self, event, data): - func: typing.Callable = self.converters.get(event.removeprefix("on_")) + func: typing.Callable = self.converters.get(event) if not func: raise NotImplementedError(f"No converter has been implemented for {event}") return func(data) diff --git a/discord/client.py b/discord/client.py index f2eca64..09c5df5 100644 --- a/discord/client.py +++ b/discord/client.py @@ -30,6 +30,7 @@ def __init__( self.lock = asyncio.Lock() self.closed = False self.events = {} + self.once_events = {} self.converter = DataConverter(self) async def login(self, token: str) -> None: @@ -73,9 +74,16 @@ def stop_loop_on_completion(_): if not future.cancelled(): return future.result() - def event(self, event: str = None, *, overwrite: bool = False): + def on(self, event: str = None, *, overwrite: bool = False): def wrapper(func): - self.add_listener(func, event, overwrite) + self.add_listener(func, event, overwrite, once=False) + return func + + return wrapper + + def once(self, event: str = None, *, overwrite: bool = False): + def wrapper(func): + self.add_listener(func, event, overwrite, once=True) return func return wrapper @@ -84,7 +92,9 @@ def add_listener( self, func: typing.Callable, event: typing.Optional[str] = None, + *, overwrite: bool = False, + once: bool = False, ) -> None: event = event or func.__name__ if not inspect.iscoroutinefunction(func): @@ -92,13 +102,19 @@ def add_listener( "The callback is not a valid coroutine function. Did you forget to add async before def?" ) - if event in self.events and not overwrite: - self.events[event].append(func) - else: - self.events[event] = [func] + if once: # if it's a once event + if event in self.once_events and not overwrite: + self.once_events[event].append(func) + else: + self.once_events[event] = [func] + else: # if it's a regular event + if event in self.events and not overwrite: + self.events[event].append(func) + else: + self.events[event] = [func] async def handle_event(self, msg): - event: str = "on_" + msg["t"].lower() + event: str = msg["t"].lower() args = self.converter.convert(event, msg["d"]) From 32b3469f72e55e2bcab3f7b1396a6fb5497960f1 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Mon, 15 Nov 2021 15:07:04 +0100 Subject: [PATCH 08/99] once events will get removed after firing --- discord/client.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/discord/client.py b/discord/client.py index 09c5df5..5fcb392 100644 --- a/discord/client.py +++ b/discord/client.py @@ -126,3 +126,14 @@ async def handle_event(self, msg): traceback.print_exception( type(error), error, error.__traceback__, file=sys.stderr ) + + for coro in self.once_events.get(event, []): + try: + await coro(*args) + except Exception as error: + print(f"Ignoring exception in event {coro.__name__}", file=sys.stderr) + traceback.print_exception( + type(error), error, error.__traceback__, file=sys.stderr + ) + finally: + del self.once_events[coro] From 9d48e7c5a23c6844b014107f150e3e1832b93a5e Mon Sep 17 00:00:00 2001 From: sebkuip Date: Mon, 15 Nov 2021 17:39:05 +0100 Subject: [PATCH 09/99] Events now fire in a task --- discord/client.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/discord/client.py b/discord/client.py index 5fcb392..da1ca3d 100644 --- a/discord/client.py +++ b/discord/client.py @@ -76,14 +76,14 @@ def stop_loop_on_completion(_): def on(self, event: str = None, *, overwrite: bool = False): def wrapper(func): - self.add_listener(func, event, overwrite, once=False) + self.add_listener(func, event, overwrite=overwrite, once=False) return func return wrapper def once(self, event: str = None, *, overwrite: bool = False): def wrapper(func): - self.add_listener(func, event, overwrite, once=True) + self.add_listener(func, event, overwrite=overwrite, once=True) return func return wrapper @@ -120,20 +120,20 @@ async def handle_event(self, msg): for coro in self.events.get(event, []): try: - await coro(*args) + task = self._loop.create_task(coro(*args)) + await task except Exception as error: print(f"Ignoring exception in event {coro.__name__}", file=sys.stderr) traceback.print_exception( type(error), error, error.__traceback__, file=sys.stderr ) - for coro in self.once_events.get(event, []): + for coro in self.once_events.pop(event, []): try: - await coro(*args) + task = self._loop.create_task(coro(*args)) + await task except Exception as error: print(f"Ignoring exception in event {coro.__name__}", file=sys.stderr) traceback.print_exception( type(error), error, error.__traceback__, file=sys.stderr ) - finally: - del self.once_events[coro] From 5b5f87af4b0d1241d284bbd31fb6bc472fb91151 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Mon, 15 Nov 2021 17:42:42 +0100 Subject: [PATCH 10/99] Quick update on converters --- discord/api/dataConverters.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index 5f2b5d2..aace13d 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -25,7 +25,7 @@ def __init__(self, client): def _get_channel(self, id): return None # TODO: get channel from cache - def convert_message(self, data): + def convert_message_create(self, data): return [Message(self.client, data)] def convert_ready(self, data): @@ -40,6 +40,9 @@ def convert_presence_update(self, data): def convert_typing_start(self, data): return [data] + def convert_guild_member_update(self, data): + return [data] + def convert(self, event, data): func: typing.Callable = self.converters.get(event) if not func: From 544d8c3a44073fb3b19b926a2ee8d042e5d6e82e Mon Sep 17 00:00:00 2001 From: sebkuip Date: Mon, 15 Nov 2021 19:23:22 +0100 Subject: [PATCH 11/99] Added logo to readme --- README.md | 2 ++ logo.png | Bin 0 -> 42211 bytes 2 files changed, 2 insertions(+) create mode 100644 logo.png diff --git a/README.md b/README.md index 27d5a18..8cc40bc 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,6 @@ [![Discord Support Server](https://img.shields.io/discord/885214547391180860?label=Disthon%20-%20Support%20Server&color=5865f2&labelColor=5865f2&&logo=discord&logoColor=ffffff&style=flat-square)](https://discord.gg/PtcfyJHKKp) +![Logo](https://github.com/AA1999/Disthon/blob/Converters/logo.png?raw=true) + Discord API wrapper for Python built from scratch diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..39ae49519fa71d7de8904f29f818da93aa7687db GIT binary patch literal 42211 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

ELm4|K&$SeQ5;T>m^XG^zAeDY2P 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%0Agg}RgU*$if|!s>pS9d#-bueA-gz{yF6 zf=>b@7w>9nDr5t`8G|AhnVEGtmDx6pjq2Bvuh>_^5*$-Zy3*mK-G$jG1kQtPho%UlVd?ciSK92Bt8hOGE0~>fptHpl>wvo$5>K$2 zqc)bl%1N%2%()K$9Y3Bu*+JI;U8(r&I-ujnvnR83E+=_tH=cci!8xe6fWE67^dz*E zHt!}>IJcQ^16jKKk|yiG?rlJq&+`CTES#jNWNvo_(CHDK8DvQSx>B(^fx(yXvY08> z=iwxb&8XHgc!-zT46^(HZM&P*CKVpyD zAkhbHAj>*VV$T??%L}I7Mg~PZ?Kb18_5J$Zl_M&M8L`D=FmAeGa?;iUsk#ejv&X{D z-t)wCgXPZtmQ#eV6|!@1ubGqdXo2od{bty$!qkVs1`R~n)<3coV^VjfP|265^NTxcBWBWH}Y7>>X5UBF!|!a znjsD6T92mXJ+Sp}pli=^405bywB^APi$oN_{tv!mSR+pL8Ia|)r!Zy<98UwW!Zwm4 zHlV&JKRFq#toU&M<0q@RYrCN<%&1!5t*hEzN>2F=J|MC{4WeCjh<hnw&{+Ag2$mk#ZES46aFEJ?|u#SCBy=kwu2`!|c9ln|_QVuJF256^PCh zjEJjl#4b6c;(}{=J(Mc^Rk^NLxvMM#%jAB>i}+Zkx4b(qc>JBaK@d%)@>3{VG_TS_(d zT#g0%>3GiDkdubP7&VumI*47Uzyv#vm&Z$Up$*rc*Bc&SQ(yK9Ln-|k@HGU&&Mvnz yC11C>J9VIM()Yur1c=e|KrKZdg5_iRp+JOC3aRK<*?29TMdJZ)$wn4o`I(T literal 0 HcmV?d00001 From 3b870e95b6bf0dfc7cca3946809efcf6eb1903c9 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Mon, 15 Nov 2021 19:24:29 +0100 Subject: [PATCH 12/99] Link is now relative --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8cc40bc..da06e5a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ [![Discord Support Server](https://img.shields.io/discord/885214547391180860?label=Disthon%20-%20Support%20Server&color=5865f2&labelColor=5865f2&&logo=discord&logoColor=ffffff&style=flat-square)](https://discord.gg/PtcfyJHKKp) -![Logo](https://github.com/AA1999/Disthon/blob/Converters/logo.png?raw=true) +![Logo](./logo.png?raw=true) Discord API wrapper for Python built from scratch From 2a2a32e19b79ad6a68634ed3845c6ff6d13d9c08 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Mon, 15 Nov 2021 19:30:55 +0100 Subject: [PATCH 13/99] resizing image --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da06e5a..254828a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ [![Discord Support Server](https://img.shields.io/discord/885214547391180860?label=Disthon%20-%20Support%20Server&color=5865f2&labelColor=5865f2&&logo=discord&logoColor=ffffff&style=flat-square)](https://discord.gg/PtcfyJHKKp) -![Logo](./logo.png?raw=true) +![Logo](./logo.png?raw=true =300x250) Discord API wrapper for Python built from scratch From ef21d3d46970119863b96261749db9d43b620a9b Mon Sep 17 00:00:00 2001 From: sebkuip Date: Mon, 15 Nov 2021 19:31:28 +0100 Subject: [PATCH 14/99] Resize did not work --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 254828a..da06e5a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,6 @@ [![Discord Support Server](https://img.shields.io/discord/885214547391180860?label=Disthon%20-%20Support%20Server&color=5865f2&labelColor=5865f2&&logo=discord&logoColor=ffffff&style=flat-square)](https://discord.gg/PtcfyJHKKp) -![Logo](./logo.png?raw=true =300x250) +![Logo](./logo.png?raw=true) Discord API wrapper for Python built from scratch From d814fe4d7d4126417ca6d7b1ceacfd8cc5a87e00 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Wed, 17 Nov 2021 22:05:43 +0100 Subject: [PATCH 15/99] No longer using our own loop/deprecated stuff --- discord/client.py | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/discord/client.py b/discord/client.py index da1ca3d..bc44a6f 100644 --- a/discord/client.py +++ b/discord/client.py @@ -21,7 +21,7 @@ def __init__( respond_self: typing.Optional[bool] = False, loop: typing.Optional[asyncio.AbstractEventLoop] = None, ) -> None: - self._loop: asyncio.AbstractEventLoop = loop or asyncio.get_event_loop() + self._loop: asyncio.AbstractEventLoop = None # create the event loop when we run our client self.intents = intents self.respond_self = respond_self @@ -63,16 +63,7 @@ async def close(self) -> None: await self.httphandler.close() def run(self, token: str): - def stop_loop_on_completion(_): - self._loop.stop() - - future = asyncio.ensure_future(self.alive_loop(token), loop=self._loop) - future.add_done_callback(stop_loop_on_completion) - - self._loop.run_forever() - - if not future.cancelled(): - return future.result() + asyncio.run(self.alive_loop(token)) def on(self, event: str = None, *, overwrite: bool = False): def wrapper(func): From 45a1575fc06d89fe2c935e7a6a3ec21fb12396d8 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Fri, 26 Nov 2021 16:55:22 +0100 Subject: [PATCH 16/99] Added a close --- discord/api/httphandler.py | 3 ++- discord/api/websocket.py | 13 +++++++++---- discord/client.py | 6 ++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/discord/api/httphandler.py b/discord/api/httphandler.py index 6225dbc..7206752 100644 --- a/discord/api/httphandler.py +++ b/discord/api/httphandler.py @@ -79,7 +79,8 @@ async def connect(self, url: str) -> aiohttp.ClientWebSocketResponse: return await self._session.ws_connect(url, **kwargs) async def close(self) -> None: - await self._session.close() + if self._session: + await self._session.close() async def send_message( self, diff --git a/discord/api/websocket.py b/discord/api/websocket.py index 1677168..7fc44ad 100644 --- a/discord/api/websocket.py +++ b/discord/api/websocket.py @@ -54,16 +54,21 @@ async def start( if reconnect: await self.resume() else: - t = threading.Thread(target=self.keep_alive, daemon=True) - t.start() + self.hb_t: threading.Thread = threading.Thread(target=self.keep_alive, daemon=True) + self.hb_t.start() return self + + async def close(self) -> None: + """Closes the websocket""" + await self.socket.close() + self.hb_t.cancel() def keep_alive(self) -> None: while True: time.sleep(self.hb_int) if not self.heartbeat_acked: # We have a zombified connection - self.socket.close() + self.socket.close(code=1000) asyncio.run(self.start(reconnect=True)) else: asyncio.run(self.heartbeat()) @@ -77,7 +82,7 @@ def on_websocket_message(self, msg: WSMessage) -> dict: if len(msg) < 4 or msg[-4:] != b"\x00\x00\xff\xff": return - msg = self.decompress.decompress(self.buffer) + msg: bytes = self.decompress.decompress(self.buffer) msg = msg.decode("utf-8") self.buffer = bytearray() diff --git a/discord/client.py b/discord/client.py index bc44a6f..14d9b54 100644 --- a/discord/client.py +++ b/discord/client.py @@ -61,10 +61,16 @@ async def alive_loop(self, token: str) -> None: async def close(self) -> None: await self.httphandler.close() + await self.ws.close() + self.loop.close() def run(self, token: str): asyncio.run(self.alive_loop(token)) + def close(self): + self.closed = True + self.loop.close() + def on(self, event: str = None, *, overwrite: bool = False): def wrapper(func): self.add_listener(func, event, overwrite=overwrite, once=False) From c26cd147276108bacd792278fb920ddcdbe37cbe Mon Sep 17 00:00:00 2001 From: sebkuip Date: Fri, 26 Nov 2021 16:55:49 +0100 Subject: [PATCH 17/99] Whitespace fix --- discord/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/cache.py b/discord/cache.py index 5ad0785..77eb8c9 100644 --- a/discord/cache.py +++ b/discord/cache.py @@ -23,7 +23,7 @@ class LFUCache: def __init__(self, capacity: int) -> None: self.capacity = capacity self._frequency = {} - self.length = 0 + self.length = 0 @classmethod def _from_lfu(cls, lfu: LFUCache): From d3af037ea575595fcab6eccdd5f92af38934714a Mon Sep 17 00:00:00 2001 From: sebkuip Date: Fri, 26 Nov 2021 16:57:56 +0100 Subject: [PATCH 18/99] swap --- discord/cache.py | 2 +- discord/client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/cache.py b/discord/cache.py index 77eb8c9..5ad0785 100644 --- a/discord/cache.py +++ b/discord/cache.py @@ -23,7 +23,7 @@ class LFUCache: def __init__(self, capacity: int) -> None: self.capacity = capacity self._frequency = {} - self.length = 0 + self.length = 0 @classmethod def _from_lfu(cls, lfu: LFUCache): diff --git a/discord/client.py b/discord/client.py index 14d9b54..6aa6470 100644 --- a/discord/client.py +++ b/discord/client.py @@ -60,8 +60,8 @@ async def alive_loop(self, token: str) -> None: await self.close() async def close(self) -> None: - await self.httphandler.close() await self.ws.close() + await self.httphandler.close() self.loop.close() def run(self, token: str): From 8341c05f27f543a6f229eeff46729d7a7b157eed Mon Sep 17 00:00:00 2001 From: sebkuip Date: Fri, 26 Nov 2021 17:29:55 +0100 Subject: [PATCH 19/99] improved close --- discord/__init__.py | 1 + discord/api/httphandler.py | 1 + discord/api/websocket.py | 22 +++++++++++++++------- discord/client.py | 7 ++----- 4 files changed, 19 insertions(+), 12 deletions(-) diff --git a/discord/__init__.py b/discord/__init__.py index 6160e43..2287a23 100644 --- a/discord/__init__.py +++ b/discord/__init__.py @@ -5,3 +5,4 @@ from .api.intents import Intents from .client import Client from .embeds import Embed +from .message import Message diff --git a/discord/api/httphandler.py b/discord/api/httphandler.py index 7206752..b64e80b 100644 --- a/discord/api/httphandler.py +++ b/discord/api/httphandler.py @@ -80,6 +80,7 @@ async def connect(self, url: str) -> aiohttp.ClientWebSocketResponse: async def close(self) -> None: if self._session: + print("closing httpsession") await self._session.close() async def send_message( diff --git a/discord/api/websocket.py b/discord/api/websocket.py index 7fc44ad..9eb233c 100644 --- a/discord/api/websocket.py +++ b/discord/api/websocket.py @@ -39,6 +39,7 @@ def __init__(self, client, token: str) -> None: self.token = token self.session_id = None self.heartbeat_acked = True + self.closed: bool = False async def start( self, @@ -55,17 +56,21 @@ async def start( await self.resume() else: self.hb_t: threading.Thread = threading.Thread(target=self.keep_alive, daemon=True) + self.hb_stop: threading.Event = threading.Event() self.hb_t.start() return self async def close(self) -> None: """Closes the websocket""" + print("setting closed ws") + self.closed = True + print("ws closing") await self.socket.close() - self.hb_t.cancel() + print("hb closing") + self.hb_stop.set() def keep_alive(self) -> None: - while True: - time.sleep(self.hb_int) + while not self.hb_stop.wait(self.hb_int): if not self.heartbeat_acked: # We have a zombified connection self.socket.close(code=1000) @@ -73,14 +78,14 @@ def keep_alive(self) -> None: else: asyncio.run(self.heartbeat()) - def on_websocket_message(self, msg: WSMessage) -> dict: + def on_websocket_message(self, msg: WSMessage) -> dict: if type(msg) is bytes: # always push the message data to your cache self.buffer.extend(msg) # check if last 4 bytes are ZLIB_SUFFIX if len(msg) < 4 or msg[-4:] != b"\x00\x00\xff\xff": - return + return msg msg: bytes = self.decompress.decompress(self.buffer) msg = msg.decode("utf-8") @@ -99,8 +104,11 @@ async def receive_events(self) -> None: aiohttp.WSMsgType.CLOSING, aiohttp.WSMsgType.CLOSED, ): - await self.socket.close() - raise ConnectionResetError(msg.extra) + if not self.closed: + await self.socket.close() + raise ConnectionResetError(msg.extra) + else: + return msg = json.loads(msg) diff --git a/discord/client.py b/discord/client.py index 6aa6470..b669f61 100644 --- a/discord/client.py +++ b/discord/client.py @@ -53,6 +53,7 @@ async def connect(self) -> None: await self.ws.receive_events() async def alive_loop(self, token: str) -> None: + self._loop: asyncio.AbstractEventLoop = asyncio.get_running_loop() await self.login(token) try: await self.connect() @@ -60,17 +61,13 @@ async def alive_loop(self, token: str) -> None: await self.close() async def close(self) -> None: + self.closed = True await self.ws.close() await self.httphandler.close() - self.loop.close() def run(self, token: str): asyncio.run(self.alive_loop(token)) - def close(self): - self.closed = True - self.loop.close() - def on(self, event: str = None, *, overwrite: bool = False): def wrapper(func): self.add_listener(func, event, overwrite=overwrite, once=False) From 5c3e68152392474e2cf239322d94e95c918805f6 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Fri, 26 Nov 2021 17:39:51 +0100 Subject: [PATCH 20/99] Made default error handler an event --- discord/client.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/discord/client.py b/discord/client.py index b669f61..621764d 100644 --- a/discord/client.py +++ b/discord/client.py @@ -117,17 +117,20 @@ async def handle_event(self, msg): task = self._loop.create_task(coro(*args)) await task except Exception as error: - print(f"Ignoring exception in event {coro.__name__}", file=sys.stderr) - traceback.print_exception( - type(error), error, error.__traceback__, file=sys.stderr - ) + error.event = coro + await self.handle_event({"d": error, "t": "event_error"}) for coro in self.once_events.pop(event, []): try: task = self._loop.create_task(coro(*args)) await task except Exception as error: - print(f"Ignoring exception in event {coro.__name__}", file=sys.stderr) - traceback.print_exception( - type(error), error, error.__traceback__, file=sys.stderr - ) + error.event = coro + await self.handle_event({"d": error, "t": "event_error"}) + + @on("event_error") + async def handle_event_error(self, error): + print(f"Ignoring exception in event {error.event.__name__}", file=sys.stderr) + traceback.print_exception( + type(error), error, error.__traceback__, file=sys.stderr + ) From 0ba2bc20d9e0e3a631396ef46a244d5041a96f44 Mon Sep 17 00:00:00 2001 From: sebkuip Date: Fri, 26 Nov 2021 17:52:21 +0100 Subject: [PATCH 21/99] further improvements to close() --- discord/api/dataConverters.py | 3 +++ discord/client.py | 18 +++++++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index aace13d..1f59c48 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -25,6 +25,9 @@ def __init__(self, client): def _get_channel(self, id): return None # TODO: get channel from cache + def convert_event_error(self, data): + return [data] + def convert_message_create(self, data): return [Message(self.client, data)] diff --git a/discord/client.py b/discord/client.py index 621764d..65e03e9 100644 --- a/discord/client.py +++ b/discord/client.py @@ -14,6 +14,13 @@ class Client: + + async def handle_event_error(self, error): + print(f"Ignoring exception in event {error.event.__name__}", file=sys.stderr) + traceback.print_exception( + type(error), error, error.__traceback__, file=sys.stderr + ) + def __init__( self, *, @@ -29,7 +36,7 @@ def __init__( self.httphandler = HTTPHandler() self.lock = asyncio.Lock() self.closed = False - self.events = {} + self.events = {"event_error": [self.handle_event_error]} self.once_events = {} self.converter = DataConverter(self) @@ -49,7 +56,7 @@ async def connect(self) -> None: ) self.ws = await asyncio.wait_for(socket.start(g_url), timeout=30) - while True: + while not self.closed: await self.ws.receive_events() async def alive_loop(self, token: str) -> None: @@ -127,10 +134,3 @@ async def handle_event(self, msg): except Exception as error: error.event = coro await self.handle_event({"d": error, "t": "event_error"}) - - @on("event_error") - async def handle_event_error(self, error): - print(f"Ignoring exception in event {error.event.__name__}", file=sys.stderr) - traceback.print_exception( - type(error), error, error.__traceback__, file=sys.stderr - ) From 6c4147bec534f491143d7c28dce5ec8676f8755a Mon Sep 17 00:00:00 2001 From: sebkuip Date: Fri, 26 Nov 2021 17:55:24 +0100 Subject: [PATCH 22/99] Now actually uses a passed loop --- discord/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index 65e03e9..bb3e697 100644 --- a/discord/client.py +++ b/discord/client.py @@ -73,7 +73,11 @@ async def close(self) -> None: await self.httphandler.close() def run(self, token: str): - asyncio.run(self.alive_loop(token)) + if not self._loop: + asyncio.run(self.alive_loop(token)) + else: + self._loop.run_forever(self.alive_loop(token)) + def on(self, event: str = None, *, overwrite: bool = False): def wrapper(func): From 41eecd01b5fffae43e4d2d89175f631e83120a4e Mon Sep 17 00:00:00 2001 From: sebkuip Date: Fri, 26 Nov 2021 18:04:08 +0100 Subject: [PATCH 23/99] removed debuggin print() statements --- discord/api/httphandler.py | 1 - discord/api/websocket.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/discord/api/httphandler.py b/discord/api/httphandler.py index b64e80b..7206752 100644 --- a/discord/api/httphandler.py +++ b/discord/api/httphandler.py @@ -80,7 +80,6 @@ async def connect(self, url: str) -> aiohttp.ClientWebSocketResponse: async def close(self) -> None: if self._session: - print("closing httpsession") await self._session.close() async def send_message( diff --git a/discord/api/websocket.py b/discord/api/websocket.py index 9eb233c..60f1b91 100644 --- a/discord/api/websocket.py +++ b/discord/api/websocket.py @@ -62,11 +62,8 @@ async def start( async def close(self) -> None: """Closes the websocket""" - print("setting closed ws") self.closed = True - print("ws closing") await self.socket.close() - print("hb closing") self.hb_stop.set() def keep_alive(self) -> None: From 91c8d748cfd41dced4be92b0f73aba55ba3dde46 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 27 Nov 2021 16:00:52 +0530 Subject: [PATCH 24/99] Made Snowflake a subclass of int --- discord/types/snowflake.py | 51 ++++++++++---------------------------- 1 file changed, 13 insertions(+), 38 deletions(-) diff --git a/discord/types/snowflake.py b/discord/types/snowflake.py index b09e264..8e27515 100644 --- a/discord/types/snowflake.py +++ b/discord/types/snowflake.py @@ -1,44 +1,19 @@ -from __future__ import annotations - -from pydantic import BaseModel - __all__ = ("Snowflake",) -class Snowflake(BaseModel): - id: int - - def __eq__(self, other) -> bool: - if isinstance(other, (int, str)): - return self.id == other - return isinstance(other, Snowflake) and self.id == other.id - - def __ne__(self, other: object) -> bool: - return not self.__eq__(other) - - def __lt__(self, other: object): - if isinstance(other, int): - return self.id < other - if isinstance(other, str) and other.isdigit(): - return self.id < int(other) - if isinstance(other, Snowflake): - return self.id < other.id - raise NotImplementedError - - def __le__(self, other: object): - return self.__eq__(other) or self.__lt__(other) - - def __gt__(self, other: object): - return not self.__le__(other) - - def __ge__(self, other: object): - return not self.__lt__(other) +class Snowflake(int): + @property + def timestamp(self): + return (self >> 22) + 1420070400000 - def __str__(self) -> str: - return str(self.id) + @property + def worker_id(self): + return (self & 0x3E0000) >> 17 - def __int__(self) -> int: - return int(self.id) + @property + def process_id(self): + return (self & 0x1F000) >> 12 - def __repr__(self) -> str: - return f"Snowflake with id {self.id}" + @property + def increment(self): + return self & 0xFFF From dd18201ed40ac74ff9d057bc6a7a189647734ba7 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 27 Nov 2021 16:01:16 +0530 Subject: [PATCH 25/99] Typehinted id to Snowflake --- discord/message.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/discord/message.py b/discord/message.py index ba8c4a7..4cecf68 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1,20 +1,26 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import TYPE_CHECKING from pydantic import BaseModel +from .types.snowflake import Snowflake + if TYPE_CHECKING: from .client import Client class Message(BaseModel): - id: int - channel_id: int + id: Snowflake + channel_id: Snowflake + guild_id: Snowflake content: str _client: Client + class Config: + arbitrary_types_allowed = True + def __init__(self, client, data): super().__init__(_client=client, **data) @@ -27,3 +33,5 @@ def __repr__(self): @property def channel(self): return self._client.converter._get_channel(self.channel_id) + + From 34f3262b5b8bcc3a4161b4336a2432e47c6ddbd7 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 27 Nov 2021 16:01:51 +0530 Subject: [PATCH 26/99] Typehinted id to Snowflake --- discord/guild.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/guild.py b/discord/guild.py index 9fc401d..c95207f 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -9,6 +9,7 @@ from .user.member import Member from .user.user import User + if TYPE_CHECKING: from .role import Role @@ -41,6 +42,7 @@ class Guild(DiscordObject): "_banner", ) + id: Snowflake _roles: set[Role] me: Member owner_id: Snowflake From 15bfc6bf3f4f93922fd0d9f666762405af1dc328 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Mon, 29 Nov 2021 21:07:24 +0530 Subject: [PATCH 27/99] Added Snowflake.__get_validators__ --- discord/types/snowflake.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/discord/types/snowflake.py b/discord/types/snowflake.py index 8e27515..089d8ff 100644 --- a/discord/types/snowflake.py +++ b/discord/types/snowflake.py @@ -1,3 +1,5 @@ +import datetime + __all__ = ("Snowflake",) @@ -17,3 +19,25 @@ def process_id(self): @property def increment(self): return self & 0xFFF + + @property + def created_at(self): + return datetime.datetime.fromtimestamp(self.timestamp) # TODO: Fix this + + @classmethod + def __get_validators__(cls): + yield cls.validate + + @classmethod + def validate(cls, value): + if isinstance(value, int): + return cls(value) + elif isinstance(value, str): + if value.isdigit(): + return cls(value) + else: + raise ValueError("Invalid Snowflake") + elif isinstance(value, Snowflake): + return value + else: + return ValueError("Invalid Snowflake") From 5df93bf720489a28d68ce17a2ad0085f010918d7 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Mon, 29 Nov 2021 21:07:45 +0530 Subject: [PATCH 28/99] Changed Message.__repr__ --- discord/message.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/discord/message.py b/discord/message.py index 4cecf68..7b29762 100644 --- a/discord/message.py +++ b/discord/message.py @@ -18,9 +18,6 @@ class Message(BaseModel): _client: Client - class Config: - arbitrary_types_allowed = True - def __init__(self, client, data): super().__init__(_client=client, **data) @@ -28,10 +25,8 @@ def __str__(self): return self.content def __repr__(self): - return f"" + return f"" @property def channel(self): return self._client.converter._get_channel(self.channel_id) - - From 3d29805dcb5e65a912987e4596bfbdecce7e3bf4 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Mon, 13 Dec 2021 19:25:30 +0530 Subject: [PATCH 29/99] Fix: Snowflake.created_at --- discord/types/snowflake.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/types/snowflake.py b/discord/types/snowflake.py index 089d8ff..ff67be9 100644 --- a/discord/types/snowflake.py +++ b/discord/types/snowflake.py @@ -22,7 +22,7 @@ def increment(self): @property def created_at(self): - return datetime.datetime.fromtimestamp(self.timestamp) # TODO: Fix this + return datetime.datetime.fromtimestamp(self.timestamp / 1000) @classmethod def __get_validators__(cls): From 5d5ecb1f34ff88e90de11f33b836114c09e77c9b Mon Sep 17 00:00:00 2001 From: Rashaad Date: Tue, 14 Dec 2021 19:18:01 +0530 Subject: [PATCH 30/99] Fix: Fixed some bugs in LFUCache --- discord/cache.py | 118 +++++++++-------------------------------------- 1 file changed, 21 insertions(+), 97 deletions(-) diff --git a/discord/cache.py b/discord/cache.py index 5ad0785..b20fee6 100644 --- a/discord/cache.py +++ b/discord/cache.py @@ -1,6 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, OrderedDict +from typing import TYPE_CHECKING, Any, Dict, OrderedDict + +from .types.snowflake import Snowflake + if TYPE_CHECKING: @@ -8,115 +11,36 @@ from .guild import Guild from .message import Message from .role import Role - from .types.snowflake import Snowflake from .user.member import Member from .user.user import User -class LFUCache: - - capacity: int - _cache: OrderedDict[Snowflake, Any] - _frequency: dict[Snowflake, int] - httphandler: HTTPHandler - +class LFUCache(OrderedDict): def __init__(self, capacity: int) -> None: - self.capacity = capacity - self._frequency = {} - self.length = 0 + self.capacity: int = capacity + self._frequency: Dict[Snowflake, int] = {} + self.length: int = 0 + super().__init__() - @classmethod - def _from_lfu(cls, lfu: LFUCache): - self = cls.__new__(cls) - self.capacity = lfu.capacity - self._cache = lfu._cache - self._frequency = lfu._frequency - return self + def __setitem__(self, key: Snowflake, value: Any) -> None: + frequency = self._frequency - def __eq__(self, other) -> bool: - return ( - isinstance(other, LFUCache) - and other._cache == self._cache - and self.capacity == other.capacity - ) + if key not in self: + self.length += 1 - def __ne__(self, other) -> bool: - return not self.__eq__(other) + super().__setitem__(key, value) + frequency[key] = 0 - def __setitem__(self, key: Snowflake, value: Any) -> None: - if key not in self._cache: - self.length += 1 - self._cache[key] = value - if self._frequency[key]: - self._frequency[key] += 1 - else: - self._frequency[key] = 0 if self.length > self.capacity: - snowflake: Snowflake - min_freq = float("inf") - for k in self._frequency.keys(): - if self._frequency[k] < min_freq: - min_freq = self._frequency[k] - snowflake = k - del self._cache[snowflake] + inverted = dict(zip(frequency.values(), frequency.keys())) + least_used = min(self._frequency.values()) + del self[inverted[least_used]] def __getitem__(self, key: Snowflake): - if self._cache[key]: - self._frequency[key] += 1 - return self._cache[key] - raise KeyError + self._frequency[key] += 1 + return self[key] def __delitem__(self, key: Snowflake): - del self._cache[key] + super().__delitem__(key) del self._frequency[key] self.length -= 1 - - -class UserCache(LFUCache): - _cache: dict[Snowflake, User] - - def __init__(self) -> None: - super().__init__(100000) - - def __setitem__(self, key: Snowflake, value: User) -> None: - return super().__setitem__(key, value) - - -class MemberCache(LFUCache): - _cache: dict[Snowflake, Member] - - def __init__(self) -> None: - super().__init__(100000) - - def __setitem__(self, key: Snowflake, value: Member) -> None: - return super().__setitem__(key, value) - - -class MessageCache(LFUCache): - _cache: dict[Snowflake, Message] - - def __init__(self) -> None: - super().__init__(2000) - - def __setitem__(self, key: Snowflake, value: Message) -> None: - return super().__setitem__(key, value) - - -class RoleCache(LFUCache): - _cache: dict[Snowflake, Role] - - def __init__(self) -> None: - super().__init__(250) - - def __setitem__(self, key: Snowflake, value: Role) -> None: - return super().__setitem__(key, value) - - -class GuildCache(LFUCache): - _cache: dict[Snowflake, Guild] - - def __init__(self) -> None: - super().__init__(20000) - - def __setitem__(self, key: Snowflake, value: Guild) -> None: - return super().__setitem__(key, value) From bae001fbc5f7b48f022457c557a57a5bcbf88e71 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Tue, 14 Dec 2021 19:20:54 +0530 Subject: [PATCH 31/99] Changed imports --- discord/user/baseuser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/user/baseuser.py b/discord/user/baseuser.py index 9c82d6c..94baef1 100644 --- a/discord/user/baseuser.py +++ b/discord/user/baseuser.py @@ -4,7 +4,7 @@ from typing import Optional from ..abc.abstractuser import AbstractUser -from ..cache import GuildCache, UserCache +from ..cache import LFUCache from ..color import Color from ..message import Message from ..types.avatar import Avatar @@ -23,10 +23,10 @@ class Config: display_avatar: Avatar display_name: str public_flags: UserFlags - _cache: UserCache - guilds: GuildCache + _cache: LFUCache + guilds: LFUCache - def __init__(self, cache: UserCache, guilds: GuildCache, payload: UserPayload): + def __init__(self, cache: LFUCache, guilds: LFUCache, payload: UserPayload): self._cache = cache self.guilds = guilds self._id = payload.id From c04d8c08325cbda0bf54a5b598ff71f470e9680f Mon Sep 17 00:00:00 2001 From: Rashaad Date: Tue, 14 Dec 2021 19:21:06 +0530 Subject: [PATCH 32/99] Changed imports --- discord/role.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/role.py b/discord/role.py index bde87b1..21c60fb 100644 --- a/discord/role.py +++ b/discord/role.py @@ -10,7 +10,7 @@ __all__ = ("RoleTags", "Role") -from .cache import RoleCache +from .cache import LFUCache from .types.rolepayload import RolePayload, RoleTagsPayload from .types.snowflake import Snowflake @@ -55,7 +55,7 @@ class Role(DiscordObject): ) _guild: Guild - _cache: RoleCache + _cache: LFUCache _id: Snowflake _name: str _color: Color @@ -66,7 +66,7 @@ class Role(DiscordObject): _mentionable: bool _tags: Optional[RoleTags] - def __init__(self, guild: Guild, cache: RoleCache, payload: RolePayload): + def __init__(self, guild: Guild, cache: LFUCache, payload: RolePayload): self._guild = guild self._cache = cache self._id = payload["id"] From 395a1aed416c2d40dcee3223acf685b5695ff56d Mon Sep 17 00:00:00 2001 From: Rashaad Date: Tue, 14 Dec 2021 19:54:26 +0530 Subject: [PATCH 33/99] Divided the timestamp by 1000 --- discord/types/snowflake.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/types/snowflake.py b/discord/types/snowflake.py index ff67be9..6580dc0 100644 --- a/discord/types/snowflake.py +++ b/discord/types/snowflake.py @@ -6,7 +6,7 @@ class Snowflake(int): @property def timestamp(self): - return (self >> 22) + 1420070400000 + return ((self >> 22) + 1420070400000) / 1000 @property def worker_id(self): @@ -22,7 +22,7 @@ def increment(self): @property def created_at(self): - return datetime.datetime.fromtimestamp(self.timestamp / 1000) + return datetime.datetime.fromtimestamp(self.timestamp) @classmethod def __get_validators__(cls): From 65e4d7fd57f54732d174b26bec7ebc85c28c5990 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 15 Dec 2021 17:17:17 +0530 Subject: [PATCH 34/99] Removed DiscordObject.created_at --- discord/abc/discordobject.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/abc/discordobject.py b/discord/abc/discordobject.py index d271fd8..a57cd4e 100644 --- a/discord/abc/discordobject.py +++ b/discord/abc/discordobject.py @@ -10,7 +10,6 @@ class DiscordObject(BaseModel): id: Snowflake - created_at: datetime def __ne__(self, other): return not self.__eq__(other) From fb4f6e952fe3d28d897e59ab830a08d7b4b254a5 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 15 Dec 2021 17:17:48 +0530 Subject: [PATCH 35/99] Added cache --- discord/api/websocket.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/discord/api/websocket.py b/discord/api/websocket.py index 60f1b91..554083d 100644 --- a/discord/api/websocket.py +++ b/discord/api/websocket.py @@ -12,6 +12,10 @@ import aiohttp from aiohttp.http_websocket import WSMessage, WSMsgType +from ..cache import LFUCache +from ..guild import Guild +from ..types.snowflake import Snowflake + if typing.TYPE_CHECKING: from ..client import Client @@ -41,6 +45,10 @@ def __init__(self, client, token: str) -> None: self.heartbeat_acked = True self.closed: bool = False + self.guild_cache = LFUCache[Snowflake, Guild](1000) + self.member_cache = LFUCache[Snowflake, dict](5000) + self.user_cache = LFUCache[Snowflake, dict](5000) + async def start( self, url: typing.Optional[str] = None, From 8982a35cbdf26732afdd2af405d5830966ab4a84 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 15 Dec 2021 17:18:31 +0530 Subject: [PATCH 36/99] Push the data to the cache --- discord/api/dataConverters.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index 1f59c48..ec66c78 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -1,18 +1,10 @@ import inspect import typing -from ..activity.activity import Activity -from ..channels.dmchannel import DMChannel -from ..channels.guildchannel import TextChannel, VoiceChannel -from ..color import Color -from ..embeds import Embed +from discord.types.snowflake import Snowflake + from ..guild import Guild -from ..interactions.components import Component, View from ..message import Message -from ..role import Role -from ..user.member import Member -from ..user.user import User - class DataConverter: def __init__(self, client): @@ -35,7 +27,15 @@ def convert_ready(self, data): return [] def convert_guild_create(self, data): - return [data] + members = data["members"] + guild = Guild(**data) + self.client.ws.guild_cache[Snowflake(data["id"])] = guild + + for member in members: + self.client.ws.member_cache[Snowflake(member["user"]["id"])] = member + self.client.ws.user_cache[Snowflake(member["user"]["id"])] = member["user"] + + return [guild] def convert_presence_update(self, data): return [data] @@ -49,5 +49,5 @@ def convert_guild_member_update(self, data): def convert(self, event, data): func: typing.Callable = self.converters.get(event) if not func: - raise NotImplementedError(f"No converter has been implemented for {event}") + return data return func(data) From fb4033e47d323b4aabec71329e370e2a72cacaee Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 15 Dec 2021 17:19:22 +0530 Subject: [PATCH 37/99] Did some stuff --- discord/user/baseuser.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/discord/user/baseuser.py b/discord/user/baseuser.py index 94baef1..75433ef 100644 --- a/discord/user/baseuser.py +++ b/discord/user/baseuser.py @@ -23,36 +23,6 @@ class Config: display_avatar: Avatar display_name: str public_flags: UserFlags - _cache: LFUCache - guilds: LFUCache - - def __init__(self, cache: LFUCache, guilds: LFUCache, payload: UserPayload): - self._cache = cache - self.guilds = guilds - self._id = payload.id - self._created_at = datetime.utcnow() - self.avatar = payload.avatar - self.username = payload.username - self.discriminator = payload.discriminator - self.banner = payload.banner - self.public_flags = payload.flags - self.system = payload.system or False - self.bot = payload.bot or False - - @classmethod - def _from_user(cls, user: BaseUser) -> BaseUser: - self = cls.__new__(cls) - self.avatar = user.avatar or user.default_avatar - self.banner = user.banner - self._cache = user._cache - self._created_at = user.created_at - self.discriminator = user.discriminator - self.display_name = user.display_name - self._id = user.id - self.public_flags = user.public_flags - self.system = user.system - self.username = user.username - return self async def create_dm(self): pass From f139da44c0b324f445319f9f988dacf263f8bdca Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 15 Dec 2021 17:20:12 +0530 Subject: [PATCH 38/99] Improved the Guild model --- discord/guild.py | 43 ++++++------------------------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/discord/guild.py b/discord/guild.py index c95207f..5436976 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, NamedTuple, Optional, Text, Union +from typing import TYPE_CHECKING, NamedTuple, Optional, List from .abc.discordobject import DiscordObject from .channels.guildchannel import TextChannel, VoiceChannel @@ -30,40 +30,9 @@ class GuildLimit(NamedTuple): class Guild(DiscordObject): - __slots__ = ( - "region", - "owner_id", - "mfa_level", - "name", - "id", - "_members", - "_channels", - "_vanity", - "_banner", - ) - - id: Snowflake - _roles: set[Role] - me: Member + owner: bool = False owner_id: Snowflake - - def __init__(self, data: GuildPayload): - self._members: dict[Snowflake, Member] = {} - self._channels: dict[Snowflake, Union[TextChannel, VoiceChannel]] = {} - self._roles = set() - - def _add_channel(self, channel: Union[TextChannel, VoiceChannel], /) -> None: - self._channels[channel.id] = channel - - def _delete_channel(self, channel: DiscordObject) -> None: - self._channels.pop(channel.id, None) - - def add_member(self, member: Member) -> None: - self._members[member.id] = member - - def add_roles(self, role: Role) -> None: - for p in self._roles.values: - p.postion += not p.is_default() - # checks if role is @everyone or not - - self._roles[role.id] = role + members: List[dict] + roles: List[dict] + emojis: List[dict] + stickers: List[dict] From 6d127d8e6c87beba18f46ce1d5d766fe84d2c38c Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 15 Dec 2021 17:21:40 +0530 Subject: [PATCH 39/99] Added .DS_Store --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f6283d1..66833da 100644 --- a/.gitignore +++ b/.gitignore @@ -131,4 +131,6 @@ dmypy.json #pycharm and vscode folders .vscode -.idea \ No newline at end of file +.idea + +.DS_Store From 8cea032bf67ad7581aeb511c40b1139760ee5c2e Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 15 Dec 2021 23:35:37 +0530 Subject: [PATCH 40/99] Added Client.get_user and Client.get_guild --- discord/client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/discord/client.py b/discord/client.py index bb3e697..e27298a 100644 --- a/discord/client.py +++ b/discord/client.py @@ -138,3 +138,9 @@ async def handle_event(self, msg): except Exception as error: error.event = coro await self.handle_event({"d": error, "t": "event_error"}) + + def get_guild(self, id: int): + return self.ws.guild_cache.get(id) + + def get_user(self, id: int): + return self.ws.user_cache.get(id) From 4f02d6dfab21670a392d0a490f4774c3e4f01cf9 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Fri, 17 Dec 2021 17:15:00 +0530 Subject: [PATCH 41/99] Added core.py --- discord/ext/commands/core.py | 56 ++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 discord/ext/commands/core.py diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py new file mode 100644 index 0000000..32fc040 --- /dev/null +++ b/discord/ext/commands/core.py @@ -0,0 +1,56 @@ +import asyncio +import inspect +import re +from typing import Optional, Any, Callable, Iterable + +from discord.message import Message + + +class Command: + def __init__( + self, + callback: Callable[[Any], Any], + name: str = None, + *, + aliases: Iterable[str] = None, + regex_command: bool = False, + regex_match_func=re.match, + regex_flags=0, + **kwargs + ): + if not asyncio.iscoroutinefunction(callback): + raise ValueError( + "Callback must be coroutine.\nMaybe you forgot to add the 'async' keyword?." + ) + + self._callback = callback + self.name = name or self._callback.__name__ + self.is_regex_command = regex_command + self.regex_match_func = regex_match_func + self.regex_flags = regex_flags + + if aliases is None: + self.aliases = [] + else: + self.aliases = aliases + + self.checks = [] + self.description = kwargs.get("description") or self._callback.__doc__ + self.signature = inspect.signature(self._callback) + self.on_error: Optional[Callable[[Any], Any]] = None + + @property + def callback(self): + return self._callback + + async def __call__(self, message: Message, *args, **kwargs): + await self.callback(message, *args, **kwargs) + + def error(self, function): + if not asyncio.iscoroutinefunction(function): + raise ValueError( + "Command error handler must be coroutine.\nMaybe you forgot to add the 'async' keyword?." + ) + + self.on_error = function + return function From da415381f0c13366d068f39176cd36bfe5d13523 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Fri, 17 Dec 2021 17:15:44 +0530 Subject: [PATCH 42/99] Added parser.py --- discord/ext/commands/parser.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 discord/ext/commands/parser.py diff --git a/discord/ext/commands/parser.py b/discord/ext/commands/parser.py new file mode 100644 index 0000000..4770fba --- /dev/null +++ b/discord/ext/commands/parser.py @@ -0,0 +1,65 @@ +from re import L +from typing import List, Dict, Tuple, Optional + +from discord.message import Message +from .core import Command + + +class CommandParser: + def __init__( + self, + command_prefix: str, + commands: Dict[str, Command], + case_sensitive: bool = True, + ): + self.command_prefix = command_prefix + self.case_sensitive = case_sensitive + self.commands = commands + + def remove_prefix(self, content): + command_prefix = self.command_prefix + if isinstance(command_prefix, (tuple, list, set)): + for prefix in self.command_prefix: + if content.startswith(prefix): + return content[len(prefix) :] + + elif isinstance(command_prefix, str): + return content[len(command_prefix) :] + + def parse_message(self, message: Message) -> Tuple[Optional[Command], List[str]]: + if not self.commands: + return + + if not message.content.startswith(self.command_prefix): + return + + no_prefix = self.remove_prefix(message.content) # The content of the message but without the command_prefix + + if not self.case_sensitive: + no_prefix = no_prefix.lower() + + command = self.commands.get(no_prefix.split()[0]) + if command: + args = no_prefix[len(command.name) :].split() + + return command, args + + for regex_command in filter( + lambda cmd: cmd.is_regex_command, self.commands.values() + ): + regex = regex_command.name + regex_match_func = regex_command.regex_match_func + regex_flags = regex_command.regex_flags + + if match := regex_match_func(regex, no_prefix, regex_flags): + try: + prefix = match.group(1) + except IndexError: + raise ValueError( + "First match group of command regex does not exist" + ) + + args = no_prefix[len(prefix) :].split() + return regex_command, args + + return (None, []) From 05b7f948ad277c17a17a79dac2616d67f2600235 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Fri, 17 Dec 2021 17:16:14 +0530 Subject: [PATCH 43/99] Added Message.author --- discord/message.py | 1 + 1 file changed, 1 insertion(+) diff --git a/discord/message.py b/discord/message.py index 7b29762..51b5e87 100644 --- a/discord/message.py +++ b/discord/message.py @@ -14,6 +14,7 @@ class Message(BaseModel): id: Snowflake channel_id: Snowflake guild_id: Snowflake + author: dict content: str _client: Client From abbc655b388de0bd270e3decff1b24d72c2975f4 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Fri, 17 Dec 2021 17:16:36 +0530 Subject: [PATCH 44/99] Added command handling --- discord/client.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index e27298a..03bbcb7 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2,6 +2,7 @@ import asyncio import inspect +from re import L import sys import traceback import typing @@ -11,6 +12,8 @@ from .api.httphandler import HTTPHandler from .api.intents import Intents from .api.websocket import WebSocket +from .ext.commands.core import Command +from .ext.commands.parser import CommandParser class Client: @@ -21,11 +24,22 @@ async def handle_event_error(self, error): type(error), error, error.__traceback__, file=sys.stderr ) + async def handle_commands(self, message): + if message.author.get("bot"): + return + + command, args = self.command_parser.parse_message(message) + + if command: + await command(message, *args) + def __init__( self, + command_prefix: str, *, intents: typing.Optional[Intents] = Intents.default(), respond_self: typing.Optional[bool] = False, + case_sensitive: bool=True, loop: typing.Optional[asyncio.AbstractEventLoop] = None, ) -> None: self._loop: asyncio.AbstractEventLoop = None # create the event loop when we run our client @@ -36,9 +50,14 @@ def __init__( self.httphandler = HTTPHandler() self.lock = asyncio.Lock() self.closed = False - self.events = {"event_error": [self.handle_event_error]} + self.events = {"message_create": [self.handle_commands], "event_error": [self.handle_event_error]} self.once_events = {} + + self.command_prefix = command_prefix + self.commands: typing.Dict[str, Command] = {} + self.converter = DataConverter(self) + self.command_parser = CommandParser(self.command_prefix, self.commands, case_sensitive) async def login(self, token: str) -> None: self.token = token @@ -93,6 +112,14 @@ def wrapper(func): return wrapper + def command(self, name=None, **kwargs): + def inner(func) -> Command: + command = Command(func, name, **kwargs) + self.commands[command.name] = command + return command + + return inner + def add_listener( self, func: typing.Callable, From c6be27cfda792023391cb001682343b08507f3f3 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Fri, 17 Dec 2021 18:18:38 +0530 Subject: [PATCH 45/99] Added checks --- discord/ext/commands/core.py | 44 +++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 32fc040..87d2d3d 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -6,6 +6,11 @@ from discord.message import Message +NOT_ASYNC_FUNCTION_MESSAGE = ( + "{0} must be coroutine.\nMaybe you forgot to add the 'async' keyword?." +) + + class Command: def __init__( self, @@ -19,9 +24,7 @@ def __init__( **kwargs ): if not asyncio.iscoroutinefunction(callback): - raise ValueError( - "Callback must be coroutine.\nMaybe you forgot to add the 'async' keyword?." - ) + raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command callback")) self._callback = callback self.name = name or self._callback.__name__ @@ -43,14 +46,39 @@ def __init__( def callback(self): return self._callback - async def __call__(self, message: Message, *args, **kwargs): - await self.callback(message, *args, **kwargs) + def add_check(self, function: Callable[[Message], bool]): + if not asyncio.iscoroutinefunction(function): + raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command error handler")) + + self.checks.append(function) def error(self, function): if not asyncio.iscoroutinefunction(function): - raise ValueError( - "Command error handler must be coroutine.\nMaybe you forgot to add the 'async' keyword?." - ) + raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command error handler")) self.on_error = function return function + + async def execute(self, message: Message, *args, **kwargs): + for check in self.checks: + await check(message) + + try: + await self.callback(message, *args, **kwargs) + except Exception as error: + if self.on_error: + await self.on_error(message, error) + else: + raise error + + async def __call__(self, message: Message, *args, **kwargs): + await self.callback(message, *args, **kwargs) + + +def check(function: Callable[[Message], bool]): + + def inner(command: Command): + command.add_check(function) + return command + + return inner From 8442d7dee9b822857865740110410846f567d8b3 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Fri, 17 Dec 2021 18:20:42 +0530 Subject: [PATCH 46/99] Use command.execute() instead of command() --- discord/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/client.py b/discord/client.py index 03bbcb7..772e587 100644 --- a/discord/client.py +++ b/discord/client.py @@ -31,7 +31,7 @@ async def handle_commands(self, message): command, args = self.command_parser.parse_message(message) if command: - await command(message, *args) + await command.execute(message, *args) def __init__( self, From 25f720924eb00189c8e4309ab11683033420663d Mon Sep 17 00:00:00 2001 From: Rashaad Date: Fri, 17 Dec 2021 18:21:54 +0530 Subject: [PATCH 47/99] Added __init__.py --- discord/ext/commands/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 discord/ext/commands/__init__.py diff --git a/discord/ext/commands/__init__.py b/discord/ext/commands/__init__.py new file mode 100644 index 0000000..634d40e --- /dev/null +++ b/discord/ext/commands/__init__.py @@ -0,0 +1 @@ +from .core import Command, check From f16112fafbe41098f4a2db172761f98cf6abd73f Mon Sep 17 00:00:00 2001 From: Rashaad Date: Fri, 17 Dec 2021 18:23:28 +0530 Subject: [PATCH 48/99] Check if the check is a coroutine or not --- discord/ext/commands/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py index 87d2d3d..1faeb2a 100644 --- a/discord/ext/commands/core.py +++ b/discord/ext/commands/core.py @@ -47,9 +47,6 @@ def callback(self): return self._callback def add_check(self, function: Callable[[Message], bool]): - if not asyncio.iscoroutinefunction(function): - raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command error handler")) - self.checks.append(function) def error(self, function): @@ -61,7 +58,10 @@ def error(self, function): async def execute(self, message: Message, *args, **kwargs): for check in self.checks: - await check(message) + if asyncio.iscoroutinefunction(check): + await check(message) + else: + check(message) try: await self.callback(message, *args, **kwargs) From 4e72a0587ffc800565caf36548007decc5db4a28 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Sun, 19 Dec 2021 19:19:06 +0530 Subject: [PATCH 49/99] Moved commands here --- discord/commands/parser.py | 64 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 discord/commands/parser.py diff --git a/discord/commands/parser.py b/discord/commands/parser.py new file mode 100644 index 0000000..a1c414e --- /dev/null +++ b/discord/commands/parser.py @@ -0,0 +1,64 @@ +from typing import List, Dict, Tuple, Optional + +from discord.message import Message +from .core import Command + + +class CommandParser: + def __init__( + self, + command_prefix: str, + commands: Dict[str, Command], + case_sensitive: bool = True, + ): + self.command_prefix = command_prefix + self.case_sensitive = case_sensitive + self.commands = commands + + def remove_prefix(self, content): + command_prefix = self.command_prefix + if isinstance(command_prefix, (tuple, list, set)): + for prefix in self.command_prefix: + if content.startswith(prefix): + return content[len(prefix):] + + elif isinstance(command_prefix, str): + return content[len(command_prefix):] + + def parse_message(self, message: Message) -> Tuple[Optional[Command], List[str]]: + if not self.commands: + return None, [] + + if not message.content.startswith(self.command_prefix): + return None, [] + + no_prefix = self.remove_prefix(message.content) # The content of the message but without the command_prefix + + if not self.case_sensitive: + no_prefix = no_prefix.lower() + + command = self.commands.get(no_prefix.split()[0]) + if command: + args = no_prefix[len(command.name):].split() + + return command, args + + for regex_command in filter( + lambda cmd: cmd.is_regex_command, self.commands.values() + ): + regex = regex_command.name + regex_match_func = regex_command.regex_match_func + regex_flags = regex_command.regex_flags + + if match := regex_match_func(regex, no_prefix, regex_flags): + try: + prefix = match.group(1) + except IndexError: + raise ValueError( + "First match group of command regex does not exist" + ) + + args = no_prefix[len(prefix):].split() + return regex_command, args + + return None, [] From a01bc2cf6d2513a8b80ce586271566b940e2e6e7 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Sun, 19 Dec 2021 19:19:20 +0530 Subject: [PATCH 50/99] Moved commands here --- discord/commands/__init__.py | 1 + discord/commands/context.py | 2 + discord/commands/core.py | 82 ++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 discord/commands/__init__.py create mode 100644 discord/commands/context.py create mode 100644 discord/commands/core.py diff --git a/discord/commands/__init__.py b/discord/commands/__init__.py new file mode 100644 index 0000000..634d40e --- /dev/null +++ b/discord/commands/__init__.py @@ -0,0 +1 @@ +from .core import Command, check diff --git a/discord/commands/context.py b/discord/commands/context.py new file mode 100644 index 0000000..e352328 --- /dev/null +++ b/discord/commands/context.py @@ -0,0 +1,2 @@ +class Context: + pass diff --git a/discord/commands/core.py b/discord/commands/core.py new file mode 100644 index 0000000..c2f8f3a --- /dev/null +++ b/discord/commands/core.py @@ -0,0 +1,82 @@ +import asyncio +import inspect +import re +from typing import Optional, Any, Callable, Iterable + +from discord.message import Message + +NOT_ASYNC_FUNCTION_MESSAGE = ( + "{0} must be coroutine.\nMaybe you forgot to add the 'async' keyword?." +) + + +class Command: + def __init__( + self, + callback: Callable[[Any], Any], + name: str = None, + *, + aliases: Iterable[str] = None, + regex_command: bool = False, + regex_match_func=re.match, + regex_flags=0, + **kwargs + ): + if not asyncio.iscoroutinefunction(callback): + raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command callback")) + + self._callback = callback + self.name = name or self._callback.__name__ + self.is_regex_command = regex_command + self.regex_match_func = regex_match_func + self.regex_flags = regex_flags + + if aliases is None: + self.aliases = [] + else: + self.aliases = aliases + + self.checks = [] + self.description = kwargs.get("description") or self._callback.__doc__ + self.signature = inspect.signature(self._callback) + self.on_error: Optional[Callable[[Any], Any]] = None + + @property + def callback(self): + return self._callback + + def add_check(self, function: Callable[[Message], bool]): + self.checks.append(function) + + def error(self, function): + if not asyncio.iscoroutinefunction(function): + raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command error handler")) + + self.on_error = function + return function + + async def execute(self, message: Message, *args, **kwargs): + for check in self.checks: + if asyncio.iscoroutinefunction(check): + await check(message) + else: + check(message) + + try: + await self.callback(message, *args, **kwargs) + except Exception as error: + if self.on_error: + await self.on_error(message, error) + else: + raise error + + async def __call__(self, message: Message, *args, **kwargs): + await self.callback(message, *args, **kwargs) + + +def check(function: Callable[[Message], bool]): + def inner(command: Command): + command.add_check(function) + return command + + return inner From f450b14dbac48d5e8a841eb0fa5d61f2fb4a2028 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Sun, 19 Dec 2021 19:19:44 +0530 Subject: [PATCH 51/99] Changed imports --- discord/client.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/discord/client.py b/discord/client.py index 772e587..b156450 100644 --- a/discord/client.py +++ b/discord/client.py @@ -2,18 +2,16 @@ import asyncio import inspect -from re import L import sys import traceback import typing -from copy import deepcopy from .api.dataConverters import DataConverter from .api.httphandler import HTTPHandler from .api.intents import Intents from .api.websocket import WebSocket -from .ext.commands.core import Command -from .ext.commands.parser import CommandParser +from discord.commands import Command +from discord.commands.parser import CommandParser class Client: From e483c8df301192393d713ade44cff9c3d36a70e7 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Sun, 19 Dec 2021 19:21:48 +0530 Subject: [PATCH 52/99] Fix: fixed error in convert --- discord/api/dataConverters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index ec66c78..cd5ee74 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -49,5 +49,5 @@ def convert_guild_member_update(self, data): def convert(self, event, data): func: typing.Callable = self.converters.get(event) if not func: - return data + return [data] return func(data) From ffef0688207f48296d01df0290b339c6d7e5be5a Mon Sep 17 00:00:00 2001 From: Rashaad Date: Sun, 19 Dec 2021 19:54:18 +0530 Subject: [PATCH 53/99] Moved commands here --- discord/ext/commands/__init__.py | 1 - discord/ext/commands/context.py | 9 ---- discord/ext/commands/core.py | 84 -------------------------------- discord/ext/commands/parser.py | 65 ------------------------ 4 files changed, 159 deletions(-) delete mode 100644 discord/ext/commands/__init__.py delete mode 100644 discord/ext/commands/context.py delete mode 100644 discord/ext/commands/core.py delete mode 100644 discord/ext/commands/parser.py diff --git a/discord/ext/commands/__init__.py b/discord/ext/commands/__init__.py deleted file mode 100644 index 634d40e..0000000 --- a/discord/ext/commands/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .core import Command, check diff --git a/discord/ext/commands/context.py b/discord/ext/commands/context.py deleted file mode 100644 index 77ba7ca..0000000 --- a/discord/ext/commands/context.py +++ /dev/null @@ -1,9 +0,0 @@ -from typing import Any, Dict, List, Optional - -from discord.user.user import User - -from ...abc.discordobject import DiscordObject - - -class Context: - pass diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py deleted file mode 100644 index 1faeb2a..0000000 --- a/discord/ext/commands/core.py +++ /dev/null @@ -1,84 +0,0 @@ -import asyncio -import inspect -import re -from typing import Optional, Any, Callable, Iterable - -from discord.message import Message - - -NOT_ASYNC_FUNCTION_MESSAGE = ( - "{0} must be coroutine.\nMaybe you forgot to add the 'async' keyword?." -) - - -class Command: - def __init__( - self, - callback: Callable[[Any], Any], - name: str = None, - *, - aliases: Iterable[str] = None, - regex_command: bool = False, - regex_match_func=re.match, - regex_flags=0, - **kwargs - ): - if not asyncio.iscoroutinefunction(callback): - raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command callback")) - - self._callback = callback - self.name = name or self._callback.__name__ - self.is_regex_command = regex_command - self.regex_match_func = regex_match_func - self.regex_flags = regex_flags - - if aliases is None: - self.aliases = [] - else: - self.aliases = aliases - - self.checks = [] - self.description = kwargs.get("description") or self._callback.__doc__ - self.signature = inspect.signature(self._callback) - self.on_error: Optional[Callable[[Any], Any]] = None - - @property - def callback(self): - return self._callback - - def add_check(self, function: Callable[[Message], bool]): - self.checks.append(function) - - def error(self, function): - if not asyncio.iscoroutinefunction(function): - raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command error handler")) - - self.on_error = function - return function - - async def execute(self, message: Message, *args, **kwargs): - for check in self.checks: - if asyncio.iscoroutinefunction(check): - await check(message) - else: - check(message) - - try: - await self.callback(message, *args, **kwargs) - except Exception as error: - if self.on_error: - await self.on_error(message, error) - else: - raise error - - async def __call__(self, message: Message, *args, **kwargs): - await self.callback(message, *args, **kwargs) - - -def check(function: Callable[[Message], bool]): - - def inner(command: Command): - command.add_check(function) - return command - - return inner diff --git a/discord/ext/commands/parser.py b/discord/ext/commands/parser.py deleted file mode 100644 index 4770fba..0000000 --- a/discord/ext/commands/parser.py +++ /dev/null @@ -1,65 +0,0 @@ -from re import L -from typing import List, Dict, Tuple, Optional - -from discord.message import Message -from .core import Command - - -class CommandParser: - def __init__( - self, - command_prefix: str, - commands: Dict[str, Command], - case_sensitive: bool = True, - ): - self.command_prefix = command_prefix - self.case_sensitive = case_sensitive - self.commands = commands - - def remove_prefix(self, content): - command_prefix = self.command_prefix - if isinstance(command_prefix, (tuple, list, set)): - for prefix in self.command_prefix: - if content.startswith(prefix): - return content[len(prefix) :] - - elif isinstance(command_prefix, str): - return content[len(command_prefix) :] - - def parse_message(self, message: Message) -> Tuple[Optional[Command], List[str]]: - if not self.commands: - return - - if not message.content.startswith(self.command_prefix): - return - - no_prefix = self.remove_prefix(message.content) # The content of the message but without the command_prefix - - if not self.case_sensitive: - no_prefix = no_prefix.lower() - - command = self.commands.get(no_prefix.split()[0]) - if command: - args = no_prefix[len(command.name) :].split() - - return command, args - - for regex_command in filter( - lambda cmd: cmd.is_regex_command, self.commands.values() - ): - regex = regex_command.name - regex_match_func = regex_command.regex_match_func - regex_flags = regex_command.regex_flags - - if match := regex_match_func(regex, no_prefix, regex_flags): - try: - prefix = match.group(1) - except IndexError: - raise ValueError( - "First match group of command regex does not exist" - ) - - args = no_prefix[len(prefix) :].split() - return regex_command, args - - return (None, []) From 35343d06af3d7f46d958ffda24cad719342bf1b8 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Sun, 19 Dec 2021 19:54:49 +0530 Subject: [PATCH 54/99] Moved commands here --- discord/commands/__init__.py | 1 + discord/commands/context.py | 9 ++++ discord/commands/core.py | 84 ++++++++++++++++++++++++++++++++++++ discord/commands/parser.py | 65 ++++++++++++++++++++++++++++ 4 files changed, 159 insertions(+) create mode 100644 discord/commands/__init__.py create mode 100644 discord/commands/context.py create mode 100644 discord/commands/core.py create mode 100644 discord/commands/parser.py diff --git a/discord/commands/__init__.py b/discord/commands/__init__.py new file mode 100644 index 0000000..634d40e --- /dev/null +++ b/discord/commands/__init__.py @@ -0,0 +1 @@ +from .core import Command, check diff --git a/discord/commands/context.py b/discord/commands/context.py new file mode 100644 index 0000000..f416be0 --- /dev/null +++ b/discord/commands/context.py @@ -0,0 +1,9 @@ +from typing import Any, Dict, List, Optional + +from discord.user.user import User + +from discord.abc.discordobject import DiscordObject + + +class Context: + pass diff --git a/discord/commands/core.py b/discord/commands/core.py new file mode 100644 index 0000000..1faeb2a --- /dev/null +++ b/discord/commands/core.py @@ -0,0 +1,84 @@ +import asyncio +import inspect +import re +from typing import Optional, Any, Callable, Iterable + +from discord.message import Message + + +NOT_ASYNC_FUNCTION_MESSAGE = ( + "{0} must be coroutine.\nMaybe you forgot to add the 'async' keyword?." +) + + +class Command: + def __init__( + self, + callback: Callable[[Any], Any], + name: str = None, + *, + aliases: Iterable[str] = None, + regex_command: bool = False, + regex_match_func=re.match, + regex_flags=0, + **kwargs + ): + if not asyncio.iscoroutinefunction(callback): + raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command callback")) + + self._callback = callback + self.name = name or self._callback.__name__ + self.is_regex_command = regex_command + self.regex_match_func = regex_match_func + self.regex_flags = regex_flags + + if aliases is None: + self.aliases = [] + else: + self.aliases = aliases + + self.checks = [] + self.description = kwargs.get("description") or self._callback.__doc__ + self.signature = inspect.signature(self._callback) + self.on_error: Optional[Callable[[Any], Any]] = None + + @property + def callback(self): + return self._callback + + def add_check(self, function: Callable[[Message], bool]): + self.checks.append(function) + + def error(self, function): + if not asyncio.iscoroutinefunction(function): + raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command error handler")) + + self.on_error = function + return function + + async def execute(self, message: Message, *args, **kwargs): + for check in self.checks: + if asyncio.iscoroutinefunction(check): + await check(message) + else: + check(message) + + try: + await self.callback(message, *args, **kwargs) + except Exception as error: + if self.on_error: + await self.on_error(message, error) + else: + raise error + + async def __call__(self, message: Message, *args, **kwargs): + await self.callback(message, *args, **kwargs) + + +def check(function: Callable[[Message], bool]): + + def inner(command: Command): + command.add_check(function) + return command + + return inner diff --git a/discord/commands/parser.py b/discord/commands/parser.py new file mode 100644 index 0000000..4770fba --- /dev/null +++ b/discord/commands/parser.py @@ -0,0 +1,65 @@ +from re import L +from typing import List, Dict, Tuple, Optional + +from discord.message import Message +from .core import Command + + +class CommandParser: + def __init__( + self, + command_prefix: str, + commands: Dict[str, Command], + case_sensitive: bool = True, + ): + self.command_prefix = command_prefix + self.case_sensitive = case_sensitive + self.commands = commands + + def remove_prefix(self, content): + command_prefix = self.command_prefix + if isinstance(command_prefix, (tuple, list, set)): + for prefix in self.command_prefix: + if content.startswith(prefix): + return content[len(prefix) :] + + elif isinstance(command_prefix, str): + return content[len(command_prefix) :] + + def parse_message(self, message: Message) -> Tuple[Optional[Command], List[str]]: + if not self.commands: + return + + if not message.content.startswith(self.command_prefix): + return + + no_prefix = self.remove_prefix(message.content) # The content of the message but without the command_prefix + + if not self.case_sensitive: + no_prefix = no_prefix.lower() + + command = self.commands.get(no_prefix.split()[0]) + if command: + args = no_prefix[len(command.name) :].split() + + return command, args + + for regex_command in filter( + lambda cmd: cmd.is_regex_command, self.commands.values() + ): + regex = regex_command.name + regex_match_func = regex_command.regex_match_func + regex_flags = regex_command.regex_flags + + if match := regex_match_func(regex, no_prefix, regex_flags): + try: + prefix = match.group(1) + except IndexError: + raise ValueError( + "First match group of command regex does not exist" + ) + + args = no_prefix[len(prefix) :].split() + return regex_command, args + + return (None, []) From b5ff607d4ad3f5f8b68619939d38859b73d9c635 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Sun, 19 Dec 2021 19:55:40 +0530 Subject: [PATCH 55/99] Changed imports --- discord/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/client.py b/discord/client.py index 772e587..f01b471 100644 --- a/discord/client.py +++ b/discord/client.py @@ -12,8 +12,8 @@ from .api.httphandler import HTTPHandler from .api.intents import Intents from .api.websocket import WebSocket -from .ext.commands.core import Command -from .ext.commands.parser import CommandParser +from .commands.core import Command +from .commands.parser import CommandParser class Client: From 4f3bc0e416dacbcad7bd887886c9adb10d5b00de Mon Sep 17 00:00:00 2001 From: Rashaad Date: Sun, 19 Dec 2021 20:17:21 +0530 Subject: [PATCH 56/99] Revert to 8cea032bf67ad7581aeb511c40b1139760ee5c2e --- discord/api/dataConverters.py | 2 +- discord/client.py | 29 +---------- discord/commands/__init__.py | 1 - discord/commands/context.py | 2 - discord/commands/core.py | 82 ------------------------------- discord/commands/parser.py | 64 ------------------------ discord/ext/commands/__init__.py | 1 - discord/ext/commands/core.py | 84 -------------------------------- discord/ext/commands/parser.py | 65 ------------------------ discord/message.py | 1 - 10 files changed, 3 insertions(+), 328 deletions(-) delete mode 100644 discord/commands/__init__.py delete mode 100644 discord/commands/context.py delete mode 100644 discord/commands/core.py delete mode 100644 discord/commands/parser.py delete mode 100644 discord/ext/commands/__init__.py delete mode 100644 discord/ext/commands/core.py delete mode 100644 discord/ext/commands/parser.py diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index cd5ee74..ec66c78 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -49,5 +49,5 @@ def convert_guild_member_update(self, data): def convert(self, event, data): func: typing.Callable = self.converters.get(event) if not func: - return [data] + return data return func(data) diff --git a/discord/client.py b/discord/client.py index b156450..e27298a 100644 --- a/discord/client.py +++ b/discord/client.py @@ -5,13 +5,12 @@ import sys import traceback import typing +from copy import deepcopy from .api.dataConverters import DataConverter from .api.httphandler import HTTPHandler from .api.intents import Intents from .api.websocket import WebSocket -from discord.commands import Command -from discord.commands.parser import CommandParser class Client: @@ -22,22 +21,11 @@ async def handle_event_error(self, error): type(error), error, error.__traceback__, file=sys.stderr ) - async def handle_commands(self, message): - if message.author.get("bot"): - return - - command, args = self.command_parser.parse_message(message) - - if command: - await command.execute(message, *args) - def __init__( self, - command_prefix: str, *, intents: typing.Optional[Intents] = Intents.default(), respond_self: typing.Optional[bool] = False, - case_sensitive: bool=True, loop: typing.Optional[asyncio.AbstractEventLoop] = None, ) -> None: self._loop: asyncio.AbstractEventLoop = None # create the event loop when we run our client @@ -48,14 +36,9 @@ def __init__( self.httphandler = HTTPHandler() self.lock = asyncio.Lock() self.closed = False - self.events = {"message_create": [self.handle_commands], "event_error": [self.handle_event_error]} + self.events = {"event_error": [self.handle_event_error]} self.once_events = {} - - self.command_prefix = command_prefix - self.commands: typing.Dict[str, Command] = {} - self.converter = DataConverter(self) - self.command_parser = CommandParser(self.command_prefix, self.commands, case_sensitive) async def login(self, token: str) -> None: self.token = token @@ -110,14 +93,6 @@ def wrapper(func): return wrapper - def command(self, name=None, **kwargs): - def inner(func) -> Command: - command = Command(func, name, **kwargs) - self.commands[command.name] = command - return command - - return inner - def add_listener( self, func: typing.Callable, diff --git a/discord/commands/__init__.py b/discord/commands/__init__.py deleted file mode 100644 index 634d40e..0000000 --- a/discord/commands/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .core import Command, check diff --git a/discord/commands/context.py b/discord/commands/context.py deleted file mode 100644 index e352328..0000000 --- a/discord/commands/context.py +++ /dev/null @@ -1,2 +0,0 @@ -class Context: - pass diff --git a/discord/commands/core.py b/discord/commands/core.py deleted file mode 100644 index c2f8f3a..0000000 --- a/discord/commands/core.py +++ /dev/null @@ -1,82 +0,0 @@ -import asyncio -import inspect -import re -from typing import Optional, Any, Callable, Iterable - -from discord.message import Message - -NOT_ASYNC_FUNCTION_MESSAGE = ( - "{0} must be coroutine.\nMaybe you forgot to add the 'async' keyword?." -) - - -class Command: - def __init__( - self, - callback: Callable[[Any], Any], - name: str = None, - *, - aliases: Iterable[str] = None, - regex_command: bool = False, - regex_match_func=re.match, - regex_flags=0, - **kwargs - ): - if not asyncio.iscoroutinefunction(callback): - raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command callback")) - - self._callback = callback - self.name = name or self._callback.__name__ - self.is_regex_command = regex_command - self.regex_match_func = regex_match_func - self.regex_flags = regex_flags - - if aliases is None: - self.aliases = [] - else: - self.aliases = aliases - - self.checks = [] - self.description = kwargs.get("description") or self._callback.__doc__ - self.signature = inspect.signature(self._callback) - self.on_error: Optional[Callable[[Any], Any]] = None - - @property - def callback(self): - return self._callback - - def add_check(self, function: Callable[[Message], bool]): - self.checks.append(function) - - def error(self, function): - if not asyncio.iscoroutinefunction(function): - raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command error handler")) - - self.on_error = function - return function - - async def execute(self, message: Message, *args, **kwargs): - for check in self.checks: - if asyncio.iscoroutinefunction(check): - await check(message) - else: - check(message) - - try: - await self.callback(message, *args, **kwargs) - except Exception as error: - if self.on_error: - await self.on_error(message, error) - else: - raise error - - async def __call__(self, message: Message, *args, **kwargs): - await self.callback(message, *args, **kwargs) - - -def check(function: Callable[[Message], bool]): - def inner(command: Command): - command.add_check(function) - return command - - return inner diff --git a/discord/commands/parser.py b/discord/commands/parser.py deleted file mode 100644 index a1c414e..0000000 --- a/discord/commands/parser.py +++ /dev/null @@ -1,64 +0,0 @@ -from typing import List, Dict, Tuple, Optional - -from discord.message import Message -from .core import Command - - -class CommandParser: - def __init__( - self, - command_prefix: str, - commands: Dict[str, Command], - case_sensitive: bool = True, - ): - self.command_prefix = command_prefix - self.case_sensitive = case_sensitive - self.commands = commands - - def remove_prefix(self, content): - command_prefix = self.command_prefix - if isinstance(command_prefix, (tuple, list, set)): - for prefix in self.command_prefix: - if content.startswith(prefix): - return content[len(prefix):] - - elif isinstance(command_prefix, str): - return content[len(command_prefix):] - - def parse_message(self, message: Message) -> Tuple[Optional[Command], List[str]]: - if not self.commands: - return None, [] - - if not message.content.startswith(self.command_prefix): - return None, [] - - no_prefix = self.remove_prefix(message.content) # The content of the message but without the command_prefix - - if not self.case_sensitive: - no_prefix = no_prefix.lower() - - command = self.commands.get(no_prefix.split()[0]) - if command: - args = no_prefix[len(command.name):].split() - - return command, args - - for regex_command in filter( - lambda cmd: cmd.is_regex_command, self.commands.values() - ): - regex = regex_command.name - regex_match_func = regex_command.regex_match_func - regex_flags = regex_command.regex_flags - - if match := regex_match_func(regex, no_prefix, regex_flags): - try: - prefix = match.group(1) - except IndexError: - raise ValueError( - "First match group of command regex does not exist" - ) - - args = no_prefix[len(prefix):].split() - return regex_command, args - - return None, [] diff --git a/discord/ext/commands/__init__.py b/discord/ext/commands/__init__.py deleted file mode 100644 index 634d40e..0000000 --- a/discord/ext/commands/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .core import Command, check diff --git a/discord/ext/commands/core.py b/discord/ext/commands/core.py deleted file mode 100644 index 1faeb2a..0000000 --- a/discord/ext/commands/core.py +++ /dev/null @@ -1,84 +0,0 @@ -import asyncio -import inspect -import re -from typing import Optional, Any, Callable, Iterable - -from discord.message import Message - - -NOT_ASYNC_FUNCTION_MESSAGE = ( - "{0} must be coroutine.\nMaybe you forgot to add the 'async' keyword?." -) - - -class Command: - def __init__( - self, - callback: Callable[[Any], Any], - name: str = None, - *, - aliases: Iterable[str] = None, - regex_command: bool = False, - regex_match_func=re.match, - regex_flags=0, - **kwargs - ): - if not asyncio.iscoroutinefunction(callback): - raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command callback")) - - self._callback = callback - self.name = name or self._callback.__name__ - self.is_regex_command = regex_command - self.regex_match_func = regex_match_func - self.regex_flags = regex_flags - - if aliases is None: - self.aliases = [] - else: - self.aliases = aliases - - self.checks = [] - self.description = kwargs.get("description") or self._callback.__doc__ - self.signature = inspect.signature(self._callback) - self.on_error: Optional[Callable[[Any], Any]] = None - - @property - def callback(self): - return self._callback - - def add_check(self, function: Callable[[Message], bool]): - self.checks.append(function) - - def error(self, function): - if not asyncio.iscoroutinefunction(function): - raise ValueError(NOT_ASYNC_FUNCTION_MESSAGE.format("Command error handler")) - - self.on_error = function - return function - - async def execute(self, message: Message, *args, **kwargs): - for check in self.checks: - if asyncio.iscoroutinefunction(check): - await check(message) - else: - check(message) - - try: - await self.callback(message, *args, **kwargs) - except Exception as error: - if self.on_error: - await self.on_error(message, error) - else: - raise error - - async def __call__(self, message: Message, *args, **kwargs): - await self.callback(message, *args, **kwargs) - - -def check(function: Callable[[Message], bool]): - - def inner(command: Command): - command.add_check(function) - return command - - return inner diff --git a/discord/ext/commands/parser.py b/discord/ext/commands/parser.py deleted file mode 100644 index 4770fba..0000000 --- a/discord/ext/commands/parser.py +++ /dev/null @@ -1,65 +0,0 @@ -from re import L -from typing import List, Dict, Tuple, Optional - -from discord.message import Message -from .core import Command - - -class CommandParser: - def __init__( - self, - command_prefix: str, - commands: Dict[str, Command], - case_sensitive: bool = True, - ): - self.command_prefix = command_prefix - self.case_sensitive = case_sensitive - self.commands = commands - - def remove_prefix(self, content): - command_prefix = self.command_prefix - if isinstance(command_prefix, (tuple, list, set)): - for prefix in self.command_prefix: - if content.startswith(prefix): - return content[len(prefix) :] - - elif isinstance(command_prefix, str): - return content[len(command_prefix) :] - - def parse_message(self, message: Message) -> Tuple[Optional[Command], List[str]]: - if not self.commands: - return - - if not message.content.startswith(self.command_prefix): - return - - no_prefix = self.remove_prefix(message.content) # The content of the message but without the command_prefix - - if not self.case_sensitive: - no_prefix = no_prefix.lower() - - command = self.commands.get(no_prefix.split()[0]) - if command: - args = no_prefix[len(command.name) :].split() - - return command, args - - for regex_command in filter( - lambda cmd: cmd.is_regex_command, self.commands.values() - ): - regex = regex_command.name - regex_match_func = regex_command.regex_match_func - regex_flags = regex_command.regex_flags - - if match := regex_match_func(regex, no_prefix, regex_flags): - try: - prefix = match.group(1) - except IndexError: - raise ValueError( - "First match group of command regex does not exist" - ) - - args = no_prefix[len(prefix) :].split() - return regex_command, args - - return (None, []) diff --git a/discord/message.py b/discord/message.py index 51b5e87..7b29762 100644 --- a/discord/message.py +++ b/discord/message.py @@ -14,7 +14,6 @@ class Message(BaseModel): id: Snowflake channel_id: Snowflake guild_id: Snowflake - author: dict content: str _client: Client From 086c332fc7c462b8a1a9b9d2b2bb88de9fd1d4e8 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Sun, 19 Dec 2021 20:27:12 +0530 Subject: [PATCH 57/99] Fix: fixed bug in DataConverter.convert --- discord/api/dataConverters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index ec66c78..cd5ee74 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -49,5 +49,5 @@ def convert_guild_member_update(self, data): def convert(self, event, data): func: typing.Callable = self.converters.get(event) if not func: - return data + return [data] return func(data) From 0e3e2cb00bade3ec315a35c4d3b854310b5b138b Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 22 Dec 2021 08:40:07 +0530 Subject: [PATCH 58/99] Added commands/errors.py --- discord/commands/errors.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 discord/commands/errors.py diff --git a/discord/commands/errors.py b/discord/commands/errors.py new file mode 100644 index 0000000..e69de29 From 47cf7e490a7526289e1bb1e552d6a1fbcb8643bd Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 22 Dec 2021 08:40:22 +0530 Subject: [PATCH 59/99] Added commands/errors.py --- discord/commands/errors.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/discord/commands/errors.py b/discord/commands/errors.py index e69de29..df494dc 100644 --- a/discord/commands/errors.py +++ b/discord/commands/errors.py @@ -0,0 +1,8 @@ +class CommandError(Exception): + pass + + +class CheckFailure(Exception): + def __init__(self, command): + self.command = command + super().__init__(f"Check failed for command {command.name}") From 1aa25941dc75c305d87ea11fd6b5455fc040435b Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 22 Dec 2021 08:43:35 +0530 Subject: [PATCH 60/99] Raise CheckFailure if check did not return True --- discord/commands/core.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/discord/commands/core.py b/discord/commands/core.py index 1faeb2a..30c0e24 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -4,6 +4,7 @@ from typing import Optional, Any, Callable, Iterable from discord.message import Message +from .errors import CheckFailure NOT_ASYNC_FUNCTION_MESSAGE = ( @@ -59,9 +60,12 @@ def error(self, function): async def execute(self, message: Message, *args, **kwargs): for check in self.checks: if asyncio.iscoroutinefunction(check): - await check(message) + result = await check(message) else: - check(message) + result = check(message) + + if result is not True: + raise CheckFailure(self) try: await self.callback(message, *args, **kwargs) From 364dc46104c2163cddc0f92c9f93196166cba0b2 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 22 Dec 2021 08:45:56 +0530 Subject: [PATCH 61/99] Removed Command.signature --- discord/commands/core.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/discord/commands/core.py b/discord/commands/core.py index 30c0e24..779b8a2 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -1,5 +1,4 @@ import asyncio -import inspect import re from typing import Optional, Any, Callable, Iterable @@ -40,7 +39,6 @@ def __init__( self.checks = [] self.description = kwargs.get("description") or self._callback.__doc__ - self.signature = inspect.signature(self._callback) self.on_error: Optional[Callable[[Any], Any]] = None @property From 0c91c96b4d5665ec50f0ff1017003eed18c9a6ff Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 22 Dec 2021 10:02:59 +0530 Subject: [PATCH 62/99] Improved CommandParser --- discord/commands/parser.py | 64 +++++++++++++++++++++++++++----------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/discord/commands/parser.py b/discord/commands/parser.py index 4770fba..d061d26 100644 --- a/discord/commands/parser.py +++ b/discord/commands/parser.py @@ -1,37 +1,66 @@ -from re import L -from typing import List, Dict, Tuple, Optional +import inspect +import re +from typing import List, Dict, Tuple, Optional, Union, Any from discord.message import Message +from . import Command from .core import Command class CommandParser: def __init__( - self, - command_prefix: str, - commands: Dict[str, Command], - case_sensitive: bool = True, + self, + command_prefix: str, + commands: Dict[str, Command], + case_sensitive: bool = True, ): self.command_prefix = command_prefix self.case_sensitive = case_sensitive self.commands = commands - def remove_prefix(self, content): + def remove_prefix(self, content: str): command_prefix = self.command_prefix if isinstance(command_prefix, (tuple, list, set)): for prefix in self.command_prefix: if content.startswith(prefix): - return content[len(prefix) :] + return content[len(prefix):] elif isinstance(command_prefix, str): - return content[len(command_prefix) :] + return content[len(command_prefix):] - def parse_message(self, message: Message) -> Tuple[Optional[Command], List[str]]: + def get_args(self, command: Command, content: str, prefix=None): + prefix = prefix or command.name + content = content[len(prefix):].strip() + signature = inspect.signature(command.callback) + parameters = signature.parameters.copy() + parameters.pop(tuple(parameters.keys())[0]) # Remove the message parameter + + args = content.split() + positional_arguments = [] + kwargs = {} + extra_kwargs = {} + + for index, (name, parameter) in enumerate(parameters.items()): + if parameter.kind is parameter.KEYWORD_ONLY: + kwargs[name] = (" ".join(args[index:])).strip() + + elif parameter.kind is parameter.VAR_KEYWORD: + extra_kwargs = dict(re.findall(r"(\D+)=(\w+)", content)) + + elif parameter.kind is parameter.VAR_POSITIONAL: + positional_arguments.extend(args) + + else: + positional_arguments.append(args[index].strip()) + + return positional_arguments, kwargs, extra_kwargs + + def parse_message(self, message: Message): if not self.commands: - return + return None, [] if not message.content.startswith(self.command_prefix): - return + return None, [] no_prefix = self.remove_prefix(message.content) # The content of the message but without the command_prefix @@ -40,12 +69,10 @@ def parse_message(self, message: Message) -> Tuple[Optional[Command], List[str]] command = self.commands.get(no_prefix.split()[0]) if command: - args = no_prefix[len(command.name) :].split() - - return command, args + return command, *self.get_args(command, no_prefix) for regex_command in filter( - lambda cmd: cmd.is_regex_command, self.commands.values() + lambda cmd: cmd.is_regex_command, self.commands.values() ): regex = regex_command.name regex_match_func = regex_command.regex_match_func @@ -59,7 +86,6 @@ def parse_message(self, message: Message) -> Tuple[Optional[Command], List[str]] "First match group of command regex does not exist" ) - args = no_prefix[len(prefix) :].split() - return regex_command, args + return regex_command, *self.get_args(command, no_prefix, prefix=prefix) - return (None, []) + return None, [] From 00f2968c3cfded23e07125319846921cd4a3d053 Mon Sep 17 00:00:00 2001 From: Rashaad Date: Wed, 22 Dec 2021 10:03:55 +0530 Subject: [PATCH 63/99] Improved command argument handling --- discord/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/client.py b/discord/client.py index f01b471..dae273a 100644 --- a/discord/client.py +++ b/discord/client.py @@ -28,10 +28,10 @@ async def handle_commands(self, message): if message.author.get("bot"): return - command, args = self.command_parser.parse_message(message) + command, args, kwargs, extra_kwargs = self.command_parser.parse_message(message) if command: - await command.execute(message, *args) + await command.execute(message, *args, **kwargs, **extra_kwargs) def __init__( self, From 4ba9e60515afa09d8d44932528a01de63be62892 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Thu, 27 Jan 2022 18:30:54 +0530 Subject: [PATCH 64/99] Added Asset --- discord/asset.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 discord/asset.py diff --git a/discord/asset.py b/discord/asset.py new file mode 100644 index 0000000..1144bcb --- /dev/null +++ b/discord/asset.py @@ -0,0 +1,24 @@ +import io +from typing import Union + +from pydantic import BaseModel + +from discord import Client + + +CDN_URL = "https://cdn.discordapp.com" + +class Asset(BaseModel): + _client: Client + url: str + + async def read(self): + return await self._client.httphandler.get_from_cdn(self.url) + + async def save(self, fp: Union[str, io.BufferedIOBase]): + data = await self.read() + if isinstance(fp, str): + with open(fp, "wb+") as file: + return file.write(data) + + return fp.write(data) \ No newline at end of file From e827172219a8855e3f5371447f8aafbfcbe7276c Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Thu, 27 Jan 2022 18:47:48 +0530 Subject: [PATCH 65/99] Did major changes --- discord/abc/abstractuser.py | 5 -- discord/abc/discordobject.py | 14 ++++-- discord/api/dataConverters.py | 17 +++++-- discord/api/httphandler.py | 13 ++++- discord/api/websocket.py | 2 +- discord/asset.py | 9 ++-- discord/channels/dmchannel.py | 19 +++++++- discord/guild.py | 1 - discord/message.py | 7 --- discord/user/baseuser.py | 89 +++++++++-------------------------- discord/user/clientuser.py | 49 ------------------- discord/user/member.py | 53 ++++++++++++++------- discord/user/user.py | 39 +++++---------- 13 files changed, 128 insertions(+), 189 deletions(-) diff --git a/discord/abc/abstractuser.py b/discord/abc/abstractuser.py index 8ef53c1..d23987f 100644 --- a/discord/abc/abstractuser.py +++ b/discord/abc/abstractuser.py @@ -9,11 +9,6 @@ class AbstractUser(DiscordObject): - avatar: Optional[Avatar] - bot: bool - username: str - discriminator: str - id: int @property def tag(self): diff --git a/discord/abc/discordobject.py b/discord/abc/discordobject.py index a57cd4e..0352e0a 100644 --- a/discord/abc/discordobject.py +++ b/discord/abc/discordobject.py @@ -1,18 +1,26 @@ from __future__ import annotations - -from datetime import datetime +from typing import TYPE_CHECKING from pydantic import BaseModel from ..types.snowflake import Snowflake +if TYPE_CHECKING: + from ..client import Client + class DiscordObject(BaseModel): + class Config: + arbitrary_types_allowed = True id: Snowflake + _client: Client + + def __init__(self, client, **payload): + super().__init__(_client=client, **payload) def __ne__(self, other): return not self.__eq__(other) def __hash__(self): - return self.id.id >> 22 + return self.id >> 22 diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index cd5ee74..9c1a57c 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -5,6 +5,9 @@ from ..guild import Guild from ..message import Message +from ..user.user import User +from ..user.member import Member + class DataConverter: def __init__(self, client): @@ -28,12 +31,18 @@ def convert_ready(self, data): def convert_guild_create(self, data): members = data["members"] - guild = Guild(**data) + guild = Guild(self.client, **data) self.client.ws.guild_cache[Snowflake(data["id"])] = guild - for member in members: - self.client.ws.member_cache[Snowflake(member["user"]["id"])] = member - self.client.ws.user_cache[Snowflake(member["user"]["id"])] = member["user"] + for member_data in members: + user_data = member_data["user"] + member_data.pop("user", None) + member_data["guild"] = guild + member_data["guild_avatar"] = member_data.get("avatar") + member_data.pop("avatar", None) + + self.client.ws.member_cache[Snowflake(user_data["id"])] = Member(self.client, **member_data, **user_data) + self.client.ws.user_cache[Snowflake(user_data["id"])] = User(self.client, **user_data) return [guild] diff --git a/discord/api/httphandler.py b/discord/api/httphandler.py index 7206752..09015e0 100644 --- a/discord/api/httphandler.py +++ b/discord/api/httphandler.py @@ -82,6 +82,17 @@ async def close(self) -> None: if self._session: await self._session.close() + async def get_from_cdn(self, url: str) -> bytes: + async with self._session.get(url) as response: + if response.status == 200: + return await response.read() + elif response.status == 404: + raise DiscordNotFound("asset not found") + elif response.status == 403: + raise DiscordForbidden("cannot retrieve asset") + else: + raise DiscordHTTPException("failed to get asset", response.status) + async def send_message( self, channel_id: int, @@ -99,7 +110,7 @@ async def send_message( if content: payload["content"] = content if embeds: - payload["embeds"] = [embed._to_dict() for embed in embeds] + payload["embeds"] = [embed.dict() for embed in embeds] if views: payload["components"] = [view._to_dict() for view in views] diff --git a/discord/api/websocket.py b/discord/api/websocket.py index 554083d..82e71a6 100644 --- a/discord/api/websocket.py +++ b/discord/api/websocket.py @@ -67,7 +67,7 @@ async def start( self.hb_stop: threading.Event = threading.Event() self.hb_t.start() return self - + async def close(self) -> None: """Closes the websocket""" self.closed = True diff --git a/discord/asset.py b/discord/asset.py index 1144bcb..04d3685 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -1,15 +1,18 @@ import io -from typing import Union +from typing import Union, TYPE_CHECKING from pydantic import BaseModel -from discord import Client + +if TYPE_CHECKING: + from discord import Client CDN_URL = "https://cdn.discordapp.com" + class Asset(BaseModel): - _client: Client + _client: "Client" url: str async def read(self): diff --git a/discord/channels/dmchannel.py b/discord/channels/dmchannel.py index c19a535..2b59b43 100644 --- a/discord/channels/dmchannel.py +++ b/discord/channels/dmchannel.py @@ -1,5 +1,20 @@ from __future__ import annotations +from typing import List -class DMChannel: - pass + +from discord.abc.discordobject import DiscordObject +from discord.types.image import Image +from discord.user.user import User + + +class DMChannel(DiscordObject): + type: int + recipients: List[User] + last_message_id: int + + +class GroupDMChannel(DMChannel): + name: str + icon: Image + owner_id: int diff --git a/discord/guild.py b/discord/guild.py index 5436976..fadeb1e 100644 --- a/discord/guild.py +++ b/discord/guild.py @@ -4,7 +4,6 @@ from .abc.discordobject import DiscordObject from .channels.guildchannel import TextChannel, VoiceChannel -from .types.guildpayload import GuildPayload from .types.snowflake import Snowflake from .user.member import Member from .user.user import User diff --git a/discord/message.py b/discord/message.py index 7b29762..3109f48 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1,14 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING - from pydantic import BaseModel from .types.snowflake import Snowflake -if TYPE_CHECKING: - from .client import Client - class Message(BaseModel): id: Snowflake @@ -16,8 +11,6 @@ class Message(BaseModel): guild_id: Snowflake content: str - _client: Client - def __init__(self, client, data): super().__init__(_client=client, **data) diff --git a/discord/user/baseuser.py b/discord/user/baseuser.py index 75433ef..0a5b21d 100644 --- a/discord/user/baseuser.py +++ b/discord/user/baseuser.py @@ -1,86 +1,39 @@ from __future__ import annotations -from datetime import datetime from typing import Optional -from ..abc.abstractuser import AbstractUser -from ..cache import LFUCache +from ..abc.discordobject import DiscordObject +from ..asset import Asset from ..color import Color from ..message import Message from ..types.avatar import Avatar from ..types.banner import Banner -from ..types.enums.defaultavatar import DefaultAvatar +from ..types.enums.premiumtype import PremiumType from ..types.enums.userflags import UserFlags -from ..types.userpayload import UserPayload -class BaseUser(AbstractUser): - class Config: - arbitrary_types_allowed = True - - banner: Banner - system: bool - display_avatar: Avatar - display_name: str - public_flags: UserFlags - - async def create_dm(self): - pass - - async def fetch_message(self): - pass - - async def send( - self, - content: str = None, - *, - tts=None, - embeds: list[Message] = None, - files=None, - stickers=None, - delete_after=None, - nonce=None, - allowed_mentions: bool = None, - reference=None, - mention_author: bool = None, - view=None, - components=None - ): - pass - - async def edit(self, *, username: str = None, avatar: bytes = None): - pass - - async def typing(self): - pass - - @property - def default_avatar(self): - return self.avatar._from_default_avatar( - self._cache, int(self.discriminator) % len(DefaultAvatar) - ) +class BaseUser(DiscordObject): + username: str + discriminator: str + # avatar: Optional[Asset] TODO: Fix 'value is not a valid dict (type=type_error.dict)' error + bot: Optional[bool] = False + system: Optional[bool] = False + mfa_enabled: Optional[bool] + banner: Optional[Banner] + accent_color: Optional[Color] + flags: Optional[UserFlags] + premium_type: Optional[PremiumType] + public_flags: Optional[UserFlags] @property def color(self): return Color.default() + colour = color + @property - def colour(self): - return self.color + def mention(self): + return f"<@!{self.id}>" - def to_json(self): - return { - "username": self.username, - "discriminator": self.discriminator, - "tag": self.tag, - "id": self.id, - "created_at": self.created_at, - "avatar": self.avatar, - "default_avatar": self.default_avatar, - "display_avatar": self.display_avatar, - "bot": self.bot, - "system": self.system, - "public_flags": self.public_flags, - "display_name": self.display_name, - "banner": self.banner, - } + def __str__(self): + return f"{self.username}#{self.discriminator}" diff --git a/discord/user/clientuser.py b/discord/user/clientuser.py index 09cdc83..47ea98a 100644 --- a/discord/user/clientuser.py +++ b/discord/user/clientuser.py @@ -2,7 +2,6 @@ from typing import Optional -from ..cache import UserCache from ..types.enums.locale import Locale from ..types.enums.userflags import UserFlags from ..types.userpayload import UserPayload @@ -10,56 +9,8 @@ class ClientUser(BaseUser): - __slots__ = ( - "_id", - "_created_at", - "_avatar", - "_bot", - "_username", - "_discriminator", - "_mention", - "_cache", - "_banner", - "_default_avatar", - "_display_name", - "_public_flags", - "_cache", - "_system", - "_verified", - "_locale", - "_two_factor_enabled", - "_flags", - ) - _verified: bool - _locale: Optional[Locale] - _two_factor_enabled: bool - _flags: UserFlags - - def __init__(self, cache: UserCache, payload: UserPayload): - self._verified = payload["verified"] - self._two_factor_enabled = payload["two_factor_enabled"] - self._locale = payload["locale"] - self._flags = payload["flags"] - super().__init__(cache, payload) - async def edit(self, *, username: str = None, avatar: bytes = None): pass def __repr__(self) -> str: return f"{self.username}#{self.discriminator} ({self.id})" - - @property - def verified(self): - return self._verified - - @property - def locale(self): - return self._locale - - @property - def two_factor_enabled(self): - return self._two_factor_enabled - - @property - def flags(self): - return self._flags diff --git a/discord/user/member.py b/discord/user/member.py index 9e070d6..5a73be1 100644 --- a/discord/user/member.py +++ b/discord/user/member.py @@ -1,27 +1,44 @@ from __future__ import annotations -from typing import TYPE_CHECKING +from datetime import datetime +from typing import TYPE_CHECKING, List, Optional, Any -from .user import User +from pydantic import validator -if TYPE_CHECKING: - from ..guild import Guild - from ..role import Role +from .user import User +from ..asset import Asset +from ..types.image import Image +from ..types.snowflake import Snowflake -class Member(User): - _top_role: Role - _roles: set[Role] - _guild: Guild +def validate_dt(val): + if val is None: + return val - @property - def top_role(self): - return self._top_role + return datetime.fromisoformat(str(val)) - @property - def roles(self): - return self._roles - @property - def guild(self): - return self._guild +class Member(User): + nick: Optional[str] = None + guild_avatar: Optional[Asset] = None + roles: List[Snowflake] + guild: Any + joined_at: datetime + premium_since: Optional[datetime] = None + deaf: Optional[bool] + mute: Optional[bool] + pending: Optional[bool] + permissions: Optional[str] = None + communication_disabled_until: Optional[datetime] = None + + @validator("joined_at") + def validate_joined_at(cls, val): + return validate_dt(val) + + @validator("premium_since") + def validate_premium_since(cls, val): + return validate_dt(val) + + @validator("communication_disabled_until") + def validate_communication_disabled_until(cls, val): + return validate_dt(val) diff --git a/discord/user/user.py b/discord/user/user.py index f34e585..a87b014 100644 --- a/discord/user/user.py +++ b/discord/user/user.py @@ -10,36 +10,21 @@ class User(BaseUser): - __slots__ = ("_stored",) + _stored: bool = False - _stored: bool - - def __init__(self, cache, payload: UserPayload): - super().__init__(cache, payload) - self._stored = False - - @classmethod - def _from_user(cls, user: BU) -> BU: - self = super()._from_user(user) - self._stored = False - return self - - def __repr__(self) -> str: - return f"{self.tag} ({self.id})" + @property + def mutual_guilds(self): + return - def __str__(self) -> str: - return self.tag + async def create_dm(self): + pass - def __del__(self): - if self._stored: - del self._cache[self.id] - else: - raise KeyError + async def fetch_message(self): + pass - @property - def dm_channel(self): - return self._cache.get_user_dms(self) + async def edit(self, *, username: str = None, avatar: bytes = None): + pass - @property - def mutual_guilds(self): + async def typing(self): pass + From e7203a872a8a00454fb63936bc172c6bf75c016f Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 29 Jan 2022 22:58:34 +0530 Subject: [PATCH 66/99] Fix(Client): Not await the task in handle_event --- discord/client.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/discord/client.py b/discord/client.py index e27298a..fe3e15f 100644 --- a/discord/client.py +++ b/discord/client.py @@ -78,7 +78,6 @@ def run(self, token: str): else: self._loop.run_forever(self.alive_loop(token)) - def on(self, event: str = None, *, overwrite: bool = False): def wrapper(func): self.add_listener(func, event, overwrite=overwrite, once=False) @@ -107,12 +106,12 @@ def add_listener( "The callback is not a valid coroutine function. Did you forget to add async before def?" ) - if once: # if it's a once event + if once: # if it's a once event if event in self.once_events and not overwrite: self.once_events[event].append(func) else: self.once_events[event] = [func] - else: # if it's a regular event + else: # if it's a regular event if event in self.events and not overwrite: self.events[event].append(func) else: @@ -125,16 +124,14 @@ async def handle_event(self, msg): for coro in self.events.get(event, []): try: - task = self._loop.create_task(coro(*args)) - await task + self._loop.create_task(coro(*args)) except Exception as error: error.event = coro await self.handle_event({"d": error, "t": "event_error"}) for coro in self.once_events.pop(event, []): try: - task = self._loop.create_task(coro(*args)) - await task + self._loop.create_task(coro(*args)) except Exception as error: error.event = coro await self.handle_event({"d": error, "t": "event_error"}) From f69ffb6eea689592074bd7481ee6b2dc0fcdbe5d Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 29 Jan 2022 23:59:52 +0530 Subject: [PATCH 67/99] fix(Snowflake): Use arrow.get instead of datetime.datetime.fromtimestamp --- discord/types/snowflake.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/types/snowflake.py b/discord/types/snowflake.py index 6580dc0..bcacf45 100644 --- a/discord/types/snowflake.py +++ b/discord/types/snowflake.py @@ -1,4 +1,4 @@ -import datetime +import arrow __all__ = ("Snowflake",) @@ -22,7 +22,7 @@ def increment(self): @property def created_at(self): - return datetime.datetime.fromtimestamp(self.timestamp) + return arrow.get(self.timestamp) @classmethod def __get_validators__(cls): From d1b810230e25eaef4e2a003bd148456a9d41fb60 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sun, 30 Jan 2022 00:01:16 +0530 Subject: [PATCH 68/99] fix(Snowflake): Use arrow.get instead of datetime.datetime.fromisoformat --- discord/user/member.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/discord/user/member.py b/discord/user/member.py index 5a73be1..1c89fd8 100644 --- a/discord/user/member.py +++ b/discord/user/member.py @@ -1,13 +1,13 @@ from __future__ import annotations from datetime import datetime -from typing import TYPE_CHECKING, List, Optional, Any +from typing import List, Optional, Any from pydantic import validator +import arrow from .user import User from ..asset import Asset -from ..types.image import Image from ..types.snowflake import Snowflake @@ -15,7 +15,7 @@ def validate_dt(val): if val is None: return val - return datetime.fromisoformat(str(val)) + return arrow.get(str(val)) class Member(User): From 10d8f43b9ed647747780a61ee7f4dae2feb8b8a8 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sun, 30 Jan 2022 00:10:45 +0530 Subject: [PATCH 69/99] refactor(Asset): Remove unused variable --- discord/asset.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/asset.py b/discord/asset.py index 04d3685..4f1de9b 100644 --- a/discord/asset.py +++ b/discord/asset.py @@ -8,9 +8,6 @@ from discord import Client -CDN_URL = "https://cdn.discordapp.com" - - class Asset(BaseModel): _client: "Client" url: str From a4be2af36c03a26232a649dece3fcbf21ed19d8d Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sun, 30 Jan 2022 00:21:17 +0530 Subject: [PATCH 70/99] fix(Role): Remove __init__ and fix pydantic model annotations --- discord/role.py | 97 ++++++++----------------------------------------- 1 file changed, 16 insertions(+), 81 deletions(-) diff --git a/discord/role.py b/discord/role.py index 21c60fb..84de9cc 100644 --- a/discord/role.py +++ b/discord/role.py @@ -1,8 +1,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, TypeVar +from typing import TYPE_CHECKING, Optional, Any from .abc.discordobject import DiscordObject +from .asset import Asset from .color import Color if TYPE_CHECKING: @@ -40,50 +41,24 @@ def __repr__(self): class Role(DiscordObject): - __slots__ = ( - "_guild", - "_cache", - "_id", - "_name", - "_color", - "_hoist", - "_position", - "_permissions", - "_managed", - "_mentionable", - "_tags", - ) - - _guild: Guild - _cache: LFUCache - _id: Snowflake - _name: str - _color: Color - _hoist: bool - _position: int - _permissions: str - _managed: bool - _mentionable: bool - _tags: Optional[RoleTags] - - def __init__(self, guild: Guild, cache: LFUCache, payload: RolePayload): - self._guild = guild - self._cache = cache - self._id = payload["id"] - self._name = payload["name"] - self._color = payload["color"] - self._hoist = payload["hoist"] - self._position = payload["position"] - self._permissions = payload["permissions"] - self._managed = payload["managed"] - self._mentionable = payload["mentionable"] - self._tags = RoleTags(payload["tags"]) + id: Snowflake + guild: Any + name: str + color: Color + hoist: bool + icon: Optional[str] = None + unicode_emoji: Optional[str] = None + position: int + permissions: str + managed: bool + mentionable: bool + tags: Optional[RoleTags] = None def __str__(self): - return self._name + return self.name def __repr__(self): - return f"Role {self._name} with id {self._id}" + return f"Role {self.name} with id {self.id}" def __eq__(self, other): return ( @@ -134,43 +109,3 @@ def is_assignable(self): and not self.managed and (me.top_role > self or me.id == self.guild.owner_id) ) - - @property - def id(self) -> Snowflake: - return self._id - - @property - def guild(self) -> Guild: - return self._guild - - @property - def name(self) -> str: - return self._name - - @property - def color(self) -> Color: - return self._color - - @property - def hoist(self) -> bool: - return self._hoist - - @property - def position(self) -> int: - return self._position - - @property - def permissions(self) -> str: - return self._permissions - - @property - def managed(self) -> bool: - return self._managed - - @property - def mentionable(self) -> bool: - return self._mentionable - - @property - def tags(self) -> RoleTags: - return self._tags From bd2cd2411ebc965fa93d4332209f7f06e6ac44a5 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sun, 30 Jan 2022 12:27:54 +0530 Subject: [PATCH 71/99] undo changes --- discord/client.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/discord/client.py b/discord/client.py index 322a323..7b24ce7 100644 --- a/discord/client.py +++ b/discord/client.py @@ -11,11 +11,9 @@ from .api.httphandler import HTTPHandler from .api.intents import Intents from .api.websocket import WebSocket -<<<<<<< HEAD + from .commands.core import Command from .commands.parser import CommandParser -======= ->>>>>>> Converters class Client: @@ -26,7 +24,6 @@ async def handle_event_error(self, error): type(error), error, error.__traceback__, file=sys.stderr ) -<<<<<<< HEAD async def handle_commands(self, message): if message.author.get("bot"): return @@ -36,8 +33,6 @@ async def handle_commands(self, message): if command: await command.execute(message, *args, **kwargs, **extra_kwargs) -======= ->>>>>>> Converters def __init__( self, *, From 11997aff66b11e97cb8902d029e9e6d8f72c2757 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sun, 30 Jan 2022 13:23:18 +0530 Subject: [PATCH 72/99] fix: set DiscordObject._client attribute --- discord/abc/discordobject.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/discord/abc/discordobject.py b/discord/abc/discordobject.py index 0352e0a..421a7f9 100644 --- a/discord/abc/discordobject.py +++ b/discord/abc/discordobject.py @@ -5,19 +5,22 @@ from ..types.snowflake import Snowflake + if TYPE_CHECKING: - from ..client import Client + from discord import Client class DiscordObject(BaseModel): - class Config: - arbitrary_types_allowed = True - id: Snowflake _client: Client + class Config: + arbitrary_types_allowed = True + def __init__(self, client, **payload): super().__init__(_client=client, **payload) + object.__setattr__(self, "_client", client) # For some reason pydantic doesn't set the client attribute + # So we'll set it manually def __ne__(self, other): return not self.__eq__(other) From 1fa20e7ba92eccb26c3d4677c1d25a607ee72618 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sun, 30 Jan 2022 13:23:59 +0530 Subject: [PATCH 73/99] feat: Create a cache to store channels --- discord/api/websocket.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/discord/api/websocket.py b/discord/api/websocket.py index 82e71a6..7602e2f 100644 --- a/discord/api/websocket.py +++ b/discord/api/websocket.py @@ -13,6 +13,7 @@ from aiohttp.http_websocket import WSMessage, WSMsgType from ..cache import LFUCache +from ..channels.basechannel import BaseChannel from ..guild import Guild from ..types.snowflake import Snowflake @@ -46,6 +47,7 @@ def __init__(self, client, token: str) -> None: self.closed: bool = False self.guild_cache = LFUCache[Snowflake, Guild](1000) + self.channel_cache = LFUCache[Snowflake, BaseChannel](5000) self.member_cache = LFUCache[Snowflake, dict](5000) self.user_cache = LFUCache[Snowflake, dict](5000) From 81a9215c77f3abfbb91301b27a280571507b3a3a Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sun, 30 Jan 2022 13:24:24 +0530 Subject: [PATCH 74/99] feat: push the data to the channel cache --- discord/api/dataConverters.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index 9c1a57c..45df0fa 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -2,6 +2,7 @@ import typing from discord.types.snowflake import Snowflake +from ..channels.guildchannel import TextChannel from ..guild import Guild from ..message import Message @@ -24,7 +25,7 @@ def convert_event_error(self, data): return [data] def convert_message_create(self, data): - return [Message(self.client, data)] + return [Message(self.client, **data)] def convert_ready(self, data): return [] @@ -44,6 +45,9 @@ def convert_guild_create(self, data): self.client.ws.member_cache[Snowflake(user_data["id"])] = Member(self.client, **member_data, **user_data) self.client.ws.user_cache[Snowflake(user_data["id"])] = User(self.client, **user_data) + for channel_data in data["channels"]: + self.client.ws.channel_cache[Snowflake(channel_data["id"])] = TextChannel(self.client, **channel_data) + return [guild] def convert_presence_update(self, data): From 49168528bb902b7c56194e00a43048f1cbea7684 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sun, 30 Jan 2022 13:25:02 +0530 Subject: [PATCH 75/99] fix: remove BaseChannel.__slots__ --- discord/channels/basechannel.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/discord/channels/basechannel.py b/discord/channels/basechannel.py index da6f910..6d18c47 100644 --- a/discord/channels/basechannel.py +++ b/discord/channels/basechannel.py @@ -5,22 +5,11 @@ class BaseChannel(DiscordObject): - __slots__ = ("_id", "_name", "_mention") - - _id: Snowflake - _name: str - - @property - def id(self) -> Snowflake: - return self._id - - @property - def name(self): - return self._name + name: str @property def mention(self): - return f"<#{self._id}>" + return f"<#{self.id}>" @property def created_at(self): From 0d13c7a5a085586dc19df776109d45d9b4b964ba Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sun, 30 Jan 2022 13:25:47 +0530 Subject: [PATCH 76/99] feat: add Message.guild --- discord/message.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/discord/message.py b/discord/message.py index 3109f48..3131365 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1,25 +1,24 @@ from __future__ import annotations -from pydantic import BaseModel - +from .abc.discordobject import DiscordObject from .types.snowflake import Snowflake -class Message(BaseModel): - id: Snowflake +class Message(DiscordObject): channel_id: Snowflake guild_id: Snowflake content: str - def __init__(self, client, data): - super().__init__(_client=client, **data) - def __str__(self): return self.content def __repr__(self): return f"" + @property + def guild(self): + return self._client.ws.guild_cache.get(self.guild_id) + @property def channel(self): - return self._client.converter._get_channel(self.channel_id) + return self._client.ws.channel_cache.get(self.channel_id) From 20b14b86039ee22555e4c57736ddd69fe34332d6 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:14:51 +0530 Subject: [PATCH 77/99] fix: Removed __slots__ --- discord/role.py | 1 - 1 file changed, 1 deletion(-) diff --git a/discord/role.py b/discord/role.py index 84de9cc..88ce682 100644 --- a/discord/role.py +++ b/discord/role.py @@ -17,7 +17,6 @@ class RoleTags: - __slots__ = ("_bot_id", "_integration_id", "_premium_subscriber") _bot_id: Snowflake _integration_id: Snowflake _premium_subscriber: bool From 1f184101db0fa5827f16bac119c83732df4b2ba3 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:19:04 +0530 Subject: [PATCH 78/99] fix: Return proper data when there is no command invoked --- discord/commands/parser.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/discord/commands/parser.py b/discord/commands/parser.py index d061d26..686bd3a 100644 --- a/discord/commands/parser.py +++ b/discord/commands/parser.py @@ -56,11 +56,13 @@ def get_args(self, command: Command, content: str, prefix=None): return positional_arguments, kwargs, extra_kwargs def parse_message(self, message: Message): + empty = None, [], {}, {} + if not self.commands: - return None, [] + return empty if not message.content.startswith(self.command_prefix): - return None, [] + return empty no_prefix = self.remove_prefix(message.content) # The content of the message but without the command_prefix @@ -86,6 +88,6 @@ def parse_message(self, message: Message): "First match group of command regex does not exist" ) - return regex_command, *self.get_args(command, no_prefix, prefix=prefix) + return regex_command, *self.get_args(regex_command, no_prefix, prefix=prefix) - return None, [] + return empty From 77d58f2dabd1ccaaea5e76eeed03e063481c1da1 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:19:37 +0530 Subject: [PATCH 79/99] feat: Add messageable --- discord/abc/messageable.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 discord/abc/messageable.py diff --git a/discord/abc/messageable.py b/discord/abc/messageable.py new file mode 100644 index 0000000..cffb069 --- /dev/null +++ b/discord/abc/messageable.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +import abc +import typing + +import discord + +if typing.TYPE_CHECKING: + from discord import Client, Embed + from discord.interactions import View + + +class Messageable(abc.ABC): + id: int + _client: Client + + def _get_channel(self): + raise NotImplementedError + + async def send(self, + content: typing.Optional[str] = None, + *, + embeds: typing.Union[Embed, typing.List[Embed]] = None, + views: typing.Union[View, typing.List[View]] = None + ): + content = str(content) if content is not None else None + + channel = self._get_channel() + data = await self._client.httphandler.send_message(channel.id, content=content, embeds=embeds, views=views) + + return discord.message.Message(self._client, **data) From f16bdaea080cf0119488f22658dd5f039949a5b4 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:20:11 +0530 Subject: [PATCH 80/99] feat: Add interaction.py --- discord/interactions/interaction.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 discord/interactions/interaction.py diff --git a/discord/interactions/interaction.py b/discord/interactions/interaction.py new file mode 100644 index 0000000..e69de29 From 42bf00c0a978d2d376cd73860bf29e60e34ce480 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:32:50 +0530 Subject: [PATCH 81/99] feat: Import Button --- discord/interactions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/interactions/__init__.py b/discord/interactions/__init__.py index b4b6730..10f8bc6 100644 --- a/discord/interactions/__init__.py +++ b/discord/interactions/__init__.py @@ -1 +1 @@ -from .components import Component, View +from .components import Component, Button, View From b9ce0c36980c5ff04ee515be59a78d801ad6d966 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:33:06 +0530 Subject: [PATCH 82/99] feat: Add maybe_await --- discord/utils/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/discord/utils/__init__.py b/discord/utils/__init__.py index e69de29..eafdc68 100644 --- a/discord/utils/__init__.py +++ b/discord/utils/__init__.py @@ -0,0 +1,8 @@ +import asyncio + + +async def maybe_await(function, *args, **kwargs): + if asyncio.iscoroutinefunction(function): + return await function(*args, **kwargs) + else: + return function(*args, **kwargs) From d83c157eb87734dbf77f1fa4c5ce68867a061d0b Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:33:25 +0530 Subject: [PATCH 83/99] feat: Implement _get_channel --- discord/channels/basechannel.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/discord/channels/basechannel.py b/discord/channels/basechannel.py index 6d18c47..41600c0 100644 --- a/discord/channels/basechannel.py +++ b/discord/channels/basechannel.py @@ -1,12 +1,16 @@ from __future__ import annotations from ..abc.discordobject import DiscordObject +from ..abc.messageable import Messageable from ..types.snowflake import Snowflake -class BaseChannel(DiscordObject): +class BaseChannel(DiscordObject, Messageable): name: str + def _get_channel(self): + return self + @property def mention(self): return f"<#{self.id}>" From b046b57b544c6a55d602f66f895e37d7194a07b4 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:36:17 +0530 Subject: [PATCH 84/99] style: Remove unwanted imports --- discord/user/baseuser.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/discord/user/baseuser.py b/discord/user/baseuser.py index 0a5b21d..ddfd568 100644 --- a/discord/user/baseuser.py +++ b/discord/user/baseuser.py @@ -3,10 +3,7 @@ from typing import Optional from ..abc.discordobject import DiscordObject -from ..asset import Asset from ..color import Color -from ..message import Message -from ..types.avatar import Avatar from ..types.banner import Banner from ..types.enums.premiumtype import PremiumType from ..types.enums.userflags import UserFlags From 8c5d6820dc16bdc97e5727ff7c5f8d6e9301d63b Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:37:09 +0530 Subject: [PATCH 85/99] feat: Implement `add_command`, `remove_command` and `process_commands` --- discord/client.py | 56 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/discord/client.py b/discord/client.py index 7b24ce7..88858ef 100644 --- a/discord/client.py +++ b/discord/client.py @@ -16,6 +16,10 @@ from .commands.parser import CommandParser +if typing.TYPE_CHECKING: + from . import Message + + class Client: async def handle_event_error(self, error): @@ -24,23 +28,19 @@ async def handle_event_error(self, error): type(error), error, error.__traceback__, file=sys.stderr ) - async def handle_commands(self, message): - if message.author.get("bot"): - return - - command, args, kwargs, extra_kwargs = self.command_parser.parse_message(message) - - if command: - await command.execute(message, *args, **kwargs, **extra_kwargs) + async def handle_commands(self, message: Message): + await self.process_commands(message) def __init__( self, + command_prefix: str, *, intents: typing.Optional[Intents] = Intents.default(), respond_self: typing.Optional[bool] = False, + case_sensitive: bool=True, loop: typing.Optional[asyncio.AbstractEventLoop] = None, ) -> None: - self._loop: asyncio.AbstractEventLoop = None # create the event loop when we run our client + self._loop: asyncio.AbstractEventLoop = None # create the event loop when we run our client self.intents = intents self.respond_self = respond_self @@ -48,9 +48,14 @@ def __init__( self.httphandler = HTTPHandler() self.lock = asyncio.Lock() self.closed = False - self.events = {"event_error": [self.handle_event_error]} + self.events = {"message_create": [self.handle_commands], "event_error": [self.handle_event_error]} self.once_events = {} + + self.command_prefix = command_prefix + self.commands: typing.Dict[str, Command] = {} + self.converter = DataConverter(self) + self.command_parser = CommandParser(self.command_prefix, self.commands, case_sensitive) async def login(self, token: str) -> None: self.token = token @@ -104,6 +109,15 @@ def wrapper(func): return wrapper + def command(self, name=None, **kwargs): + """The decorator used to register functions as commands""" + def inner(func) -> Command: + command = Command(func, name, **kwargs) + self.add_command(command) + return command + + return inner + def add_listener( self, func: typing.Callable, @@ -148,6 +162,28 @@ async def handle_event(self, msg): error.event = coro await self.handle_event({"d": error, "t": "event_error"}) + def add_command(self, command: Command): + if command.name in self.commands: + raise ValueError("Duplicate command name") + self.commands[command.name] = command + return command + + def remove_command(self, command: Command): + return self.commands.pop(command.name) + + async def process_commands(self, message: Message): + """Command handling""" + from .commands.context import Context + + if message.author.bot: + return + + command, args, kwargs, extra_kwargs = self.command_parser.parse_message(message) + context = Context(client=self, message=message, command=command) + + if command: + await command.execute(context, *args, **kwargs, **extra_kwargs) + def get_guild(self, id: int): return self.ws.guild_cache.get(id) From 7dbef5110f53c7adbc6223aaec3696e2c4dcb253 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:38:14 +0530 Subject: [PATCH 86/99] feat: Add Button --- discord/interactions/components.py | 68 ++++++++++++++++++++++++------ 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/discord/interactions/components.py b/discord/interactions/components.py index 2cf55eb..da06402 100644 --- a/discord/interactions/components.py +++ b/discord/interactions/components.py @@ -1,5 +1,7 @@ import os -from typing import Optional, Tuple, Union +from typing import Optional, Tuple, Union, Callable, Any + +from discord.utils import maybe_await OptInt = Optional[int] OptStr = Optional[str] @@ -7,18 +9,18 @@ class Component: def __init__( - self, - type: int, - disabled: bool = None, - style: OptInt = None, - label: OptStr = None, - emoji: OptStr = None, - url: OptStr = None, - options: list = None, - placeholder: OptStr = None, - min_values: OptInt = None, - max_values: OptInt = None, - custom_id: OptStr = None, + self, + type: int, + disabled: bool = None, + style: OptInt = None, + label: OptStr = None, + emoji: OptStr = None, + url: OptStr = None, + options: list = None, + placeholder: OptStr = None, + min_values: OptInt = None, + max_values: OptInt = None, + custom_id: OptStr = None, ): self.type: int = type self.disabled: bool = disabled @@ -35,14 +37,52 @@ def __init__( if self.custom_id is None and self.url is None: self.custom_id = os.urandom(16).hex() + async def run_callback(self, *args, **kwargs): + raise NotImplementedError + def _to_dict(self): - return {k: v for k, v in self.__dict__.items() if v is not None} + exclude = ("_callback", "callback", "run_callback") + return {k: v for k, v in self.__dict__.items() if v is not None and k not in exclude} + + +class Button(Component): + def __init__(self, style: int, *, label: str = None, emoji: dict = None, url: str = None, disabled: bool = None, + callback: Callable[[Any, Any], Any] = None): + super().__init__(type=2, style=style, label=label, emoji=emoji, url=url, disabled=disabled) + self._callback = callback + + async def run_callback(self, *args, **kwargs): + """Runs the callback function with the given arguments""" + if hasattr(self, "callback") and self._callback: + raise ValueError("Callback is specified twice") + + if hasattr(self, "callback"): + return await maybe_await(self.callback, *args, **kwargs) + + elif self._callback: + return await maybe_await(self._callback, *args, **kwargs) + + else: + raise ValueError("Callback not specified") + + def __init_subclass__(cls, **kwargs): + # Check if subclasses implement the callback method + super().__init_subclass__(**kwargs) + + if not hasattr(cls, "callback"): + raise TypeError("Subclasses of Button must implement callback method") class View: def __init__(self, *components: Component): self.components: Tuple[Component] = components + async def run_component_callback(self, custom_id: str, *args, **kwargs): + """Runs the callback of the component with the given custom_id""" + for component in self.components: + if component.custom_id == custom_id: + await component.run_callback(*args, **kwargs) + def _to_dict(self): return { "type": 1, From 037ae6102f759df0700772722f44661e0b4d5710 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:38:26 +0530 Subject: [PATCH 87/99] feat: Added Context model --- discord/commands/context.py | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/discord/commands/context.py b/discord/commands/context.py index f416be0..d596710 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -1,9 +1,43 @@ -from typing import Any, Dict, List, Optional +from __future__ import annotations -from discord.user.user import User +from typing import Optional, Any -from discord.abc.discordobject import DiscordObject +from pydantic import BaseModel +from discord import Message +from discord.abc.messageable import Messageable +from discord.commands import Command -class Context: - pass + +class Context(BaseModel, Messageable): + """The context model which will be used in commands""" + class Config: + arbitrary_types_allowed = True + + client: Any + message: Message + command: Optional[Command] = None + + @property + def _client(self): + return self.client # Make an alias for client because Messageable uses it + + @property + def guild(self): + """The guild the command was used in""" + return self.message.guild + + @property + def channel(self): + """The channel the command was used in""" + return self.message.channel + + @property + def author(self): + return self.message.author + + def _get_channel(self): + return self.channel + + +Context.update_forward_refs() From df5b010ff481830e73cf06011673c2f065435eb5 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:38:46 +0530 Subject: [PATCH 88/99] feat: Handle components in convert_interaction_create --- discord/api/dataConverters.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index 45df0fa..764b19f 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -59,6 +59,18 @@ def convert_typing_start(self, data): def convert_guild_member_update(self, data): return [data] + def convert_interaction_create(self, payload): + message = payload.get("message") + + if message: + message = Message(self.client, **payload) + + if payload["type"] == 3: + component = self.client.httphandler.component_cache.get(payload["data"]["custom_id"]) + self.client._loop.create_task(component.run_callback(message, payload["data"])) + + return [payload] + def convert(self, event, data): func: typing.Callable = self.converters.get(event) if not func: From 30e64c1996ad773817be86f7276fb5da60096c4f Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:39:18 +0530 Subject: [PATCH 89/99] fix: Remove __slots__ --- discord/channels/guildchannel.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/discord/channels/guildchannel.py b/discord/channels/guildchannel.py index 23640ee..3fc2c91 100644 --- a/discord/channels/guildchannel.py +++ b/discord/channels/guildchannel.py @@ -11,29 +11,12 @@ class TextChannel(BaseChannel): - __slots__ = ("name", "id", "guild", "nsfw", "category_id", "position", "topic") + ... class ThreadChannel(BaseChannel): - __slots__ = ( - "name", - "id", - "guild", - "nsfw", - "category_id", - "position", - "topic", - "parent", - ) + ... class VoiceChannel(BaseChannel): - __slots__ = ( - "name", - "id", - "guild", - "bitrate", - "user_limit", - "category_id", - "position", - ) + ... From 9dc8b36c505ed7d68864337e53d6637c2a87fc11 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:39:55 +0530 Subject: [PATCH 90/99] feat: Cache the message components in `Handler.send_message` --- discord/api/httphandler.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/discord/api/httphandler.py b/discord/api/httphandler.py index 09015e0..3f9c9ff 100644 --- a/discord/api/httphandler.py +++ b/discord/api/httphandler.py @@ -19,6 +19,8 @@ def __init__(self): self.base_url: str = "https://discord.com/api/v9/" self.user_agent: str = "Disthon Discord API wrapper V0.0.1b" + self.component_cache = {} + async def request( self, method: str, @@ -109,20 +111,30 @@ async def send_message( if content: payload["content"] = content + if embeds: payload["embeds"] = [embed.dict() for embed in embeds] + if views: - payload["components"] = [view._to_dict() for view in views] + def _cache_view_components(view: View): + for component in view.components: + self.component_cache[str(component.custom_id)] = component + return view._to_dict() + + payload["components"] = [_cache_view_components(view) for view in views] data = await self.request( "POST", f"channels/{channel_id}/messages", data=payload ) try: if isinstance(data, dict): - if data["code"] == 50008: - raise DiscordChannelNotFound - elif data["code"] == 10003: - raise DiscordChannelForbidden + code = data["code"] + if code == 50008: + raise DiscordChannelNotFound() + elif code == 10003: + raise DiscordChannelForbidden() + else: + raise DiscordHTTPException(data.get("message"), code) except KeyError: return data From 94e78a7b5fe820aac9297f8ea8a6cb118b6e3aa1 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Fri, 25 Mar 2022 17:40:45 +0530 Subject: [PATCH 91/99] feat: Override __init__ because Pydantic cannot convet the author field --- discord/message.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/discord/message.py b/discord/message.py index 3131365..43f49d2 100644 --- a/discord/message.py +++ b/discord/message.py @@ -1,13 +1,22 @@ from __future__ import annotations +from typing import Optional + from .abc.discordobject import DiscordObject from .types.snowflake import Snowflake +from .user.user import User class Message(DiscordObject): channel_id: Snowflake - guild_id: Snowflake - content: str + guild_id: Optional[Snowflake] = None + content: Optional[str] + author: Optional[User] = None + + def __init__(self, client, **data): + if data.get("author"): + data["author"] = User(client, **data["author"]) + super().__init__(client, **data) def __str__(self): return self.content From a617affd475038ea81c07d398033795a73b56123 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 26 Mar 2022 13:13:52 +0530 Subject: [PATCH 92/99] feat: Added a default help command --- discord/commands/help_command.py | 69 ++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 discord/commands/help_command.py diff --git a/discord/commands/help_command.py b/discord/commands/help_command.py new file mode 100644 index 0000000..e3a4bdb --- /dev/null +++ b/discord/commands/help_command.py @@ -0,0 +1,69 @@ +import abc +import sys +import traceback +import typing + +from discord.embeds import Embed + +from .context import Context +from .core import Command +from .errors import CommandNotFound + + +async def help_cmd_callback(context: Context, *args): pass + + +class HelpCommand(abc.ABC, Command): + def __init__(self, + name: str = "help", + *, + description: str = None, + aliases: typing.Iterable[str] = None, + regex_command: bool = False, + regex_flags=0 + ): + help_cmd_callback.__doc__ = description + super().__init__(callback=help_cmd_callback, name=name, aliases=aliases, regex_command=regex_command, + regex_flags=regex_flags) + self.on_error = self.on_help_error + + async def execute(self, context: Context, *args, **kwargs): + await self.run_checks(context) + + client = context.client + + target_command_name = args[0] if args else None + + if target_command_name is None: + await self.send_bot_help(context) + return + + command = client.get_command_named(target_command_name) + + if command is None: + error = CommandNotFound(target_command_name) + await self.on_error(context, error) + return + + await self.send_command_help(context, command) + + @abc.abstractmethod + async def send_bot_help(self, context: Context): + pass + + @abc.abstractmethod + async def send_command_help(self, context: Context, command: Command): + pass + + async def on_help_error(self, context: Context, error): + traceback.print_exception(type(error), error, error.__traceback__, file=sys.stderr) + + +class DefaultHelpCommand(HelpCommand): + async def send_bot_help(self, context: Context): + embed = Embed(title="Help", description=f"All commands: {', '.join([str(cmd.qualified_name) for cmd in context.client.commands.values()])}") + await context.send(embeds=embed) + + async def send_command_help(self, context: Context, command: Command): + embed = Embed(title=command.qualified_name, description=f"Description: {command.description}") + await context.send(embeds=embed) From 5bbabc5cadc38f67f042ef688081dfdb2fa23165 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 26 Mar 2022 13:49:38 +0530 Subject: [PATCH 93/99] feat: Added a default help command --- discord/client.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/discord/client.py b/discord/client.py index 88858ef..2546313 100644 --- a/discord/client.py +++ b/discord/client.py @@ -14,6 +14,7 @@ from .commands.core import Command from .commands.parser import CommandParser +from .commands.help_command import HelpCommand, DefaultHelpCommand if typing.TYPE_CHECKING: @@ -36,6 +37,7 @@ def __init__( command_prefix: str, *, intents: typing.Optional[Intents] = Intents.default(), + help_command: HelpCommand = DefaultHelpCommand(), respond_self: typing.Optional[bool] = False, case_sensitive: bool=True, loop: typing.Optional[asyncio.AbstractEventLoop] = None, @@ -57,6 +59,9 @@ def __init__( self.converter = DataConverter(self) self.command_parser = CommandParser(self.command_prefix, self.commands, case_sensitive) + if help_command: + self.add_command(help_command) + async def login(self, token: str) -> None: self.token = token async with self.lock: @@ -171,6 +176,15 @@ def add_command(self, command: Command): def remove_command(self, command: Command): return self.commands.pop(command.name) + def get_command_named(self, name: str) -> typing.Optional[Command]: + for command_name, command in self.commands.items(): + if command.is_regex_command: + if command.regex_match_func(command_name, name, command.regex_flags): + return command + + elif command_name == name: + return command + async def process_commands(self, message: Message): """Command handling""" from .commands.context import Context From 9e93dcb741c6168e337c7eb51b0519bffc4cbe75 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 26 Mar 2022 13:50:01 +0530 Subject: [PATCH 94/99] fix: Fix circular import errors --- discord/commands/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/commands/context.py b/discord/commands/context.py index d596710..6406d41 100644 --- a/discord/commands/context.py +++ b/discord/commands/context.py @@ -4,8 +4,8 @@ from pydantic import BaseModel -from discord import Message from discord.abc.messageable import Messageable + from discord.commands import Command @@ -15,7 +15,7 @@ class Config: arbitrary_types_allowed = True client: Any - message: Message + message: Any # To avoid circular import errors command: Optional[Command] = None @property From 336cf4bbe19545b41b58528c609c89c319192725 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 26 Mar 2022 13:50:37 +0530 Subject: [PATCH 95/99] fix: Fix circular import errors and added some documentation --- discord/commands/core.py | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/discord/commands/core.py b/discord/commands/core.py index 779b8a2..5d70987 100644 --- a/discord/commands/core.py +++ b/discord/commands/core.py @@ -1,10 +1,15 @@ +from __future__ import annotations + import asyncio import re -from typing import Optional, Any, Callable, Iterable +from typing import Optional, Any, Callable, Iterable, TYPE_CHECKING from discord.message import Message from .errors import CheckFailure +if TYPE_CHECKING: + from .context import Context + NOT_ASYNC_FUNCTION_MESSAGE = ( "{0} must be coroutine.\nMaybe you forgot to add the 'async' keyword?." @@ -17,6 +22,7 @@ def __init__( callback: Callable[[Any], Any], name: str = None, *, + qualified_name: str = None, aliases: Iterable[str] = None, regex_command: bool = False, regex_match_func=re.match, @@ -31,6 +37,13 @@ def __init__( self.is_regex_command = regex_command self.regex_match_func = regex_match_func self.regex_flags = regex_flags + self.qualified_name = qualified_name + + if self.qualified_name is None and regex_command is True: + raise TypeError("You need to supply the qualified_name for regex commands") + + elif self.qualified_name is None and regex_command is False: + self.qualified_name = name if aliases is None: self.aliases = [] @@ -55,26 +68,33 @@ def error(self, function): self.on_error = function return function - async def execute(self, message: Message, *args, **kwargs): + async def run_checks(self, context): for check in self.checks: if asyncio.iscoroutinefunction(check): - result = await check(message) + result = await check(context) else: - result = check(message) + result = check(context) if result is not True: raise CheckFailure(self) + + async def execute(self, context: Context, *args, **kwargs): + """Runs the checks and execute the command""" + await self.run_checks(context) + try: - await self.callback(message, *args, **kwargs) + await self.callback(context, *args, **kwargs) except Exception as error: if self.on_error: - await self.on_error(message, error) + await self.on_error(context, error) else: raise error - async def __call__(self, message: Message, *args, **kwargs): - await self.callback(message, *args, **kwargs) + async def __call__(self, context: Context, *args, **kwargs): + """Execute the command when the instance is called + NOTE: This method does not validate checks""" + await self.callback(context, *args, **kwargs) def check(function: Callable[[Message], bool]): From f01ce682b378727f6cc5f1f7671aa623f0310543 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 26 Mar 2022 13:50:56 +0530 Subject: [PATCH 96/99] fix: Fix bug when the component does not exist --- discord/api/dataConverters.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/discord/api/dataConverters.py b/discord/api/dataConverters.py index 764b19f..d40c96e 100644 --- a/discord/api/dataConverters.py +++ b/discord/api/dataConverters.py @@ -67,7 +67,10 @@ def convert_interaction_create(self, payload): if payload["type"] == 3: component = self.client.httphandler.component_cache.get(payload["data"]["custom_id"]) - self.client._loop.create_task(component.run_callback(message, payload["data"])) + + # When the bot restarts the previously cached components are gone + if component: # so check if the component is a newly created + self.client._loop.create_task(component.run_callback(message, payload["data"])) return [payload] From f1dbe4f89e492a349a8f19e03772ef3b0a8314a0 Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 26 Mar 2022 13:51:10 +0530 Subject: [PATCH 97/99] fix: Fix bug in description and colour --- discord/embeds.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/discord/embeds.py b/discord/embeds.py index a6b48de..3338630 100644 --- a/discord/embeds.py +++ b/discord/embeds.py @@ -106,12 +106,12 @@ def _validate_url(cls, url): class Embed(BaseModel): - color: Union[Color, int] = Color.random() + color: Union[Color, int] = None title: Optional[str] _type: Final[EmbedType] = EmbedType.rich author: Optional[EmbedAuthor] url: Optional[str] - description = Optional[str] + description: Optional[str] timestamp: Optional[Arrow] thumbnail: Optional[EmbedMedia] = None image: Optional[EmbedMedia] = None From 1391150748d3d32c370599186224d089ba189cbd Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 26 Mar 2022 13:51:31 +0530 Subject: [PATCH 98/99] fix: Added CommandNotFound error --- discord/commands/errors.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/discord/commands/errors.py b/discord/commands/errors.py index df494dc..7ff00eb 100644 --- a/discord/commands/errors.py +++ b/discord/commands/errors.py @@ -2,7 +2,14 @@ class CommandError(Exception): pass -class CheckFailure(Exception): +class CheckFailure(CommandError): def __init__(self, command): self.command = command super().__init__(f"Check failed for command {command.name}") + + +class CommandNotFound(CommandError): + def __init__(self, command_name): + self.command_name = command_name + + super().__init__(f"Command with the name {self.command_name} does not exist") From ade1d11b7f73cb6d6ea2f6034e66b0778a5f335b Mon Sep 17 00:00:00 2001 From: Rashaad Akbar Date: Sat, 26 Mar 2022 13:52:01 +0530 Subject: [PATCH 99/99] style: Rename comment --- discord/commands/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord/commands/parser.py b/discord/commands/parser.py index 686bd3a..0a40155 100644 --- a/discord/commands/parser.py +++ b/discord/commands/parser.py @@ -33,7 +33,7 @@ def get_args(self, command: Command, content: str, prefix=None): content = content[len(prefix):].strip() signature = inspect.signature(command.callback) parameters = signature.parameters.copy() - parameters.pop(tuple(parameters.keys())[0]) # Remove the message parameter + parameters.pop(tuple(parameters.keys())[0]) # Remove the context parameter args = content.split() positional_arguments = []