feat: add config helpers, decorator stacks, and lifecycle managers to… - #129
Conversation
… eliminate plugin boilerplate Additional improvements in plugin infrastructure: 1. **PluginConfigHelper mixin**: Eliminates repeated load→mutate→save ceremony - `await plugin.config_set(guild_id, key, value)` instead of 5-line pattern - `await plugin.config_get/config_update/config_delete/config_mutate` shortcuts - All operations are atomic under per-guild locks 2. **Pre-composed decorator stacks**: Common command patterns in single decorator - `@slash_admin_command()` replaces `@slash` + `require_admin=True` - `@slash_management_command()` for `manage_guild` permission gates - `@slash_mod_command()` for moderation commands with auto-perms - `@slash_user_command()` for public commands - `@slash_with_confirm()` for dangerous operations needing confirmation 3. **TaskManager**: Automatic task lifecycle tracking - Eliminates manual `_tasks: dict` + `on_unload()` cancellation - `await tasks.start_recurring(fn, interval)` auto-cancels on unload - `await tasks.start_once(name, coro)` prevents duplicate tasks - Single `await tasks.cancel_all()` cleans up everything 4. **TimerManager**: Hierarchical timer tracking for delayed events - Replaces manual `_timers: dict[int, dict[int, asyncio.Task]]` patterns - `await timers.schedule(timer_id, fn, seconds, guild_id=...)` auto-cancels on unload - `await timers.cancel_guild(timer_id, guild_id)` bulk-cancels per guild - Single `await timers.cancel_all()` cleans up all in-flight timers Example plugin reductions: - **Before**: BirthdayPlugin ~400 lines (includes lock/store ceremonies) - **After**: ~250 lines (with TaskManager + PluginConfigHelper) - **Before**: GiveawayPlugin ~300 lines (timer tracking boilerplate) - **After**: ~150 lines (with TimerManager)
Reviewer's GuideIntroduces helper mixins and utility classes to reduce boilerplate in plugins: a PluginConfigHelper for atomic, lock-guarded config CRUD; pre-composed slash-command decorator stacks for common permission/cooldown patterns; and TaskManager/TimerManager classes for centralized tracking and automatic cancellation of background tasks and timers in plugin lifecycles. Sequence diagram for atomic config_set operationsequenceDiagram
participant Plugin
participant GuildLockManager
participant ServerConfigStore
participant ServerConfig
Plugin->>Plugin: config_set(guild_id, key, value, section)
Plugin->>Plugin: _apply(cfg)
Plugin->>GuildLockManager: lock(guild_id)
activate GuildLockManager
GuildLockManager-->>Plugin: lock context
Plugin->>ServerConfigStore: load(guild_id)
activate ServerConfigStore
ServerConfigStore-->>Plugin: ServerConfig
deactivate ServerConfigStore
Plugin->>ServerConfig: get_other(section, {})
activate ServerConfig
ServerConfig-->>Plugin: data dict
Plugin->>ServerConfig: set_other(section, data with key=value)
deactivate ServerConfig
Plugin->>ServerConfigStore: save(ServerConfig)
activate ServerConfigStore
ServerConfigStore-->>Plugin: save complete
deactivate ServerConfigStore
Plugin-->>GuildLockManager: release lock
deactivate GuildLockManager
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThis PR adds reusable slash-command decorators, atomic plugin configuration helpers, asynchronous task and timer managers, and comprehensive tests for their behavior. ChangesPlugin helper foundations
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested labels: Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| from .decorators import ( | ||
| slash as _slash, | ||
| cooldown as _cooldown, | ||
| require_permissions as _require_permissions, | ||
| describe as _describe, | ||
| ) |
| try: | ||
| await asyncio.sleep(seconds) | ||
| await fn(*args, **kwargs) | ||
| except asyncio.CancelledError: |
PR Summary by QodoAdd plugin helpers for config CRUD, command decorator stacks, and task/timer lifecycle
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In
slash_admin_command,ephemeralis effectively forced toTrueviaephemeral=ephemeral or True, so passingephemeral=Falseis ignored; consider using a sentinel orephemeral if ephemeral is not None else Trueto respect explicit False. - In
TimerManager.schedule, timers created without aguild_idare never stored inself.timers, so they cannot be cancelled viacancel_timer/cancel_all; if non-guild timers are expected, you may want to track them under a special key or separate collection.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `slash_admin_command`, `ephemeral` is effectively forced to `True` via `ephemeral=ephemeral or True`, so passing `ephemeral=False` is ignored; consider using a sentinel or `ephemeral if ephemeral is not None else True` to respect explicit False.
- In `TimerManager.schedule`, timers created without a `guild_id` are never stored in `self.timers`, so they cannot be cancelled via `cancel_timer`/`cancel_all`; if non-guild timers are expected, you may want to track them under a special key or separate collection.
## Individual Comments
### Comment 1
<location path="easycord/_decorator_stacks.py" line_range="71" />
<code_context>
+ require_admin=True,
+ cooldown=cooldown,
+ bot_permissions=bot_permissions,
+ ephemeral=ephemeral or True,
+ )(func)
+ return func
</code_context>
<issue_to_address>
**issue (bug_risk):** The `ephemeral` argument to `slash_admin_command` is effectively forced to `True`, ignoring any `False` passed by callers.
`ephemeral=ephemeral or True` will always be `True`, so callers cannot set it to `False`, and the docstring’s stated default of `False` is incorrect.
If you want a default of `True` but still allow `False`, either:
- Use `ephemeral: bool | None = None` and `ephemeral = True if ephemeral is None else ephemeral`, or
- Keep `ephemeral: bool = True` and pass it through as `ephemeral=ephemeral`.
This will align the behavior with the signature and documentation.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Issue: Timers scheduled without a guild_id were created but never stored in self.timers, making them uncancellable via cancel_all(). Solution: Store timers without a guild_id under a special "__ungrouped__" key so they can be cancelled with cancel_all().
Code Review by Qodo
1.
|
Issue: `ephemeral=ephemeral or True` forces ephemeral to always be True, ignoring explicit False values from callers. Solution: Use ternary to default to True only when not explicitly set. This allows callers to override the default if needed.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f83ac03c1f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
🤖 Augment PR SummarySummary: This PR introduces helper abstractions intended to reduce custom-plugin boilerplate. Changes:
Technical notes: The helpers wrap existing slash metadata, configuration storage, and 🤖 Was this summary useful? React with 👍 or 👎 |
- PluginConfigHelper: route all writes through ServerConfigStore.mutate so load-modify-save is atomic under the store's own per-guild lock (one lock domain, no lost updates); drop the separate GuildLockManager requirement. Fix the module-docstring example arg order and guard against a missing _store. - TaskManager.track: in the done-callback, only evict a task if it is still the one registered under that name, so a replacement started under the same name isn't untracked (and left uncancellable) by the finished task's callback. - Remove an unused TYPE_CHECKING import. - Add tests/test_plugin_helpers.py (22 tests) covering the decorator stacks, TaskManager/TimerManager, and PluginConfigHelper, including the identity-race and concurrent no-lost-updates regressions.
- Reframe the concurrent-writes test: empirically the synchronous store never suspends between load and save, so the old separate-lock pattern did not lose updates (verified 200/200 both ways). The rewrite to store.mutate is a simplification (single lock domain, drops the _locks dependency), not a data-loss fix, so the test no longer overclaims to guard a cross-domain race. - Make the TaskManager tests deterministic: await the task then a single sleep(0) rather than a wall-clock sleep. - config_mutate docstring example now returns a meaningful (non-None) value.
| def test_slash_admin_command_defaults_to_admin_and_ephemeral() -> None: | ||
| @slash_admin_command(description="d") | ||
| async def cmd(self, ctx): # pragma: no cover - body never runs | ||
| ... |
| # Regression: ephemeral=False must be honored, not overridden to True. | ||
| @slash_admin_command(description="d", ephemeral=False) | ||
| async def cmd(self, ctx): # pragma: no cover | ||
| ... |
| def test_slash_management_command_respects_ephemeral_false() -> None: | ||
| @slash_management_command(description="d", ephemeral=False) | ||
| async def cmd(self, ctx): # pragma: no cover | ||
| ... |
| def test_slash_mod_command_sets_user_and_bot_perms() -> None: | ||
| @slash_mod_command(description="d") | ||
| async def cmd(self, ctx): # pragma: no cover | ||
| ... |
| def test_slash_with_confirm_registers_slash() -> None: | ||
| @slash_with_confirm(description="d", permissions=["manage_guild"]) | ||
| async def cmd(self, ctx): # pragma: no cover | ||
| ... |
| async def test_track_removes_task_when_done() -> None: | ||
| tm = TaskManager() | ||
| task = await tm.start_once("job", asyncio.sleep, 0) | ||
| await task |
| second = await tm.start_once("w", waiter) | ||
| assert first is second | ||
| ev.set() | ||
| await first |
| replacement = asyncio.create_task(asyncio.sleep(3600)) | ||
| tm.track("bg", replacement) # replace under the same name, synchronously | ||
|
|
||
| await finishing # deterministically wait for the first task to complete |
| tmr = TimerManager() | ||
|
|
||
| async def cb() -> None: # pragma: no cover - never fires within the test | ||
| ... |
There was a problem hiding this comment.
Actionable comments posted: 9
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@easycord/_decorator_stacks.py`:
- Around line 61-63: Update the user-facing response strings in nuke and the
other referenced command examples to pass through ctx.localize(...), preserving
each existing message and response options such as ephemeral=True.
- Around line 148-156: In _decorator_stacks.py, replace timeout_members with
moderate_members in the user permission list and add moderate_members to the
default bot permissions. In tests/test_plugin_helpers.py, update the relevant
assertions to verify the complete expected user and bot permission lists,
including moderate_members. Apply changes at easycord/_decorator_stacks.py lines
148-156 and tests/test_plugin_helpers.py lines 65-72.
In `@easycord/_plugin_config_helper.py`:
- Line 49: Update the command response strings in the relevant plugin
configuration helper methods to use ctx.localize(...) instead of hardcoded text,
including both responses near the Set and Remove operations. Preserve the
existing key and value interpolation while routing each message through the
localization API.
- Line 48: Add an `assert ctx.guild is not None` immediately before each
`ctx.guild.id` access in the affected command methods, including the
`config_set` calls at both locations. Preserve the existing guild configuration
behavior for valid guild invocations.
- Around line 229-235: Update the _apply mutation callback to detect whether
fn(data) returns an awaitable before calling cfg.set_other. Close coroutine
results, raise TypeError for any asynchronous result, and ensure cfg.set_other
is only called for synchronous results so unchanged data is not saved.
In `@easycord/_plugin_lifecycle_helpers.py`:
- Around line 95-109: Update the recurring-task registration flow around loop
and track so an existing active task with the same name is not orphaned when
creating a replacement. Reuse and return the active task, or cancel the
previously tracked task before calling track(name, task), ensuring cancel_all()
retains control of every running recurring task.
- Around line 184-199: The timer registration flow around delayed and the
self.timers assignment must handle duplicate timer_id/grouping keys by
cancelling or rejecting the existing task before storing its replacement. Update
delayed’s finally cleanup to remove the entry only when the stored task is the
task that is finishing, preserving tracking of any replacement for
cancel_timer() and cancel_all().
- Around line 221-223: Align cancel_guild with its documented all-timers scope
by retrieving and cancelling every timer associated with guild_id, rather than
forwarding only the supplied timer_id; alternatively, rename the method to
reflect single-timer cancellation and update its docstring and callers
consistently.
- Around line 135-141: Update the timer scheduling call in the giveaway
lifecycle flow to pass guild_id as the keyword argument expected by
TimerManager.schedule, while retaining message_id as the callback argument for
_end_giveaway. Ensure the timer is registered under its guild grouping key so
per-guild cancellation can locate it.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 792ce096-d33a-42d7-88a2-e74d64036c26
📒 Files selected for processing (4)
easycord/_decorator_stacks.pyeasycord/_plugin_config_helper.pyeasycord/_plugin_lifecycle_helpers.pytests/test_plugin_helpers.py
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
- GitHub Check: Analyze (Python)
- GitHub Check: coverage
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{py,md}
📄 CodeRabbit inference engine (AGENTS.md)
Before answering questions about EasyCord architecture, module relationships, data flows, or cross-cutting patterns, query
graphify-out/graph.jsonwith/graphify; do not manually read source first.
Files:
easycord/_plugin_config_helper.pytests/test_plugin_helpers.pyeasycord/_plugin_lifecycle_helpers.pyeasycord/_decorator_stacks.py
easycord/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
easycord/**/*.py: Add bot-level behavior to the appropriate_bot_<area>.pymixin rather than directly expandingbot.py; context behavior belongs in_context_<area>.pymixins.
Treat only symbols re-exported fromeasycord/__init__.pyas stable public API; modules prefixed with_are internal.
Use@ai_toolfunctions with an explicitToolSafetypermission annotation and register them throughToolRegistry.
Await everyToolLimitermethod because all per-tool rate-limiting methods are asynchronous.
Route all per-guild state through the database layer; never store per-guild state directly on theBotinstance.
Usectx.userorctx.member;ctx.authordoes not exist.
Treatctx.is_adminas a property and do not call it asctx.is_admin().
Usefloat("-inf"), rather than0.0, as the default cooldown sentinel so first-message events pass on fresh runners.
Files:
easycord/_plugin_config_helper.pyeasycord/_plugin_lifecycle_helpers.pyeasycord/_decorator_stacks.py
**/*
📄 CodeRabbit inference engine (CLAUDE.md)
**/*: Before answering any question about the repository, query the knowledge graph atgraphify-out/graph.jsonusinggraphify query,graphify path, orgraphify explainbefore opening source, documentation, or context files.
Treat documentation and context files as potentially stale secondary references; prefer knowledge-graph results, do not rebuild the graph unless explicitly asked, and never read the deprecated vault atC:\Users\Tom\Desktop\Wiki.
Files:
easycord/_plugin_config_helper.pytests/test_plugin_helpers.pyeasycord/_plugin_lifecycle_helpers.pyeasycord/_decorator_stacks.py
**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
**/*.py: Usectx.userorctx.member;ctx.authordoes not exist. Treatctx.is_adminas a property and never call it as a method.
For guild-only slash commands, narrowctx.guildwithassert ctx.guild is not None; for event handlers that may run in DMs, return whenctx.guild is None.
Never hardcode user-facing command response strings; usectx.localize(...)for localization.
AI providers must implement theAIProviderprotocol, and providers should be lazy-imported fromplugins/ai_*.
Use theMiddlewareFnchain for middleware with signatureCallable[[Context, next], Awaitable[None]]; outer middleware executes before inner middleware.
UseEventBus.subscribe(event, callback)and.publish(event, **kwargs)for asynchronous plugin pub/sub; listeners execute in registration order and subscriber failures must be logged with handler identity.
Use@deprecated(version, replacement)and@version_introduced(version)fromeasycord.decorators; deprecations emitDeprecationWarningat call time and include a migration hint.
ToolLimitermethods are asynchronous; always awaitcheck_limit(...).
@ai_toolrequires an explicitToolSafetyannotation to register a tool.
Route every destructive action in event-path plugins such as@on("message")through one governed method that owns rate limiting, channel narrowing, and Discord error handling; catch at leastdiscord.Forbidden,discord.NotFound, anddiscord.HTTPException.
Before calling.send()on a channel obtained fromctxor Discord, narrow it withisinstance(channel, SENDABLE_CHANNEL_TYPES)fromeasycord.helpers.tools.
For config read-modify-write operations, useServerConfigStore.mutate(guild_id, fn);fnmust be synchronous and perform no Discord I/O. A singleload()orsave()is atomic, but a load-modify-save sequence is not.
sync_commands()raisesRuntimeErrorwhen removals are detected unlessconfirm_removals=Trueis passed explicitly.
For plugin construction in t...
Files:
easycord/_plugin_config_helper.pytests/test_plugin_helpers.pyeasycord/_plugin_lifecycle_helpers.pyeasycord/_decorator_stacks.py
tests/**/*.py
📄 CodeRabbit inference engine (AGENTS.md)
tests/**/*.py: Usepytestandpytest-asynciowithasyncio_mode = "auto"; asynchronous tests do not need manual event-loop setup.
Run the full test suite withpytest tests/; individual tests may be run using their test node IDs.
tests/**/*.py: Prefer EasyCord testing helpers such asinvoke,FakeContextBuilder, andPluginTestSuitefor command, context, and plugin tests.
Usepytest-asynciowithasyncio_mode = "auto"; do not manually create or manage an event loop in tests.
Files:
tests/test_plugin_helpers.py
🔇 Additional comments (8)
easycord/_decorator_stacks.py (1)
27-32: Remove the unused decorator imports.
_cooldown,_require_permissions, and_describeare unused. This duplicates the existing CodeQL finding.tests/test_plugin_helpers.py (1)
30-62: LGTM!Also applies to: 74-262
easycord/_plugin_lifecycle_helpers.py (5)
1-72: LGTM!
111-118: LGTM!
151-164: LGTM!
202-219: LGTM!
225-235: LGTM!easycord/_plugin_config_helper.py (1)
1-36: LGTM!Also applies to: 57-190, 237-257
…eout_members) (#131) ## Summary Follow-up to #129 (merged). Fixes the one **real, still-present** issue from that PR's review; the rest of the bot-review findings were already resolved by the fixes merged into #129. **Bug:** `slash_mod_command()` set its default permissions to `["kick_members", "ban_members", "timeout_members"]`, but `timeout_members` is **not** a `discord.Permissions` attribute — discord.py (2.7.1) calls it **`moderate_members`**. Permissions are enforced with `getattr(member.guild_permissions, name, False)`, so the `timeout_members` entry was silently always-`False`, breaking the intended gate for every command built on this stack. **Also:** removed three imports CodeQL flagged as unused (`cooldown`, `require_permissions`, `describe`) — the decorator stacks only call `slash()`. ## Changes - `easycord/_decorator_stacks.py`: `timeout_members` → `moderate_members` (default list + docstring); trim unused imports. - `tests/test_plugin_helpers.py`: assert `moderate_members` is present and `timeout_members` is not. ## Test plan - [x] Decorator-stack tests pass; full suite 1865 passed. - [x] `ruff --select E9,F63,F7,F82` clean; pyright clean on changed files. ## Note on the #129 review CodeRabbit/qodo/augment/Sourcery posted ~46 inline comments on #129, but most were against its **original** commit and were already fixed before merge (the `ephemeral or True` bug is now `True if ephemeral is None else ephemeral`; the TaskManager name-reuse race now has the identity check; the config helper routes through `store.mutate`; the `config_set` docstring order and the unused `Plugin` import were corrected). The remaining valid items were the `moderate_members` bug and the unused imports fixed here. (The over-broad `bot_permissions` on `slash_mod_command` and the CodeQL "statement has no effect" notes on `...` test stubs are low-value and left as-is.) ## Summary by Sourcery Correct default moderation permissions for slash commands and remove unused decorator imports. Bug Fixes: - Replace the nonexistent timeout_members permission with moderate_members in the default permissions for slash_mod_command to ensure moderation commands are properly gated. Enhancements: - Clean up _decorator_stacks by trimming unused decorator imports. Tests: - Extend plugin helper tests to validate that moderate_members is included and timeout_members is excluded from slash_mod_command permissions. Co-authored-by: tee <Thomas_BIRRELL@proton.me>
Release **v5.61.0**, bundling everything merged since v5.60.0. ## Included - **#128** — offline `easycord try` CLI (run a slash command without Discord) - **#129** — plugin ergonomics helpers: `PluginConfigHelper` (atomic config via `store.mutate`), decorator stacks (`slash_admin_command` etc.), `TaskManager`/`TimerManager` - **#130** — `discord_errors()` middleware + `server_stats` `store.mutate` migration - **#131** — `slash_mod_command` now uses `moderate_members` (real perm) instead of the nonexistent `timeout_members` ## Version bump `5.60.0 → 5.61.0` (minor: additive features + fixes, no breaking changes) across `pyproject.toml`, `easycord/__init__.py`, `README.md`, `docs/getting-started.md`; CHANGELOG entry added. ## Gates (per RELEASE.md) - [x] `check_release_metadata.py` — no drift - [x] `ruff check ... --select E9,F63,F7,F82` — clean - [x] `verify_plugin_tests.py` — thresholds met - [x] `pytest` — 1865 passed - [ ] `pyright easycord tests` — 12 errors / 135 warnings, all **pre-existing on main** and unrelated to this release (CI does not gate on pyright); noted, not introduced here. After merge: tag `v5.61.0` and publish a GitHub Release with the built wheel + sdist. PyPI `twine upload` left as a manual step (requires credentials). ## Summary by Sourcery Release EasyCord v5.61.0 with new CLI tooling, plugin ergonomics helpers, Discord error handling middleware, and minor fixes, plus corresponding version and metadata updates. New Features: - Add offline `easycord try` CLI to run registered slash commands without Discord connectivity. - Introduce plugin ergonomics helpers including config mixin, decorator stacks, and task/timer lifecycle managers. - Add `discord_errors()` middleware to handle common Discord API errors with user-facing messages. Bug Fixes: - Correct `slash_mod_command` to require the real `moderate_members` permission instead of nonexistent `timeout_members`. - Fix `TaskManager.track` race that could leave replacement tasks untracked and uncancellable. - Remove unused imports in decorator stacks flagged by static analysis. Enhancements: - Route `server_stats` configuration writes through `ServerConfigStore.mutate` for consolidated locking and consistency. Build: - Bump project version to 5.61.0 in pyproject, package metadata, and module `__version__`, and add corresponding changelog entry. - Update release metadata entries for wheel and source distribution artifacts. Documentation: - Update README and getting-started docs to reference the v5.61.0 release, download URLs, and badges. Co-authored-by: tee <Thomas_BIRRELL@proton.me>
The v5.61.0 entry undersold the release — it flattened #129's many additions into one bullet and omitted the work bundled via the docs/hardening PR. Expanded to break out the config mixin, five decorator stacks, and task/timer managers; document the starboard/suggestions resilience fixes, new giveaway/tickets test suites, shared-helper refinements, and the CLAUDE.md context framework + CI workflow refresh. Release-metadata gate still passes. ## Summary by Sourcery Expand the v5.61.0 changelog entry to fully document the release scope, including new helpers, resilience fixes, tests, and internal/process updates. New Features: - Document the PluginConfigHelper mixin, pre-composed decorator stacks, and TaskManager/TimerManager helpers as first-class additions in v5.61.0. - Clarify the `easycord try` CLI feature with its formatter and developer-toolkit documentation reference. Bug Fixes: - Describe the starboard and suggestions resilience improvements that prevent Discord permission/HTTP errors from crashing handlers. - Call out the corrected `slash_mod_command` permission and TaskManager race fix as part of the release notes. - Note removal of unused imports flagged by CodeQL as a minor cleanup. Enhancements: - Highlight refinements to shared plugin helpers and the updated `server_stats` config pattern as guidance for future plugin migrations. CI: - Record the refresh of CodeQL, release-drafter, stale, and Claude-related CI workflows in the changelog. Documentation: - Broaden the v5.61.0 changelog section to cover all major features, fixes, tests, and internal changes. - Summarize the CLAUDE.md context framework updates, including restored context, troubleshooting guidance, documentation freshness, and contributor invariants. Tests: - Add a dedicated tests section noting new giveaway and tickets command suites and expanded coverage for suggestions, shared helpers, CLI, and middleware. Chores: - Capture internal maintenance work such as the CLAUDE.md framework and CI workflow updates under the release metadata narrative. Co-authored-by: tee <Thomas_BIRRELL@proton.me>
… eliminate plugin boilerplate
Additional improvements in plugin infrastructure:
PluginConfigHelper mixin: Eliminates repeated load→mutate→save ceremony
await plugin.config_set(guild_id, key, value)instead of 5-line patternawait plugin.config_get/config_update/config_delete/config_mutateshortcutsPre-composed decorator stacks: Common command patterns in single decorator
@slash_admin_command()replaces@slash+require_admin=True@slash_management_command()formanage_guildpermission gates@slash_mod_command()for moderation commands with auto-perms@slash_user_command()for public commands@slash_with_confirm()for dangerous operations needing confirmationTaskManager: Automatic task lifecycle tracking
_tasks: dict+on_unload()cancellationawait tasks.start_recurring(fn, interval)auto-cancels on unloadawait tasks.start_once(name, coro)prevents duplicate tasksawait tasks.cancel_all()cleans up everythingTimerManager: Hierarchical timer tracking for delayed events
_timers: dict[int, dict[int, asyncio.Task]]patternsawait timers.schedule(timer_id, fn, seconds, guild_id=...)auto-cancels on unloadawait timers.cancel_guild(timer_id, guild_id)bulk-cancels per guildawait timers.cancel_all()cleans up all in-flight timersExample plugin reductions:
Before: BirthdayPlugin ~400 lines (includes lock/store ceremonies)
After: ~250 lines (with TaskManager + PluginConfigHelper)
Before: GiveawayPlugin ~300 lines (timer tracking boilerplate)
After: ~150 lines (with TimerManager)
Summary by Sourcery
Introduce helper utilities to reduce boilerplate in plugin configuration, command declaration, and task/timer lifecycle management.
New Features: