Files
dpy_commons/README.md
T

90 lines
3.2 KiB
Markdown

# dpy_commons
Shared discord.py utilities — the discord-side sibling of `commons`. A module of functions
grouped by concern: message/embed parsing, embed sanitizing + limit-fitting, link extraction,
text chunking, timestamp helpers, interactive await-prompts, and a limit-safe send.
## Install
```
dpy_commons @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_commons.git@v0.1.0
```
## Usage
```python
import dpy_commons as dc
# structured parse of a rich message (async — it reads attachments from the CDN)
payload = await dc.parse_message(message)
payload["mentions"]["users"] # [123, ...]
payload["poll"] # {"question": ..., "options": [...]} or None
# sanitize an embed to be safe-to-send (links wrapped, color normalized, within limits)
safe = dc.sanitize_embed(raw_embed)
# send that never trips a Discord limit: chunks content, fits + splits embeds
await dc.safe_send(channel, content=long_text, embeds=many_embeds)
# split a 5000-char blob into <=2000 pieces on clean boundaries
for piece in dc.chunk_text(blob):
await channel.send(piece)
# a live, timezone-local timestamp rendered by the Discord client
dc.discord_timestamp(dt, "R") # "<t:1751500000:R>"
```
### Interactive await-prompts
Throw a prompt, `await` it, get the answer back right there — no listener, no view subclass,
no state plumbing:
```python
if await dc.confirm(ctx, "Delete 500 messages?"):
await purge()
action = await dc.choose(ctx, "Pick:", {
"✅": "approve",
"❌": "deny",
"<:escalate:123456789>": "escalate",
})
# action -> "approve" | "deny" | "escalate" | None (timeout)
```
`confirm` returns `True`/`False`/`None`; `choose` returns the mapped **value** (never the raw
interaction). Both scope to a user (a stranger's click gets an ephemeral "not for you" and the
prompt stays live), disable their components after resolve/timeout, accept custom emojis
anywhere an emoji goes, and take `cleanup=True` to delete the prompt afterward. `choose`
auto-switches to a select dropdown for more than 5 options or long labels.
## What's inside
| Concern | Functions |
|---|---|
| Parsing | `parse_message` (async), `extract_message_links`, `sanitize_mentions` |
| Embeds | `fit_embed`, `sanitize_embed`, `split_embeds` |
| Text | `chunk_text`, `format_table`, `discord_timestamp`, `humanize_delta` |
| Prompts | `confirm`, `choose` |
| Send | `safe_send` |
All Discord hard limits live as module constants (`MSG_LIMIT`, `EMBED_TOTAL`, …) — the single
source of truth; nothing hardcodes a limit.
## Contract
Config-free (functions take the discord objects they act on, never a global). Fail-loud:
`format_table` raises `ValueError` on ragged rows, `discord_timestamp` on a bad style,
`choose` on empty options; `safe_send` and the prompts propagate Discord perms/HTTP errors
(a prompt **timeout** is a normal `None`, not an error). The one tolerated swallow is a single
bad attachment in `parse_message` (warn + skip) — pass `strict=True` to raise instead.
## Notes / deviations
- **`parse_message` is `async`.** The spec wrote it sync, but attachments are read from the
CDN (network I/O), which cannot be synchronous. Await it.
- Targets **`discord.py>=2.2`** (not `discord.py-self`).
## Versioning
Tagged `vX.Y.Z`; pin a tag in your install line.