init: async webhook sender (aiohttp) with retry & proxy rotation, optional discord embeds
Signed-off-by: disqualifier <disqualifierca@gmail.com>
This commit is contained in:
commit
818d9ab31d
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# claude
|
||||||
|
CLAUDE.md
|
||||||
|
|
||||||
|
# python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
.eggs/
|
||||||
|
|
||||||
|
# env
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
.env
|
||||||
|
.pytest_cache/
|
||||||
135
README.md
Normal file
135
README.md
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
# aiowebhooks
|
||||||
|
|
||||||
|
Async webhook sender over aiohttp. Give it a URL (or a round-robin pool) and a JSON
|
||||||
|
payload; it POSTs it with 429/5xx retry and optional proxy rotation, and **always
|
||||||
|
returns a `WebhookResult`** — it never raises on a send failure. A `[discord]` extra
|
||||||
|
adds `DiscordWebhook` (username/avatar identity + `Embed` handling) layered over the
|
||||||
|
same core.
|
||||||
|
|
||||||
|
The base is generic (aiohttp only, no Discord knowledge). A Discord webhook is just a
|
||||||
|
URL you POST JSON to, so the Discord layer only *builds* the payload and delegates the
|
||||||
|
send to the core — inheriting rotation, proxy, retry, and result for free.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```
|
||||||
|
aiowebhooks @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.0
|
||||||
|
# discord embeds / identity helpers need the extra:
|
||||||
|
aiowebhooks[discord] @ git+ssh://git@git.rethinkstudios.io/rethink-public/aiowebhooks.git@v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The base pulls `aiohttp`. Only `aiowebhooks[discord]` adds `discord.py` (>=2.3,
|
||||||
|
mainline — not discord.py-self), and only for `DiscordWebhook`.
|
||||||
|
|
||||||
|
## Core sender
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aiowebhooks import Webhook
|
||||||
|
|
||||||
|
wh = Webhook("https://example.com/hook") # or a list of urls (round-robin)
|
||||||
|
result = await wh.send({"content": "hello"}) # raw json dict
|
||||||
|
if not result.ok:
|
||||||
|
log.warning("webhook failed: %s (%s)", result.error, result.status)
|
||||||
|
```
|
||||||
|
|
||||||
|
Constructor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
Webhook(
|
||||||
|
urls, # a single url or a list (cycled round-robin per send)
|
||||||
|
*,
|
||||||
|
session=None, # injected aiohttp.ClientSession; created+closed per call if None
|
||||||
|
proxies=None, # duck-typed provider (see below)
|
||||||
|
timeout=15, # per-request seconds
|
||||||
|
max_retries=3, # 429 + 5xx retry cap
|
||||||
|
max_proxy_retries=3, # proxy-rotation cap on timeout/connection errors
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Inject a shared `session` for throughput (one session per process); without one, each
|
||||||
|
send opens and closes its own.
|
||||||
|
|
||||||
|
## WebhookResult
|
||||||
|
|
||||||
|
Every send returns this — branch on `ok`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result.ok # bool
|
||||||
|
result.status # final HTTP status, or None if the request never completed (timeout)
|
||||||
|
result.url # which pool url was used
|
||||||
|
result.attempts # total tries across retries / proxy rotations
|
||||||
|
result.error # short cause string on failure, else None
|
||||||
|
result.response # response payload: json dict if parseable, else text, else None
|
||||||
|
result.proxy # canonical proxy string used (host:port:user:pass / host:port), or None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Retry & rate limits
|
||||||
|
|
||||||
|
- **429** — waits the `retry_after` from the body first (Discord sends seconds), then
|
||||||
|
the `Retry-After` header, then retries; capped by `max_retries`.
|
||||||
|
- **5xx** — retried, capped by `max_retries`.
|
||||||
|
- **4xx** (other than 429) — fails immediately (no retry), returned as `ok=False`.
|
||||||
|
|
||||||
|
Exceeding a cap returns a failed result rather than looping.
|
||||||
|
|
||||||
|
## Proxy rotation (optional, duck-typed)
|
||||||
|
|
||||||
|
Pass any provider exposing `.get()` (returns an aiohttp proxies dict) and
|
||||||
|
`.burn(proxy)` — [`aioproxies`](https://git.rethinkstudios.io/rethink-public/aioproxies)
|
||||||
|
satisfies this, but it is **not a dependency** (never imported). The pairing is
|
||||||
|
failure-driven:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aioproxies import AioProxies
|
||||||
|
from aiowebhooks import Webhook
|
||||||
|
|
||||||
|
pm = AioProxies(proxies=[...])
|
||||||
|
wh = Webhook(urls, proxies=pm, max_proxy_retries=3)
|
||||||
|
|
||||||
|
result = await wh.send(payload) # sends through pm.get(); on a timeout/connection
|
||||||
|
# error, pm.burn(proxy) and rotates to the next
|
||||||
|
```
|
||||||
|
|
||||||
|
On a timeout/connection error the current proxy is burned and the next is tried, up to
|
||||||
|
`max_proxy_retries`. A provider `ProxiesExhaustedError` (or hitting the cap) returns a
|
||||||
|
failed result — never an infinite loop. With no provider, a timeout just fails after
|
||||||
|
normal retry.
|
||||||
|
|
||||||
|
## Discord (`aiowebhooks[discord]`)
|
||||||
|
|
||||||
|
```python
|
||||||
|
from aiowebhooks.discord import DiscordWebhook
|
||||||
|
import discord
|
||||||
|
|
||||||
|
dw = DiscordWebhook(
|
||||||
|
"https://discord.com/api/webhooks/...",
|
||||||
|
username="my-bot",
|
||||||
|
avatar_url="https://.../avatar.png",
|
||||||
|
proxies=pm, # same core options pass through
|
||||||
|
)
|
||||||
|
|
||||||
|
embed = discord.Embed(title="deploy", description="shipped v0.1.0")
|
||||||
|
result = await dw.send("done", embeds=[embed]) # discord.Embed or raw dict
|
||||||
|
result = await dw.send("override", username="other-name") # per-send identity override
|
||||||
|
```
|
||||||
|
|
||||||
|
`embeds` accepts `discord.Embed` objects (via `.to_dict()`) and/or raw dicts. Per-send
|
||||||
|
`username` / `avatar_url` override the manager identity. The build delegates to the
|
||||||
|
core `Webhook`, so Discord sends inherit rotation / proxy / retry / `WebhookResult`.
|
||||||
|
|
||||||
|
Without the extra installed, importing `aiowebhooks` still works; constructing or using
|
||||||
|
`DiscordWebhook` raises `RuntimeError("discord support requires aiowebhooks[discord]")`.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Every send returns a `WebhookResult`; the core never raises on a send failure and
|
||||||
|
never prints. Callers check `result.ok`.
|
||||||
|
- v0.1.0 is JSON-only: **files/attachments, `tts`, and `allowed_mentions` are out**
|
||||||
|
(deliberate scope cut, addable later). The Discord surface is content + embeds +
|
||||||
|
identity.
|
||||||
|
- v0.1.0 rotation is round-robin only; try-next-on-failure across URLs is a later
|
||||||
|
feature.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Tagged `vX.Y.Z`. Pin the tag.
|
||||||
Loading…
Reference in New Issue
Block a user