6 Commits

4 changed files with 50 additions and 22 deletions

View File

@@ -1,8 +1,18 @@
# Pycord REST
<div align="center">
<h1>Pycord REST</h1>
![PyPI - Version](https://img.shields.io/pypi/v/pycord-rest-bot)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pycord-rest-bot)
![PyPI - Types](https://img.shields.io/pypi/types/pycord-rest-bot)
![PyPI - License](https://img.shields.io/pypi/l/pycord-rest-bot)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Paillat-dev/pycord-rest/CI.yaml)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/Paillat-dev/pycord-rest/main.svg)](https://results.pre-commit.ci/latest/github/Paillat-dev/pycord-rest/main)
A lightweight wrapper for Discord's HTTP interactions and webhook events using py-cord
and FastAPI.
</div>
## Table of Contents
- [Overview](#overview)
@@ -14,6 +24,7 @@ and FastAPI.
- [Features](#features)
- [Interaction Handling](#interaction-handling)
- [Webhook Events](#webhook-events)
- [Type Safety](#type-safety)
- [Usage Examples](#usage-examples)
- [Basic Commands](#basic-commands)
- [Event Handling](#event-handling)
@@ -124,6 +135,15 @@ Handle Discord webhook events such as:
user
- **Entitlement creation** - When a user subscribes to your app's premium features
### Type Safety
Pycord REST is fully type-annotated and type-safe. It uses `basedpyright` for type
checking.
<!-- prettier-ignore -->
> [!NOTE]
> While Pycord REST itself is fully typed, the underlying py-cord library has limited type annotations, which may affect type checking in some areas.
## Usage Examples
<!-- prettier-ignore -->
@@ -243,6 +263,7 @@ These tools provide temporary URLs for testing without deploying to production.
**Development Tools**:
- **uv**: For dependency management
- **Ruff**: For linting and formatting
- **HashiCorp Copywrite**: For managing license headers
- **basedpyright**: For type checking

View File

@@ -1,12 +1,8 @@
# Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT
"""Basic Discord bot example using Pycord REST.
"""Example showing how to work with webhook events in Pycord REST."""
This is a minimal example showing how to create slash commands.
"""
import logging
import os
from dotenv import load_dotenv
@@ -16,10 +12,6 @@ from pycord_rest import App, ApplicationAuthorizedEvent
# Load environment variables from .env file
load_dotenv()
# Set up logging
logging.basicConfig(level=logging.DEBUG)
app = App()
@@ -31,7 +23,7 @@ async def on_application_authorized(event: ApplicationAuthorizedEvent) -> None:
else:
print(
f"Bot {event.user.display_name} ({event.user.id}) installed the application" # noqa: ISC003
f"Bot {event.user.display_name} ({event.user.id}) installed the application"
+ f" to guild {event.guild.name} ({event.guild.id})."
)

View File

@@ -17,7 +17,9 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.12"
"Programming Language :: Python :: 3.12",
"Typing :: Typed",
"Operating System :: OS Independent",
]
keywords = ["discord", "bot", "rest", "pycord"]
dependencies = [
@@ -116,5 +118,6 @@ extend-ignore = [
"FBT002",
"PLR2004",
"PLR0913",
"C901"
"C901",
"ISC003" # conflicts with basedpyright reportImplicitStringConcatenation
]

View File

@@ -11,7 +11,6 @@ import discord
import uvicorn
from discord import Entitlement, Interaction, InteractionType
from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, Response
from fastapi.exceptions import FastAPIError
from nacl.exceptions import BadSignatureError
from nacl.signing import VerifyKey
@@ -35,18 +34,26 @@ class ApplicationAuthorizedEvent:
)
class PycordRestError(discord.DiscordException):
pass
class InvalidCredentialsError(PycordRestError):
pass
class App(discord.Bot):
def __init__(self, *args: Any, **options: Any) -> None: # pyright: ignore [reportExplicitAny]
super().__init__(*args, **options) # pyright: ignore [reportUnknownMemberType]
self.app: FastAPI = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
self._app: FastAPI = FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
self.router: APIRouter = APIRouter()
self.public_key: str | None = None
self._public_key: str | None = None
@cached_property
def _verify_key(self) -> VerifyKey:
if self.public_key is None:
raise FastAPIError("No public key provided")
return VerifyKey(bytes.fromhex(self.public_key))
if self._public_key is None:
raise InvalidCredentialsError("No public key provided")
return VerifyKey(bytes.fromhex(self._public_key))
async def _dispatch_view(self, component_type: int, custom_id: str, interaction: Interaction) -> None:
# Code taken from ViewStore.dispatch
@@ -110,6 +117,7 @@ class App(discord.Bot):
async def process_application_commands( # noqa: PLR0912
self, interaction: Interaction, auto_sync: bool | None = None
) -> None:
# Code taken from super().process_application_commands
if auto_sync is None:
auto_sync = self._bot.auto_sync_commands # pyright: ignore [reportUnknownVariableType, reportUnknownMemberType]
# TODO: find out why the isinstance check below doesn't stop the type errors below # noqa: FIX002, TD002, TD003
@@ -231,15 +239,15 @@ class App(discord.Bot):
uvicorn_options: dict[str, Any] | None = None, # pyright: ignore [reportExplicitAny]
health: bool = True,
) -> None:
self.public_key = public_key
self._public_key = public_key
_ = self._process_interaction_factory()
_ = self._webhook_event_factory()
if health:
_ = self._health_factory()
self.app.include_router(self.router)
self._app.include_router(self.router)
uvicorn_options = uvicorn_options or {}
uvicorn_options["log_level"] = uvicorn_options.get("log_level", logging.root.level)
config = uvicorn.Config(self.app, **uvicorn_options)
config = uvicorn.Config(self._app, **uvicorn_options)
server = uvicorn.Server(config)
try:
self.dispatch("connect")
@@ -260,6 +268,10 @@ class App(discord.Bot):
uvicorn_options: dict[str, Any] | None = None, # pyright: ignore [reportExplicitAny]
health: bool = True,
) -> None:
if not token:
raise InvalidCredentialsError("No token provided")
if not public_key:
raise InvalidCredentialsError("No public key provided")
await self.login(token)
await self.connect(
token=token,