scurrypy 0.3.2__tar.gz → 0.7.2__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.
- scurrypy-0.7.2/LICENSE +16 -0
- scurrypy-0.7.2/PKG-INFO +114 -0
- scurrypy-0.7.2/README.md +102 -0
- scurrypy-0.7.2/pyproject.toml +27 -0
- scurrypy-0.7.2/scurrypy/__init__.py +16 -0
- scurrypy-0.7.2/scurrypy/client.py +335 -0
- scurrypy-0.7.2/scurrypy/core/__init__.py +16 -0
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/client_like.py +8 -1
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/error.py +6 -18
- scurrypy-0.7.2/scurrypy/core/gateway.py +183 -0
- scurrypy-0.7.2/scurrypy/core/http.py +310 -0
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/intents.py +5 -7
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/logger.py +14 -60
- scurrypy-0.7.2/scurrypy/core/model.py +71 -0
- scurrypy-0.7.2/scurrypy/dispatch/__init__.py +7 -0
- scurrypy-0.7.2/scurrypy/dispatch/command_dispatcher.py +205 -0
- scurrypy-0.7.2/scurrypy/dispatch/event_dispatcher.py +99 -0
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/dispatch/prefix_dispatcher.py +32 -13
- scurrypy-0.7.2/scurrypy/events/__init__.py +62 -0
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/channel_events.py +9 -2
- scurrypy-0.7.2/scurrypy/events/gateway_events.py +31 -0
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/guild_events.py +24 -1
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/hello_event.py +1 -1
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/interaction_events.py +35 -18
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/message_events.py +12 -8
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/reaction_events.py +6 -6
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/events/ready_event.py +2 -4
- scurrypy-0.7.2/scurrypy/models/__init__.py +15 -0
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/models/emoji.py +18 -2
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/models/guild.py +5 -3
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/models/interaction.py +6 -1
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/models/role.py +2 -1
- scurrypy-0.7.2/scurrypy/models/user.py +95 -0
- scurrypy-0.7.2/scurrypy/parts/__init__.py +79 -0
- scurrypy-0.7.2/scurrypy/parts/channel.py +42 -0
- scurrypy-0.7.2/scurrypy/parts/command.py +90 -0
- scurrypy-0.7.2/scurrypy/parts/components.py +224 -0
- scurrypy-0.7.2/scurrypy/parts/components_v2.py +144 -0
- scurrypy-0.7.2/scurrypy/parts/embed.py +83 -0
- scurrypy-0.7.2/scurrypy/parts/message.py +134 -0
- scurrypy-0.7.2/scurrypy/parts/modal.py +16 -0
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/parts/role.py +2 -14
- scurrypy-0.7.2/scurrypy/resources/__init__.py +45 -0
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/application.py +3 -4
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/bot_emojis.py +2 -2
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/channel.py +11 -10
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/guild.py +16 -18
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/interaction.py +59 -38
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/message.py +25 -18
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/resources/user.py +5 -6
- scurrypy-0.7.2/scurrypy.egg-info/PKG-INFO +114 -0
- scurrypy-0.7.2/scurrypy.egg-info/SOURCES.txt +56 -0
- scurrypy-0.7.2/scurrypy.egg-info/requires.txt +2 -0
- scurrypy-0.7.2/scurrypy.egg-info/top_level.txt +1 -0
- scurrypy-0.3.2/LICENSE +0 -5
- scurrypy-0.3.2/PKG-INFO +0 -85
- scurrypy-0.3.2/README.md +0 -75
- scurrypy-0.3.2/discord/__init__.py +0 -10
- scurrypy-0.3.2/discord/client.py +0 -349
- scurrypy-0.3.2/discord/dispatch/__init__.py +0 -1
- scurrypy-0.3.2/discord/dispatch/command_dispatcher.py +0 -163
- scurrypy-0.3.2/discord/dispatch/event_dispatcher.py +0 -91
- scurrypy-0.3.2/discord/events/__init__.py +0 -33
- scurrypy-0.3.2/discord/gateway.py +0 -175
- scurrypy-0.3.2/discord/http.py +0 -292
- scurrypy-0.3.2/discord/model.py +0 -90
- scurrypy-0.3.2/discord/models/__init__.py +0 -8
- scurrypy-0.3.2/discord/models/application.py +0 -37
- scurrypy-0.3.2/discord/models/integration.py +0 -23
- scurrypy-0.3.2/discord/models/member.py +0 -27
- scurrypy-0.3.2/discord/models/user.py +0 -15
- scurrypy-0.3.2/discord/parts/__init__.py +0 -28
- scurrypy-0.3.2/discord/parts/action_row.py +0 -257
- scurrypy-0.3.2/discord/parts/attachment.py +0 -18
- scurrypy-0.3.2/discord/parts/channel.py +0 -20
- scurrypy-0.3.2/discord/parts/command.py +0 -102
- scurrypy-0.3.2/discord/parts/components_v2.py +0 -270
- scurrypy-0.3.2/discord/parts/embed.py +0 -154
- scurrypy-0.3.2/discord/parts/message.py +0 -179
- scurrypy-0.3.2/discord/parts/modal.py +0 -21
- scurrypy-0.3.2/discord/resources/__init__.py +0 -10
- scurrypy-0.3.2/pyproject.toml +0 -23
- scurrypy-0.3.2/scurrypy.egg-info/PKG-INFO +0 -85
- scurrypy-0.3.2/scurrypy.egg-info/SOURCES.txt +0 -57
- scurrypy-0.3.2/scurrypy.egg-info/top_level.txt +0 -1
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy/core}/config.py +0 -0
- {scurrypy-0.3.2/discord → scurrypy-0.7.2/scurrypy}/parts/component_types.py +0 -0
- {scurrypy-0.3.2 → scurrypy-0.7.2}/scurrypy.egg-info/dependency_links.txt +0 -0
- {scurrypy-0.3.2 → scurrypy-0.7.2}/setup.cfg +0 -0
scurrypy-0.7.2/LICENSE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Copyright (c) 2025 Furmissile. All rights reserved.
|
|
2
|
+
|
|
3
|
+
Permission is granted to view, use, modify, and distribute copies of this software
|
|
4
|
+
and its source code, provided that:
|
|
5
|
+
|
|
6
|
+
1. Attribution to the original author, Furmissile, is preserved in all copies and
|
|
7
|
+
derivative works.
|
|
8
|
+
2. The name "ScurryPy" and associated branding may not be used to promote derived
|
|
9
|
+
projects without explicit permission.
|
|
10
|
+
3. This license and copyright notice must be included in all copies or substantial
|
|
11
|
+
portions of the software.
|
|
12
|
+
4. This software is provided "as is", without warranty of any kind, express or
|
|
13
|
+
implied. The author assumes no liability for any damages arising from its use.
|
|
14
|
+
|
|
15
|
+
5. This software may not be used for commercial purposes without written consent
|
|
16
|
+
from the author.
|
scurrypy-0.7.2/PKG-INFO
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: scurrypy
|
|
3
|
+
Version: 0.7.2
|
|
4
|
+
Summary: Dataclass-driven Discord API Wrapper in Python
|
|
5
|
+
Author: Furmissile
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
10
|
+
Requires-Dist: websockets>=11.0.0
|
|
11
|
+
Dynamic: license-file
|
|
12
|
+
|
|
13
|
+
## __<center> ScurryPy </center>__
|
|
14
|
+
|
|
15
|
+
[](https://badge.fury.io/py/scurrypy)
|
|
16
|
+
|
|
17
|
+
A lightweight, fully readable Discord API framework built to accommodate everything from basic bots to custom frameworks.
|
|
18
|
+
|
|
19
|
+
While ScurryPy powers many squirrel-related shenanigans, it works just as well for game bots, interactive components, and educational projects.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
* Easy to extend and build frameworks on top
|
|
23
|
+
* Lightweight core (<1000 lines)
|
|
24
|
+
* Command, and event handling
|
|
25
|
+
* Unix shell-style wildcards for component routing
|
|
26
|
+
* Declarative style using decorators
|
|
27
|
+
* Supports both legacy and new features
|
|
28
|
+
* Respects Discord's rate limits
|
|
29
|
+
* No `__future__` hacks to avoid circular import
|
|
30
|
+
* Capable of sharding
|
|
31
|
+
|
|
32
|
+
## Getting Started
|
|
33
|
+
|
|
34
|
+
*Note: This section also appears in the documentation, but here are complete examples ready to use with your bot credentials.*
|
|
35
|
+
|
|
36
|
+
### Installation
|
|
37
|
+
|
|
38
|
+
To install the ScurryPy package, run:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install scurrypy
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Minimal Slash Command
|
|
45
|
+
|
|
46
|
+
The following demonstrates building and responding to a slash command.
|
|
47
|
+
|
|
48
|
+
```py
|
|
49
|
+
import scurrypy
|
|
50
|
+
|
|
51
|
+
client = scurrypy.Client(
|
|
52
|
+
token='your-token',
|
|
53
|
+
application_id=APPLICATION_ID # your bot's application ID
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
@client.command(
|
|
57
|
+
scurrypy.SlashCommand('example', 'Demonstrate the minimal slash command!'),
|
|
58
|
+
GUILD_ID # must be a guild ID your bot is in
|
|
59
|
+
)
|
|
60
|
+
async def example(bot: scurrypy.Client, event: scurrypy.InteractionEvent):
|
|
61
|
+
await event.interaction.respond(f'Hello, {event.interaction.member.user.username}!')
|
|
62
|
+
|
|
63
|
+
client.run()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Minimal Prefix Command (Legacy)
|
|
67
|
+
|
|
68
|
+
The following demonstrates building and responding to a message prefix command.
|
|
69
|
+
|
|
70
|
+
```py
|
|
71
|
+
import scurrypy
|
|
72
|
+
|
|
73
|
+
client = scurrypy.Client(
|
|
74
|
+
token='your-token',
|
|
75
|
+
application_id=APPLICATION_ID, # your bot's application ID
|
|
76
|
+
intents=scurrypy.set_intents(message_content=True),
|
|
77
|
+
prefix='!' # your custom prefix
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@client.prefix_command("ping")
|
|
81
|
+
async def on_ping(bot: scurrypy.Client, event: scurrypy.MessageCreateEvent):
|
|
82
|
+
await event.message.send("Pong!")
|
|
83
|
+
|
|
84
|
+
client.run()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Building on Top of ScurryPy
|
|
88
|
+
|
|
89
|
+
ScurryPy is designed to be easy to extend with your own abstractions.
|
|
90
|
+
|
|
91
|
+
The following demonstrates integrating a custom cache into your client configuration:
|
|
92
|
+
|
|
93
|
+
```py
|
|
94
|
+
class CacheProtocol(Protocol):
|
|
95
|
+
async def get_user(self, user_id: int) ...
|
|
96
|
+
|
|
97
|
+
# and the rest...
|
|
98
|
+
|
|
99
|
+
class MyCache(CacheProtocol):
|
|
100
|
+
# your implementation...
|
|
101
|
+
|
|
102
|
+
class MyConfig(BaseConfig):
|
|
103
|
+
cache: MyCache
|
|
104
|
+
# other stuff here...
|
|
105
|
+
|
|
106
|
+
client = scurrypy.Client(
|
|
107
|
+
token = 'your-token',
|
|
108
|
+
application_id = 123456789012345,
|
|
109
|
+
config = MyConfig()
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Like What You See?
|
|
114
|
+
Explore the full [documentation](https://furmissile.github.io/scurrypy) for more examples, guides, and API reference.
|
scurrypy-0.7.2/README.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
## __<center> ScurryPy </center>__
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/scurrypy)
|
|
4
|
+
|
|
5
|
+
A lightweight, fully readable Discord API framework built to accommodate everything from basic bots to custom frameworks.
|
|
6
|
+
|
|
7
|
+
While ScurryPy powers many squirrel-related shenanigans, it works just as well for game bots, interactive components, and educational projects.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
* Easy to extend and build frameworks on top
|
|
11
|
+
* Lightweight core (<1000 lines)
|
|
12
|
+
* Command, and event handling
|
|
13
|
+
* Unix shell-style wildcards for component routing
|
|
14
|
+
* Declarative style using decorators
|
|
15
|
+
* Supports both legacy and new features
|
|
16
|
+
* Respects Discord's rate limits
|
|
17
|
+
* No `__future__` hacks to avoid circular import
|
|
18
|
+
* Capable of sharding
|
|
19
|
+
|
|
20
|
+
## Getting Started
|
|
21
|
+
|
|
22
|
+
*Note: This section also appears in the documentation, but here are complete examples ready to use with your bot credentials.*
|
|
23
|
+
|
|
24
|
+
### Installation
|
|
25
|
+
|
|
26
|
+
To install the ScurryPy package, run:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install scurrypy
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Minimal Slash Command
|
|
33
|
+
|
|
34
|
+
The following demonstrates building and responding to a slash command.
|
|
35
|
+
|
|
36
|
+
```py
|
|
37
|
+
import scurrypy
|
|
38
|
+
|
|
39
|
+
client = scurrypy.Client(
|
|
40
|
+
token='your-token',
|
|
41
|
+
application_id=APPLICATION_ID # your bot's application ID
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@client.command(
|
|
45
|
+
scurrypy.SlashCommand('example', 'Demonstrate the minimal slash command!'),
|
|
46
|
+
GUILD_ID # must be a guild ID your bot is in
|
|
47
|
+
)
|
|
48
|
+
async def example(bot: scurrypy.Client, event: scurrypy.InteractionEvent):
|
|
49
|
+
await event.interaction.respond(f'Hello, {event.interaction.member.user.username}!')
|
|
50
|
+
|
|
51
|
+
client.run()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Minimal Prefix Command (Legacy)
|
|
55
|
+
|
|
56
|
+
The following demonstrates building and responding to a message prefix command.
|
|
57
|
+
|
|
58
|
+
```py
|
|
59
|
+
import scurrypy
|
|
60
|
+
|
|
61
|
+
client = scurrypy.Client(
|
|
62
|
+
token='your-token',
|
|
63
|
+
application_id=APPLICATION_ID, # your bot's application ID
|
|
64
|
+
intents=scurrypy.set_intents(message_content=True),
|
|
65
|
+
prefix='!' # your custom prefix
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@client.prefix_command("ping")
|
|
69
|
+
async def on_ping(bot: scurrypy.Client, event: scurrypy.MessageCreateEvent):
|
|
70
|
+
await event.message.send("Pong!")
|
|
71
|
+
|
|
72
|
+
client.run()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Building on Top of ScurryPy
|
|
76
|
+
|
|
77
|
+
ScurryPy is designed to be easy to extend with your own abstractions.
|
|
78
|
+
|
|
79
|
+
The following demonstrates integrating a custom cache into your client configuration:
|
|
80
|
+
|
|
81
|
+
```py
|
|
82
|
+
class CacheProtocol(Protocol):
|
|
83
|
+
async def get_user(self, user_id: int) ...
|
|
84
|
+
|
|
85
|
+
# and the rest...
|
|
86
|
+
|
|
87
|
+
class MyCache(CacheProtocol):
|
|
88
|
+
# your implementation...
|
|
89
|
+
|
|
90
|
+
class MyConfig(BaseConfig):
|
|
91
|
+
cache: MyCache
|
|
92
|
+
# other stuff here...
|
|
93
|
+
|
|
94
|
+
client = scurrypy.Client(
|
|
95
|
+
token = 'your-token',
|
|
96
|
+
application_id = 123456789012345,
|
|
97
|
+
config = MyConfig()
|
|
98
|
+
)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Like What You See?
|
|
102
|
+
Explore the full [documentation](https://furmissile.github.io/scurrypy) for more examples, guides, and API reference.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "scurrypy"
|
|
7
|
+
version = "0.7.2"
|
|
8
|
+
|
|
9
|
+
description = "Dataclass-driven Discord API Wrapper in Python"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
authors = [{ name = "Furmissile" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"aiohttp>=3.8.0",
|
|
15
|
+
"websockets>=11.0.0"
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.setuptools]
|
|
19
|
+
packages = [
|
|
20
|
+
"scurrypy",
|
|
21
|
+
"scurrypy.dispatch",
|
|
22
|
+
"scurrypy.events",
|
|
23
|
+
"scurrypy.resources",
|
|
24
|
+
"scurrypy.parts",
|
|
25
|
+
"scurrypy.core",
|
|
26
|
+
"scurrypy.models"
|
|
27
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# scurrypy
|
|
2
|
+
|
|
3
|
+
from .client import Client
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
# top-level modules
|
|
7
|
+
"Client"
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
# imports listed __all__ libs
|
|
11
|
+
from .events import *
|
|
12
|
+
from .parts import *
|
|
13
|
+
from .resources import *
|
|
14
|
+
from .dispatch import *
|
|
15
|
+
from .models import *
|
|
16
|
+
from .core import *
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from .core.config import BaseConfig
|
|
4
|
+
from .core.intents import Intents
|
|
5
|
+
from .core.gateway import GatewayClient
|
|
6
|
+
from .core.client_like import ClientLike
|
|
7
|
+
|
|
8
|
+
from .parts.command import SlashCommand, MessageCommand, UserCommand
|
|
9
|
+
|
|
10
|
+
class Client(ClientLike):
|
|
11
|
+
"""Main entry point for Discord bots.
|
|
12
|
+
Ties together the moving parts: gateway, HTTP, event dispatching, command handling, and resource managers.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self,
|
|
15
|
+
*,
|
|
16
|
+
token: str,
|
|
17
|
+
application_id: int,
|
|
18
|
+
intents: int = Intents.DEFAULT,
|
|
19
|
+
config: BaseConfig = None,
|
|
20
|
+
debug_mode: bool = False,
|
|
21
|
+
sync_commands: bool = True,
|
|
22
|
+
prefix = None,
|
|
23
|
+
quiet: bool = False
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
Args:
|
|
27
|
+
token (str): the bot's token
|
|
28
|
+
application_id (int): the bot's user ID
|
|
29
|
+
intents (int, optional): gateway intents. Defaults to Intents.DEFAULT.
|
|
30
|
+
config (BaseConfig, optional): user-defined config data
|
|
31
|
+
sync_commands (bool, optional): toggle registering commands. Defaults to True.
|
|
32
|
+
debug_mode (bool, optional): toggle debug messages. Defaults to False.
|
|
33
|
+
prefix (str, optional): set message prefix if using command prefixes
|
|
34
|
+
quiet (bool, optional): if INFO, DEBUG, and WARN should be logged
|
|
35
|
+
"""
|
|
36
|
+
if not token:
|
|
37
|
+
raise ValueError("Token is required")
|
|
38
|
+
if not application_id:
|
|
39
|
+
raise ValueError("Application ID is required")
|
|
40
|
+
|
|
41
|
+
from .core.logger import Logger
|
|
42
|
+
from .core.http import HTTPClient
|
|
43
|
+
from .resources.bot_emojis import BotEmojis
|
|
44
|
+
from .dispatch.event_dispatcher import EventDispatcher
|
|
45
|
+
from .dispatch.prefix_dispatcher import PrefixDispatcher
|
|
46
|
+
from .dispatch.command_dispatcher import CommandDispatcher
|
|
47
|
+
|
|
48
|
+
self.token = token
|
|
49
|
+
self.intents = intents
|
|
50
|
+
self.application_id = application_id
|
|
51
|
+
self.config = config
|
|
52
|
+
self.sync_commands = sync_commands
|
|
53
|
+
|
|
54
|
+
self._logger = Logger(debug_mode, quiet)
|
|
55
|
+
|
|
56
|
+
self._http = HTTPClient(self._logger)
|
|
57
|
+
|
|
58
|
+
self.shards: list[GatewayClient] = []
|
|
59
|
+
self.dispatcher = EventDispatcher(self)
|
|
60
|
+
self.prefix_dispatcher = PrefixDispatcher(self, prefix)
|
|
61
|
+
self.command_dispatcher = CommandDispatcher(self)
|
|
62
|
+
|
|
63
|
+
self._setup_hooks = []
|
|
64
|
+
self._shutdown_hooks = []
|
|
65
|
+
|
|
66
|
+
self.emojis = BotEmojis(self._http, self.application_id)
|
|
67
|
+
|
|
68
|
+
def prefix_command(self, name: str):
|
|
69
|
+
"""Decorator registers prefix commands by the name of the function.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name (str): name of the command
|
|
73
|
+
!!! warning "Important"
|
|
74
|
+
Prefix commands are CASE-INSENSITIVE.
|
|
75
|
+
"""
|
|
76
|
+
def decorator(func):
|
|
77
|
+
self.prefix_dispatcher.register(name.lower(), func)
|
|
78
|
+
return func
|
|
79
|
+
return decorator
|
|
80
|
+
|
|
81
|
+
def component(self, custom_id: str):
|
|
82
|
+
"""Decorator registers a function for a component handler.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
custom_id (str): Identifier of the component. Must match the `custom_id` set where the component was created.
|
|
86
|
+
"""
|
|
87
|
+
def decorator(func):
|
|
88
|
+
self.command_dispatcher.component(func, custom_id)
|
|
89
|
+
return func
|
|
90
|
+
return decorator
|
|
91
|
+
|
|
92
|
+
def command(self, command: SlashCommand | MessageCommand | UserCommand, guild_ids: list[int] = None):
|
|
93
|
+
"""Decorator registers a function to a command handler.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
command (SlashCommand | MessageCommand | UserCommand): the command object
|
|
97
|
+
guild_ids (list[int], optional): Guild IDs to register command to (if any). If omitted, the command is **global**.
|
|
98
|
+
"""
|
|
99
|
+
def decorator(func):
|
|
100
|
+
if not isinstance(command, (SlashCommand, MessageCommand, UserCommand)):
|
|
101
|
+
raise ValueError(f"Expected SlashCommand, MessageCommand, or UserCommand; got {type(command).__name__}")
|
|
102
|
+
|
|
103
|
+
# maps command type -> command registry
|
|
104
|
+
handler_map = {
|
|
105
|
+
SlashCommand: self.command_dispatcher.add_slash_command,
|
|
106
|
+
MessageCommand: self.command_dispatcher.add_message_command,
|
|
107
|
+
UserCommand: self.command_dispatcher.add_user_command
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# can guarantee at this point command is one of SlashCommand | MessageCommand | UserCommand
|
|
111
|
+
handler = handler_map[type(command)]
|
|
112
|
+
|
|
113
|
+
handler(command, func, guild_ids)
|
|
114
|
+
return func
|
|
115
|
+
return decorator
|
|
116
|
+
|
|
117
|
+
def event(self, event_name: str):
|
|
118
|
+
"""Decorator registers a function for an event handler.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
event_name (str): event name (must be a valid event)
|
|
122
|
+
"""
|
|
123
|
+
def decorator(func):
|
|
124
|
+
self.dispatcher.register(event_name, func)
|
|
125
|
+
return func
|
|
126
|
+
return decorator
|
|
127
|
+
|
|
128
|
+
def setup_hook(self, func):
|
|
129
|
+
"""Decorator registers a setup hook.
|
|
130
|
+
(Runs once before the bot starts listening)
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
func (callable): callback to the setup function
|
|
134
|
+
"""
|
|
135
|
+
self._setup_hooks.append(func)
|
|
136
|
+
|
|
137
|
+
def shutdown_hook(self, func):
|
|
138
|
+
"""Decorator registers a shutdown hook.
|
|
139
|
+
(Runs once before the bot exits the loop)
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
func (callable): callback to the shutdown function
|
|
143
|
+
"""
|
|
144
|
+
self._shutdown_hooks.append(func)
|
|
145
|
+
|
|
146
|
+
def fetch_application(self, application_id: int):
|
|
147
|
+
"""Creates an interactable application resource.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
application_id (int): ID of target application
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
(Application): the Application resource
|
|
154
|
+
"""
|
|
155
|
+
from .resources.application import Application
|
|
156
|
+
|
|
157
|
+
return Application(application_id, self._http)
|
|
158
|
+
|
|
159
|
+
def fetch_guild(self, guild_id: int):
|
|
160
|
+
"""Creates an interactable guild resource.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
guild_id (int): ID of target guild
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
(Guild): the Guild resource
|
|
167
|
+
"""
|
|
168
|
+
from .resources.guild import Guild
|
|
169
|
+
|
|
170
|
+
return Guild(guild_id, self._http)
|
|
171
|
+
|
|
172
|
+
def fetch_channel(self, channel_id: int):
|
|
173
|
+
"""Creates an interactable channel resource.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
channel_id (int): ID of target channel
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
(Channel): the Channel resource
|
|
180
|
+
"""
|
|
181
|
+
from .resources.channel import Channel
|
|
182
|
+
|
|
183
|
+
return Channel(channel_id, self._http)
|
|
184
|
+
|
|
185
|
+
def fetch_message(self, channel_id: int, message_id: int):
|
|
186
|
+
"""Creates an interactable message resource.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
message_id (int): ID of target message
|
|
190
|
+
channel_id (int): channel ID of target message
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
(Message): the Message resource
|
|
194
|
+
"""
|
|
195
|
+
from .resources.message import Message
|
|
196
|
+
|
|
197
|
+
return Message(message_id, channel_id, self._http)
|
|
198
|
+
|
|
199
|
+
def fetch_user(self, user_id: int):
|
|
200
|
+
"""Creates an interactable user resource.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
user_id (int): ID of target user
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
(User): the User resource
|
|
207
|
+
"""
|
|
208
|
+
from .resources.user import User
|
|
209
|
+
|
|
210
|
+
return User(user_id, self._http)
|
|
211
|
+
|
|
212
|
+
async def clear_commands(self, guild_ids: list[int] = None):
|
|
213
|
+
"""Clear a guild's or global commands (all types).
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
guild_ids (list[int]): ID of the target guild. If omitted, **global** commands will be cleared.
|
|
217
|
+
"""
|
|
218
|
+
self.command_dispatcher.clear_commands(guild_ids)
|
|
219
|
+
|
|
220
|
+
async def _start_shards(self):
|
|
221
|
+
"""Starts all shards batching by max_concurrency."""
|
|
222
|
+
|
|
223
|
+
from .events.gateway_events import GatewayEvent
|
|
224
|
+
|
|
225
|
+
data = await self._http.request('GET', '/gateway/bot')
|
|
226
|
+
|
|
227
|
+
gateway = GatewayEvent.from_dict(data)
|
|
228
|
+
|
|
229
|
+
# pull important values for easier access
|
|
230
|
+
total_shards = gateway.shards
|
|
231
|
+
batch_size = gateway.session_start_limit.max_concurrency
|
|
232
|
+
|
|
233
|
+
tasks = []
|
|
234
|
+
|
|
235
|
+
for batch_start in range(0, total_shards, batch_size):
|
|
236
|
+
batch_end = min(batch_start + batch_size, total_shards)
|
|
237
|
+
|
|
238
|
+
self._logger.log_info(f"Starting shards {batch_start}-{batch_end - 1} of {total_shards}")
|
|
239
|
+
|
|
240
|
+
for shard_id in range(batch_start, batch_end):
|
|
241
|
+
shard = GatewayClient(self, gateway.url, shard_id, total_shards)
|
|
242
|
+
self.shards.append(shard)
|
|
243
|
+
|
|
244
|
+
# fire and forget
|
|
245
|
+
tasks.append(asyncio.create_task(shard.start()))
|
|
246
|
+
tasks.append(asyncio.create_task(self._listen_shard(shard)))
|
|
247
|
+
|
|
248
|
+
# wait before next batch to respect identify rate limit
|
|
249
|
+
await asyncio.sleep(5)
|
|
250
|
+
|
|
251
|
+
return tasks
|
|
252
|
+
|
|
253
|
+
async def _listen_shard(self, shard: GatewayClient):
|
|
254
|
+
"""Listen to websocket queue for events. Only OP code 0 passes!
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
shard (GatewayClient): the shard or gateway to listen on
|
|
258
|
+
"""
|
|
259
|
+
while True:
|
|
260
|
+
try:
|
|
261
|
+
dispatch_type, event_data = await shard.event_queue.get()
|
|
262
|
+
|
|
263
|
+
# check prefix first (only if a prefix is set)
|
|
264
|
+
if self.prefix_dispatcher.prefix and dispatch_type == 'MESSAGE_CREATE':
|
|
265
|
+
await self.prefix_dispatcher.dispatch(event_data)
|
|
266
|
+
|
|
267
|
+
# then try interaction
|
|
268
|
+
elif dispatch_type == 'INTERACTION_CREATE':
|
|
269
|
+
await self.command_dispatcher.dispatch(event_data)
|
|
270
|
+
|
|
271
|
+
# otherwise this must be an event!
|
|
272
|
+
await self.dispatcher.dispatch(dispatch_type, event_data)
|
|
273
|
+
except:
|
|
274
|
+
break # stop task if an error occurred
|
|
275
|
+
|
|
276
|
+
async def _start(self):
|
|
277
|
+
"""Starts the HTTP/Websocket client, run startup hooks, and registers commands."""
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
await self._http.start(self.token)
|
|
281
|
+
|
|
282
|
+
if self._setup_hooks:
|
|
283
|
+
for hook in self._setup_hooks:
|
|
284
|
+
self._logger.log_info(f"Setting hook {hook.__name__}")
|
|
285
|
+
await hook(self)
|
|
286
|
+
self._logger.log_high_priority("Hooks set up.")
|
|
287
|
+
|
|
288
|
+
if self.sync_commands:
|
|
289
|
+
await self.command_dispatcher.register_guild_commands()
|
|
290
|
+
|
|
291
|
+
await self.command_dispatcher.register_global_commands()
|
|
292
|
+
|
|
293
|
+
self._logger.log_high_priority("Commands set up.")
|
|
294
|
+
|
|
295
|
+
tasks = await asyncio.create_task(self._start_shards())
|
|
296
|
+
|
|
297
|
+
# end all ongoing tasks
|
|
298
|
+
await asyncio.gather(*tasks)
|
|
299
|
+
|
|
300
|
+
except asyncio.CancelledError:
|
|
301
|
+
self._logger.log_high_priority("Connection cancelled via KeyboardInterrupt.")
|
|
302
|
+
except Exception as e:
|
|
303
|
+
self._logger.log_error(f"{type(e).__name__} - {e}")
|
|
304
|
+
finally:
|
|
305
|
+
await self._close()
|
|
306
|
+
|
|
307
|
+
async def _close(self):
|
|
308
|
+
"""Gracefully close HTTP session, websocket connections, and run shutdown hooks."""
|
|
309
|
+
|
|
310
|
+
for hook in self._shutdown_hooks:
|
|
311
|
+
try:
|
|
312
|
+
self._logger.log_info(f"Executing shutdown hook {hook.__name__}")
|
|
313
|
+
await hook(self)
|
|
314
|
+
except Exception as e:
|
|
315
|
+
self._logger.log_error(f"Shutdown hook failed: {type(e).__name__}: {e}")
|
|
316
|
+
|
|
317
|
+
self._logger.log_info("Closing HTTP session...")
|
|
318
|
+
await self._http.close()
|
|
319
|
+
|
|
320
|
+
# close each connection or shard
|
|
321
|
+
for shard in self.shards:
|
|
322
|
+
await shard.close_ws()
|
|
323
|
+
|
|
324
|
+
def run(self):
|
|
325
|
+
"""User-facing entry point for starting the client."""
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
asyncio.run(self._start())
|
|
329
|
+
except KeyboardInterrupt:
|
|
330
|
+
self._logger.log_info("Shutdown requested via KeyboardInterrupt.")
|
|
331
|
+
except Exception as e:
|
|
332
|
+
self._logger.log_error(f"{type(e).__name__} {e}")
|
|
333
|
+
finally:
|
|
334
|
+
self._logger.log_high_priority("Bot shutting down.")
|
|
335
|
+
self._logger.close()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# scurrypy/core
|
|
2
|
+
|
|
3
|
+
# from .client_like import ClientLike
|
|
4
|
+
from .config import BaseConfig
|
|
5
|
+
# from .error import DiscordError
|
|
6
|
+
# from .gateway import GatewayClient
|
|
7
|
+
# from .http import HTTPClient
|
|
8
|
+
from .intents import Intents, set_intents
|
|
9
|
+
from .logger import Logger
|
|
10
|
+
# from .model import DataModel
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"BaseConfig",
|
|
14
|
+
"Intents", 'set_intents',
|
|
15
|
+
"Logger"
|
|
16
|
+
]
|
|
@@ -5,10 +5,17 @@ from .http import HTTPClient
|
|
|
5
5
|
from .logger import Logger
|
|
6
6
|
|
|
7
7
|
class ClientLike(Protocol):
|
|
8
|
-
"""Exposes a common interface for [`Client`][
|
|
8
|
+
"""Exposes a common interface for [`Client`][scurrypy.client.Client]."""
|
|
9
|
+
|
|
10
|
+
token: str
|
|
11
|
+
"""Bot's token."""
|
|
12
|
+
|
|
9
13
|
application_id: int
|
|
10
14
|
"""Bot's application ID."""
|
|
11
15
|
|
|
16
|
+
intents: int
|
|
17
|
+
"""Bot intents for listening to events."""
|
|
18
|
+
|
|
12
19
|
config: BaseConfig
|
|
13
20
|
"""User-defined config."""
|
|
14
21
|
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
METHOD_SUCCESS_CODES = {
|
|
2
|
-
"GET": (200),
|
|
3
|
-
"POST": (200, 201),
|
|
4
|
-
"PATCH": (200, 201),
|
|
5
|
-
"DELETE": (200, 204)
|
|
6
|
-
}
|
|
7
|
-
|
|
8
1
|
class DiscordError(Exception):
|
|
9
2
|
"""Represents a Discord API error."""
|
|
3
|
+
|
|
10
4
|
def __init__(self, status: int, data: dict):
|
|
11
5
|
"""Initialize the error with Discord's response.
|
|
12
6
|
Extracts reason, code, and walks the nested errors.
|
|
@@ -15,22 +9,14 @@ class DiscordError(Exception):
|
|
|
15
9
|
data (dict): Discord's error JSON
|
|
16
10
|
"""
|
|
17
11
|
self.data = data
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
self.status = status
|
|
20
13
|
self.reason = data.get('message', 'Unknown Error')
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
self.code = data.get('code', '???')
|
|
24
|
-
"""Discord-generated code of error."""
|
|
14
|
+
self.code = data.get('code', 'Unknown Code')
|
|
25
15
|
|
|
26
16
|
self.error_data = data.get('errors', {})
|
|
27
|
-
"""Error-specific data."""
|
|
28
|
-
|
|
29
17
|
self.details = self.walk(self.error_data)
|
|
30
|
-
"""Error details."""
|
|
31
18
|
|
|
32
|
-
self.
|
|
33
|
-
"""If this error is considered fatal."""
|
|
19
|
+
self.is_fatal = status in (401, 403)
|
|
34
20
|
|
|
35
21
|
errors = [f"→ {path}: {reason}" for path, reason in self.details]
|
|
36
22
|
full_message = f"{self.reason} ({self.code})"
|
|
@@ -58,6 +44,8 @@ class DiscordError(Exception):
|
|
|
58
44
|
if key == '_errors' and isinstance(value, list):
|
|
59
45
|
msg = value[0].get('message', 'Unknown error')
|
|
60
46
|
result.append(('.'.join(path), msg))
|
|
47
|
+
|
|
48
|
+
# the value should not be a dict -- keep going
|
|
61
49
|
elif isinstance(value, dict):
|
|
62
50
|
result.extend(self.walk(value, path + [key]))
|
|
63
51
|
return result
|