first stable release. pre-1.0.0 verification complete: all surviving MED regressions and
gaps resolved and independently re-fired, tree audited clean across the suite.
Signed-off-by: disqualifier <dev@disqualifier.me>
Both were annotated -> discord.WebhookMessage and README-documented that
way, but discord.py's Webhook.send defaults wait=False and returns None
unless the caller passes wait=True - so the annotation overpromised and a
consumer chaining .id off the result without knowing to pass wait=True would
get an AttributeError. Widen both annotations to Optional and add a
one-line note (module docstring + README) on the wait=True requirement.
Signed-off-by: disqualifier <dev@disqualifier.me>
Webhook.partial does no token validation of its own, so a store record with
token=None (e.g. a mongo/file store deserializing a missing field as null)
rebuilt and cached a token-less webhook with no error. Webhook.send then
raised a raw ValueError, not HTTPException, so send()'s dead-webhook except
never fired and the poisoned record was never cleared - the channel got
permanently stuck. Validate the token type before partial() so a bad record
takes the same clear-and-fall-through path as any other stale record.
Signed-off-by: disqualifier <dev@disqualifier.me>
send() and send_any() retry with the exact same discord.File objects
after healing a dead webhook, but discord.py closes every File on the
first send's exit - a buffer-backed File's retry then uploads 0 bytes
silently (the buffer sits at EOF and nothing calls reset()), and a
path-backed File raises "I/O operation on closed file" instead.
The healed retry in both methods now routes kwargs through
_rebuild_files_in_kwargs(), which rebuilds a fresh discord.File per
entry in file=/files= from its original source (reopens a path-backed
File, rewinds a still-seekable buffer) before the retry send. A File
that can't be safely rebuilt raises ValueError rather than silently
sending an empty attachment. The first, non-retry send is untouched.
Signed-off-by: disqualifier <dev@disqualifier.me>
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>