├── .gitignore
├── src
├── core
│ ├── __init__.py
│ └── command_handler.py
├── reqs.txt
├── utils
│ ├── __init__.py
│ ├── verify.py
│ └── constants.py
├── example.env
├── upsert.py
├── main.py
└── commands.py
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .env
3 | __pycache__
--------------------------------------------------------------------------------
/src/core/__init__.py:
--------------------------------------------------------------------------------
1 | from .command_handler import *
--------------------------------------------------------------------------------
/src/reqs.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | requests
4 | python-dotenv
--------------------------------------------------------------------------------
/src/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .constants import *
2 | from .verify import *
--------------------------------------------------------------------------------
/src/example.env:
--------------------------------------------------------------------------------
1 | CLIENT_PUBLIC_KEY = ""
2 | APPLICATION_ID = ""
3 | TOKEN = ""
4 |
--------------------------------------------------------------------------------
/src/core/command_handler.py:
--------------------------------------------------------------------------------
1 | from commands import commands
2 |
3 |
4 | def get_command(json_data: dict):
5 | for command in commands:
6 | if command.name == json_data["data"]["name"]:
7 | return command
8 | return None
9 |
10 |
11 | class CommandHandler:
12 | def __init__(self, json_data: dict):
13 | self.json_data = json_data
14 |
15 | async def execute(self):
16 | command = get_command(self.json_data)
17 | if command is None:
18 | return None
19 | result = await command.respond(self.json_data)
20 | return result
21 |
--------------------------------------------------------------------------------
/src/upsert.py:
--------------------------------------------------------------------------------
1 | from dotenv import load_dotenv
2 | import os
3 | from requests import Request, Session
4 | from commands import commands
5 |
6 | load_dotenv()
7 |
8 | TOKEN = os.environ.get("TOKEN")
9 | APPLICATION_ID = os.environ.get("APPLICATION_ID")
10 |
11 | s = Session()
12 | s.headers.update({"Authorization": f"Bot {TOKEN}"})
13 |
14 | r = s.send(
15 | s.prepare_request(
16 | Request(
17 | "PUT",
18 | f"https://discord.com/api/v10/applications/{APPLICATION_ID}/commands",
19 | json=[c.to_dict() for c in commands],
20 | )
21 | )
22 | )
23 |
24 | print(f"{r.json()}")
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Gaurav
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.
22 |
--------------------------------------------------------------------------------
/src/main.py:
--------------------------------------------------------------------------------
1 | import uvicorn
2 | from fastapi import FastAPI, Request
3 | from starlette.middleware import Middleware
4 | from utils import (
5 | InteractionType,
6 | InteractionResponseType,
7 | InteractionResponseFlags,
8 | CustomHeaderMiddleware,
9 | )
10 | from core import CommandHandler
11 |
12 | app = FastAPI(middleware=[Middleware(CustomHeaderMiddleware)])
13 |
14 |
15 | @app.post("/")
16 | async def interactions(request: Request):
17 | json_data = await request.json()
18 |
19 | if json_data["type"] == InteractionType.PING:
20 | # A ping test sent by discord to check if your server works
21 | return {"type": InteractionResponseType.PONG}
22 |
23 | if json_data["type"] == InteractionType.APPLICATION_COMMAND:
24 | # We only want to handle slash commands
25 | handler = CommandHandler(json_data)
26 | result = await handler.execute()
27 | if result is not None:
28 | return result
29 |
30 | # No result means either the command is not found or the command is not registered
31 | # Or you havent implemented the command yet
32 | # Or you forgot to return the result
33 | # Or idk just check it man i cant do everything for you
34 |
35 | return {
36 | "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
37 | "data": {
38 | "content": "Hello Buddy, This is a by default message for any unrecognized interaction.",
39 | "flags": InteractionResponseFlags.EPHEMERAL,
40 | },
41 | }
42 |
43 |
44 | if __name__ == "__main__":
45 | uvicorn.run(app, host="0.0.0.0", port=3000)
46 |
--------------------------------------------------------------------------------
/src/utils/verify.py:
--------------------------------------------------------------------------------
1 | from nacl.signing import VerifyKey
2 | from starlette.middleware.base import BaseHTTPMiddleware
3 | from fastapi import Request, Response
4 | from dotenv import load_dotenv
5 | import os
6 |
7 | load_dotenv()
8 |
9 | CLIENT_PUBLIC_KEY = os.environ.get("CLIENT_PUBLIC_KEY")
10 |
11 |
12 | def verify_key(
13 | raw_body: bytes, signature: str, timestamp: str, client_public_key: str
14 | ) -> bool:
15 | message = timestamp.encode() + raw_body
16 | try:
17 | vk = VerifyKey(bytes.fromhex(client_public_key))
18 | vk.verify(message, bytes.fromhex(signature))
19 | return True
20 | except Exception as ex:
21 | print(ex)
22 | return False
23 |
24 |
25 | async def set_body(request: Request, body: bytes):
26 | async def receive():
27 | return {"type": "http.request", "body": body}
28 |
29 | request._receive = receive
30 |
31 |
32 | async def get_body(request: Request) -> bytes:
33 | body = await request.body()
34 | await set_body(request, body)
35 | return body
36 |
37 |
38 | class CustomHeaderMiddleware(BaseHTTPMiddleware):
39 | async def dispatch(self, request, call_next):
40 | signature = request.headers["X-Signature-Ed25519"]
41 | timestamp = request.headers["X-Signature-Timestamp"]
42 | request_body = await get_body(request)
43 | if (
44 | signature is None
45 | or timestamp is None
46 | or not verify_key(request_body, signature, timestamp, CLIENT_PUBLIC_KEY)
47 | ):
48 | return Response("Bad request signature", status_code=401)
49 | response = await call_next(request)
50 | return response
51 |
--------------------------------------------------------------------------------
/src/utils/constants.py:
--------------------------------------------------------------------------------
1 | from enum import Enum
2 |
3 |
4 | class InteractionType:
5 | PING = 1
6 | APPLICATION_COMMAND = 2
7 | MESSAGE_COMPONENT = 3
8 | APPLICATION_COMMAND_AUTOCOMPLETE = 4
9 | MODAL_SUBMIT = 5
10 |
11 |
12 | class InteractionResponseType:
13 | PONG = 1
14 | CHANNEL_MESSAGE_WITH_SOURCE = 4
15 | DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE = 5
16 | DEFERRED_UPDATE_MESSAGE = 6
17 | UPDATE_MESSAGE = 7
18 | APPLICATION_COMMAND_AUTOCOMPLETE_RESULT = 8
19 | MODAL = 9
20 |
21 |
22 | class InteractionResponseFlags:
23 | EPHEMERAL = 1 << 6
24 |
25 |
26 | class ApplicationCommandOptionType(Enum):
27 | SUB_COMMAND = 1
28 | SUB_COMMAND_GROUP = 2
29 | STRING = 3
30 | INTEGER = 4
31 | BOOLEAN = 5
32 | USER = 6
33 | CHANNEL = 7
34 | ROLE = 8
35 |
36 |
37 | class Option:
38 | def __init__(
39 | self,
40 | name: str,
41 | type: ApplicationCommandOptionType,
42 | description: str = "...",
43 | required: bool = False,
44 | ):
45 | self.name = name
46 | self.description = description
47 | self.type = type
48 | self.required = required
49 |
50 | def to_dict(self):
51 | return {
52 | "name": self.name,
53 | "description": self.description,
54 | "type": self.type.value,
55 | "required": self.required,
56 | }
57 |
58 |
59 | class SlashCommand:
60 | def __init__(self, name: str, description: str = "...", options: list = None):
61 | self.name = name
62 | self.description = description
63 | self.options = options or []
64 |
65 | async def respond(self, json_data: dict):
66 | # This function is async just so that fastapi supports async poggies
67 | ...
68 |
69 | def to_dict(self):
70 | return {
71 | "type": 1, # Slash command
72 | "name": self.name,
73 | "description": self.description,
74 | "options": [option.to_dict() for option in self.options],
75 | }
76 |
--------------------------------------------------------------------------------
/src/commands.py:
--------------------------------------------------------------------------------
1 | from utils import (
2 | SlashCommand,
3 | Option,
4 | InteractionResponseFlags,
5 | InteractionResponseType,
6 | ApplicationCommandOptionType,
7 | )
8 |
9 |
10 | class HelloCommand(SlashCommand):
11 | def __init__(self):
12 | super().__init__(
13 | name="hello",
14 | description="Say hello to someone",
15 | options=[
16 | Option(
17 | name="user",
18 | type=ApplicationCommandOptionType.USER,
19 | description="The user to say hello",
20 | required=True,
21 | ),
22 | ],
23 | )
24 |
25 | async def respond(self, json_data: dict):
26 | # This function is async just so that fastapi supports async poggies
27 | user_id = json_data["data"]["options"][0]["value"]
28 | return {
29 | "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
30 | "data": {
31 | "content": f"Hello <@!{user_id}>",
32 | "flags": InteractionResponseFlags.EPHEMERAL,
33 | },
34 | }
35 |
36 |
37 | class ByeCommand(SlashCommand):
38 | def __init__(self):
39 | super().__init__(
40 | name="bye",
41 | description="Say bye to someone",
42 | options=[
43 | Option(
44 | name="user",
45 | type=ApplicationCommandOptionType.USER,
46 | description="The user to say bye",
47 | required=True,
48 | ),
49 | ],
50 | )
51 |
52 | async def respond(self, json_data: dict):
53 | # This function is async just so that fastapi supports async poggies
54 | user_id = json_data["data"]["options"][0]["value"]
55 | return {
56 | "type": InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE,
57 | "data": {
58 | "content": f"Bye <@!{user_id}>",
59 | "flags": InteractionResponseFlags.EPHEMERAL,
60 | },
61 | }
62 |
63 |
64 | commands = [HelloCommand(), ByeCommand()]
65 | # NOTE: Please updates this `commands` list with your commands
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [![Contributors][contributors-shield]][contributors-url]
4 | [![Forks][forks-shield]][forks-url]
5 | [![Stargazers][stars-shield]][stars-url]
6 | [![Issues][issues-shield]][issues-url]
7 | [![MIT License][license-shield]][license-url]
8 |
9 |
10 |
11 |
15 | An awesome open source project to demonstrate usage of discord http interactions in python!
16 |
17 | ·
18 | Report Bug
19 | ·
20 | Request Feature
21 |