commit 48de7d7065bcf9d14abdd666ad1891d0edda3b78 Author: disqualifier Date: Tue Jun 23 15:12:03 2026 -0400 init: leveled discord logger Signed-off-by: disqualifier diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae590bc --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# claude +CLAUDE.md + +# 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..26412b5 --- /dev/null +++ b/README.md @@ -0,0 +1,101 @@ +# dpy_logger + +Leveled Discord channel logger for discord.py. Logs to a channel via embeds at +`debug` / `info` / `success` / `fail` / `task` / `critical` levels. Config-free: +static identity is injected at construction; per-guild channel routing is read +live from `bot.settings` so it can change at runtime via a command. + +## Install + +```bash +# requirements.txt +dpy_logger @ git+ssh://git@git.rethinkstudios.io/rethink-public/dpy_logger.git@v0.1.0 +``` + +## Usage + +```python +from dpy_logger import DPYLogger + +bot.log = DPYLogger( + bot, guild_id, channel_id, + colors=cfg.log_colors, # optional; merged over sensible defaults + pings=cfg.authorized_devs, # mentioned on critical() + timezone=cfg.timezone, + footer=cfg.bot_footer, + avatar=cfg.bot_avatar, +) +await bot.log.initialize() # resolves ids -> objects, call once + +await bot.log.info("started") +await bot.log.success("user promoted", action="promote", actor=ctx.author) +await bot.log.critical("db unreachable") # pings configured devs +``` + +## Dynamic routing + +Pass `guild=` on any call to log to that guild's configured channel +(`bot.settings[guild.id]['channels']['logs']`) instead of the construction +channel. Because it reads `bot.settings` at call time, a command that updates +that setting changes routing with no restart. + +## Construction contract + +The host injects everything; the lib never imports `config`: + +- `colors` (dict, optional) — per-level colors, merged over defaults +- `pings` (list of user ids) — mentioned on `critical()` +- `timezone`, `footer`, `avatar` — embed identity +- `alert_here` (bool) — if no `pings` are set, `critical()` falls back to + `@here` only when this is `True`; otherwise it sends no mention + +It also expects `bot.settings[guild.id]['channels']['logs']` to exist for the +dynamic-routing path. A project with a different settings shape should override +`_get_channel`. + +## Customizing the embed + +Three ways, easiest first. + +**Pass a function** — no subclass. The callable receives the logger instance +(so it can read `self.colors`, `self.footer`, etc.) plus the call args, and +returns a `discord.Embed`. Used for every level including `critical`: + +```python +def my_embed(logger, level, action, actor, details): + em = discord.Embed(description=details, color=logger.colors[level]) + if level == "critical": + em.set_thumbnail(url=logger.avatar) + if actor: + em.set_author(name=str(actor)) + return em + +bot.log = DPYLogger(bot, guild, channel, embed_builder=my_embed) +``` + +**Override the method** — for complex/stateful customization, subclass and +override `build_embed(level, action, actor, details)`. Same routing: every +level flows through it. + +**Default** — pass neither and you get the standard fielded embed. + +## Adding a log type + +To add new methods (e.g. a feed), subclass and reuse `_resolve`. This base has +no feed by design; a project that wants one adds it in its own local `libs/`: + +```python +class FeedLogger(DPYLogger): + """project-local: dpy_logger plus a feed-style announcement embed""" + + async def feed(self, log, title, icon=None, guild=None): + channel = await self._resolve(guild) + embed = discord.Embed( + description=log, color=self.colors.get("feed", self.colors["info"]) + ).set_author(name=title, icon_url=icon) + return await channel.send(embed=embed) +``` + +## Versioning + +Tagged `vX.Y.Z`. Pin the tag in `requirements.txt`.