9 Commits

4 changed files with 111 additions and 20 deletions

View File

@@ -1,18 +1,28 @@
<div align="center"> <div align="center">
<h1>Pycord REST</h1> <h1>Pycord REST</h1>
![PyPI - Version](https://img.shields.io/pypi/v/pycord-rest-bot) <!-- badges -->
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pycord-rest-bot)
![PyPI - Types](https://img.shields.io/pypi/types/pycord-rest-bot) [![PyPI - Version](https://img.shields.io/pypi/v/pycord-rest-bot)](https://pypi.org/project/pycord-rest-bot/)
![PyPI - License](https://img.shields.io/pypi/l/pycord-rest-bot) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pycord-rest-bot)](https://pypi.org/project/pycord-rest-bot/)
![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Paillat-dev/pycord-rest/CI.yaml) [![PyPI - Types](https://img.shields.io/pypi/types/pycord-rest-bot)](https://pypi.org/project/pycord-rest-bot/)
[![PyPI - License](https://img.shields.io/pypi/l/pycord-rest-bot)](https://pypi.org/project/pycord-rest-bot/)
[![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Paillat-dev/pycord-rest/CI.yaml)](https://github.com/Paillat-dev/pycord-rest/actions/workflows/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) [![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)
<!-- end badges -->
<!-- short description -->
A lightweight wrapper for Discord's HTTP interactions and webhook events using py-cord A lightweight wrapper for Discord's HTTP interactions and webhook events using py-cord
and FastAPI. and FastAPI.
<!-- end short description -->
</div> </div>
<!-- toc -->
## Table of Contents ## Table of Contents
- [Overview](#overview) - [Overview](#overview)
@@ -31,6 +41,7 @@ and FastAPI.
- [Custom Routes](#custom-routes) - [Custom Routes](#custom-routes)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Limitations](#limitations) - [Limitations](#limitations)
- [Getting Help](#getting-help)
- [Development](#development) - [Development](#development)
- [Local Testing](#local-testing) - [Local Testing](#local-testing)
- [Contributing](#contributing) - [Contributing](#contributing)
@@ -59,6 +70,8 @@ pip install pycord-rest-bot --prerelease=allow
> [!NOTE] > [!NOTE]
> The package is currently in pre-release. > The package is currently in pre-release.
<!-- quick-start -->
## Quick Start ## Quick Start
```python ```python
@@ -104,7 +117,7 @@ Unlike traditional WebSocket-based Discord bots, HTTP-based applications:
1. Create an application on the 1. Create an application on the
[Discord Developer Portal](https://discord.com/developers/applications) [Discord Developer Portal](https://discord.com/developers/applications)
2. Copy your public key to verify signatures 2. Copy your public key to verify signatures
3. Run your FastAPI server 3. Run the Pycord REST app
4. Configure the endpoints: 4. Configure the endpoints:
- **Interactions Endpoint URL** - For slash commands and component interactions - **Interactions Endpoint URL** - For slash commands and component interactions
@@ -232,6 +245,22 @@ Since Pycord REST doesn't use Discord's WebSocket gateway:
- Member tracking - Member tracking
- **Limited Events** - Only interaction-based and webhook events work - **Limited Events** - Only interaction-based and webhook events work
## Getting Help
If you encounter issues or have questions about pycord-rest:
- **GitHub Issues**:
[Submit a bug report or feature request](https://github.com/Paillat-dev/pycord-rest/issues)
- **Discord Support**:
- For py-cord related questions: Join the
[Pycord Official Server](https://discord.gg/pycord)
- For pycord-rest specific help: Join the
[Pycord Official Server](https://discord.gg/pycord) and mention `@paillat`
<!-- prettier-ignore -->
> [!TIP]
> Before asking for help, check if your question is already answered in the [examples directory](/examples) or existing GitHub issues.
## Development ## Development
### Local Testing ### Local Testing

View File

@@ -1,12 +1,11 @@
[build-system] [build-system]
requires = ["hatchling", "hatch-vcs"] requires = ["hatchling", "hatch-vcs", "hatch-fancy-pypi-readme"]
build-backend = "hatchling.build" build-backend = "hatchling.build"
[project] [project]
name = "pycord-rest-bot" name = "pycord-rest-bot"
dynamic = ["version", "urls"] dynamic = ["version", "urls", "readme"]
description = "A discord rest-bot wrapper for pycord" description = "A lightweight wrapper for Discord's HTTP interactions and webhook events using py-cord and FastAPI"
readme = "README.md"
authors = [ authors = [
{ name = "Paillat-dev", email = "me@paillat.dev" } { name = "Paillat-dev", email = "me@paillat.dev" }
] ]
@@ -47,6 +46,34 @@ version-file = "src/pycord_rest/_version.py"
Homepage = "https://github.com/Paillat-dev/pycord-rest" Homepage = "https://github.com/Paillat-dev/pycord-rest"
source_archive = "https://github.com/Paillat-dev/pycord-rest/archive/{commit_hash}.zip" source_archive = "https://github.com/Paillat-dev/pycord-rest/archive/{commit_hash}.zip"
[tool.hatch.metadata.hooks.fancy-pypi-readme]
content-type = "text/markdown"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
path = "README.md"
start-after = "<!-- badges -->\n"
end-before = "\n<!-- end badges -->"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
text = "\n\n---\n"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
path = "README.md"
start-after = "## Overview\n"
end-before = "\n## Installation"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.fragments]]
path = "README.md"
start-after = "<!-- quick-start -->"
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = '\[(.+?)\]\(((?!https?://)\S+?)\)'
replacement = '[\1](https://github.com/Paillat-dev/pycord-rest/tree/main\g<2>)'
[[tool.hatch.metadata.hooks.fancy-pypi-readme.substitutions]]
pattern = '\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]'
replacement = '**\1**:'
[tool.hatchling] [tool.hatchling]
name = "pycord-rest-bot" name = "pycord-rest-bot"

View File

@@ -1,7 +1,9 @@
# Copyright (c) Paillat-dev # Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import functools
import logging import logging
import warnings
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
from functools import cached_property from functools import cached_property
from typing import Any, Never, override from typing import Any, Never, override
@@ -14,6 +16,7 @@ from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, Respons
from nacl.exceptions import BadSignatureError from nacl.exceptions import BadSignatureError
from nacl.signing import VerifyKey from nacl.signing import VerifyKey
from .errors import InvalidCredentialsError
from .models import EventType, WebhookEventPayload, WebhookType from .models import EventType, WebhookEventPayload, WebhookType
logger = logging.getLogger("pycord.rest") logger = logging.getLogger("pycord.rest")
@@ -34,21 +37,38 @@ class ApplicationAuthorizedEvent:
) )
class PycordRestError(discord.DiscordException): def not_supported[T, U](func: Callable[[T], U]) -> Callable[[T], U]:
pass @functools.wraps(func)
def inner(*args: T, **kwargs: T) -> U:
logger.warning(f"{func.__qualname__} is not supported by REST apps.")
warnings.warn(
f"{func.__qualname__} is not supported by REST apps.",
SyntaxWarning,
stacklevel=2,
)
return func(*args, **kwargs)
return inner
class InvalidCredentialsError(PycordRestError):
pass
class App(discord.Bot): class App(discord.Bot):
_UvicornConfig: type[uvicorn.Config] = uvicorn.Config
_UvicornServer: type[uvicorn.Server] = uvicorn.Server
_FastAPI: type[FastAPI] = FastAPI
_APIRouter: type[APIRouter] = APIRouter
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(openapi_url=None, docs_url=None, redoc_url=None) self._app: FastAPI = self._FastAPI(openapi_url=None, docs_url=None, redoc_url=None)
self.router: APIRouter = APIRouter() self.router: APIRouter = self._APIRouter()
self._public_key: str | None = None self._public_key: str | None = None
@property
@override
@not_supported
def latency(self) -> float:
return 0.0
@cached_property @cached_property
def _verify_key(self) -> VerifyKey: def _verify_key(self) -> VerifyKey:
if self._public_key is None: if self._public_key is None:
@@ -247,8 +267,8 @@ class App(discord.Bot):
self._app.include_router(self.router) self._app.include_router(self.router)
uvicorn_options = uvicorn_options or {} uvicorn_options = uvicorn_options or {}
uvicorn_options["log_level"] = uvicorn_options.get("log_level", logging.root.level) uvicorn_options["log_level"] = uvicorn_options.get("log_level", logging.root.level)
config = uvicorn.Config(self._app, **uvicorn_options) config = self._UvicornConfig(self._app, **uvicorn_options)
server = uvicorn.Server(config) server = self._UvicornServer(config)
try: try:
self.dispatch("connect") self.dispatch("connect")
await server.serve() await server.serve()
@@ -258,7 +278,10 @@ class App(discord.Bot):
@override @override
async def close(self) -> None: async def close(self) -> None:
pass self._closed: bool = True
await self.http.close()
self._ready.clear()
@override @override
async def start( # pyright: ignore [reportIncompatibleMethodOverride] async def start( # pyright: ignore [reportIncompatibleMethodOverride]

12
src/pycord_rest/errors.py Normal file
View File

@@ -0,0 +1,12 @@
# Copyright (c) Paillat-dev
# SPDX-License-Identifier: MIT
import discord
class PycordRestError(discord.DiscordException):
pass
class InvalidCredentialsError(PycordRestError):
pass