mizuki 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mizuki-0.1.0/LICENSE +21 -0
- mizuki-0.1.0/PKG-INFO +67 -0
- mizuki-0.1.0/README.md +51 -0
- mizuki-0.1.0/mizuki/__init__.py +11 -0
- mizuki-0.1.0/mizuki/_event_dispatch.py +164 -0
- mizuki-0.1.0/mizuki/_utils.py +39 -0
- mizuki-0.1.0/mizuki/bot.py +310 -0
- mizuki-0.1.0/mizuki/cache.py +290 -0
- mizuki-0.1.0/mizuki/enums/__init__.py +10 -0
- mizuki-0.1.0/mizuki/enums/channel.py +41 -0
- mizuki-0.1.0/mizuki/enums/command.py +30 -0
- mizuki-0.1.0/mizuki/enums/embed.py +14 -0
- mizuki-0.1.0/mizuki/enums/event_dispatch.py +43 -0
- mizuki-0.1.0/mizuki/enums/guild.py +122 -0
- mizuki-0.1.0/mizuki/enums/interaction.py +33 -0
- mizuki-0.1.0/mizuki/enums/message.py +64 -0
- mizuki-0.1.0/mizuki/enums/presence.py +26 -0
- mizuki-0.1.0/mizuki/enums/sticker.py +24 -0
- mizuki-0.1.0/mizuki/enums/user.py +11 -0
- mizuki-0.1.0/mizuki/errors.py +90 -0
- mizuki-0.1.0/mizuki/flags.py +140 -0
- mizuki-0.1.0/mizuki/gateway.py +280 -0
- mizuki-0.1.0/mizuki/http.py +198 -0
- mizuki-0.1.0/mizuki/managers/__init__.py +5 -0
- mizuki-0.1.0/mizuki/managers/_types.py +12 -0
- mizuki-0.1.0/mizuki/managers/channel.py +106 -0
- mizuki-0.1.0/mizuki/managers/command.py +382 -0
- mizuki-0.1.0/mizuki/managers/guild.py +89 -0
- mizuki-0.1.0/mizuki/managers/message.py +90 -0
- mizuki-0.1.0/mizuki/managers/user.py +103 -0
- mizuki-0.1.0/mizuki/objects/__init__.py +17 -0
- mizuki-0.1.0/mizuki/objects/asset.py +223 -0
- mizuki-0.1.0/mizuki/objects/avatar_decoration.py +36 -0
- mizuki-0.1.0/mizuki/objects/channel.py +553 -0
- mizuki-0.1.0/mizuki/objects/collectibles.py +35 -0
- mizuki-0.1.0/mizuki/objects/command.py +486 -0
- mizuki-0.1.0/mizuki/objects/embed.py +264 -0
- mizuki-0.1.0/mizuki/objects/emoji.py +127 -0
- mizuki-0.1.0/mizuki/objects/guild.py +276 -0
- mizuki-0.1.0/mizuki/objects/interaction.py +376 -0
- mizuki-0.1.0/mizuki/objects/member.py +103 -0
- mizuki-0.1.0/mizuki/objects/message.py +342 -0
- mizuki-0.1.0/mizuki/objects/permissions.py +69 -0
- mizuki-0.1.0/mizuki/objects/presence.py +149 -0
- mizuki-0.1.0/mizuki/objects/primary_guild.py +26 -0
- mizuki-0.1.0/mizuki/objects/role.py +68 -0
- mizuki-0.1.0/mizuki/objects/snowflake.py +29 -0
- mizuki-0.1.0/mizuki/objects/sticker.py +61 -0
- mizuki-0.1.0/mizuki/objects/user.py +90 -0
- mizuki-0.1.0/mizuki/parameter.py +158 -0
- mizuki-0.1.0/mizuki/payloads/_types.py +8 -0
- mizuki-0.1.0/mizuki/payloads/avatar_decoration.py +6 -0
- mizuki-0.1.0/mizuki/payloads/channel.py +106 -0
- mizuki-0.1.0/mizuki/payloads/collectibles.py +11 -0
- mizuki-0.1.0/mizuki/payloads/command.py +83 -0
- mizuki-0.1.0/mizuki/payloads/embed.py +54 -0
- mizuki-0.1.0/mizuki/payloads/emoji.py +23 -0
- mizuki-0.1.0/mizuki/payloads/guild.py +112 -0
- mizuki-0.1.0/mizuki/payloads/interaction.py +88 -0
- mizuki-0.1.0/mizuki/payloads/member.py +25 -0
- mizuki-0.1.0/mizuki/payloads/message.py +148 -0
- mizuki-0.1.0/mizuki/payloads/presence.py +59 -0
- mizuki-0.1.0/mizuki/payloads/primary_guild.py +8 -0
- mizuki-0.1.0/mizuki/payloads/role.py +29 -0
- mizuki-0.1.0/mizuki/payloads/sticker.py +18 -0
- mizuki-0.1.0/mizuki/payloads/user.py +33 -0
- mizuki-0.1.0/mizuki.egg-info/PKG-INFO +67 -0
- mizuki-0.1.0/mizuki.egg-info/SOURCES.txt +71 -0
- mizuki-0.1.0/mizuki.egg-info/dependency_links.txt +1 -0
- mizuki-0.1.0/mizuki.egg-info/requires.txt +6 -0
- mizuki-0.1.0/mizuki.egg-info/top_level.txt +1 -0
- mizuki-0.1.0/pyproject.toml +20 -0
- mizuki-0.1.0/setup.cfg +4 -0
mizuki-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sora Takemi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
mizuki-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mizuki
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A modern async-based API wrapper for Discord API.
|
|
5
|
+
Author: Sora
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: aiohttp>=3.13.5
|
|
11
|
+
Provides-Extra: docs
|
|
12
|
+
Requires-Dist: shibuya>=2026.5.19; extra == "docs"
|
|
13
|
+
Requires-Dist: sphinx>=9.1.0; extra == "docs"
|
|
14
|
+
Requires-Dist: sphinx-copybutton>=0.5.2; extra == "docs"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Mizuki
|
|
18
|
+
|
|
19
|
+
A modern async-based discord API wrapper written for Python. Currently in early development, not meant to be used in production as of now.
|
|
20
|
+
I aim for this library to closely mirror the discord API.
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
pip install git+https://github.com/TakemiSora/mizuki
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick Example
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
import mizuki
|
|
32
|
+
|
|
33
|
+
bot = mizuki.Bot(
|
|
34
|
+
intents=mizuki.IntentFlags.standard()
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@bot.command(name="ping", description="Send a ping to the bot")
|
|
38
|
+
async def ping(interaction: mizuki.Interaction):
|
|
39
|
+
await interaction.response.send_response("Pong!")
|
|
40
|
+
|
|
41
|
+
bot.run("TOKEN-HERE")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Documentation
|
|
45
|
+
|
|
46
|
+
There is no current hosted documentation (yet), but a local version of the documentation can be viewed by doing the following steps:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
# 1. Clone the repository
|
|
50
|
+
git clone https://github.com/TakemiSora/mizuki
|
|
51
|
+
|
|
52
|
+
# 2. Navigate into the docs directory
|
|
53
|
+
cd mizuki/docs/
|
|
54
|
+
|
|
55
|
+
# 3. Build the HTML documentation
|
|
56
|
+
## Linux/Mac
|
|
57
|
+
make html
|
|
58
|
+
|
|
59
|
+
## Windows
|
|
60
|
+
make.bat html
|
|
61
|
+
|
|
62
|
+
# 4. Start a local server to view them
|
|
63
|
+
cd build/html
|
|
64
|
+
python -m http.server 8000
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Then open `https://localhost:8000/` to open the documentation.
|
mizuki-0.1.0/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Mizuki
|
|
2
|
+
|
|
3
|
+
A modern async-based discord API wrapper written for Python. Currently in early development, not meant to be used in production as of now.
|
|
4
|
+
I aim for this library to closely mirror the discord API.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
pip install git+https://github.com/TakemiSora/mizuki
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Quick Example
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
import mizuki
|
|
16
|
+
|
|
17
|
+
bot = mizuki.Bot(
|
|
18
|
+
intents=mizuki.IntentFlags.standard()
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
@bot.command(name="ping", description="Send a ping to the bot")
|
|
22
|
+
async def ping(interaction: mizuki.Interaction):
|
|
23
|
+
await interaction.response.send_response("Pong!")
|
|
24
|
+
|
|
25
|
+
bot.run("TOKEN-HERE")
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Documentation
|
|
29
|
+
|
|
30
|
+
There is no current hosted documentation (yet), but a local version of the documentation can be viewed by doing the following steps:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
# 1. Clone the repository
|
|
34
|
+
git clone https://github.com/TakemiSora/mizuki
|
|
35
|
+
|
|
36
|
+
# 2. Navigate into the docs directory
|
|
37
|
+
cd mizuki/docs/
|
|
38
|
+
|
|
39
|
+
# 3. Build the HTML documentation
|
|
40
|
+
## Linux/Mac
|
|
41
|
+
make html
|
|
42
|
+
|
|
43
|
+
## Windows
|
|
44
|
+
make.bat html
|
|
45
|
+
|
|
46
|
+
# 4. Start a local server to view them
|
|
47
|
+
cd build/html
|
|
48
|
+
python -m http.server 8000
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Then open `https://localhost:8000/` to open the documentation.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from .objects.command import ApplicationCommandOption
|
|
7
|
+
|
|
8
|
+
from .enums.channel import ChannelType
|
|
9
|
+
from .enums.command import CommandOptionType
|
|
10
|
+
from .enums.interaction import InteractionType
|
|
11
|
+
from .objects.channel import ThreadChannel, ThreadMember, parse_channel_payload
|
|
12
|
+
from .objects.guild import Guild, UnavailableGuild, parse_guild_payload
|
|
13
|
+
from .objects.interaction import Interaction, ResolvedData, InvokedApplicationCommandOption
|
|
14
|
+
from .payloads.channel import (
|
|
15
|
+
GuildChannelPayload,
|
|
16
|
+
PrivateChannelPayload,
|
|
17
|
+
ThreadCreatePayload,
|
|
18
|
+
ThreadDeletePayload,
|
|
19
|
+
ThreadPayload,
|
|
20
|
+
)
|
|
21
|
+
from .payloads.guild import GuildPayload, UnavailableGuildPayload
|
|
22
|
+
from .payloads.interaction import InteractionPayload
|
|
23
|
+
from ._utils import scls
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from .bot import Bot
|
|
27
|
+
|
|
28
|
+
_log = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
class EventDispatcher:
|
|
31
|
+
__slots__ = (
|
|
32
|
+
"_dispatch_handlers",
|
|
33
|
+
"bot"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
def __init__(self, bot: Bot):
|
|
37
|
+
self.bot = bot
|
|
38
|
+
self._dispatch_handlers = {
|
|
39
|
+
# GUILDS
|
|
40
|
+
"GUILD_CREATE": self._handle_guild_create,
|
|
41
|
+
"GUILD_UPDATE": self._handle_guild_update,
|
|
42
|
+
"GUILD_DELETE": self._handle_guild_delete,
|
|
43
|
+
"CHANNEL_CREATE": self._handle_channel_create,
|
|
44
|
+
"CHANNEL_UPDATE": self._handle_channel_update,
|
|
45
|
+
"CHANNEL_DELETE": self._handle_channel_delete,
|
|
46
|
+
"THREAD_CREATE": self._handle_thread_create,
|
|
47
|
+
"THREAD_UPDATE": self._handle_thread_update,
|
|
48
|
+
"THREAD_DELETE": self._handle_thread_delete,
|
|
49
|
+
"INTERACTION_CREATE": self._handle_interaction_create,
|
|
50
|
+
"READY": self._handle_ready
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
def _on_task_done(self, task: asyncio.Task, data: str):
|
|
54
|
+
if task.cancelled():
|
|
55
|
+
return
|
|
56
|
+
exc = task.exception()
|
|
57
|
+
if exc is not None:
|
|
58
|
+
_log.error("%s failed with exception", data, exc_info=exc)
|
|
59
|
+
|
|
60
|
+
async def _dispatch(self, key: str, *args: Any):
|
|
61
|
+
for f in self.bot._listeners.get(key, []):
|
|
62
|
+
asyncio.create_task(f(*args)).add_done_callback(lambda t: self._on_task_done(t, f"Function {f.__name__} listening to '{key}'"))
|
|
63
|
+
_log.debug("Dispatched %s to function '%s'.", key, f.__name__)
|
|
64
|
+
|
|
65
|
+
def _parse_options(self, command_options: dict[str, ApplicationCommandOption], resolved: ResolvedData, options: list[InvokedApplicationCommandOption]) -> dict[str, Any]:
|
|
66
|
+
kwargs: dict[str, Any] = {}
|
|
67
|
+
display_to_callback_keys: dict[str, str] = {o.name: p for p, o in command_options.items()}
|
|
68
|
+
for option in options:
|
|
69
|
+
match option.type:
|
|
70
|
+
case CommandOptionType.CHANNEL:
|
|
71
|
+
assert isinstance(option.value, str)
|
|
72
|
+
value = resolved.channels[int(option.value)]
|
|
73
|
+
case CommandOptionType.ROLE:
|
|
74
|
+
assert isinstance(option.value, str)
|
|
75
|
+
value = resolved.roles[int(option.value)]
|
|
76
|
+
case CommandOptionType.USER:
|
|
77
|
+
assert isinstance(option.value, str)
|
|
78
|
+
if (m := resolved.members.get(int(option.value))) is not None: value = m
|
|
79
|
+
else: value = resolved.users[int(option.value)]
|
|
80
|
+
case CommandOptionType.MENTIONABLE:
|
|
81
|
+
assert isinstance(option.value, str)
|
|
82
|
+
val = int(option.value)
|
|
83
|
+
if (r := resolved.roles.get(val)) is not None:
|
|
84
|
+
value = r
|
|
85
|
+
else:
|
|
86
|
+
if (m := resolved.members.get(val)) is not None: value = m
|
|
87
|
+
else: value = resolved.users[val]
|
|
88
|
+
case _:
|
|
89
|
+
value = None
|
|
90
|
+
kwargs[display_to_callback_keys.get(option.name, option.name)] = value or option.value
|
|
91
|
+
# ^^^^^^^^^^^^^^^^^^^
|
|
92
|
+
# Attempts to fjnd correct CallbackParameterName, if not defaults to DisplayParameterName
|
|
93
|
+
return kwargs
|
|
94
|
+
|
|
95
|
+
async def _dispatch_commands(self, name: str, interaction: Interaction):
|
|
96
|
+
command_data = self.bot._commands_data.get(name)
|
|
97
|
+
callback = command_data[1]._callback if command_data else None
|
|
98
|
+
if callback and command_data:
|
|
99
|
+
assert interaction.type is InteractionType.APPLICATION_COMMAND and interaction.data is not None
|
|
100
|
+
kwargs = self._parse_options(
|
|
101
|
+
getattr(callback, "__command_options__", {}),
|
|
102
|
+
interaction.data.resolved,
|
|
103
|
+
interaction.data.options
|
|
104
|
+
) if interaction.data.resolved else {}
|
|
105
|
+
task = asyncio.create_task(callback(interaction, **kwargs))
|
|
106
|
+
task.add_done_callback(lambda t: self._on_task_done(t, f"Handler Function {callback.__name__} for command '{name}'"))
|
|
107
|
+
_log.debug("Command %s (func=%s) dispatched.", name, callback.__name__)
|
|
108
|
+
else:
|
|
109
|
+
_log.warning("Recieved command %s, but no handler was found for it.", name)
|
|
110
|
+
|
|
111
|
+
async def _handle_guild_create(self, data: GuildPayload | UnavailableGuildPayload):
|
|
112
|
+
guild = self.bot._storage.update_guilds(g) if isinstance((g := parse_guild_payload(data)), Guild) else g
|
|
113
|
+
await self._dispatch("on_guild_create", guild)
|
|
114
|
+
|
|
115
|
+
async def _handle_guild_update(self, data: GuildPayload):
|
|
116
|
+
guild = self.bot._storage.update_guilds(Guild(data))
|
|
117
|
+
await self._dispatch("on_guild_update", guild)
|
|
118
|
+
|
|
119
|
+
async def _handle_guild_delete(self, data: UnavailableGuildPayload):
|
|
120
|
+
guild = UnavailableGuild(data)
|
|
121
|
+
kicked = not guild.unavailable
|
|
122
|
+
await self._dispatch("on_guild_delete", guild, kicked)
|
|
123
|
+
|
|
124
|
+
async def _handle_channel_create(self, data: GuildChannelPayload | PrivateChannelPayload):
|
|
125
|
+
channel = self.bot._storage.update_channels(parse_channel_payload(data))
|
|
126
|
+
await self._dispatch("on_channel_create", channel)
|
|
127
|
+
|
|
128
|
+
async def _handle_channel_update(self, data: GuildChannelPayload | PrivateChannelPayload):
|
|
129
|
+
channel = self.bot._storage.update_channels(parse_channel_payload(data))
|
|
130
|
+
await self._dispatch("on_channel_update", channel)
|
|
131
|
+
|
|
132
|
+
async def _handle_channel_delete(self, data: GuildChannelPayload | PrivateChannelPayload):
|
|
133
|
+
channel = self.bot._storage.remove_channel(int(data["id"]))
|
|
134
|
+
await self._dispatch("on_channel_delete", channel or parse_channel_payload(data))
|
|
135
|
+
|
|
136
|
+
async def _handle_thread_create(self, data: ThreadCreatePayload):
|
|
137
|
+
newly_created = data.get("newly_created", False)
|
|
138
|
+
thread_member = scls(ThreadMember, data.get("member"))
|
|
139
|
+
channel = self.bot._storage.update_channels(ThreadChannel(data))
|
|
140
|
+
await self._dispatch("on_thread_create", channel, newly_created, thread_member)
|
|
141
|
+
|
|
142
|
+
async def _handle_thread_update(self, data: ThreadPayload):
|
|
143
|
+
channel = self.bot._storage.update_channels(ThreadChannel(data))
|
|
144
|
+
await self._dispatch("on_thread_update", channel)
|
|
145
|
+
|
|
146
|
+
async def _handle_thread_delete(self, data: ThreadDeletePayload):
|
|
147
|
+
id = int(data["id"])
|
|
148
|
+
guild_id = int(data["guild_id"])
|
|
149
|
+
parent_id = int(data["parent_id"])
|
|
150
|
+
type = ChannelType(data["type"])
|
|
151
|
+
self.bot._storage.remove_channel(id)
|
|
152
|
+
await self._dispatch("on_thread_delete", id, guild_id, parent_id, type)
|
|
153
|
+
|
|
154
|
+
async def _handle_interaction_create(self, data: InteractionPayload):
|
|
155
|
+
guild = self.bot.guilds.get(int(g)) if (g := data.get("guild_id")) else None
|
|
156
|
+
interaction = Interaction(self.bot.http, data, guild=guild)
|
|
157
|
+
match interaction.type:
|
|
158
|
+
case InteractionType.APPLICATION_COMMAND:
|
|
159
|
+
if interaction.data: await self._dispatch_commands(interaction.data.name, interaction)
|
|
160
|
+
|
|
161
|
+
await self._dispatch("on_interaction_create", interaction)
|
|
162
|
+
|
|
163
|
+
async def _handle_ready(self, _):
|
|
164
|
+
await self._dispatch("on_ready")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Any, Protocol
|
|
3
|
+
from collections.abc import Callable, Coroutine
|
|
4
|
+
|
|
5
|
+
class Missing:
|
|
6
|
+
__slots__ = ()
|
|
7
|
+
|
|
8
|
+
def __bool__(self) -> bool:
|
|
9
|
+
return False
|
|
10
|
+
|
|
11
|
+
_MISSING: Any = Missing()
|
|
12
|
+
|
|
13
|
+
type CoroFunc = Callable[..., Coroutine[Any, Any, Any]]
|
|
14
|
+
type CoroDecorator = Callable[[CoroFunc], CoroFunc]
|
|
15
|
+
|
|
16
|
+
class SupportsToDict(Protocol):
|
|
17
|
+
def _to_dict(self) -> Any: ...
|
|
18
|
+
|
|
19
|
+
def assign_val[T](obj: T, check_against: Any = _MISSING, /, **kwargs: Any) -> T:
|
|
20
|
+
for key, val in kwargs.items():
|
|
21
|
+
if val is not check_against: setattr(obj, key, val)
|
|
22
|
+
return obj
|
|
23
|
+
|
|
24
|
+
def mtd[T: SupportsToDict](obj: T | None) -> T | None:
|
|
25
|
+
return obj._to_dict() if obj is not None else None
|
|
26
|
+
|
|
27
|
+
def assign_val_dict[T](d: T, check_against: Any = None, /, **kwargs: Any) -> T:
|
|
28
|
+
for key, val in kwargs.items():
|
|
29
|
+
if val is not check_against: d[key] = val # type: ignore
|
|
30
|
+
return d
|
|
31
|
+
|
|
32
|
+
def sint(txt: str | None) -> int | None:
|
|
33
|
+
return int(txt) if txt else None
|
|
34
|
+
|
|
35
|
+
def siso(txt: str | None) -> datetime | None:
|
|
36
|
+
return datetime.fromisoformat(txt) if txt else None
|
|
37
|
+
|
|
38
|
+
def scls[C](cls: Callable[..., C], data: Any, **kwargs: Any) -> C | None:
|
|
39
|
+
return cls(data, **kwargs) if data else None
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import inspect
|
|
5
|
+
from typing import overload
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from .cache import CacheSettings, CacheStorage
|
|
10
|
+
from .enums.event_dispatch import Event
|
|
11
|
+
from .errors import ImproperToken, Unauthorized
|
|
12
|
+
from .flags import IntentFlags
|
|
13
|
+
from .gateway import GatewayClient
|
|
14
|
+
from .http import HTTPClient
|
|
15
|
+
from ._utils import _MISSING, CoroFunc, CoroDecorator
|
|
16
|
+
|
|
17
|
+
from .enums.command import ApplicationCommandType
|
|
18
|
+
from .enums.interaction import InteractionContextType, ApplicationIntegrationType
|
|
19
|
+
|
|
20
|
+
from .objects.command import PartialApplicationCommand, Localization, ApplicationCommandOption
|
|
21
|
+
from .objects.user import User
|
|
22
|
+
from .objects.permissions import Permissions
|
|
23
|
+
|
|
24
|
+
from .managers.channel import ChannelManager
|
|
25
|
+
from .managers.guild import GuildManager
|
|
26
|
+
from .managers.message import MessageManager
|
|
27
|
+
from .managers.user import UserManager
|
|
28
|
+
from .managers.command import CommandManager
|
|
29
|
+
|
|
30
|
+
__all__ = (
|
|
31
|
+
"Bot",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
_log = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
class Bot:
|
|
37
|
+
"""
|
|
38
|
+
Represents a Discord Bot.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
intents : :class:`IntentFlags <mizuki.flags.IntentFlags>`
|
|
43
|
+
The IntentFlags to be passed to the GatewayClient.
|
|
44
|
+
cache_settings : :class:`CacheSettings <mizuki.cache.CacheSettings>`, optional
|
|
45
|
+
The CacheSettings for managing the Cache System of the Bot instance. Defaults to ``CacheSettings()``
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
intents: IntentFlags
|
|
49
|
+
"The IntentFlags to be passed to the :class:`GatewayClient <mizuki.gateway.GatewayClient>`."
|
|
50
|
+
|
|
51
|
+
http: HTTPClient
|
|
52
|
+
"The HTTPClient used for the REST API."
|
|
53
|
+
|
|
54
|
+
gateway: GatewayClient
|
|
55
|
+
"The GatewayClient that manages the Gateway Connection."
|
|
56
|
+
|
|
57
|
+
users: UserManager
|
|
58
|
+
"The UserManager used to managers User objects."
|
|
59
|
+
|
|
60
|
+
messages: MessageManager
|
|
61
|
+
"The MessageManager used to manage Message objects."
|
|
62
|
+
|
|
63
|
+
channels: ChannelManager
|
|
64
|
+
"The ChannelManager used to manage Channel objects."
|
|
65
|
+
|
|
66
|
+
guilds: GuildManager
|
|
67
|
+
"The GuildManager used to manage Guild objects."
|
|
68
|
+
|
|
69
|
+
commands: CommandManager
|
|
70
|
+
"The CommandManager used to manage Commands."
|
|
71
|
+
|
|
72
|
+
user: User
|
|
73
|
+
"The User object of the bot."
|
|
74
|
+
|
|
75
|
+
__slots__ = (
|
|
76
|
+
"intents",
|
|
77
|
+
"http",
|
|
78
|
+
"gateway",
|
|
79
|
+
"_listeners",
|
|
80
|
+
"_setup_hook",
|
|
81
|
+
"_commands_data",
|
|
82
|
+
"_storage",
|
|
83
|
+
"users",
|
|
84
|
+
"messages",
|
|
85
|
+
"channels",
|
|
86
|
+
"guilds",
|
|
87
|
+
"commands",
|
|
88
|
+
"user",
|
|
89
|
+
"_session"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self, *,
|
|
94
|
+
intents: IntentFlags,
|
|
95
|
+
cache_settings: CacheSettings = CacheSettings()
|
|
96
|
+
):
|
|
97
|
+
self.intents = intents
|
|
98
|
+
self.http = HTTPClient()
|
|
99
|
+
self._listeners: dict[str, list[CoroFunc]] = {}
|
|
100
|
+
self._setup_hook: CoroFunc | None = None
|
|
101
|
+
self._commands_data: dict[str, tuple[int, PartialApplicationCommand]] = {}
|
|
102
|
+
|
|
103
|
+
self._storage = CacheStorage(cache_settings)
|
|
104
|
+
self.users = UserManager(self.http, self._storage)
|
|
105
|
+
self.messages = MessageManager(self.http, self._storage)
|
|
106
|
+
self.channels = ChannelManager(self.http, self._storage)
|
|
107
|
+
self.guilds = GuildManager(self.http, self._storage)
|
|
108
|
+
|
|
109
|
+
self._session: aiohttp.ClientSession | None = None
|
|
110
|
+
|
|
111
|
+
def run(self, token: str) -> None:
|
|
112
|
+
"""
|
|
113
|
+
A synchronous method to start a event loop and run the :meth:`Bot.start()` method.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
token : :class:`str`
|
|
118
|
+
The bot token used to authenticate with discord. Do not prefix this, the library will handle prefixing.
|
|
119
|
+
|
|
120
|
+
Raises
|
|
121
|
+
------
|
|
122
|
+
:class:`ImproperToken`
|
|
123
|
+
An improper token was passed.
|
|
124
|
+
"""
|
|
125
|
+
asyncio.run(self.start(token))
|
|
126
|
+
|
|
127
|
+
async def _verify_token(self) -> User:
|
|
128
|
+
try:
|
|
129
|
+
return await self.users.fetch_me()
|
|
130
|
+
except Unauthorized:
|
|
131
|
+
raise ImproperToken(401, "Improper token has been passed.")
|
|
132
|
+
|
|
133
|
+
async def start(self, token: str) -> None:
|
|
134
|
+
"""
|
|
135
|
+
Verifies the token and connects to the gateway.
|
|
136
|
+
|
|
137
|
+
Parameters
|
|
138
|
+
----------
|
|
139
|
+
token : :class:`str`
|
|
140
|
+
The bot token used to authenticate with discord. Do not prefix this, the library will handle prefixing.
|
|
141
|
+
|
|
142
|
+
Raises
|
|
143
|
+
------
|
|
144
|
+
:class:`ImproperToken`
|
|
145
|
+
An improper token was passed.
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
if self._storage.settings.cache_invalidation: self._storage.start_cleanup_tasks()
|
|
149
|
+
self._session = aiohttp.ClientSession(
|
|
150
|
+
"https://discord.com/api/v10/",
|
|
151
|
+
headers={
|
|
152
|
+
"Authorization": f"Bot {token}"
|
|
153
|
+
}
|
|
154
|
+
)
|
|
155
|
+
self.http._session = self._session
|
|
156
|
+
_log.debug("Attempting to verify token (length=%s)", len(token))
|
|
157
|
+
self.user = await self._verify_token()
|
|
158
|
+
_log.info("Verified token successfully.")
|
|
159
|
+
self.commands = CommandManager(self.http, self._storage, self.user.id, self._commands_data)
|
|
160
|
+
self.gateway = GatewayClient(self, self._session, token, self.intents)
|
|
161
|
+
await self.gateway.connect()
|
|
162
|
+
if self._setup_hook is not None:
|
|
163
|
+
await self._setup_hook()
|
|
164
|
+
await self.gateway.wait_until_closed()
|
|
165
|
+
except asyncio.CancelledError:
|
|
166
|
+
raise
|
|
167
|
+
finally:
|
|
168
|
+
await self.stop()
|
|
169
|
+
|
|
170
|
+
async def stop(self) -> None:
|
|
171
|
+
"""
|
|
172
|
+
Disconnects the gateway and closes the session.
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
if self.gateway:
|
|
176
|
+
await self.gateway.close()
|
|
177
|
+
finally:
|
|
178
|
+
if self._session:
|
|
179
|
+
await self._session.close()
|
|
180
|
+
|
|
181
|
+
def listen(self, event: Event | None = None) -> CoroDecorator:
|
|
182
|
+
"""
|
|
183
|
+
This function is a decotstor.
|
|
184
|
+
|
|
185
|
+
Registers an asynchronous listener for a gateway event.
|
|
186
|
+
|
|
187
|
+
Parameters
|
|
188
|
+
----------
|
|
189
|
+
event : :class:`Event <mizuki.enums.event_dispatch.Event>` | :class:`None`, optional
|
|
190
|
+
The Gateway Event to listen to. Defaults to name of function in format such as ``on_interaction_create``. Defaults to ``None``
|
|
191
|
+
|
|
192
|
+
Raises
|
|
193
|
+
------
|
|
194
|
+
:class:`TypeError`
|
|
195
|
+
The decorator was applied to a synchronous function.
|
|
196
|
+
|
|
197
|
+
Examples
|
|
198
|
+
--------
|
|
199
|
+
Registering based on the function name:
|
|
200
|
+
|
|
201
|
+
.. code-block:: python
|
|
202
|
+
|
|
203
|
+
@bot.listen()
|
|
204
|
+
async def on_message_create(message: mizuki.Message) -> None:
|
|
205
|
+
...
|
|
206
|
+
|
|
207
|
+
Explicitly passing event name:
|
|
208
|
+
|
|
209
|
+
.. code-block:: python
|
|
210
|
+
|
|
211
|
+
@bot.listen(mizuki.Event.MESSAGE_CREATE)
|
|
212
|
+
async def can_be_named_anything(message: mizuki.Message) -> None:
|
|
213
|
+
...
|
|
214
|
+
"""
|
|
215
|
+
def decorator(func: CoroFunc) -> CoroFunc:
|
|
216
|
+
if not inspect.iscoroutinefunction(func): raise TypeError(f"Event listener '{func.__name__}' has to be a coroutine function.")
|
|
217
|
+
self._listeners.setdefault(event.value if event is not None else func.__name__, []).append(func)
|
|
218
|
+
return func
|
|
219
|
+
return decorator
|
|
220
|
+
|
|
221
|
+
def setup(self) -> CoroDecorator:
|
|
222
|
+
"""
|
|
223
|
+
This function is a decorator.
|
|
224
|
+
|
|
225
|
+
Registers a setup hook which runs once after connecting to the gateway.
|
|
226
|
+
|
|
227
|
+
Raises
|
|
228
|
+
------
|
|
229
|
+
:class:`TypeError`
|
|
230
|
+
The decorator was applied to a synchronous function.
|
|
231
|
+
"""
|
|
232
|
+
def decorator(func: CoroFunc) -> CoroFunc:
|
|
233
|
+
if not inspect.iscoroutinefunction(func): raise TypeError(f"Setup hook '{func.__name__}' has to be a coroutine function.")
|
|
234
|
+
self._setup_hook = func
|
|
235
|
+
return func
|
|
236
|
+
return decorator
|
|
237
|
+
|
|
238
|
+
@overload
|
|
239
|
+
def command(
|
|
240
|
+
self, *,
|
|
241
|
+
guild_id: int,
|
|
242
|
+
name: str,
|
|
243
|
+
name_localizations: Localization = _MISSING,
|
|
244
|
+
description: str,
|
|
245
|
+
description_localizations: Localization = _MISSING,
|
|
246
|
+
default_member_permissions: Permissions = _MISSING,
|
|
247
|
+
nsfw: bool = False
|
|
248
|
+
) -> CoroDecorator: ...
|
|
249
|
+
|
|
250
|
+
@overload
|
|
251
|
+
def command(
|
|
252
|
+
self, *,
|
|
253
|
+
name: str,
|
|
254
|
+
name_localizations: Localization = _MISSING,
|
|
255
|
+
description: str,
|
|
256
|
+
description_localizations: Localization = _MISSING,
|
|
257
|
+
default_member_permissions: Permissions = _MISSING,
|
|
258
|
+
integration_types: list[ApplicationIntegrationType] = _MISSING,
|
|
259
|
+
contexts: list[InteractionContextType] = _MISSING,
|
|
260
|
+
nsfw: bool = False
|
|
261
|
+
) -> CoroDecorator: ...
|
|
262
|
+
|
|
263
|
+
def command(
|
|
264
|
+
self, *,
|
|
265
|
+
guild_id: int | None = None,
|
|
266
|
+
name: str,
|
|
267
|
+
name_localizations: Localization = _MISSING,
|
|
268
|
+
description: str,
|
|
269
|
+
description_localizations: Localization = _MISSING,
|
|
270
|
+
default_member_permissions: Permissions = _MISSING,
|
|
271
|
+
integration_types: list[ApplicationIntegrationType] = _MISSING,
|
|
272
|
+
contexts: list[InteractionContextType] = _MISSING,
|
|
273
|
+
nsfw: bool = False
|
|
274
|
+
) -> CoroDecorator:
|
|
275
|
+
"""
|
|
276
|
+
This function is a decorator.
|
|
277
|
+
|
|
278
|
+
Registers a command callback for a slash (application) command.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
name : :class:`str`
|
|
283
|
+
The name of the application command.
|
|
284
|
+
description : :class:`str`, optional
|
|
285
|
+
The description of the application command.
|
|
286
|
+
|
|
287
|
+
Raises
|
|
288
|
+
------
|
|
289
|
+
:class:`TypeError`
|
|
290
|
+
The decorator was applied to a synchronous function.
|
|
291
|
+
"""
|
|
292
|
+
def decorator(func: CoroFunc) -> CoroFunc:
|
|
293
|
+
if not inspect.iscoroutinefunction(func): raise TypeError(f"Command callback for '{name}:{func.__name__}' has to be a coroutine function.")
|
|
294
|
+
|
|
295
|
+
self._commands_data[name] = guild_id or 0, PartialApplicationCommand._from_command(
|
|
296
|
+
func,
|
|
297
|
+
name=name,
|
|
298
|
+
name_localizations=name_localizations,
|
|
299
|
+
description=description,
|
|
300
|
+
description_localizations=description_localizations,
|
|
301
|
+
default_member_permissions=default_member_permissions,
|
|
302
|
+
integration_types=integration_types,
|
|
303
|
+
contexts=contexts,
|
|
304
|
+
type=ApplicationCommandType.CHAT_INPUT,
|
|
305
|
+
nsfw=nsfw
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
return func
|
|
309
|
+
|
|
310
|
+
return decorator
|