fix: skip token-less webhooks in pick(); self-heal deletes the dead pick, not the managed webhook

dpywebhooks-1 (HIGH): pick()/send_any could select token-less channel-follower
(type 2) or application-owned (type 3) webhooks returned by channel.webhooks().
Webhook.send raises a raw ValueError on those before any HTTP call, and
send_any only caught discord.HTTPException, so the ValueError bypassed
fallback and self-heal entirely. strategy='first' picks the lowest id, so any
channel following an announcement channel could be permanently broken. Fix:
pick() now filters candidates to w.token is not None.

dpywebhooks-2 (HIGH): send_any's self-heal always called clear(), which
targets the managed webhook (cache/store), not the dead webhook that was
actually picked. When the dead pick and the managed webhook differed, this
deleted a healthy managed webhook and churned delete+create toward the
10-cap on every send. Fix: self-heal now deletes the dead picked webhook
directly (bot-auth webhook.delete() authorizes on it without its own token);
clear() is only used when the pick IS the managed webhook.

dpywebhooks-8 (nit): count() and _create() now read live webhooks through
list() instead of calling channel.webhooks() directly, so the token filter
and any future list() change reach all three call sites from one source.

Version 0.1.0 -> 0.1.1. README + module docstrings updated to document the
token filter and the corrected self-heal delete target.

Signed-off-by: disqualifier <dev@disqualifier.me>
This commit is contained in:
2026-07-03 15:31:06 -04:00
parent 74fd1bc4a5
commit ce48f2aea8
4 changed files with 49 additions and 20 deletions
+8 -4
View File
@@ -8,7 +8,7 @@ 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
dpy_webhooks @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_webhooks.git@v0.1.1
```
## Usage
@@ -38,9 +38,13 @@ await hooks.send_any(channel, strategy="round_robin", content="hi") # send via
`pick` strategies are `"first"` (oldest by id), `"random"`, and `"round_robin"` (per-channel
in-process index that wraps on the live count and rebuilds if the count changed); an unknown
strategy raises `ValueError`. `send_any` sends through a picked existing webhook, **falling
back to `get_or_create`** when the channel has none, and reuses the same dead-webhook
self-heal. None of these three create or touch the store — they read live from the channel.
strategy raises `ValueError`. `pick` only considers webhooks that have a token — channel-
follower and application-owned webhooks have none and can't be sent through, so they're
skipped rather than picked. `send_any` sends through a picked existing webhook, **falling
back to `get_or_create`** when the channel has none. On a dead pick, self-heal deletes that
specific dead webhook (not the managed one) and recreates + retries once — unless the pick
IS the managed webhook, in which case it goes through the normal `clear()` path. None of
these three create or touch the store — they read live from the channel.
## What you inject