From 4518cbf30e453d728f2758fc538bca2677843a56 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Thu, 2 Jul 2026 19:52:25 -0400 Subject: [PATCH] =?UTF-8?q?init:=20per-channel=20discord=20webhook=20manag?= =?UTF-8?q?ement=20for=20discord.py=20=E2=80=94=20get-or-create,=20persist?= =?UTF-8?q?,=20cap,=20send?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: disqualifier --- .gitignore | 15 +++++++++++ README.md | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be64430 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# claude +.claude/ + +# python +__pycache__/ +*.py[cod] +*.egg-info/ +build/ +dist/ +.eggs/ + +# env +.venv/ +venv/ +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..9c4a993 --- /dev/null +++ b/README.md @@ -0,0 +1,73 @@ +# dpy_webhooks + +Per-channel Discord webhook management for discord.py: get-or-create one webhook per channel, +persist its `{id, token}` so it survives restarts, enforce Discord's 10-per-channel cap, and +send through it. One webhook per channel, reused across sends — instead of creating a new one +each time (wasteful, rate-limited, and it leaks toward the cap). + +## Install + +``` +dpy_webhooks @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_webhooks.git@v0.1.0 +``` + +## Usage + +```python +from dpy_webhooks import DPYWebhooks + +hooks = DPYWebhooks(bot) # store optional; in-memory if omitted +await hooks.send(channel, content="hi", embeds=[e]) # get-or-create + send +wh = await hooks.get_or_create(channel) # reuse the webhook directly +``` + +With a persistence store so webhooks survive a restart: + +```python +hooks = DPYWebhooks(bot, store=my_store) +``` + +## What you inject + +- **`client`** — your `discord.Client` / `commands.Bot`. Used to rebuild persisted webhooks + via `Webhook.partial(id, token, client=...)`, so discord.py supplies its own session and + state. Requires **`discord.py>=2.2`**. +- **`store`** (optional) — any object matching the `WebhookStore` protocol. Omit it and the + lib uses a non-durable in-memory store (records lost on restart). + +### The store protocol + +```python +class WebhookStore(Protocol): + async def get(self, channel_id: int) -> dict | None: ... # {"webhook_id", "token", "guild_id"} or None + async def set(self, channel_id: int, record: dict) -> None: ... + async def delete(self, channel_id: int) -> None: ... +``` + +A file- or mongo-backed store satisfies this — the lib depends only on the protocol, not on +any concrete store class. `InMemoryWebhookStore` is the bundled default. + +## API & contract + +The module docstring (`help(dpy_webhooks)` / IDE hover) is the source of truth. In short: + +- `get_or_create(channel, *, evict_oldest=False) -> discord.Webhook` +- `get(channel) -> discord.Webhook | None` — cache then store; never creates +- `send(channel, **kwargs) -> discord.WebhookMessage` — get-or-create then send, with + self-heal +- `clear(channel) -> None` — delete server-side + store record (idempotent) +- `count(channel) -> int` — webhooks currently on the channel + +**Self-heal.** If `send` hits a dead webhook (deleted server-side / invalid token), the lib +clears the record, recreates the webhook, and retries the send **once**; a second failure +raises loud. + +**Fail-loud.** Nothing is swallowed to `None`. A channel already at 10 webhooks raises +`WebhookCapacityError` (pass `evict_oldest=True` to reclaim the oldest instead). Missing +Manage Webhooks perms propagate discord's own `Forbidden` unwrapped. Store failures propagate +— the store owns its durability. + +## Versioning + +Tagged `vX.Y.Z`; pin a tag in your install line. Targets `discord.py>=2.2` (not +`discord.py-self`).