fastapi-interactions 0.1.0__tar.gz → 0.1.1__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.
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/PKG-INFO +1 -1
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions/__init__.py +2 -0
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions/bot.py +42 -12
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions/commands.py +50 -8
- fastapi_interactions-0.1.1/fastapi_interactions/context.py +83 -0
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions/middleware.py +25 -0
- fastapi_interactions-0.1.1/fastapi_interactions/models.py +286 -0
- fastapi_interactions-0.1.1/fastapi_interactions/responses.py +104 -0
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions.egg-info/PKG-INFO +1 -1
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/pyproject.toml +2 -2
- fastapi_interactions-0.1.0/fastapi_interactions/context.py +0 -43
- fastapi_interactions-0.1.0/fastapi_interactions/models.py +0 -166
- fastapi_interactions-0.1.0/fastapi_interactions/responses.py +0 -61
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/README.md +0 -0
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions.egg-info/SOURCES.txt +0 -0
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions.egg-info/dependency_links.txt +0 -0
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions.egg-info/requires.txt +0 -0
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions.egg-info/top_level.txt +0 -0
- {fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/setup.cfg +0 -0
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""The bot application: routing, command dispatch, and Discord API syncing.
|
|
2
|
+
|
|
3
|
+
A :class:`Bot` wraps a FastAPI application, verifies Discord webhook signatures,
|
|
4
|
+
routes interaction payloads to registered commands, and registers commands with
|
|
5
|
+
Discord's API.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
import importlib
|
|
2
9
|
import inspect
|
|
3
10
|
import json
|
|
@@ -10,6 +17,7 @@ from fastapi import FastAPI, Request
|
|
|
10
17
|
from fastapi.responses import JSONResponse
|
|
11
18
|
from loguru import logger
|
|
12
19
|
from pydantic import ValidationError
|
|
20
|
+
from starlette.background import BackgroundTask
|
|
13
21
|
|
|
14
22
|
from .commands import Command, CommandRouter
|
|
15
23
|
from .context import Context
|
|
@@ -20,7 +28,13 @@ from .models import (
|
|
|
20
28
|
InteractionType,
|
|
21
29
|
Snowflake,
|
|
22
30
|
)
|
|
23
|
-
from .responses import
|
|
31
|
+
from .responses import (
|
|
32
|
+
DeferResponse,
|
|
33
|
+
InteractionResponse,
|
|
34
|
+
MessageResponse,
|
|
35
|
+
PongResponse,
|
|
36
|
+
_JSONResponse,
|
|
37
|
+
)
|
|
24
38
|
|
|
25
39
|
|
|
26
40
|
async def call_with_options(callback: callable, ctx: Context) -> Any:
|
|
@@ -62,6 +76,14 @@ async def call_with_options(callback: callable, ctx: Context) -> Any:
|
|
|
62
76
|
|
|
63
77
|
|
|
64
78
|
class Bot:
|
|
79
|
+
"""A Discord HTTP interactions bot built on FastAPI.
|
|
80
|
+
|
|
81
|
+
The bot exposes a single interactions endpoint (``/interactions`` by
|
|
82
|
+
default) and keeps commands in an internal registry. Use
|
|
83
|
+
:meth:`attach_router` or :meth:`load_extension` to register commands, then
|
|
84
|
+
sync them with Discord using :meth:`sync_commands`.
|
|
85
|
+
"""
|
|
86
|
+
|
|
65
87
|
def __init__(
|
|
66
88
|
self,
|
|
67
89
|
app_id: int,
|
|
@@ -96,7 +118,7 @@ class Bot:
|
|
|
96
118
|
payload = json.loads(request.state.raw_body)
|
|
97
119
|
|
|
98
120
|
if payload["type"] == InteractionType.PING:
|
|
99
|
-
return
|
|
121
|
+
return _JSONResponse(PongResponse())
|
|
100
122
|
|
|
101
123
|
if payload["type"] == InteractionType.APPLICATION_COMMAND:
|
|
102
124
|
"""Construct Context"""
|
|
@@ -105,11 +127,9 @@ class Bot:
|
|
|
105
127
|
application_command = ApplicationCommandData.model_validate(
|
|
106
128
|
interaction.data
|
|
107
129
|
)
|
|
108
|
-
except (ValidationError, Exception):
|
|
109
|
-
|
|
110
|
-
return await MessageResponse("Unexpected error", ephemeral=True)
|
|
130
|
+
except (ValidationError, Exception) as e:
|
|
131
|
+
return _JSONResponse(MessageResponse("Unexpected error", ephemeral=True))
|
|
111
132
|
|
|
112
|
-
raise Exception("Unexpected error occurred")
|
|
113
133
|
context = Context(
|
|
114
134
|
interaction=interaction, options=application_command, http=self.http
|
|
115
135
|
)
|
|
@@ -118,13 +138,13 @@ class Bot:
|
|
|
118
138
|
command_name=application_command.name, ctx=context
|
|
119
139
|
)
|
|
120
140
|
|
|
121
|
-
return
|
|
141
|
+
return _JSONResponse(MessageResponse("Unsupported interaction received", ephemeral=True))
|
|
122
142
|
|
|
123
143
|
async def add_error_handler(self, exc: Exception, func: callable):
|
|
124
144
|
pass
|
|
125
145
|
|
|
126
146
|
async def _handle_exception(self, request: Request, exc: Exception):
|
|
127
|
-
return
|
|
147
|
+
return _JSONResponse(MessageResponse(str(exc), ephemeral=True))
|
|
128
148
|
|
|
129
149
|
def _register_routes(self) -> None:
|
|
130
150
|
"""Set up middleware and the interactions endpoint.
|
|
@@ -289,14 +309,24 @@ class Bot:
|
|
|
289
309
|
"""
|
|
290
310
|
command = self.commands.get(command_name)
|
|
291
311
|
if command is None:
|
|
292
|
-
return
|
|
312
|
+
return _JSONResponse(MessageResponse("Unknown command", ephemeral=True))
|
|
293
313
|
|
|
294
314
|
result = await call_with_options(command.callback, ctx)
|
|
295
315
|
|
|
296
|
-
|
|
297
|
-
|
|
316
|
+
background = None
|
|
317
|
+
if isinstance(result, DeferResponse) and result.finish:
|
|
318
|
+
background = BackgroundTask(self._run_finish, result.finish, ctx=ctx)
|
|
319
|
+
if isinstance(result, str):
|
|
320
|
+
result = MessageResponse(result)
|
|
321
|
+
|
|
322
|
+
return _JSONResponse(result, background=background)
|
|
298
323
|
|
|
299
|
-
|
|
324
|
+
async def _run_finish(self, finish, ctx: Context) -> None:
|
|
325
|
+
try:
|
|
326
|
+
await finish()
|
|
327
|
+
except Exception as e:
|
|
328
|
+
logger.exception('Defer handler failed')
|
|
329
|
+
# await self._report_failure(ctx)
|
|
300
330
|
|
|
301
331
|
def delete_all_commands(self, guild_id: Snowflake = None) -> None:
|
|
302
332
|
"""
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
+
"""Command registration, routing, and option decorators.
|
|
2
|
+
|
|
3
|
+
Routers group commands and carry settings such as guild scope, integration
|
|
4
|
+
types, and interaction contexts. The :class:`Option` decorators attach option
|
|
5
|
+
metadata that is later bound to command callback parameters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
import sys
|
|
9
|
+
from collections.abc import Callable
|
|
2
10
|
from dataclasses import dataclass, field
|
|
3
11
|
|
|
4
12
|
from .models import (
|
|
@@ -13,6 +21,8 @@ OptionType = ApplicationCommandOptionType
|
|
|
13
21
|
|
|
14
22
|
@dataclass
|
|
15
23
|
class CommandOption:
|
|
24
|
+
"""A single option declared on a command."""
|
|
25
|
+
|
|
16
26
|
name: str
|
|
17
27
|
description: str
|
|
18
28
|
type: OptionType = OptionType.STRING
|
|
@@ -29,6 +39,12 @@ class CommandOption:
|
|
|
29
39
|
|
|
30
40
|
@dataclass(slots=True)
|
|
31
41
|
class CommandMeta:
|
|
42
|
+
"""Registration metadata sent to Discord for a command.
|
|
43
|
+
|
|
44
|
+
``integration_types`` and ``contexts`` control where the command can be
|
|
45
|
+
installed and invoked. They are only valid for globally scoped commands.
|
|
46
|
+
"""
|
|
47
|
+
|
|
32
48
|
name: str
|
|
33
49
|
description: str
|
|
34
50
|
options: list[CommandOption] = field(default_factory=list)
|
|
@@ -52,12 +68,31 @@ class CommandMeta:
|
|
|
52
68
|
|
|
53
69
|
@dataclass(slots=True)
|
|
54
70
|
class Command:
|
|
71
|
+
"""A command callback paired with its registration metadata."""
|
|
72
|
+
|
|
55
73
|
callback: callable
|
|
56
74
|
meta: CommandMeta
|
|
57
75
|
guild_id: Snowflake | None = None
|
|
58
76
|
|
|
59
77
|
|
|
60
78
|
class CommandRouter:
|
|
79
|
+
"""A collection of commands that can be attached to a bot.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
name: Optional router name used in logs. Inferred from the caller's
|
|
83
|
+
module when omitted.
|
|
84
|
+
guild_id: Scope every command in the router to a single guild. Guild
|
|
85
|
+
scoped commands cannot set ``integration_types`` or ``contexts``.
|
|
86
|
+
integration_types: Where commands can be installed. Use
|
|
87
|
+
:class:`~fastapi_interactions.models.IntegrationType` flags.
|
|
88
|
+
contexts: Where commands can be invoked. Use
|
|
89
|
+
:class:`~fastapi_interactions.models.InteractionContext` flags.
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
ValueError: If a guild-scoped router also sets ``integration_types`` or
|
|
93
|
+
``contexts``, or if either flag value is empty.
|
|
94
|
+
"""
|
|
95
|
+
|
|
61
96
|
def __init__(
|
|
62
97
|
self,
|
|
63
98
|
name: str | None = None,
|
|
@@ -133,6 +168,13 @@ class CommandRouter:
|
|
|
133
168
|
|
|
134
169
|
|
|
135
170
|
class Option:
|
|
171
|
+
"""Decorators that attach typed options to command callbacks.
|
|
172
|
+
|
|
173
|
+
Each static method registers one Discord option type. Decorate the command
|
|
174
|
+
below ``@router.command(...)`` so the option metadata is collected before
|
|
175
|
+
the command is registered.
|
|
176
|
+
"""
|
|
177
|
+
|
|
136
178
|
@staticmethod
|
|
137
179
|
def __create_operation_decorator(
|
|
138
180
|
name: str, description: str, option_type: OptionType, required: bool = True
|
|
@@ -157,7 +199,7 @@ class Option:
|
|
|
157
199
|
return decorator
|
|
158
200
|
|
|
159
201
|
@staticmethod
|
|
160
|
-
def string(name: str, description: str, required: bool = True):
|
|
202
|
+
def string(name: str, description: str, required: bool = True) -> Callable:
|
|
161
203
|
"""Register a string option on a command.
|
|
162
204
|
|
|
163
205
|
Decorates a command callback to add a string option parameter. The option name
|
|
@@ -187,7 +229,7 @@ class Option:
|
|
|
187
229
|
)
|
|
188
230
|
|
|
189
231
|
@staticmethod
|
|
190
|
-
def integer(name: str, description: str, required: bool = True):
|
|
232
|
+
def integer(name: str, description: str, required: bool = True) -> Callable:
|
|
191
233
|
"""Register an integer option on a command.
|
|
192
234
|
|
|
193
235
|
Decorates a command callback to add an integer option parameter. The option name
|
|
@@ -217,7 +259,7 @@ class Option:
|
|
|
217
259
|
)
|
|
218
260
|
|
|
219
261
|
@staticmethod
|
|
220
|
-
def user(name: str, description: str, required: bool = True):
|
|
262
|
+
def user(name: str, description: str, required: bool = True) -> Callable:
|
|
221
263
|
"""Register a user option on a command.
|
|
222
264
|
|
|
223
265
|
Decorates a command callback to add a user option parameter. The option name
|
|
@@ -248,7 +290,7 @@ class Option:
|
|
|
248
290
|
)
|
|
249
291
|
|
|
250
292
|
@staticmethod
|
|
251
|
-
def channel(name: str, description: str, required: bool = True):
|
|
293
|
+
def channel(name: str, description: str, required: bool = True) -> Callable:
|
|
252
294
|
"""Register a channel option on a command.
|
|
253
295
|
|
|
254
296
|
Decorates a command callback to add a channel option parameter. The option name
|
|
@@ -279,7 +321,7 @@ class Option:
|
|
|
279
321
|
)
|
|
280
322
|
|
|
281
323
|
@staticmethod
|
|
282
|
-
def role(name: str, description: str, required: bool = True):
|
|
324
|
+
def role(name: str, description: str, required: bool = True) -> Callable:
|
|
283
325
|
"""Register a role option on a command.
|
|
284
326
|
|
|
285
327
|
Decorates a command callback to add a channel option parameter. The option name
|
|
@@ -310,7 +352,7 @@ class Option:
|
|
|
310
352
|
)
|
|
311
353
|
|
|
312
354
|
@staticmethod
|
|
313
|
-
def mentionable(name: str, description: str, required: bool = True):
|
|
355
|
+
def mentionable(name: str, description: str, required: bool = True) -> Callable:
|
|
314
356
|
"""Register a mentionable option on a command.
|
|
315
357
|
|
|
316
358
|
Decorates a command callback to add a channel option parameter. The option name
|
|
@@ -340,7 +382,7 @@ class Option:
|
|
|
340
382
|
)
|
|
341
383
|
|
|
342
384
|
@staticmethod
|
|
343
|
-
def boolean(name: str, description: str, required: bool = True):
|
|
385
|
+
def boolean(name: str, description: str, required: bool = True) -> Callable:
|
|
344
386
|
"""Register a boolean option on a command.
|
|
345
387
|
|
|
346
388
|
Decorates a command callback to add a boolean option parameter. The option name
|
|
@@ -375,7 +417,7 @@ class Option:
|
|
|
375
417
|
)
|
|
376
418
|
|
|
377
419
|
@staticmethod
|
|
378
|
-
def number(name: str, description: str, required: bool = True):
|
|
420
|
+
def number(name: str, description: str, required: bool = True) -> Callable:
|
|
379
421
|
"""Register a number option on a command.
|
|
380
422
|
|
|
381
423
|
Decorates a command callback to add a number option parameter. The option name
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""The interaction context passed to command handlers.
|
|
2
|
+
|
|
3
|
+
A :class:`Context` bundles the parsed interaction, the command's option values,
|
|
4
|
+
and an async HTTP client for followup webhook requests.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from .models import Interaction, ApplicationCommandData, User, Snowflake
|
|
9
|
+
from typing import Optional, Any
|
|
10
|
+
import httpx
|
|
11
|
+
from .responses import MessageResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Context:
|
|
16
|
+
"""State and helpers for a single interaction.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
interaction: The parsed interaction payload from Discord.
|
|
20
|
+
options: The application command data, including supplied option values.
|
|
21
|
+
http: Async HTTP client used for followup webhook requests.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
interaction: Interaction
|
|
25
|
+
options: ApplicationCommandData
|
|
26
|
+
http: httpx.AsyncClient
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def user(self) -> User:
|
|
30
|
+
"""The user who invoked the interaction.
|
|
31
|
+
|
|
32
|
+
Guild interactions expose the user through ``member.user``; direct
|
|
33
|
+
message interactions expose it directly on the interaction.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If the payload contains neither ``user`` nor ``member.user``.
|
|
37
|
+
"""
|
|
38
|
+
if self.interaction.user is not None:
|
|
39
|
+
return self.interaction.user
|
|
40
|
+
if (
|
|
41
|
+
self.interaction.member is not None
|
|
42
|
+
and self.interaction.member.user is not None
|
|
43
|
+
):
|
|
44
|
+
return self.interaction.member.user
|
|
45
|
+
raise ValueError("Interaction does not contain `user` or `member.user`")
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def guild_id(self) -> Optional[Snowflake]:
|
|
49
|
+
"""The guild ID where the interaction occurred, or ``None`` in DMs."""
|
|
50
|
+
return self.interaction.guild_id
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def channel_id(self) -> Optional[Snowflake]:
|
|
54
|
+
"""The channel ID where the interaction occurred, if present."""
|
|
55
|
+
return self.interaction.channel_id
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def _webhook_base(self) -> str:
|
|
59
|
+
return f"https://discord.com/api/v10/webhooks/{self.interaction.application_id}/{self.interaction.token}"
|
|
60
|
+
|
|
61
|
+
async def send(self, content: str, ephemeral: bool = False) -> None:
|
|
62
|
+
"""Send a followup message through the interaction webhook.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
content: The message text to send.
|
|
66
|
+
ephemeral: Whether the message should only be visible to the
|
|
67
|
+
invoking user. Defaults to ``False``.
|
|
68
|
+
"""
|
|
69
|
+
message = MessageResponse(content, ephemeral=ephemeral)
|
|
70
|
+
await self.http.post(url=self._webhook_base, json=message.to_payload())
|
|
71
|
+
|
|
72
|
+
def get_option_value(self, name: str, default: Any = None) -> Any:
|
|
73
|
+
"""Return the value of a command option by name.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
name: The option name to look up.
|
|
77
|
+
default: Value returned when the option was not supplied.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The option value, or ``default`` when it is missing.
|
|
81
|
+
"""
|
|
82
|
+
option = self.options.options_by_name.get(name)
|
|
83
|
+
return option.value if option is not None else default
|
{fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions/middleware.py
RENAMED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
"""Middleware for verifying Discord webhook signatures.
|
|
2
|
+
|
|
3
|
+
Discord signs every interaction request with Ed25519. The middleware validates
|
|
4
|
+
the signature before the bot parses or acts on the payload, and stores the raw
|
|
5
|
+
body on ``request.state.raw_body`` for handlers that need it.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from fastapi import Request
|
|
2
9
|
from fastapi.responses import JSONResponse
|
|
3
10
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
@@ -8,6 +15,17 @@ from nacl.exceptions import BadSignatureError
|
|
|
8
15
|
def verify_signature(
|
|
9
16
|
public_key: str, signature: str, timestamp: str, body: bytes
|
|
10
17
|
) -> bool:
|
|
18
|
+
"""Verify a Discord interaction signature.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
public_key: The application's public key as a hex string.
|
|
22
|
+
signature: The ``x-signature-ed25519`` header value.
|
|
23
|
+
timestamp: The ``x-signature-timestamp`` header value.
|
|
24
|
+
body: The raw request body that was signed.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
``True`` if the signature is valid, otherwise ``False``.
|
|
28
|
+
"""
|
|
11
29
|
try:
|
|
12
30
|
VerifyKey(bytes.fromhex(public_key)).verify(
|
|
13
31
|
timestamp.encode() + body, bytes.fromhex(signature)
|
|
@@ -18,6 +36,13 @@ def verify_signature(
|
|
|
18
36
|
|
|
19
37
|
|
|
20
38
|
class VerifySignatureMiddleware(BaseHTTPMiddleware):
|
|
39
|
+
"""Reject interaction requests whose Discord signature is invalid.
|
|
40
|
+
|
|
41
|
+
Requests to paths other than ``interaction_path`` pass through unchanged.
|
|
42
|
+
Valid interaction requests have their raw body stored on
|
|
43
|
+
``request.state.raw_body`` for downstream handlers.
|
|
44
|
+
"""
|
|
45
|
+
|
|
21
46
|
def __init__(self, app, public_key: str, interaction_path: str = "/interactions"):
|
|
22
47
|
super().__init__(app)
|
|
23
48
|
self.public_key = public_key
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Enums and Pydantic models for Discord interaction payloads.
|
|
2
|
+
|
|
3
|
+
This module mirrors the parts of Discord's interaction and application command
|
|
4
|
+
objects that the framework needs. Enums use the same integer values Discord sends
|
|
5
|
+
and expects, while Pydantic models parse incoming payloads with unknown fields
|
|
6
|
+
ignored.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from enum import IntEnum, IntFlag
|
|
10
|
+
from typing import Any, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
13
|
+
|
|
14
|
+
Snowflake = str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _ApiFlag(IntFlag):
|
|
18
|
+
"""Base class for flags that are serialized as Discord's integer lists.
|
|
19
|
+
|
|
20
|
+
Each flag value is a bit position rather than the API integer itself. For
|
|
21
|
+
example, the first flag in a subclass is ``1 << 0`` and is sent to Discord
|
|
22
|
+
as ``0``. Use :meth:`to_api` to convert a combination of flags into the
|
|
23
|
+
list of integers Discord expects.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def to_api(self) -> list[int]:
|
|
27
|
+
"""Bit positions set in this flag, e.g. GUILD|PRIVATE_CHANNEL -> [0, 2]."""
|
|
28
|
+
return [i for i in range(self.bit_length()) if self & (1 << i)]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class IntegrationType(_ApiFlag):
|
|
32
|
+
"""Where a command can be installed and used.
|
|
33
|
+
|
|
34
|
+
Discord calls these *integration types*. Combine values with ``|`` when a
|
|
35
|
+
command should support more than one installation surface, or use
|
|
36
|
+
:attr:`ALL` as a shorthand. The selected flags are serialized to the
|
|
37
|
+
``integration_types`` field when commands are registered.
|
|
38
|
+
|
|
39
|
+
Attributes:
|
|
40
|
+
GUILD_INSTALL: The command can be installed on a guild (API value ``0``).
|
|
41
|
+
USER_INSTALL: The command can be installed on a user account (API value ``1``).
|
|
42
|
+
ALL: Both guild and user installation (API values ``[0, 1]``).
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
GUILD_INSTALL = 1 << 0 # API VALUE 0
|
|
46
|
+
USER_INSTALL = 1 << 1 # API VALUE 1
|
|
47
|
+
ALL = GUILD_INSTALL | USER_INSTALL
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class InteractionContext(_ApiFlag):
|
|
51
|
+
"""Where an application command can be invoked.
|
|
52
|
+
|
|
53
|
+
Combine values with ``|`` to allow a command in several places, or use
|
|
54
|
+
:attr:`ALL` as a shorthand. The selected flags are serialized to the
|
|
55
|
+
``contexts`` field when commands are registered.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
GUILD: Usable in guild channels (API value ``0``).
|
|
59
|
+
BOT_DM: Usable in direct messages with the bot (API value ``1``).
|
|
60
|
+
PRIVATE_CHANNEL: Usable in private channels such as group DMs (API value ``2``).
|
|
61
|
+
ALL: Usable in every supported context (API values ``[0, 1, 2]``).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
GUILD = 1 << 0 # API VALUE 0
|
|
65
|
+
BOT_DM = 1 << 1 # API VALUE 1
|
|
66
|
+
PRIVATE_CHANNEL = 1 << 2 # VALUE VALUE 2
|
|
67
|
+
ALL = GUILD | BOT_DM | PRIVATE_CHANNEL
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class InteractionType(IntEnum):
|
|
71
|
+
"""Type of interaction delivered to the bot's webhook endpoint.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
PING: Discord's endpoint verification request.
|
|
75
|
+
APPLICATION_COMMAND: A slash command invocation.
|
|
76
|
+
MESSAGE_COMPONENT: A button or select menu interaction.
|
|
77
|
+
APPLICATION_COMMAND_AUTOCOMPLETE: An autocomplete request for a command option.
|
|
78
|
+
MODAL_SUBMIT: A submitted modal dialog.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
PING = 1
|
|
82
|
+
APPLICATION_COMMAND = 2
|
|
83
|
+
MESSAGE_COMPONENT = 3
|
|
84
|
+
APPLICATION_COMMAND_AUTOCOMPLETE = 4
|
|
85
|
+
MODAL_SUBMIT = 5
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CommandType(IntEnum):
|
|
89
|
+
"""Type of application command registered with Discord.
|
|
90
|
+
|
|
91
|
+
Attributes:
|
|
92
|
+
CHAT_INPUT: A slash command (API value ``1``).
|
|
93
|
+
USER: A user context menu command (API value ``2``).
|
|
94
|
+
MESSAGE: A message context menu command (API value ``3``).
|
|
95
|
+
PRIMARY_ENTRY_POINT: The application's primary entry point command (API value ``4``).
|
|
96
|
+
CHAT: Alias for :attr:`CHAT_INPUT`.
|
|
97
|
+
ENTRY: Alias for :attr:`PRIMARY_ENTRY_POINT`.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
CHAT_INPUT = 1
|
|
101
|
+
USER = 2
|
|
102
|
+
MESSAGE = 3
|
|
103
|
+
PRIMARY_ENTRY_POINT = 4
|
|
104
|
+
|
|
105
|
+
CHAT = 1
|
|
106
|
+
ENTRY = 4
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class InteractionCallbackType(IntEnum):
|
|
110
|
+
"""Response type sent back to Discord for an interaction.
|
|
111
|
+
|
|
112
|
+
Attributes:
|
|
113
|
+
PONG: Acknowledge a PING interaction.
|
|
114
|
+
CHANNEL_MESSAGE_WITH_SOURCE: Send a message in the interaction's channel.
|
|
115
|
+
DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE: Acknowledge and send a message later.
|
|
116
|
+
DEFERRED_UPDATE_MESSAGE: Acknowledge a component interaction and update later.
|
|
117
|
+
UPDATE_MESSAGE: Update the message a component is attached to.
|
|
118
|
+
APPLICATION_COMMAND_AUTOCOMPLETE_RESULT: Return autocomplete choices.
|
|
119
|
+
MODAL: Open a modal dialog.
|
|
120
|
+
PREMIUM_REQUIRED: Tell the user a premium upgrade is required.
|
|
121
|
+
LAUNCH_ACTIVITY: Launch an embedded activity.
|
|
122
|
+
MESSAGE: Alias for :attr:`CHANNEL_MESSAGE_WITH_SOURCE`.
|
|
123
|
+
DEFER: Alias for :attr:`DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE`.
|
|
124
|
+
UPDATE: Alias for :attr:`UPDATE_MESSAGE`.
|
|
125
|
+
AUTOCOMPLETE: Alias for :attr:`APPLICATION_COMMAND_AUTOCOMPLETE_RESULT`.
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
PONG = 1
|
|
129
|
+
CHANNEL_MESSAGE_WITH_SOURCE = 4
|
|
130
|
+
DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5
|
|
131
|
+
DEFERRED_UPDATE_MESSAGE = 6
|
|
132
|
+
UPDATE_MESSAGE = 7
|
|
133
|
+
APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8
|
|
134
|
+
MODAL = 9
|
|
135
|
+
PREMIUM_REQUIRED = 10
|
|
136
|
+
LAUNCH_ACTIVITY = 12
|
|
137
|
+
|
|
138
|
+
MESSAGE = CHANNEL_MESSAGE_WITH_SOURCE
|
|
139
|
+
DEFER = DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
|
|
140
|
+
UPDATE = UPDATE_MESSAGE
|
|
141
|
+
AUTOCOMPLETE = APPLICATION_COMMAND_AUTOCOMPLETE_RESULT
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class MessageFlags(IntFlag):
|
|
145
|
+
"""Bit flags describing special message properties."""
|
|
146
|
+
|
|
147
|
+
CROSSPOSTED = 1 << 0
|
|
148
|
+
IS_CROSSPOST = 1 << 1
|
|
149
|
+
SUPPRESS_EMBEDS = 1 << 2
|
|
150
|
+
SOURCE_MESSAGE_DELETED = 1 << 3
|
|
151
|
+
URGENT = 1 << 4
|
|
152
|
+
HAS_THREAD = 1 << 5
|
|
153
|
+
EPHEMERAL = 1 << 6
|
|
154
|
+
LOADING = 1 << 7
|
|
155
|
+
FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8
|
|
156
|
+
|
|
157
|
+
SUPPRESS_NOTIFICATIONS = 1 << 12
|
|
158
|
+
IS_VOICE_MESSAGE = 1 << 13
|
|
159
|
+
HAS_SNAPSHOT = 1 << 14
|
|
160
|
+
IS_COMPONENTS_V2 = 1 << 15
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class ApplicationCommandOptionType(IntEnum):
|
|
164
|
+
"""Type of value accepted by an application command option.
|
|
165
|
+
|
|
166
|
+
Attributes:
|
|
167
|
+
SUB_COMMAND: A nested subcommand.
|
|
168
|
+
SUB_COMMAND_GROUP: A group of related subcommands.
|
|
169
|
+
STRING: A free-form string.
|
|
170
|
+
INTEGER: A whole number.
|
|
171
|
+
BOOLEAN: A true/false value.
|
|
172
|
+
USER: A Discord user.
|
|
173
|
+
CHANNEL: A Discord channel.
|
|
174
|
+
ROLE: A Discord role.
|
|
175
|
+
MENTIONABLE: A user or role mention.
|
|
176
|
+
NUMBER: A floating-point number.
|
|
177
|
+
ATTACHMENT: A file attachment.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
SUB_COMMAND = 1
|
|
181
|
+
SUB_COMMAND_GROUP = 2
|
|
182
|
+
STRING = 3
|
|
183
|
+
INTEGER = 4
|
|
184
|
+
BOOLEAN = 5
|
|
185
|
+
USER = 6
|
|
186
|
+
CHANNEL = 7
|
|
187
|
+
ROLE = 8
|
|
188
|
+
MENTIONABLE = 9
|
|
189
|
+
NUMBER = 10
|
|
190
|
+
ATTACHMENT = 11
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class DiscordModel(BaseModel):
|
|
194
|
+
"""Base model for Discord payload objects.
|
|
195
|
+
|
|
196
|
+
Unknown fields are ignored so payloads can gain new properties without
|
|
197
|
+
breaking existing bots.
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
model_config = ConfigDict(extra="ignore")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class User(DiscordModel):
|
|
204
|
+
"""A Discord user."""
|
|
205
|
+
|
|
206
|
+
id: Snowflake
|
|
207
|
+
username: str
|
|
208
|
+
descriminator: Optional[str] = None
|
|
209
|
+
global_name: Optional[str] = None
|
|
210
|
+
avatar: Optional[str] = None
|
|
211
|
+
bot: Optional[bool] = None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class Member(DiscordModel):
|
|
215
|
+
"""A guild member and their guild-specific attributes."""
|
|
216
|
+
|
|
217
|
+
user: Optional[User] = None
|
|
218
|
+
nick: Optional[str] = None
|
|
219
|
+
roles: list[Snowflake] = Field(default_factory=list)
|
|
220
|
+
permissions: Optional[str] = None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ApplicationCommandInteractionOption(DiscordModel):
|
|
224
|
+
"""A command option value supplied with an interaction.
|
|
225
|
+
|
|
226
|
+
Options may contain nested options for subcommands and subcommand groups.
|
|
227
|
+
"""
|
|
228
|
+
|
|
229
|
+
name: str
|
|
230
|
+
type: ApplicationCommandOptionType
|
|
231
|
+
value: Optional[str | int | float | bool] = None
|
|
232
|
+
options: list["ApplicationCommandInteractionOption"] = Field(default_factory=list)
|
|
233
|
+
focused: Optional[bool] = None
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def options_by_name(self) -> dict[str, "ApplicationCommandInteractionOption"]:
|
|
237
|
+
return {opt.name for opt in self.options}
|
|
238
|
+
|
|
239
|
+
def get_option_value(self, name: str, default: Any = None) -> Any:
|
|
240
|
+
option = self.options_by_name.get(name)
|
|
241
|
+
return option.value if option is not None else default
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
ApplicationCommandInteractionOption.model_rebuild()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
class ApplicationCommandData(DiscordModel):
|
|
248
|
+
"""The command data attached to an application command interaction."""
|
|
249
|
+
|
|
250
|
+
id: Snowflake
|
|
251
|
+
name: str
|
|
252
|
+
type: CommandType
|
|
253
|
+
guild_id: Optional[Snowflake] = None
|
|
254
|
+
target_id: Optional[Snowflake] = None
|
|
255
|
+
options: list[ApplicationCommandInteractionOption] = Field(default_factory=list)
|
|
256
|
+
|
|
257
|
+
@property
|
|
258
|
+
def options_by_name(self) -> dict[str, "ApplicationCommandInteractionOption"]:
|
|
259
|
+
return {opt.name: opt for opt in self.options}
|
|
260
|
+
|
|
261
|
+
def get_option_value(self, name: str, default: Any = None) -> Any:
|
|
262
|
+
option = self.options_by_name.get(name)
|
|
263
|
+
return option.value if option is not None else default
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class Interaction(DiscordModel):
|
|
267
|
+
"""An interaction payload received from Discord.
|
|
268
|
+
|
|
269
|
+
The framework validates the raw webhook body into this model before building
|
|
270
|
+
a :class:`fastapi_interactions.context.Context` for command handlers.
|
|
271
|
+
"""
|
|
272
|
+
|
|
273
|
+
id: Snowflake
|
|
274
|
+
application_id: Snowflake
|
|
275
|
+
type: InteractionType
|
|
276
|
+
token: str
|
|
277
|
+
version: int
|
|
278
|
+
|
|
279
|
+
data: Optional[dict[str, Any]] = None
|
|
280
|
+
|
|
281
|
+
guild_id: Optional[Snowflake] = None
|
|
282
|
+
channel_id: Optional[Snowflake] = None
|
|
283
|
+
member: Optional[Member] = None
|
|
284
|
+
user: Optional[User] = None
|
|
285
|
+
locale: Optional[str] = None
|
|
286
|
+
guild_locale: Optional[str] = None
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Interaction response objects returned by command handlers.
|
|
2
|
+
|
|
3
|
+
Each response serializes itself into the callback payload Discord expects. The
|
|
4
|
+
bot converts returned strings into :class:`MessageResponse` instances and wraps
|
|
5
|
+
responses in a JSON HTTP response.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import InitVar, dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from fastapi.responses import JSONResponse
|
|
15
|
+
|
|
16
|
+
from .models import (
|
|
17
|
+
InteractionCallbackType,
|
|
18
|
+
MessageFlags,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class _JSONResponse(JSONResponse):
|
|
23
|
+
"""A JSON response that serializes :class:`InteractionResponse` objects."""
|
|
24
|
+
|
|
25
|
+
def render(self, content: Any) -> bytes:
|
|
26
|
+
if isinstance(content, InteractionResponse):
|
|
27
|
+
content = content.to_dict()
|
|
28
|
+
return super().render(content)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class InteractionResponse(ABC):
|
|
32
|
+
"""Base class for all interaction callback responses."""
|
|
33
|
+
|
|
34
|
+
@abstractmethod
|
|
35
|
+
def to_dict(self):
|
|
36
|
+
"""Return the Discord callback payload for this response."""
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
async def __call__(self) -> dict:
|
|
40
|
+
return self.to_dict()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class PongResponse(InteractionResponse):
|
|
44
|
+
"""Acknowledge a PING interaction.
|
|
45
|
+
|
|
46
|
+
The bot sends this automatically when Discord verifies the interactions
|
|
47
|
+
endpoint, so handlers normally never need to create it.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
callback_type = InteractionCallbackType.PONG
|
|
51
|
+
|
|
52
|
+
def to_dict(self):
|
|
53
|
+
return {"type": self.callback_type}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(slots=True)
|
|
57
|
+
class MessageResponse(InteractionResponse):
|
|
58
|
+
"""Send a message in the interaction's channel.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
content: The message text.
|
|
62
|
+
ephemeral: Whether only the invoking user can see the message.
|
|
63
|
+
Defaults to ``False``.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
callback_type = InteractionCallbackType.MESSAGE
|
|
67
|
+
content: str
|
|
68
|
+
ephemeral: InitVar[bool] = False
|
|
69
|
+
flags: MessageFlags = field(init=False)
|
|
70
|
+
|
|
71
|
+
def __post_init__(self, ephemeral: bool):
|
|
72
|
+
self.flags = MessageFlags.EPHEMERAL if ephemeral else MessageFlags(0)
|
|
73
|
+
|
|
74
|
+
def to_payload(self):
|
|
75
|
+
return {"content": self.content, "flags": int(self.flags)}
|
|
76
|
+
|
|
77
|
+
def to_dict(self):
|
|
78
|
+
return {"type": self.callback_type, "data": self.to_payload()}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(slots=True)
|
|
82
|
+
class DeferResponse(InteractionResponse):
|
|
83
|
+
"""Acknowledge an interaction now and deliver the real response later.
|
|
84
|
+
|
|
85
|
+
The bot sends the defer acknowledgement before running ``finish`` as a
|
|
86
|
+
background task. Use :meth:`fastapi_interactions.context.Context.send`
|
|
87
|
+
inside ``finish`` to deliver followup messages.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
ephemeral: Whether followup messages should be ephemeral. Defaults to
|
|
91
|
+
``False``.
|
|
92
|
+
finish: Optional callback run in the background after the defer is sent.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
callback_type = InteractionCallbackType.DEFER
|
|
96
|
+
ephemeral: InitVar[bool] = False
|
|
97
|
+
finish: Callable | None = field(default=None, repr=None, compare=None)
|
|
98
|
+
flags: MessageFlags = field(init=False)
|
|
99
|
+
|
|
100
|
+
def __post_init__(self, ephemeral: bool):
|
|
101
|
+
self.flags = MessageFlags.EPHEMERAL if ephemeral else MessageFlags(0)
|
|
102
|
+
|
|
103
|
+
def to_dict(self):
|
|
104
|
+
return {"type": self.callback_type, "data": {"flags": int(self.flags)}}
|
|
@@ -4,7 +4,7 @@ build-backend = 'setuptools.build_meta'
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = 'fastapi-interactions'
|
|
7
|
-
version = '0.1.
|
|
7
|
+
version = '0.1.1'
|
|
8
8
|
description='A lightweight framework for discord interactions over http built on FastAPI'
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.13"
|
|
@@ -28,4 +28,4 @@ docs = [
|
|
|
28
28
|
|
|
29
29
|
[tool.setuptools.packages.find]
|
|
30
30
|
include = ['fastapi_interactions*']
|
|
31
|
-
exclude = ['docs*', 'exts*', 'tests*']
|
|
31
|
+
exclude = ['docs*', 'exts*', 'tests*']
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from .models import Interaction, ApplicationCommandData, User, Snowflake
|
|
3
|
-
from typing import Optional, Any
|
|
4
|
-
import httpx
|
|
5
|
-
from .responses import MessageResponse
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@dataclass
|
|
9
|
-
class Context:
|
|
10
|
-
interaction: Interaction
|
|
11
|
-
options: ApplicationCommandData
|
|
12
|
-
http: httpx.AsyncClient
|
|
13
|
-
|
|
14
|
-
@property
|
|
15
|
-
def user(self) -> User:
|
|
16
|
-
if self.interaction.user is not None:
|
|
17
|
-
return self.interaction.user
|
|
18
|
-
if (
|
|
19
|
-
self.interaction.member is not None
|
|
20
|
-
and self.interaction.member.user is not None
|
|
21
|
-
):
|
|
22
|
-
return self.interaction.member.user
|
|
23
|
-
raise ValueError("Interaction does not contain `user` or `member.user`")
|
|
24
|
-
|
|
25
|
-
@property
|
|
26
|
-
def guild_id(self) -> Optional[Snowflake]:
|
|
27
|
-
return self.interaction.guild_id
|
|
28
|
-
|
|
29
|
-
@property
|
|
30
|
-
def channel_id(self) -> Optional[Snowflake]:
|
|
31
|
-
return self.interaction.channel_id
|
|
32
|
-
|
|
33
|
-
@property
|
|
34
|
-
def _webhook_base(self) -> str:
|
|
35
|
-
return f"https://discord.com/api/v10/webhooks/{self.interaction.application_id}/{self.interaction.token}"
|
|
36
|
-
|
|
37
|
-
async def send(self, content: str, ephemeral: bool = False) -> None:
|
|
38
|
-
message = MessageResponse(content, ephemeral=ephemeral)
|
|
39
|
-
await self.http.post(url=self._webhook_base, json=message.to_payload())
|
|
40
|
-
|
|
41
|
-
def get_option_value(self, name: str, default: Any = None) -> Any:
|
|
42
|
-
option = self.options.options_by_name.get(name)
|
|
43
|
-
return option.value if option is not None else default
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
from enum import IntEnum, IntFlag
|
|
2
|
-
from typing import Any, Optional
|
|
3
|
-
|
|
4
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
-
|
|
6
|
-
Snowflake = str
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class _ApiFlag(IntFlag):
|
|
10
|
-
def to_api(self) -> list[int]:
|
|
11
|
-
"""Bit positions set in this flag, e.g. GUILD|PRIVATE_CHANNEL -> [0, 2]."""
|
|
12
|
-
return [i for i in range(self.bit_length()) if self & (1 << i)]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class IntegrationType(_ApiFlag):
|
|
16
|
-
GUILD_INSTALL = 1 << 0 # API VALUE 0
|
|
17
|
-
USER_INSTALL = 1 << 1 # API VALUE 1
|
|
18
|
-
ALL = GUILD_INSTALL | USER_INSTALL
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class InteractionContext(_ApiFlag):
|
|
22
|
-
GUILD = 1 << 0 # API VALUE 0
|
|
23
|
-
BOT_DM = 1 << 1 # API VALUE 1
|
|
24
|
-
PRIVATE_CHANNEL = 1 << 2 # VALUE VALUE 2
|
|
25
|
-
ALL = GUILD | BOT_DM | PRIVATE_CHANNEL
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class InteractionType(IntEnum):
|
|
29
|
-
PING = 1
|
|
30
|
-
APPLICATION_COMMAND = 2
|
|
31
|
-
MESSAGE_COMPONENT = 3
|
|
32
|
-
APPLICATION_COMMAND_AUTOCOMPLETE = 4
|
|
33
|
-
MODAL_SUBMIT = 5
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class CommandType(IntEnum):
|
|
37
|
-
CHAT_INPUT = 1
|
|
38
|
-
USER = 2
|
|
39
|
-
MESSAGE = 3
|
|
40
|
-
PRIMARY_ENTRY_POINT = 4
|
|
41
|
-
|
|
42
|
-
CHAT = 1
|
|
43
|
-
ENTRY = 4
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
class InteractionCallbackType(IntEnum):
|
|
47
|
-
PONG = 1
|
|
48
|
-
CHANNEL_MESSAGE_WITH_SOURCE = 4
|
|
49
|
-
DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5
|
|
50
|
-
DEFERRED_UPDATE_MESSAGE = 6
|
|
51
|
-
UPDATE_MESSAGE = 7
|
|
52
|
-
APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8
|
|
53
|
-
MODAL = 9
|
|
54
|
-
PREMIUM_REQUIRED = 10
|
|
55
|
-
LAUNCH_ACTIVITY = 12
|
|
56
|
-
|
|
57
|
-
MESSAGE = CHANNEL_MESSAGE_WITH_SOURCE
|
|
58
|
-
DEFER = DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE
|
|
59
|
-
UPDATE = UPDATE_MESSAGE
|
|
60
|
-
AUTOCOMPLETE = APPLICATION_COMMAND_AUTOCOMPLETE_RESULT
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class MessageFlags(IntFlag):
|
|
64
|
-
"""Bit flags describing special message properties."""
|
|
65
|
-
|
|
66
|
-
CROSSPOSTED = 1 << 0
|
|
67
|
-
IS_CROSSPOST = 1 << 1
|
|
68
|
-
SUPPRESS_EMBEDS = 1 << 2
|
|
69
|
-
SOURCE_MESSAGE_DELETED = 1 << 3
|
|
70
|
-
URGENT = 1 << 4
|
|
71
|
-
HAS_THREAD = 1 << 5
|
|
72
|
-
EPHEMERAL = 1 << 6
|
|
73
|
-
LOADING = 1 << 7
|
|
74
|
-
FAILED_TO_MENTION_SOME_ROLES_IN_THREAD = 1 << 8
|
|
75
|
-
|
|
76
|
-
SUPPRESS_NOTIFICATIONS = 1 << 12
|
|
77
|
-
IS_VOICE_MESSAGE = 1 << 13
|
|
78
|
-
HAS_SNAPSHOT = 1 << 14
|
|
79
|
-
IS_COMPONENTS_V2 = 1 << 15
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
class ApplicationCommandOptionType(IntEnum):
|
|
83
|
-
SUB_COMMAND = 1
|
|
84
|
-
SUB_COMMAND_GROUP = 2
|
|
85
|
-
STRING = 3
|
|
86
|
-
INTEGER = 4
|
|
87
|
-
BOOLEAN = 5
|
|
88
|
-
USER = 6
|
|
89
|
-
CHANNEL = 7
|
|
90
|
-
ROLE = 8
|
|
91
|
-
MENTIONABLE = 9
|
|
92
|
-
NUMBER = 10
|
|
93
|
-
ATTACHMENT = 11
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
class DiscordModel(BaseModel):
|
|
97
|
-
model_config = ConfigDict(extra="ignore")
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
class User(DiscordModel):
|
|
101
|
-
id: Snowflake
|
|
102
|
-
username: str
|
|
103
|
-
descriminator: Optional[str] = None
|
|
104
|
-
global_name: Optional[str] = None
|
|
105
|
-
avatar: Optional[str] = None
|
|
106
|
-
bot: Optional[bool] = None
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class Member(DiscordModel):
|
|
110
|
-
user: Optional[User] = None
|
|
111
|
-
nick: Optional[str] = None
|
|
112
|
-
roles: list[Snowflake] = Field(default_factory=list)
|
|
113
|
-
permissions: Optional[str] = None
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
class ApplicationCommandInteractionOption(DiscordModel):
|
|
117
|
-
name: str
|
|
118
|
-
type: ApplicationCommandOptionType
|
|
119
|
-
value: Optional[str | int | float | bool] = None
|
|
120
|
-
options: list["ApplicationCommandInteractionOption"] = Field(default_factory=list)
|
|
121
|
-
focused: Optional[bool] = None
|
|
122
|
-
|
|
123
|
-
@property
|
|
124
|
-
def options_by_name(self) -> dict[str, "ApplicationCommandInteractionOption"]:
|
|
125
|
-
return {opt.name for opt in self.options}
|
|
126
|
-
|
|
127
|
-
def get_option_value(self, name: str, default: Any = None) -> Any:
|
|
128
|
-
option = self.options_by_name.get(name)
|
|
129
|
-
return option.value if option is not None else default
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
ApplicationCommandInteractionOption.model_rebuild()
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
class ApplicationCommandData(DiscordModel):
|
|
136
|
-
id: Snowflake
|
|
137
|
-
name: str
|
|
138
|
-
type: CommandType
|
|
139
|
-
guild_id: Optional[Snowflake] = None
|
|
140
|
-
target_id: Optional[Snowflake] = None
|
|
141
|
-
options: list[ApplicationCommandInteractionOption] = Field(default_factory=list)
|
|
142
|
-
|
|
143
|
-
@property
|
|
144
|
-
def options_by_name(self) -> dict[str, "ApplicationCommandInteractionOption"]:
|
|
145
|
-
return {opt.name: opt for opt in self.options}
|
|
146
|
-
|
|
147
|
-
def get_option_value(self, name: str, default: Any = None) -> Any:
|
|
148
|
-
option = self.options_by_name.get(name)
|
|
149
|
-
return option.value if option is not None else default
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
class Interaction(DiscordModel):
|
|
153
|
-
id: Snowflake
|
|
154
|
-
application_id: Snowflake
|
|
155
|
-
type: InteractionType
|
|
156
|
-
token: str
|
|
157
|
-
version: int
|
|
158
|
-
|
|
159
|
-
data: Optional[dict[str, Any]] = None
|
|
160
|
-
|
|
161
|
-
guild_id: Optional[Snowflake] = None
|
|
162
|
-
channel_id: Optional[Snowflake] = None
|
|
163
|
-
member: Optional[Member] = None
|
|
164
|
-
user: Optional[User] = None
|
|
165
|
-
locale: Optional[str] = None
|
|
166
|
-
guild_locale: Optional[str] = None
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
2
|
-
from abc import ABC, abstractmethod
|
|
3
|
-
from .models import (
|
|
4
|
-
InteractionCallbackType,
|
|
5
|
-
MessageFlags,
|
|
6
|
-
)
|
|
7
|
-
import asyncio
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class InteractionResponse(ABC):
|
|
11
|
-
|
|
12
|
-
@abstractmethod
|
|
13
|
-
def to_dict(self):
|
|
14
|
-
pass
|
|
15
|
-
|
|
16
|
-
async def __call__(self) -> dict:
|
|
17
|
-
return self.to_dict()
|
|
18
|
-
|
|
19
|
-
def __await__(self):
|
|
20
|
-
return self.__call__().__await__()
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
class PongResponse(InteractionResponse):
|
|
24
|
-
callback_type = InteractionCallbackType.PONG
|
|
25
|
-
|
|
26
|
-
def to_dict(self):
|
|
27
|
-
return {"type": self.callback_type}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
@dataclass(slots=True)
|
|
31
|
-
class MessageResponse(InteractionResponse):
|
|
32
|
-
callback_type = InteractionCallbackType.MESSAGE
|
|
33
|
-
content: str
|
|
34
|
-
ephemeral: bool = False
|
|
35
|
-
|
|
36
|
-
def __init__(self, content: str, ephemeral: bool = False):
|
|
37
|
-
self.content = content
|
|
38
|
-
self.flags = MessageFlags.EPHEMERAL if ephemeral else MessageFlags(0)
|
|
39
|
-
|
|
40
|
-
def to_payload(self):
|
|
41
|
-
return {"content": self.content, "flags": int(self.flags)}
|
|
42
|
-
|
|
43
|
-
def to_dict(self):
|
|
44
|
-
return {"type": self.callback_type, "data": self.to_payload()}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
@dataclass(slots=True)
|
|
48
|
-
class DeferResponse(InteractionResponse):
|
|
49
|
-
callback_type = InteractionCallbackType.DEFER
|
|
50
|
-
|
|
51
|
-
def __init__(self, ephemeral: bool = False, finish: callable = None):
|
|
52
|
-
self.flags = MessageFlags.EPHEMERAL if ephemeral else MessageFlags(0)
|
|
53
|
-
self.finish = finish
|
|
54
|
-
|
|
55
|
-
def to_dict(self):
|
|
56
|
-
return {"type": self.callback_type, "data": {"flags": int(self.flags)}}
|
|
57
|
-
|
|
58
|
-
async def __call__(self):
|
|
59
|
-
if self.finish:
|
|
60
|
-
asyncio.create_task(self.finish())
|
|
61
|
-
return self.to_dict()
|
|
File without changes
|
{fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{fastapi_interactions-0.1.0 → fastapi_interactions-0.1.1}/fastapi_interactions.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|