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.
Files changed (73) hide show
  1. mizuki-0.1.0/LICENSE +21 -0
  2. mizuki-0.1.0/PKG-INFO +67 -0
  3. mizuki-0.1.0/README.md +51 -0
  4. mizuki-0.1.0/mizuki/__init__.py +11 -0
  5. mizuki-0.1.0/mizuki/_event_dispatch.py +164 -0
  6. mizuki-0.1.0/mizuki/_utils.py +39 -0
  7. mizuki-0.1.0/mizuki/bot.py +310 -0
  8. mizuki-0.1.0/mizuki/cache.py +290 -0
  9. mizuki-0.1.0/mizuki/enums/__init__.py +10 -0
  10. mizuki-0.1.0/mizuki/enums/channel.py +41 -0
  11. mizuki-0.1.0/mizuki/enums/command.py +30 -0
  12. mizuki-0.1.0/mizuki/enums/embed.py +14 -0
  13. mizuki-0.1.0/mizuki/enums/event_dispatch.py +43 -0
  14. mizuki-0.1.0/mizuki/enums/guild.py +122 -0
  15. mizuki-0.1.0/mizuki/enums/interaction.py +33 -0
  16. mizuki-0.1.0/mizuki/enums/message.py +64 -0
  17. mizuki-0.1.0/mizuki/enums/presence.py +26 -0
  18. mizuki-0.1.0/mizuki/enums/sticker.py +24 -0
  19. mizuki-0.1.0/mizuki/enums/user.py +11 -0
  20. mizuki-0.1.0/mizuki/errors.py +90 -0
  21. mizuki-0.1.0/mizuki/flags.py +140 -0
  22. mizuki-0.1.0/mizuki/gateway.py +280 -0
  23. mizuki-0.1.0/mizuki/http.py +198 -0
  24. mizuki-0.1.0/mizuki/managers/__init__.py +5 -0
  25. mizuki-0.1.0/mizuki/managers/_types.py +12 -0
  26. mizuki-0.1.0/mizuki/managers/channel.py +106 -0
  27. mizuki-0.1.0/mizuki/managers/command.py +382 -0
  28. mizuki-0.1.0/mizuki/managers/guild.py +89 -0
  29. mizuki-0.1.0/mizuki/managers/message.py +90 -0
  30. mizuki-0.1.0/mizuki/managers/user.py +103 -0
  31. mizuki-0.1.0/mizuki/objects/__init__.py +17 -0
  32. mizuki-0.1.0/mizuki/objects/asset.py +223 -0
  33. mizuki-0.1.0/mizuki/objects/avatar_decoration.py +36 -0
  34. mizuki-0.1.0/mizuki/objects/channel.py +553 -0
  35. mizuki-0.1.0/mizuki/objects/collectibles.py +35 -0
  36. mizuki-0.1.0/mizuki/objects/command.py +486 -0
  37. mizuki-0.1.0/mizuki/objects/embed.py +264 -0
  38. mizuki-0.1.0/mizuki/objects/emoji.py +127 -0
  39. mizuki-0.1.0/mizuki/objects/guild.py +276 -0
  40. mizuki-0.1.0/mizuki/objects/interaction.py +376 -0
  41. mizuki-0.1.0/mizuki/objects/member.py +103 -0
  42. mizuki-0.1.0/mizuki/objects/message.py +342 -0
  43. mizuki-0.1.0/mizuki/objects/permissions.py +69 -0
  44. mizuki-0.1.0/mizuki/objects/presence.py +149 -0
  45. mizuki-0.1.0/mizuki/objects/primary_guild.py +26 -0
  46. mizuki-0.1.0/mizuki/objects/role.py +68 -0
  47. mizuki-0.1.0/mizuki/objects/snowflake.py +29 -0
  48. mizuki-0.1.0/mizuki/objects/sticker.py +61 -0
  49. mizuki-0.1.0/mizuki/objects/user.py +90 -0
  50. mizuki-0.1.0/mizuki/parameter.py +158 -0
  51. mizuki-0.1.0/mizuki/payloads/_types.py +8 -0
  52. mizuki-0.1.0/mizuki/payloads/avatar_decoration.py +6 -0
  53. mizuki-0.1.0/mizuki/payloads/channel.py +106 -0
  54. mizuki-0.1.0/mizuki/payloads/collectibles.py +11 -0
  55. mizuki-0.1.0/mizuki/payloads/command.py +83 -0
  56. mizuki-0.1.0/mizuki/payloads/embed.py +54 -0
  57. mizuki-0.1.0/mizuki/payloads/emoji.py +23 -0
  58. mizuki-0.1.0/mizuki/payloads/guild.py +112 -0
  59. mizuki-0.1.0/mizuki/payloads/interaction.py +88 -0
  60. mizuki-0.1.0/mizuki/payloads/member.py +25 -0
  61. mizuki-0.1.0/mizuki/payloads/message.py +148 -0
  62. mizuki-0.1.0/mizuki/payloads/presence.py +59 -0
  63. mizuki-0.1.0/mizuki/payloads/primary_guild.py +8 -0
  64. mizuki-0.1.0/mizuki/payloads/role.py +29 -0
  65. mizuki-0.1.0/mizuki/payloads/sticker.py +18 -0
  66. mizuki-0.1.0/mizuki/payloads/user.py +33 -0
  67. mizuki-0.1.0/mizuki.egg-info/PKG-INFO +67 -0
  68. mizuki-0.1.0/mizuki.egg-info/SOURCES.txt +71 -0
  69. mizuki-0.1.0/mizuki.egg-info/dependency_links.txt +1 -0
  70. mizuki-0.1.0/mizuki.egg-info/requires.txt +6 -0
  71. mizuki-0.1.0/mizuki.egg-info/top_level.txt +1 -0
  72. mizuki-0.1.0/pyproject.toml +20 -0
  73. 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,11 @@
1
+ from .cache import *
2
+ from .objects import *
3
+ from .flags import *
4
+ from .bot import *
5
+ from .parameter import *
6
+ from .enums import *
7
+ from .errors import *
8
+
9
+ import logging
10
+
11
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
@@ -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