From 464a1171c36190d1e98fb041ae7a0257c88778f9 Mon Sep 17 00:00:00 2001 From: disqualifier Date: Thu, 2 Jul 2026 23:12:53 -0400 Subject: [PATCH] =?UTF-8?q?init:=20shared=20discord.py=20utilities=20?= =?UTF-8?q?=E2=80=94=20parsing,=20embeds,=20text,=20await-prompts,=20limit?= =?UTF-8?q?-safe=20send?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: disqualifier --- .gitignore | 15 +++++++++ README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 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..1026850 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# 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") # "" +``` + +### 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.