Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
df64df1
Start of converters
sebkuip Nov 11, 2021
e330338
Fixed a lot of weird issues
sebkuip Nov 11, 2021
bf5d124
Merge branch 'AA1999:Converters' into Converters
Rashaad1268 Nov 11, 2021
75780da
Messages now get converted!
sebkuip Nov 11, 2021
2971258
event decorator now has overwrite
sebkuip Nov 11, 2021
6d89f95
ran black + isort
sebkuip Nov 11, 2021
0f4a521
some init goofyness
sebkuip Nov 11, 2021
f926733
Changed event decorators to on and once
sebkuip Nov 15, 2021
32b3469
once events will get removed after firing
sebkuip Nov 15, 2021
8b2417a
Merge branch 'AA1999:Converters' into Converters
Rashaad1268 Nov 15, 2021
9d48e7c
Events now fire in a task
sebkuip Nov 15, 2021
5b5f87a
Quick update on converters
sebkuip Nov 15, 2021
544d8c3
Added logo to readme
sebkuip Nov 15, 2021
3b870e9
Link is now relative
sebkuip Nov 15, 2021
2a2a32e
resizing image
sebkuip Nov 15, 2021
ef21d3d
Resize did not work
sebkuip Nov 15, 2021
d814fe4
No longer using our own loop/deprecated stuff
sebkuip Nov 17, 2021
45a1575
Added a close
sebkuip Nov 26, 2021
c26cd14
Whitespace fix
sebkuip Nov 26, 2021
d3af037
swap
sebkuip Nov 26, 2021
8341c05
improved close
sebkuip Nov 26, 2021
7d8bf13
Merge branch 'AA1999:Converters' into Converters
Rashaad1268 Nov 26, 2021
5c3e681
Made default error handler an event
sebkuip Nov 26, 2021
0ba2bc2
further improvements to close()
sebkuip Nov 26, 2021
6c4147b
Now actually uses a passed loop
sebkuip Nov 26, 2021
41eecd0
removed debuggin print() statements
sebkuip Nov 26, 2021
77818d6
Merge branch 'AA1999:Converters' into Converters
Rashaad1268 Nov 26, 2021
91c8d74
Made Snowflake a subclass of int
Rashaad1268 Nov 27, 2021
dd18201
Typehinted id to Snowflake
Rashaad1268 Nov 27, 2021
34f3262
Typehinted id to Snowflake
Rashaad1268 Nov 27, 2021
15bfc6b
Added Snowflake.__get_validators__
Rashaad1268 Nov 29, 2021
5df93bf
Changed Message.__repr__
Rashaad1268 Nov 29, 2021
3d29805
Fix: Snowflake.created_at
Dec 13, 2021
5d5ecb1
Fix: Fixed some bugs in LFUCache
Rashaad1268 Dec 14, 2021
bae001f
Changed imports
Rashaad1268 Dec 14, 2021
c04d8c0
Changed imports
Rashaad1268 Dec 14, 2021
395a1ae
Divided the timestamp by 1000
Rashaad1268 Dec 14, 2021
65e4d7f
Removed DiscordObject.created_at
Rashaad1268 Dec 15, 2021
fb4f6e9
Added cache
Rashaad1268 Dec 15, 2021
8982a35
Push the data to the cache
Rashaad1268 Dec 15, 2021
fb4033e
Did some stuff
Rashaad1268 Dec 15, 2021
f139da4
Improved the Guild model
Rashaad1268 Dec 15, 2021
6d127d8
Added .DS_Store
Rashaad1268 Dec 15, 2021
8cea032
Added Client.get_user and Client.get_guild
Rashaad1268 Dec 15, 2021
4f02d6d
Added core.py
Rashaad1268 Dec 17, 2021
da41538
Added parser.py
Rashaad1268 Dec 17, 2021
05b7f94
Added Message.author
Rashaad1268 Dec 17, 2021
abbc655
Added command handling
Rashaad1268 Dec 17, 2021
c6be27c
Added checks
Rashaad1268 Dec 17, 2021
8442d7d
Use command.execute() instead of command()
Rashaad1268 Dec 17, 2021
25f7209
Added __init__.py
Rashaad1268 Dec 17, 2021
f16112f
Check if the check is a coroutine or not
Rashaad1268 Dec 17, 2021
4e72a05
Moved commands here
Rashaad1268 Dec 19, 2021
a01bc2c
Moved commands here
Rashaad1268 Dec 19, 2021
f450b14
Changed imports
Rashaad1268 Dec 19, 2021
e483c8d
Fix: fixed error in convert
Rashaad1268 Dec 19, 2021
ffef068
Moved commands here
Rashaad1268 Dec 19, 2021
35343d0
Moved commands here
Rashaad1268 Dec 19, 2021
b5ff607
Changed imports
Rashaad1268 Dec 19, 2021
4f3bc0e
Revert to 8cea032bf67ad7581aeb511c40b1139760ee5c2e
Rashaad1268 Dec 19, 2021
086c332
Fix: fixed bug in DataConverter.convert
Rashaad1268 Dec 19, 2021
0e3e2cb
Added commands/errors.py
Rashaad1268 Dec 22, 2021
47cf7e4
Added commands/errors.py
Rashaad1268 Dec 22, 2021
1aa2594
Raise CheckFailure if check did not return True
Rashaad1268 Dec 22, 2021
364dc46
Removed Command.signature
Rashaad1268 Dec 22, 2021
0c91c96
Improved CommandParser
Rashaad1268 Dec 22, 2021
00f2968
Improved command argument handling
Rashaad1268 Dec 22, 2021
4ba9e60
Added Asset
Rashaad1268 Jan 27, 2022
e827172
Did major changes
Rashaad1268 Jan 27, 2022
e7203a8
Fix(Client): Not await the task in handle_event
Rashaad1268 Jan 29, 2022
f69ffb6
fix(Snowflake): Use arrow.get instead of datetime.datetime.fromtimestamp
Rashaad1268 Jan 29, 2022
d1b8102
fix(Snowflake): Use arrow.get instead of datetime.datetime.fromisoformat
Rashaad1268 Jan 29, 2022
10d8f43
refactor(Asset): Remove unused variable
Rashaad1268 Jan 29, 2022
a4be2af
fix(Role): Remove __init__ and fix pydantic model annotations
Rashaad1268 Jan 29, 2022
38d3570
Major changes
Rashaad1268 Jan 30, 2022
bd2cd24
undo changes
Rashaad1268 Jan 30, 2022
11997af
fix: set DiscordObject._client attribute
Rashaad1268 Jan 30, 2022
1fa20e7
feat: Create a cache to store channels
Rashaad1268 Jan 30, 2022
81a9215
feat: push the data to the channel cache
Rashaad1268 Jan 30, 2022
4916852
fix: remove BaseChannel.__slots__
Rashaad1268 Jan 30, 2022
0d13c7a
feat: add Message.guild
Rashaad1268 Jan 30, 2022
f79242f
Merge branch 'Converters' into Commands
Rashaad1268 Feb 5, 2022
20b14b8
fix: Removed __slots__
Rashaad1268 Mar 25, 2022
1f18410
fix: Return proper data when there is no command invoked
Rashaad1268 Mar 25, 2022
77d58f2
feat: Add messageable
Rashaad1268 Mar 25, 2022
f16bdae
feat: Add interaction.py
Rashaad1268 Mar 25, 2022
42bf00c
feat: Import Button
Rashaad1268 Mar 25, 2022
b9ce0c3
feat: Add maybe_await
Rashaad1268 Mar 25, 2022
d83c157
feat: Implement _get_channel
Rashaad1268 Mar 25, 2022
b046b57
style: Remove unwanted imports
Rashaad1268 Mar 25, 2022
8c5d682
feat: Implement `add_command`, `remove_command` and `process_commands`
Rashaad1268 Mar 25, 2022
7dbef51
feat: Add Button
Rashaad1268 Mar 25, 2022
037ae61
feat: Added Context model
Rashaad1268 Mar 25, 2022
df5b010
feat: Handle components in convert_interaction_create
Rashaad1268 Mar 25, 2022
30e64c1
fix: Remove __slots__
Rashaad1268 Mar 25, 2022
9dc8b36
feat: Cache the message components in `Handler.send_message`
Rashaad1268 Mar 25, 2022
94e78a7
feat: Override __init__ because Pydantic cannot convet the author field
Rashaad1268 Mar 25, 2022
a617aff
feat: Added a default help command
Rashaad1268 Mar 26, 2022
5bbabc5
feat: Added a default help command
Rashaad1268 Mar 26, 2022
9e93dcb
fix: Fix circular import errors
Rashaad1268 Mar 26, 2022
336cf4b
fix: Fix circular import errors
Rashaad1268 Mar 26, 2022
f01ce68
fix: Fix bug when the component does not exist
Rashaad1268 Mar 26, 2022
f1dbe4f
fix: Fix bug in description and colour
Rashaad1268 Mar 26, 2022
1391150
fix: Added CommandNotFound error
Rashaad1268 Mar 26, 2022
ade1d11
style: Rename comment
Rashaad1268 Mar 26, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,6 @@ dmypy.json

#pycharm and vscode folders
.vscode
.idea
.idea

.DS_Store
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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](./logo.png?raw=true)

Discord API wrapper for Python built from scratch
7 changes: 4 additions & 3 deletions discord/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
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 .message import Message
16 changes: 5 additions & 11 deletions discord/abc/abstractuser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,30 @@

from typing import Optional

from discordobject import DiscordObject

from ..message import Message
from ..types.avatar import Avatar
from .discordobject import DiscordObject


class AbstractUser(DiscordObject):

avatar: Optional[Avatar]
bot: bool
username: str
discriminator: str
id: int

@property
def tag(self):
return f"{self.username}#{self.discriminator}"

@property
def discriminator(self):
return self.discriminator

@property
def mention(self):
return f"<@!{self.id}>"
@propery

@property
def name(self):
return self.username

@property
def id(self):
return self.id
Expand Down
20 changes: 15 additions & 5 deletions discord/abc/discordobject.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING

from pydantic import BaseModel

from ..types.snowflake import Snowflake


class DiscordObject(BaseModel):
if TYPE_CHECKING:
from discord import Client


class DiscordObject(BaseModel):
id: Snowflake
created_at: datetime
_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)

def __hash__(self):
return self.id.id >> 22
return self.id >> 22
31 changes: 31 additions & 0 deletions discord/abc/messageable.py
Original file line number Diff line number Diff line change
@@ -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)
8 changes: 6 additions & 2 deletions discord/activity/presenseassets.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
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]):
Expand Down
81 changes: 81 additions & 0 deletions discord/api/dataConverters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import inspect
import typing

from discord.types.snowflake import Snowflake
from ..channels.guildchannel import TextChannel

from ..guild import Guild
from ..message import Message
from ..user.user import User
from ..user.member import Member


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[8:]] = func

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)]

def convert_ready(self, data):
return []

def convert_guild_create(self, data):
members = data["members"]
guild = Guild(self.client, **data)
self.client.ws.guild_cache[Snowflake(data["id"])] = guild

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)

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):
return [data]

def convert_typing_start(self, data):
return [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"])

# 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]

def convert(self, event, data):
func: typing.Callable = self.converters.get(event)
if not func:
return [data]
return func(data)
40 changes: 32 additions & 8 deletions discord/api/handler.py → discord/api/httphandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
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"

self.component_cache = {}

async def request(
self,
method: str,
Expand Down Expand Up @@ -79,7 +81,19 @@ 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 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,
Expand All @@ -97,20 +111,30 @@ 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]
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

Expand Down
44 changes: 32 additions & 12 deletions discord/api/websocket.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
import aiohttp
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

if typing.TYPE_CHECKING:
from ..client import Client

Expand Down Expand Up @@ -39,6 +44,12 @@ def __init__(self, client, token: str) -> None:
self.token = token
self.session_id = None
self.heartbeat_acked = True
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)

async def start(
self,
Expand All @@ -47,37 +58,43 @@ 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:
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_stop: threading.Event = threading.Event()
self.hb_t.start()
return self

async def close(self) -> None:
"""Closes the websocket"""
self.closed = True
await self.socket.close()
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()
self.socket.close(code=1000)
asyncio.run(self.start(reconnect=True))
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 = self.decompress.decompress(self.buffer)
msg: bytes = self.decompress.decompress(self.buffer)
msg = msg.decode("utf-8")
self.buffer = bytearray()

Expand All @@ -94,8 +111,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)

Expand Down
Loading