9 Commits

7 changed files with 76 additions and 65 deletions

View File

@@ -20,14 +20,17 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
check: [format, lint] check: [format, lint, basedpyright]
include: include:
- check: format - check: format
name: "Format Check" name: "Format Check"
command: "uv run ruff format --check ." command: "uv run ruff format --check src"
- check: lint - check: lint
name: "Lint Check" name: "Lint Check"
command: "uv run ruff check ." command: "uv run ruff check src"
- check: basedpyright
name: "Type Check"
command: "uv run basedpyright src"
name: ${{ matrix.name }} name: ${{ matrix.name }}

3
.gitignore vendored
View File

@@ -171,4 +171,5 @@ cython_debug/
.pypirc .pypirc
.python-version .python-version
.idea .idea
_version.py

View File

@@ -14,7 +14,7 @@ This project is built on:
## Installation ## Installation
```bash ```bash
pip install pycord-reactive-bot pip install pycord-rest-bot
``` ```
## Quick Start ## Quick Start
@@ -78,8 +78,8 @@ async def button(ctx):
This library works differently than traditional bots because it does not use Discord's WebSocket gateway: This library works differently than traditional bots because it does not use Discord's WebSocket gateway:
- **No Cache**: Since there's no gateway connection, there's no cache of guilds, channels, users, etc. - **No Cache**: Since there's no gateway connection, there's no cache of guilds, channels, users, etc.
- **Limited API Methods**: Many standard Discord.py/py-cord methods that rely on cache won't work properly: - **Limited API Methods**: Many standard py-cord methods that rely on cache won't work properly:
- `app.get_channel()`, `app.get_guild()`, `app.get_user()` - `app.get_channel()`, `app.get_guild()`, `app.get_user()`, etc.
- Presence updates - Presence updates
- Voice support - Voice support
- Member tracking - Member tracking

View File

@@ -1,12 +1,22 @@
[project] [project]
name = "pycord-rest-bot" name = "pycord-rest-bot"
version = "0.1.0" dynamic = ["version", "urls"]
description = "A discord rest-bot wrapper for pycord" description = "A discord rest-bot wrapper for pycord"
readme = "README.md" readme = "README.md"
authors = [ authors = [
{ name = "Paillat-dev", email = "me@paillat.dev" } { name = "Paillat-dev", email = "me@paillat.dev" }
] ]
license = "MIT"
requires-python = "==3.12.*" requires-python = "==3.12.*"
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12"
]
keywords = ["discord", "bot", "rest", "pycord"]
dependencies = [ dependencies = [
"fastapi>=0.115.11", "fastapi>=0.115.11",
"orjson>=3.10.15", "orjson>=3.10.15",
@@ -15,7 +25,6 @@ dependencies = [
"uvicorn>=0.34.0", "uvicorn>=0.34.0",
] ]
[dependency-groups] [dependency-groups]
dev = [ dev = [
"basedpyright>=1.28.1", "basedpyright>=1.28.1",
@@ -24,18 +33,37 @@ dev = [
] ]
[build-system] [build-system]
requires = ["hatchling"] requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[tool.hatch.version]
source = "vcs"
[tool.hatch.build.hooks.vcs]
version-file = "src/pycord_rest/_version.py"
[tool.hatch.metadata.hooks.vcs.urls]
Homepage = "https://github.com/Paillat-dev/pycord-rest"
source_archive = "https://github.com/Paillat-dev/pycord-rest/archive/{commit_hash}.zip"
[tool.hatchling] [tool.hatchling]
name = "pycord-rest-bot" name = "pycord-rest-bot"
[tool.hatch.build.targets.wheel] [tool.hatch.build]
packages = ["src/pycord_rest"] exclude = [
".copywrite.hcl",
".github",
".python-version",
"uv.lock",
]
include = [
"src/pycord_rest/",
]
[tool.pyright] [tool.pyright]
pythonVersion = "3.12" pythonVersion = "3.12"
reportAny = false reportAny = false
executionEnvironments = [{ root = "src/pycord_rest/_version.py", reportDeprecated = false }]
[tool.ruff] [tool.ruff]
target-version = "py312" target-version = "py312"
@@ -44,20 +72,17 @@ indent-width = 4
[tool.ruff.format] [tool.ruff.format]
quote-style = "double" quote-style = "double"
indent-style = "space" indent-style = "space"
skip-magic-trailing-comma = false skip-magic-trailing-comma = false
line-ending = "auto" line-ending = "auto"
docstring-code-format = false docstring-code-format = false
docstring-code-line-length = "dynamic" docstring-code-line-length = "dynamic"
exclude = [
"src/pycord_rest/_version.py"
]
[tool.ruff.lint] [tool.ruff.lint]
select = ["ALL"] select = ["ALL"]
per-file-ignores = {}
extend-ignore = [ extend-ignore = [
"N999", "N999",
"D104", "D104",
@@ -85,6 +110,3 @@ extend-ignore = [
"PLR0913", "PLR0913",
"C901" "C901"
] ]
[tool.uv.workspace]
members = ["test"]

View File

@@ -1,6 +1,8 @@
# Copyright (c) Paillat-dev # Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from discord import * # noqa: F403, I001 # pyright: ignore [reportWildcardImportFromLibrary]
from .app import App from .app import App
__all__ = ["App"] Bot = App
__all__ = ["App", "Bot"]

View File

@@ -10,7 +10,6 @@ import aiohttp
import discord import discord
import uvicorn import uvicorn
from discord import Interaction, InteractionType from discord import Interaction, InteractionType
from discord.ui.view import ViewStore
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request
from fastapi.exceptions import FastAPIError from fastapi.exceptions import FastAPIError
from nacl.exceptions import BadSignatureError from nacl.exceptions import BadSignatureError
@@ -19,34 +18,10 @@ from nacl.signing import VerifyKey
logger = logging.getLogger("pycord.rest") logger = logging.getLogger("pycord.rest")
async def _dispatch_view(view_store: ViewStore, component_type: int, custom_id: str, interaction: Interaction) -> None:
# Code taken from ViewStore.dispatch
view_store._ViewStore__verify_integrity() # noqa: SLF001 # pyright: ignore [reportUnknownMemberType, reportAttributeAccessIssue]
message_id: int | None = interaction.message and interaction.message.id
key = (component_type, message_id, custom_id)
value = view_store._views.get(key) or view_store._views.get( # pyright: ignore [reportUnknownVariableType, reportUnknownMemberType, reportPrivateUsage] # noqa: SLF001
(component_type, None, custom_id)
)
if value is None:
return
view, item = value # pyright: ignore [reportUnknownVariableType]
item.refresh_state(interaction)
# Code taken from View._dispatch_item
if view._View__stopped.done(): # noqa: SLF001 # pyright: ignore [reportAttributeAccessIssue, reportUnknownMemberType]
return
if interaction.message:
view.message = interaction.message
await view._scheduled_task(item, interaction) # noqa: SLF001 # pyright: ignore [reportPrivateUsage, reportUnknownMemberType]
class App(discord.Bot): class App(discord.Bot):
def __init__(self, *args: Any, **options: Any) -> None: # pyright: ignore [reportExplicitAny] def __init__(self, *args: Any, **options: Any) -> None: # pyright: ignore [reportExplicitAny]
super().__init__(*args, **options) # pyright: ignore [reportUnknownMemberType] super().__init__(*args, **options) # pyright: ignore [reportUnknownMemberType]
self.app: FastAPI = FastAPI() self.app: FastAPI = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
self.router: APIRouter = APIRouter() self.router: APIRouter = APIRouter()
self.public_key: str | None = None self.public_key: str | None = None
@@ -89,22 +64,31 @@ class App(discord.Bot):
raise HTTPException(status_code=401, detail="Invalid request signature") from e raise HTTPException(status_code=401, detail="Invalid request signature") from e
async def _process_interaction(self, request: Request) -> dict[str, Any]: # pyright: ignore [reportExplicitAny] async def _process_interaction(self, request: Request) -> dict[str, Any]: # pyright: ignore [reportExplicitAny]
# Code taken from ConnectionState.parse_interaction_create
data = await request.json() data = await request.json()
interaction = Interaction(data=data, state=self._connection) interaction = Interaction(data=data, state=self._connection)
if data["type"] == 3: # interaction component match interaction.type:
custom_id: str = interaction.data["custom_id"] # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript, reportUnknownVariableType] case InteractionType.component:
component_type = interaction.data["component_type"] # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript, reportUnknownVariableType] custom_id: str = interaction.data["custom_id"] # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript, reportUnknownVariableType]
await self._dispatch_view(component_type, custom_id, interaction) # pyright: ignore [reportUnknownArgumentType] component_type = interaction.data["component_type"] # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript, reportUnknownVariableType]
await self._dispatch_view(component_type, custom_id, interaction) # pyright: ignore [reportUnknownArgumentType]
if interaction.type == InteractionType.modal_submit: case InteractionType.modal_submit:
user_id, custom_id = ( # pyright: ignore [reportUnknownVariableType] user_id, custom_id = ( # pyright: ignore [reportUnknownVariableType]
interaction.user.id, # pyright: ignore [reportOptionalMemberAccess] interaction.user.id, # pyright: ignore [reportOptionalMemberAccess]
interaction.data["custom_id"], # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript] interaction.data["custom_id"], # pyright: ignore [reportGeneralTypeIssues, reportOptionalSubscript]
) )
await self._connection._modal_store.dispatch(user_id, custom_id, interaction) # pyright: ignore [reportUnknownArgumentType, reportPrivateUsage] # noqa: SLF001 await self._connection._modal_store.dispatch(user_id, custom_id, interaction) # pyright: ignore [reportUnknownArgumentType, reportPrivateUsage] # noqa: SLF001
await self.process_application_commands(interaction) case InteractionType.ping:
return {"type": 1}
case InteractionType.application_command | InteractionType.auto_complete:
await self.process_application_commands(interaction)
self.dispatch("interaction", interaction)
return {"ok": True} return {"ok": True}
@override
async def on_interaction(self, *args: Never, **kwargs: Never) -> None:
pass
@override @override
async def process_application_commands( # noqa: PLR0912 async def process_application_commands( # noqa: PLR0912
self, interaction: Interaction, auto_sync: bool | None = None self, interaction: Interaction, auto_sync: bool | None = None
@@ -143,8 +127,8 @@ class App(discord.Bot):
return self._bot.dispatch("unknown_application_command", interaction) return self._bot.dispatch("unknown_application_command", interaction)
if interaction.type is InteractionType.auto_complete: if interaction.type is InteractionType.auto_complete:
self._bot.dispatch("application_command_auto_complete", interaction, command)
await super().on_application_command_auto_complete(interaction, command) # pyright: ignore [reportArgumentType, reportUnknownMemberType] await super().on_application_command_auto_complete(interaction, command) # pyright: ignore [reportArgumentType, reportUnknownMemberType]
return self._bot.dispatch("application_command_auto_complete", interaction, command)
return None return None
ctx = await self.get_application_context(interaction) ctx = await self.get_application_context(interaction)
@@ -212,7 +196,7 @@ class App(discord.Bot):
self, self,
token: str, token: str,
public_key: str, public_key: str,
uvicorn_options: dict[str, Any] | None = None, uvicorn_options: dict[str, Any] | None = None, # pyright: ignore [reportExplicitAny]
health: bool = True, health: bool = True,
) -> None: ) -> None:
await self.login(token) await self.login(token)

3
uv.lock generated
View File

@@ -298,8 +298,7 @@ wheels = [
] ]
[[package]] [[package]]
name = "pycord-rest" name = "pycord-rest-bot"
version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "fastapi" }, { name = "fastapi" },