Skip to content

refactor: DRY pass — GuildLockManager, send_safe, require_admin, EmbedBuilder#123

Merged
rolling-codes merged 32 commits into
mainfrom
refactor/dry-phase1-config-helpers
Jul 25, 2026
Merged

refactor: DRY pass — GuildLockManager, send_safe, require_admin, EmbedBuilder#123
rolling-codes merged 32 commits into
mainfrom
refactor/dry-phase1-config-helpers

Conversation

@rolling-codes

Copy link
Copy Markdown
Owner

Summary

  • Phase 2: Extracted GuildLockManager into easycord/plugins/_shared.py and migrated all 13 plugins that had hand-rolled _guild_lock dicts. Removed unused asyncio imports from 4 plugins. Tests updated to use _locks.lock() throughout.
  • Phase 3: Adopted the existing send_safe helper at ~20 narrow-then-send sites across 12 plugins, removing ~30 lines of inline try/except discord.Forbidden/HTTPException boilerplate.
  • Phase 4a: Replaced 6 inline if not ctx.is_admin: respond_error; return blocks with @slash(require_admin=True) on ticket_setup, stats_setup, stats_teardown, verification_setup, verification_panel, verification_question.
  • Phase 4b: Migrated discord.Embed(...) construction to EmbedBuilder at opportunistic sites in tickets.py and verification.py.

27 commits, all mechanical — no behaviour changes.

Test plan

  • ruff check --select E9,F63,F7,F82 . — clean
  • ruff check . — advisory, no new errors
  • python scripts/verify_plugin_tests.py — all plugin thresholds met
  • pytest tests/ -q1,813 passed (2 new decorator-assertion tests added for stats_teardown and verification_panel)
  • Spot-check _shared.py GuildLockManager eviction logic still correct
  • Spot-check ai_moderator._execute_action governed path untouched

tee1339 and others added 27 commits July 25, 2026 10:52
…essors

Add five pure helpers to easycord/plugins/_shared.py:
- respond_error(ctx, message): collapses the 107-site ephemeral error pattern
- get_id / set_id: str↔int coercion over ServerConfig.get_other/set_other; None deletes
- get_ids / set_ids: list variant with per-item coercion; missing key returns []

All are pure over ServerConfig (no I/O, no lock) so they slot inside existing
mutate/guild_lock spans without changing concurrency semantics.

Tests: 29 new cases in test_shared_helpers.py covering coercion, defaults,
None-delete, non-coercible skipping, and respond ephemeral flag.
…tion

Replace bare ctx.respond(msg, ephemeral=True) error/guard calls with
respond_error() from _shared. Success embeds and confirmations stay explicit.
economy, starboard, polls, birthday, verification, word_filter,
suggestions, tickets, server_stats, levels, giveaway,
scheduled_announcements — error/guard paths now use respond_error()
from _shared. Success confirms and embeds stay explicit.
Replace error/guard ephemeral responds. Success and info responses
(status display, history list, cancellation confirm) stay explicit.
…nd log_channel_id

Replace bare cfg.get_other("support_role_id"/"log_channel_id") and
cfg.set_other(...) calls with get_id/set_id from _shared. Adds str→int
coercion and None-removal semantics that raw get_other/set_other lack.
tags: not-found, unauthorized, and empty-list paths.
welcome: five guild-guard paths.
Success responses stay explicit.
…cation

Move the per-guild lock pattern from EconomyPlugin to _shared.py as
GuildLockManager. Encapsulates idle-eviction, hard cap, and timestamp refresh.
All 1,811 tests passing. Ready for rollout to 13 other plugins.
…ication

Replace manual _guild_lock pattern with shared GuildLockManager.
Eliminates per-guild Lock dict initialization and cleanup logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cation

Replace manual _guild_lock pattern with shared GuildLockManager.
Update test fixtures to use new API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cation

Replace manual _guild_lock pattern with shared GuildLockManager.
Update test fixtures to use new API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ication

Replace manual _guild_lock pattern with shared GuildLockManager.
Eliminates per-guild Lock dict initialization and cleanup logic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace manual _guild_lock pattern with shared GuildLockManager.
Update test fixtures to use new API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cation

Replace manual _guild_lock pattern with shared GuildLockManager.
Update test fixtures to use new API.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lication

Replace manual _guild_lock pattern with shared GuildLockManager.
Remove now-unused asyncio import.
…ld lock deduplication

Replace manual _guild_lock pattern with shared GuildLockManager.
…uplication

Replace manual _guild_lock pattern with shared GuildLockManager.
Update test fixtures to use new API.
…ation

Replace manual _guild_lock pattern with shared GuildLockManager in both
the plugin and the embedded view class.
Update test fixtures to use new API.
…uplication

Replace manual _guild_lock pattern with shared GuildLockManager.
Remove now-unused asyncio import.
Update test fixtures to use new API.
…plication

Replace manual _guild_lock pattern with shared GuildLockManager.
Remove now-unused asyncio import.
Replace manual _get_lock pattern on TagsStore with shared GuildLockManager.
Remove now-unused asyncio import.
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @rolling-codes, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@rolling-codes, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 34 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 0a592d76-f3e3-4031-b15a-5dbda9ad5b3d

📥 Commits

Reviewing files that changed from the base of the PR and between 6fb99cc and fd1cf29.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • easycord/plugins/_shared.py
  • easycord/plugins/juicewrld.py
  • easycord/plugins/server_stats.py
  • easycord/plugins/starboard.py
  • easycord/plugins/suggestions.py
  • tests/test_economy.py
  • tests/test_giveaway_commands.py
  • tests/test_shared_helpers.py
  • tests/test_stress.py
  • tests/test_suggestions.py
  • tests/test_tickets_commands.py
📝 Walkthrough

Walkthrough

EasyCord adds shared guild locking, typed configuration ID helpers, standardized error responses, and safe message delivery. Plugins and tests migrate to these utilities, selected admin checks move to slash-command decorators, embeds use EmbedBuilder, and project metadata is updated to v5.60.0.

Changes

Shared infrastructure and plugin migration

Layer / File(s) Summary
Shared helpers and bounded locking
easycord/plugins/_shared.py, tests/test_shared_helpers.py
Adds respond_error, typed ID accessors, and bounded GuildLockManager behavior with focused tests.
Guild-scoped state synchronization
easycord/plugins/*.py
Replaces plugin-local lock maps with GuildLockManager across stateful plugin operations.
Safe messaging and command errors
easycord/plugins/*.py
Routes notification sends through send_safe and user-facing failures through respond_error.
Ticket and verification workflows
easycord/plugins/tickets.py, easycord/plugins/verification.py
Adds typed configuration access, builder-based embeds, safe sends, shared locks, and decorator-level admin requirements.
Regression validation
tests/*.py
Updates store, concurrency, stress, and decorator tests for the new shared contracts.
Release metadata
CHANGELOG.md, README.md, docs/getting-started.md, easycord/__init__.py, pyproject.toml
Updates release references and package metadata to v5.60.0.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

Suggested labels: enhancement, release:minor, plugin, tests

Poem

A bunny hops through locks so neat,
Safe messages softly greet.
IDs turn crisp, errors clear,
Five-point-six-oh is here!
Tests thump paws in cheer.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.16% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main mechanical refactor across GuildLockManager, send_safe, require_admin, and EmbedBuilder.
Description check ✅ Passed The description matches the changeset and summarizes the shared-lock, send_safe, require_admin, and EmbedBuilder refactor phases.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/dry-phase1-config-helpers

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot added enhancement New feature or request plugin tests labels Jul 25, 2026
@augmentcode

augmentcode Bot commented Jul 25, 2026

Copy link
Copy Markdown
🤖 Augment PR Summary

Summary: Mechanical DRY refactor across bundled plugins to centralize common patterns (locks, error replies, safe sends, and embed creation) without intended behavior changes.

Changes:

  • Added shared plugin helpers in easycord/plugins/_shared.py (new GuildLockManager, respond_error, and typed ID accessors get_id/set_id/get_ids/set_ids).
  • Migrated many plugins from per-plugin _guild_lock dicts to GuildLockManager; updated tests to acquire locks via _locks.lock(...).
  • Replaced repetitive ephemeral validation replies with respond_error(...) in multiple commands.
  • Adopted the existing send_safe helper across plugins to unify Discord send error handling and reduce boilerplate.
  • Moved several admin permission checks into @slash(..., require_admin=True) decorator flags (e.g., tickets/stats/verification setup flows).
  • Swapped selected discord.Embed(...) constructions to EmbedBuilder in tickets/verification.

Technical Notes: Lock manager includes idle eviction to bound memory growth; typed ID helpers normalize persisted snowflake IDs back to int.

🤖 Was this summary useful? React with 👍 or 👎

@augmentcode augmentcode Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed. 2 suggestions posted.

Fix All in Augment

Comment augment review to trigger a new review at any time.

Comment thread easycord/plugins/starboard.py
Comment thread easycord/plugins/suggestions.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (6)
easycord/plugins/tickets.py (2)

356-371: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Unconditional panel_msg.id access crashes when send_safe returns None.

send_safe returns None on a Forbidden/HTTPException send failure (per _shared/send_safe contract used elsewhere in this PR). Line 358-359 correctly guards panel_msg is not None before storing panel_message_id, but line 368's self.bot.add_view(view, message_id=panel_msg.id) is unconditional — if the panel send fails, this raises AttributeError: 'NoneType' object has no attribute 'id', which propagates out of the command and the final ctx.respond(...) never runs, leaving the user with no response even though the ticket thread/DB record was already created and saved.

Compare with verification_panel in easycord/plugins/verification.py, which correctly does if message is None: await ctx.respond(...); return before calling add_view.

🐛 Proposed fix
         panel_msg = await send_safe(thread, log=logger, what="ticket panel", embed=_ticket_embed(data), view=view)
         if panel_msg is not None:
             data["panel_message_id"] = panel_msg.id
 
         async with self._locks.lock(guild_id):
             cfg = await self._store.load(guild_id)
             tickets: dict = cfg.get_other("tickets", {})
             tickets[str(thread.id)] = data
             cfg.set_other("tickets", tickets)
             await self._store.save(cfg)
 
-        self.bot.add_view(view, message_id=panel_msg.id)
+        if panel_msg is not None:
+            self.bot.add_view(view, message_id=panel_msg.id)
         await ctx.respond(
             f"Your ticket has been opened: {thread.mention}", ephemeral=True
         )
🤖 Prompt for 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.

In `@easycord/plugins/tickets.py` around lines 356 - 371, Guard the view
registration in the ticket-opening flow after the existing panel_msg check: only
call self.bot.add_view(view, message_id=panel_msg.id) when panel_msg is not
None, while still allowing the ticket save and final ctx.respond(...) to run
when sending the panel fails. Use the existing panel_msg handling near
_TicketView and send_safe as the change point.

108-127: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Discord API calls executed while self._locks/self._plugin._locks is held, across 5 sites in tickets.py.

Each site interleaves a validation check with a Discord response call before releasing the per-guild GuildLockManager lock, holding it across a network round-trip. This repeats a pattern the coding guidelines explicitly forbid for per-guild locks, and verification.py in this same PR demonstrates the fix (compute/validate inside the lock, release, then respond) is straightforward to apply here too.

  • easycord/plugins/tickets.py#L108-L127: move the two interaction.response.send_message(...) calls (lines 113-115, 119-121) so they execute after releasing self._plugin._locks.lock(self._guild_id), e.g. by returning an early-exit sentinel from the locked block and responding outside it.
  • easycord/plugins/tickets.py#L139-L160: same restructuring for the interaction.response.send_message(...) calls at lines 144-146 and 151-153 in _on_close.
  • easycord/plugins/tickets.py#L391-L411: move respond_error(ctx, ...) (line 396) and ctx.respond(...) (lines 401-403) outside async with self._locks.lock(guild_id): in ticket_close.
  • easycord/plugins/tickets.py#L434-L450: move respond_error(ctx, ...) (line 439) and ctx.respond(...) (lines 443-445) outside the lock in ticket_claim.
  • easycord/plugins/tickets.py#L470-L478: move ctx.respond(...) (lines 474-477) outside the lock in ticket_add.

As per coding guidelines: "Discord or network I/O must not occur while the configuration lock is held." (Stated for ServerConfigStore.mutate, generalized to "Equivalent plugin-specific per-guild locks... held across the entire operation.")

🤖 Prompt for 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.

In `@easycord/plugins/tickets.py` around lines 108 - 127, The ticket handlers
perform Discord/network I/O while holding per-guild locks; compute validation
and state changes inside the lock, return an outcome or error sentinel, release
the lock, then perform responses afterward. Apply this to
easycord/plugins/tickets.py lines 108-127 in the claim handler, lines 139-160 in
_on_close, lines 391-411 in ticket_close, lines 434-450 in ticket_claim, and
lines 470-478 in ticket_add; move each listed send_message, respond_error, and
ctx.respond call outside its async with lock block while preserving existing
messages and behavior.

Source: Coding guidelines

easycord/plugins/birthday.py (1)

306-324: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Discord I/O runs while the per-guild lock is held.

respond_error(ctx, ...) at line 317 executes (and returns) inside async with self._locks.lock(guild_id):. This performs a network call to Discord while still holding the guild lock, stalling every other BirthdayPlugin operation on that guild for the duration of the request. economy.py's /daily shows the correct pattern in this same PR (defer the response until after the lock releases via a sentinel).

🔒 Proposed fix: defer respond_error until after the lock releases
         guild_id = ctx.guild.id
         user_id = ctx.user.id

+        not_registered = False
         async with self._locks.lock(guild_id):
             cfg = await self._store.load(guild_id)
             data: dict = cfg.get_other("birthday", {})
             birthdays: dict = data.get("birthdays", {})
             if str(user_id) not in birthdays:
-                await respond_error(ctx, "You don't have a birthday registered.")
-                return
-            birthdays.pop(str(user_id), None)
-            data["birthdays"] = birthdays
-            cfg.set_other("birthday", data)
-            await self._store.save(cfg)
+                not_registered = True
+            else:
+                birthdays.pop(str(user_id), None)
+                data["birthdays"] = birthdays
+                cfg.set_other("birthday", data)
+                await self._store.save(cfg)

+        if not_registered:
+            await respond_error(ctx, "You don't have a birthday registered.")
+            return
         await ctx.respond("Your birthday has been removed.", ephemeral=True)
🤖 Prompt for 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.

In `@easycord/plugins/birthday.py` around lines 306 - 324, Update birthday_unset
so no Discord I/O occurs inside self._locks.lock(guild_id): record a sentinel
when the user has no registered birthday, exit the lock, then call
respond_error(ctx, ...) afterward and return. Keep the existing removal and save
flow unchanged for registered birthdays, and preserve the success response after
the lock is released.

Source: Coding guidelines

easycord/plugins/reputation.py (1)

83-115: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Discord I/O runs while the per-guild lock is held.

respond_error(ctx, "You already gave rep in the last 24 hours...") (line 114) executes and returns inside async with self._locks.lock(guild_id): (opened line 108). This performs a network call to Discord while holding the guild lock, blocking every other ReputationPlugin operation for that guild for the round-trip duration — the exact anti-pattern the shared-locking guideline warns against ("Discord or network I/O must not occur while the configuration lock is held"). economy.py's /daily in this same PR shows the correct deferred-response pattern.

🔒 Proposed fix: defer the cooldown response until after the lock releases
         guild_id = ctx.guild.id
         now = datetime.now(timezone.utc)

+        on_cooldown = False
         async with self._locks.lock(guild_id):
             cfg = await self._store.load(guild_id)
             data = cfg.get_other("reputation", {})
             cooldowns: dict = data.get("cooldowns", {})

             if _is_on_cooldown(cooldowns.get(str(giver_id)), now):
-                await respond_error(ctx, "You already gave rep in the last 24 hours. Try again later.")
-                return
-
-            scores: dict = data.get("scores", {})
-            scores[str(target_id)] = scores.get(str(target_id), 0) + 1
-            cooldowns[str(giver_id)] = now.isoformat()
-            data["scores"] = scores
-            data["cooldowns"] = cooldowns
-            cfg.set_other("reputation", data)
-            await self._store.save(cfg)
-            new_score = scores[str(target_id)]
+                on_cooldown = True
+            else:
+                scores: dict = data.get("scores", {})
+                scores[str(target_id)] = scores.get(str(target_id), 0) + 1
+                cooldowns[str(giver_id)] = now.isoformat()
+                data["scores"] = scores
+                data["cooldowns"] = cooldowns
+                cfg.set_other("reputation", data)
+                await self._store.save(cfg)
+                new_score = scores[str(target_id)]

+        if on_cooldown:
+            await respond_error(ctx, "You already gave rep in the last 24 hours. Try again later.")
+            return
         await ctx.respond(f"Gave rep to {user.mention}! They now have {new_score} rep.")
🤖 Prompt for 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.

In `@easycord/plugins/reputation.py` around lines 83 - 115, Defer the cooldown
error response in ReputationPlugin.rep until after the
self._locks.lock(guild_id) context exits. Record that the giver is on cooldown
inside the lock, leave the lock without performing Discord I/O, then call
respond_error with the existing message and return; preserve the current
behavior for non-cooldown requests.

Source: Coding guidelines

easycord/plugins/word_filter.py (1)

92-169: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Hardcoded plugin response strings instead of ctx.t(...) across the migrated plugins. The coding guideline is explicit: "Never hardcode plugin response strings; retrieve localized text through ctx.t(...)." polls.py's newly-added respond_error(ctx, ctx.t("polls.min_options", default="...")) calls in this same PR show the correct pattern (localization key + English default), but the following files pass raw literals to respond_error/ctx.respond on the very lines touched by this refactor:

  • easycord/plugins/word_filter.py#L92-L169: wrap the five "This command only works in a server." messages plus the blocklist/action strings with ctx.t(key, default="...").
  • easycord/plugins/auto_role.py#L78-L135: localize "This command only works in a server.", "No auto-roles configured.", "Delay must be 0 or greater.".
  • easycord/plugins/birthday.py#L306-L389: localize "You don't have a birthday registered." and "No birthdays have been registered yet.".
  • easycord/plugins/economy.py#L265-L273: localize "❌ Amount must be positive" and "❌ Can't transfer to yourself".
  • easycord/plugins/reputation.py#L83-L207: localize the self/bot-target, cooldown, empty-leaderboard, and permission-denial messages.
  • easycord/plugins/server_stats.py#L127-L178: localize the "server-only" and channel-creation-failure messages.
  • easycord/plugins/starboard.py#L271-L278: localize "Threshold must be at least 1.".
  • easycord/plugins/suggestions.py#L75-L202: localize the channel-not-configured/not-found, post-failure, and permission-denial messages.
🤖 Prompt for 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.

In `@easycord/plugins/word_filter.py` around lines 92 - 169, Replace hardcoded
plugin response strings with localized ctx.t(key, default="...") calls,
preserving each English message as its default. Update filter_add,
filter_remove, filter_list, filter_action, and filter_exempt in
easycord/plugins/word_filter.py#L92-L169; the affected command handlers in
easycord/plugins/auto_role.py#L78-L135, birthday.py#L306-L389,
economy.py#L265-L273, reputation.py#L83-L207, server_stats.py#L127-L178,
starboard.py#L271-L278, and suggestions.py#L75-L202. Cover every listed
server-only, validation, blocklist, target, cooldown, leaderboard, permission,
channel, failure, and threshold response.

Source: Coding guidelines

easycord/plugins/giveaway.py (1)

91-111: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

These migrations bypass ServerConfigStore’s atomic mutation contract. A separate GuildLockManager does not serialize with writers using the store’s own lock; use ServerConfigStore.mutate for every read-modify-save and perform Discord responses after it returns.

  • easycord/plugins/giveaway.py#L91-L111: mutate giveaway entries through ServerConfigStore.mutate; send the ended response after mutation.
  • easycord/plugins/giveaway.py#L203-L215: atomically select and persist winners through ServerConfigStore.mutate.
  • easycord/plugins/giveaway.py#L309-L322: create the giveaway through ServerConfigStore.mutate.
  • easycord/plugins/giveaway.py#L373-L386: atomically persist rerolled winners through ServerConfigStore.mutate.
  • easycord/plugins/reminder.py#L147-L163: atomically mark the reminder complete through ServerConfigStore.mutate.
  • easycord/plugins/reminder.py#L210-L228: atomically allocate the ID and append the reminder through ServerConfigStore.mutate.
  • easycord/plugins/reminder.py#L288-L305: return the cancellation outcome from ServerConfigStore.mutate; call respond_error after the mutation.
  • easycord/plugins/scheduled_announcements.py#L152-L162: atomically advance next_fire through ServerConfigStore.mutate.
  • easycord/plugins/scheduled_announcements.py#L213-L230: atomically allocate and save the announcement through ServerConfigStore.mutate.
  • easycord/plugins/scheduled_announcements.py#L293-L306: return whether removal occurred from ServerConfigStore.mutate; respond after it returns.

As per coding guidelines, “Use ServerConfigStore.mutate(guild_id, fn) for every per-guild load-modify-save operation” and “Discord or network I/O must not occur while the configuration lock is held.”

🤖 Prompt for 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.

In `@easycord/plugins/giveaway.py` around lines 91 - 111, Replace every listed
per-guild load-modify-save flow with ServerConfigStore.mutate(guild_id, fn),
removing reliance on GuildLockManager: easycord/plugins/giveaway.py lines
91-111, 203-215, 309-322, and 373-386; easycord/plugins/reminder.py lines
147-163, 210-228, and 288-305; and easycord/plugins/scheduled_announcements.py
lines 152-162, 213-230, and 293-306. Keep mutation callbacks limited to
configuration changes, return required outcomes such as cancellation/removal
status from mutate, and perform all Discord responses after mutate returns; the
giveaway entry flow must send the ended response after mutation, while winner
selection, creation, rerolls, reminder completion/allocation, and announcement
scheduling/allocation must remain atomic.

Source: Coding guidelines

🤖 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/plugins/giveaway.py`:
- Line 16: Re-export GuildLockManager, respond_error, and the JSON helpers from
easycord’s public API, then update consumers to import only from easycord:
easycord/plugins/giveaway.py:16, juicewrld.py:105, levels.py:15, openclaw.py:15,
reminder.py:14, scheduled_announcements.py:15, and tags.py:8. Replace each
_shared import with the corresponding public easycord import while preserving
existing usage.
- Line 145: Update GuildLockManager and its usages to acquire locks through a
lease/context-manager entry that increments a reference count before awaiting
acquisition and decrements it on release; make _cleanup evict only entries with
a zero reference count. Apply the migration at
easycord/plugins/giveaway.py:145-145, easycord/plugins/reminder.py:73-73,
easycord/plugins/scheduled_announcements.py:84-84, and
easycord/plugins/tags.py:17-17, updating each guild critical-section usage to
use the new lease API.

In `@easycord/plugins/levels.py`:
- Around line 363-366: Update the validation error responses in the surrounding
command handler to pass localization keys through ctx.t(...) instead of
hardcoded multiplier and duration messages. Preserve the existing validation
conditions, response flow, and user-facing wording via the corresponding
localized strings.

In `@easycord/plugins/server_stats.py`:
- Around line 127-131: Update the class docstring’s documentation for
/stats_setup and /stats_teardown to state that both require Administrator
permission instead of manage_guild, matching their require_admin=True
decorators. Leave the command behavior unchanged.

In `@easycord/plugins/suggestions.py`:
- Around line 103-124: Guard the reaction calls in the suggestion-post flow
around msg.add_reaction so reaction permission/API failures are handled before
proceeding. Ensure a failure reports the error through respond_error and exits
without leaving the posted suggestion untracked, while preserving persistence
via self.config.store.mutate for successful reactions.

In `@easycord/plugins/welcome.py`:
- Line 146: Replace the hardcoded guild-only message passed to respond_error in
easycord/plugins/welcome.py at lines 146-146, 155-155, 164-164, 181-181, and
198-198 with the localized ctx.t("errors.guild_only", ...) result, preserving
the existing ephemeral response behavior.

---

Outside diff comments:
In `@easycord/plugins/birthday.py`:
- Around line 306-324: Update birthday_unset so no Discord I/O occurs inside
self._locks.lock(guild_id): record a sentinel when the user has no registered
birthday, exit the lock, then call respond_error(ctx, ...) afterward and return.
Keep the existing removal and save flow unchanged for registered birthdays, and
preserve the success response after the lock is released.

In `@easycord/plugins/giveaway.py`:
- Around line 91-111: Replace every listed per-guild load-modify-save flow with
ServerConfigStore.mutate(guild_id, fn), removing reliance on GuildLockManager:
easycord/plugins/giveaway.py lines 91-111, 203-215, 309-322, and 373-386;
easycord/plugins/reminder.py lines 147-163, 210-228, and 288-305; and
easycord/plugins/scheduled_announcements.py lines 152-162, 213-230, and 293-306.
Keep mutation callbacks limited to configuration changes, return required
outcomes such as cancellation/removal status from mutate, and perform all
Discord responses after mutate returns; the giveaway entry flow must send the
ended response after mutation, while winner selection, creation, rerolls,
reminder completion/allocation, and announcement scheduling/allocation must
remain atomic.

In `@easycord/plugins/reputation.py`:
- Around line 83-115: Defer the cooldown error response in ReputationPlugin.rep
until after the self._locks.lock(guild_id) context exits. Record that the giver
is on cooldown inside the lock, leave the lock without performing Discord I/O,
then call respond_error with the existing message and return; preserve the
current behavior for non-cooldown requests.

In `@easycord/plugins/tickets.py`:
- Around line 356-371: Guard the view registration in the ticket-opening flow
after the existing panel_msg check: only call self.bot.add_view(view,
message_id=panel_msg.id) when panel_msg is not None, while still allowing the
ticket save and final ctx.respond(...) to run when sending the panel fails. Use
the existing panel_msg handling near _TicketView and send_safe as the change
point.
- Around line 108-127: The ticket handlers perform Discord/network I/O while
holding per-guild locks; compute validation and state changes inside the lock,
return an outcome or error sentinel, release the lock, then perform responses
afterward. Apply this to easycord/plugins/tickets.py lines 108-127 in the claim
handler, lines 139-160 in _on_close, lines 391-411 in ticket_close, lines
434-450 in ticket_claim, and lines 470-478 in ticket_add; move each listed
send_message, respond_error, and ctx.respond call outside its async with lock
block while preserving existing messages and behavior.

In `@easycord/plugins/word_filter.py`:
- Around line 92-169: Replace hardcoded plugin response strings with localized
ctx.t(key, default="...") calls, preserving each English message as its default.
Update filter_add, filter_remove, filter_list, filter_action, and filter_exempt
in easycord/plugins/word_filter.py#L92-L169; the affected command handlers in
easycord/plugins/auto_role.py#L78-L135, birthday.py#L306-L389,
economy.py#L265-L273, reputation.py#L83-L207, server_stats.py#L127-L178,
starboard.py#L271-L278, and suggestions.py#L75-L202. Cover every listed
server-only, validation, blocklist, target, cooldown, leaderboard, permission,
channel, failure, and threshold response.
🪄 Autofix (Beta)

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: 17e2c9dd-65ce-4d0c-975f-9cb9219a700e

📥 Commits

Reviewing files that changed from the base of the PR and between 662f589 and 446e48b.

📒 Files selected for processing (35)
  • easycord/plugins/_shared.py
  • easycord/plugins/ai_moderator.py
  • easycord/plugins/auto_role.py
  • easycord/plugins/birthday.py
  • easycord/plugins/economy.py
  • easycord/plugins/giveaway.py
  • easycord/plugins/invite_tracker.py
  • easycord/plugins/juicewrld.py
  • easycord/plugins/levels.py
  • easycord/plugins/member_logging.py
  • easycord/plugins/moderation.py
  • easycord/plugins/openclaw.py
  • easycord/plugins/polls.py
  • easycord/plugins/reminder.py
  • easycord/plugins/reputation.py
  • easycord/plugins/scheduled_announcements.py
  • easycord/plugins/server_stats.py
  • easycord/plugins/starboard.py
  • easycord/plugins/suggestions.py
  • easycord/plugins/tags.py
  • easycord/plugins/tickets.py
  • easycord/plugins/verification.py
  • easycord/plugins/welcome.py
  • easycord/plugins/word_filter.py
  • tests/test_birthday.py
  • tests/test_economy.py
  • tests/test_giveaway_commands.py
  • tests/test_plugin_logic.py
  • tests/test_polls.py
  • tests/test_reminder.py
  • tests/test_server_stats.py
  • tests/test_shared_helpers.py
  • tests/test_stress.py
  • tests/test_tickets_commands.py
  • tests/test_verification.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

For any question about the codebase, query graphify against graphify-out/graph.json before opening source, documentation, or context files; do not read the deprecated Wiki or rebuild the graph unless explicitly asked.

Files:

  • easycord/plugins/invite_tracker.py
  • tests/test_plugin_logic.py
  • tests/test_tickets_commands.py
  • easycord/plugins/ai_moderator.py
  • easycord/plugins/moderation.py
  • tests/test_polls.py
  • tests/test_giveaway_commands.py
  • tests/test_shared_helpers.py
  • easycord/plugins/starboard.py
  • easycord/plugins/member_logging.py
  • tests/test_stress.py
  • tests/test_reminder.py
  • tests/test_economy.py
  • easycord/plugins/welcome.py
  • easycord/plugins/polls.py
  • easycord/plugins/reputation.py
  • easycord/plugins/openclaw.py
  • tests/test_birthday.py
  • easycord/plugins/server_stats.py
  • easycord/plugins/suggestions.py
  • easycord/plugins/tags.py
  • easycord/plugins/scheduled_announcements.py
  • tests/test_server_stats.py
  • easycord/plugins/verification.py
  • easycord/plugins/birthday.py
  • tests/test_verification.py
  • easycord/plugins/levels.py
  • easycord/plugins/_shared.py
  • easycord/plugins/giveaway.py
  • easycord/plugins/juicewrld.py
  • easycord/plugins/word_filter.py
  • easycord/plugins/auto_role.py
  • easycord/plugins/economy.py
  • easycord/plugins/reminder.py
  • easycord/plugins/tickets.py
easycord/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

easycord/**/*.py: Treat easycord/__init__.py as the stable public API. Import consumers from easycord, never from _-prefixed internal modules or helpers.
Use ctx.user or ctx.member, not ctx.author; access ctx.is_admin as a property and never call it as ctx.is_admin().
Store per-guild plugin state in the database layer using per-guild namespaced records, never on the Plugin instance itself.
Never hardcode plugin response strings; retrieve localized text through ctx.t(...).
Always await asynchronous ToolLimiter methods, including check_limit, reset_user, and reset_tool.
Require an explicit ToolSafety annotation when registering an @ai_tool; keep RESTRICTED tools unexposed.
Preserve Discord command-registration constraints before synchronization: names must be at most 32 characters and match [-_a-z0-9], descriptions at most 100 characters, and commands/options at most 25 choices or options as applicable.
Use ServerConfigStore.mutate(guild_id, fn) for every per-guild load-modify-save operation. The callback must be synchronous and local; perform Discord or network I/O outside the lock.
Never perform an unguarded per-guild configuration read-modify-write. Equivalent plugin-specific per-guild locks are acceptable when held across the entire operation, but Discord or network I/O must not occur while the configuration lock is held.
For event-path plugins such as @on("message"), route every destructive action through one governed method that owns rate limiting, channel narrowing, and Discord error handling; Discord exceptions must not escape into the dispatcher.
Before calling .send() on a channel obtained from context or Discord, narrow its type with SENDABLE_CHANNEL_TYPES using isinstance; do not call .send() on an unnarrowed channel.
Initialize LevelsPlugin._cooldowns sentinels to float("-inf"), not 0.0, so the first message always passes.
sync_commands() must raise on removals unless confirm_removals=True is exp...

Files:

  • easycord/plugins/invite_tracker.py
  • easycord/plugins/ai_moderator.py
  • easycord/plugins/moderation.py
  • easycord/plugins/starboard.py
  • easycord/plugins/member_logging.py
  • easycord/plugins/welcome.py
  • easycord/plugins/polls.py
  • easycord/plugins/reputation.py
  • easycord/plugins/openclaw.py
  • easycord/plugins/server_stats.py
  • easycord/plugins/suggestions.py
  • easycord/plugins/tags.py
  • easycord/plugins/scheduled_announcements.py
  • easycord/plugins/verification.py
  • easycord/plugins/birthday.py
  • easycord/plugins/levels.py
  • easycord/plugins/_shared.py
  • easycord/plugins/giveaway.py
  • easycord/plugins/juicewrld.py
  • easycord/plugins/word_filter.py
  • easycord/plugins/auto_role.py
  • easycord/plugins/economy.py
  • easycord/plugins/reminder.py
  • easycord/plugins/tickets.py
**/*.{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.json with /graphify; do not manually read source first.

Files:

  • easycord/plugins/invite_tracker.py
  • tests/test_plugin_logic.py
  • tests/test_tickets_commands.py
  • easycord/plugins/ai_moderator.py
  • easycord/plugins/moderation.py
  • tests/test_polls.py
  • tests/test_giveaway_commands.py
  • tests/test_shared_helpers.py
  • easycord/plugins/starboard.py
  • easycord/plugins/member_logging.py
  • tests/test_stress.py
  • tests/test_reminder.py
  • tests/test_economy.py
  • easycord/plugins/welcome.py
  • easycord/plugins/polls.py
  • easycord/plugins/reputation.py
  • easycord/plugins/openclaw.py
  • tests/test_birthday.py
  • easycord/plugins/server_stats.py
  • easycord/plugins/suggestions.py
  • easycord/plugins/tags.py
  • easycord/plugins/scheduled_announcements.py
  • tests/test_server_stats.py
  • easycord/plugins/verification.py
  • easycord/plugins/birthday.py
  • tests/test_verification.py
  • easycord/plugins/levels.py
  • easycord/plugins/_shared.py
  • easycord/plugins/giveaway.py
  • easycord/plugins/juicewrld.py
  • easycord/plugins/word_filter.py
  • easycord/plugins/auto_role.py
  • easycord/plugins/economy.py
  • easycord/plugins/reminder.py
  • easycord/plugins/tickets.py
easycord/plugins/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

Look up localization keys through LocalizationManager; do not hardcode strings in plugin responses.

Files:

  • easycord/plugins/invite_tracker.py
  • easycord/plugins/ai_moderator.py
  • easycord/plugins/moderation.py
  • easycord/plugins/starboard.py
  • easycord/plugins/member_logging.py
  • easycord/plugins/welcome.py
  • easycord/plugins/polls.py
  • easycord/plugins/reputation.py
  • easycord/plugins/openclaw.py
  • easycord/plugins/server_stats.py
  • easycord/plugins/suggestions.py
  • easycord/plugins/tags.py
  • easycord/plugins/scheduled_announcements.py
  • easycord/plugins/verification.py
  • easycord/plugins/birthday.py
  • easycord/plugins/levels.py
  • easycord/plugins/_shared.py
  • easycord/plugins/giveaway.py
  • easycord/plugins/juicewrld.py
  • easycord/plugins/word_filter.py
  • easycord/plugins/auto_role.py
  • easycord/plugins/economy.py
  • easycord/plugins/reminder.py
  • easycord/plugins/tickets.py
tests/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

tests/**/*.py: Use easycord.testing helpers such as invoke, FakeContextBuilder, and PluginTestSuite for command and plugin tests instead of requiring a live Discord connection.
When constructing plugins manually in tests, use MyPlugin.__new__, assign plugin._bot = bot, and then call Plugin.__init__(plugin); do not assign plugin.bot.

tests/**/*.py: Use pytest and pytest-asyncio with asyncio_mode = "auto"; asynchronous tests do not need manual event-loop setup.
Run the full test suite with pytest tests/; individual tests may be run using their test node IDs.

Files:

  • tests/test_plugin_logic.py
  • tests/test_tickets_commands.py
  • tests/test_polls.py
  • tests/test_giveaway_commands.py
  • tests/test_shared_helpers.py
  • tests/test_stress.py
  • tests/test_reminder.py
  • tests/test_economy.py
  • tests/test_birthday.py
  • tests/test_server_stats.py
  • tests/test_verification.py
**/test*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Maintain at least 20 tests per plugin, as verified by scripts/verify_plugin_tests.py.

Files:

  • tests/test_plugin_logic.py
  • tests/test_tickets_commands.py
  • tests/test_polls.py
  • tests/test_giveaway_commands.py
  • tests/test_shared_helpers.py
  • tests/test_stress.py
  • tests/test_reminder.py
  • tests/test_economy.py
  • tests/test_birthday.py
  • tests/test_server_stats.py
  • tests/test_verification.py
🔇 Additional comments (27)
easycord/plugins/welcome.py (1)

16-16: LGTM!

easycord/plugins/tickets.py (1)

12-15: LGTM!

Also applies to: 57-64, 193-193, 252-275, 275-290, 313-317, 332-332

easycord/plugins/verification.py (1)

11-13: LGTM!

Also applies to: 28-32, 195-195, 222-222, 239-243, 256-260, 302-302, 313-313, 323-329

tests/test_birthday.py (1)

170-170: LGTM!

Also applies to: 186-193, 209-209

tests/test_economy.py (1)

47-55: LGTM!

Also applies to: 142-149

tests/test_giveaway_commands.py (1)

193-200: LGTM!

Also applies to: 208-215

tests/test_plugin_logic.py (1)

52-55: LGTM!

tests/test_polls.py (1)

56-56: LGTM!

Also applies to: 508-508, 525-525

tests/test_reminder.py (1)

125-125: LGTM!

Also applies to: 144-152, 171-171, 189-189

tests/test_server_stats.py (1)

124-140: LGTM!

Also applies to: 143-162, 165-174, 181-188, 263-291, 294-318

tests/test_stress.py (1)

94-107: LGTM!

Also applies to: 118-126, 588-591

tests/test_tickets_commands.py (1)

221-221: LGTM!

Also applies to: 266-266, 285-285

tests/test_verification.py (1)

80-87: LGTM!

Also applies to: 95-103, 109-116, 130-136, 166-170, 218-232, 235-236, 239-248, 270-281, 299-308

easycord/plugins/_shared.py (1)

4-77: LGTM!

Also applies to: 113-182

tests/test_shared_helpers.py (1)

1-214: LGTM!

easycord/plugins/birthday.py (1)

12-14: LGTM on the lock-migration and send_safe adoption elsewhere in this file. Hardcoded strings (e.g. lines 317, 389) are addressed in the cross-file consolidated comment.

Also applies to: 120-123, 180-180, 231-231, 291-291, 338-338, 362-362, 380-380, 389-389

easycord/plugins/economy.py (1)

4-12: LGTM! Lock rename is consistent, and /daily's deferred-response pattern (respond only after the lock releases) is the correct approach — worth pointing to as the reference pattern for other plugins in this PR.

Also applies to: 53-56, 79-82, 90-100, 102-123, 143-152, 186-198

easycord/plugins/polls.py (1)

14-14: LGTM! Nice use of ctx.t(...) with default= fallback for the new respond_error calls — this is the pattern the other migrated plugins in this batch should follow (see consolidated comment).

Also applies to: 115-115, 168-171, 238-238, 292-301, 313-313

easycord/plugins/reputation.py (1)

12-12: LGTM on the lock renaming and on rep_reset's correct lock-boundary placement. Hardcoded strings (e.g. "You cannot give rep to yourself.", "No rep has been given yet.") are covered in the consolidated comment.

Also applies to: 73-77, 155-166, 188-207

easycord/plugins/server_stats.py (1)

12-12: LGTM on the lock migration; lock is never held across a Discord response. Hardcoded error strings (e.g. "This command can only be used in a server.") are covered in the consolidated comment.

Also applies to: 54-59, 156-159, 214-217

easycord/plugins/starboard.py (1)

10-12: LGTM! send_safe adoption in _archive_message is clean and correctly guarded. The hardcoded string at line 275 is covered in the consolidated comment.

Also applies to: 128-139, 275-275

easycord/plugins/suggestions.py (1)

11-13: LGTM on the send_safe/respond_error migration and lock-free mutate usage. Hardcoded strings (e.g. "❌ Suggestions channel not configured") are covered in the consolidated comment.

Also applies to: 83-88, 157-177, 181-202

easycord/plugins/word_filter.py (1)

4-12: LGTM! Lock boundaries are correctly scoped everywhere (no response calls inside async with self._locks.lock(...)), and the DM-warning send_safe use is appropriate. Hardcoded strings are covered in the consolidated comment.

Also applies to: 58-58, 83-97, 111-114, 124-150, 161-163

easycord/plugins/ai_moderator.py (1)

17-17: LGTM!

Also applies to: 219-219

easycord/plugins/invite_tracker.py (1)

10-10: LGTM!

Also applies to: 98-98

easycord/plugins/member_logging.py (1)

11-11: LGTM!

Also applies to: 70-70

easycord/plugins/moderation.py (1)

11-11: LGTM!

Also applies to: 110-110

Comment thread easycord/plugins/giveaway.py
Comment thread easycord/plugins/giveaway.py
Comment thread easycord/plugins/levels.py
Comment thread easycord/plugins/server_stats.py
Comment thread easycord/plugins/suggestions.py
Comment thread easycord/plugins/welcome.py
@github-actions github-actions Bot added documentation Improvements or additions to documentation dependencies labels Jul 25, 2026
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Comment thread easycord/plugins/juicewrld.py Fixed
Comment thread tests/test_shared_helpers.py Fixed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@CHANGELOG.md`:
- Around line 9-10: Update the plugin counts in the GuildLockManager and
send_safe adoption entries in CHANGELOG.md to match their respective lists,
ensuring both release-note summaries are accurate before publishing.
🪄 Autofix (Beta)

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: eb2f8abb-a14d-4c66-9036-3028ca208d0e

📥 Commits

Reviewing files that changed from the base of the PR and between 446e48b and 6fb99cc.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • README.md
  • docs/getting-started.md
  • easycord/__init__.py
  • pyproject.toml
📜 Review details
⏰ Context from checks skipped due to timeout. (2)
  • GitHub Check: Sourcery review
  • GitHub Check: Analyze (Python)
🧰 Additional context used
📓 Path-based instructions (3)
**/*

📄 CodeRabbit inference engine (CLAUDE.md)

For any question about the codebase, query graphify against graphify-out/graph.json before opening source, documentation, or context files; do not read the deprecated Wiki or rebuild the graph unless explicitly asked.

Files:

  • pyproject.toml
  • docs/getting-started.md
  • README.md
  • easycord/__init__.py
  • CHANGELOG.md
**/*.{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.json with /graphify; do not manually read source first.

Files:

  • docs/getting-started.md
  • README.md
  • easycord/__init__.py
  • CHANGELOG.md
easycord/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

easycord/**/*.py: Treat easycord/__init__.py as the stable public API. Import consumers from easycord, never from _-prefixed internal modules or helpers.
Use ctx.user or ctx.member, not ctx.author; access ctx.is_admin as a property and never call it as ctx.is_admin().
Store per-guild plugin state in the database layer using per-guild namespaced records, never on the Plugin instance itself.
Never hardcode plugin response strings; retrieve localized text through ctx.t(...).
Always await asynchronous ToolLimiter methods, including check_limit, reset_user, and reset_tool.
Require an explicit ToolSafety annotation when registering an @ai_tool; keep RESTRICTED tools unexposed.
Preserve Discord command-registration constraints before synchronization: names must be at most 32 characters and match [-_a-z0-9], descriptions at most 100 characters, and commands/options at most 25 choices or options as applicable.
Use ServerConfigStore.mutate(guild_id, fn) for every per-guild load-modify-save operation. The callback must be synchronous and local; perform Discord or network I/O outside the lock.
Never perform an unguarded per-guild configuration read-modify-write. Equivalent plugin-specific per-guild locks are acceptable when held across the entire operation, but Discord or network I/O must not occur while the configuration lock is held.
For event-path plugins such as @on("message"), route every destructive action through one governed method that owns rate limiting, channel narrowing, and Discord error handling; Discord exceptions must not escape into the dispatcher.
Before calling .send() on a channel obtained from context or Discord, narrow its type with SENDABLE_CHANNEL_TYPES using isinstance; do not call .send() on an unnarrowed channel.
Initialize LevelsPlugin._cooldowns sentinels to float("-inf"), not 0.0, so the first message always passes.
sync_commands() must raise on removals unless confirm_removals=True is exp...

Files:

  • easycord/__init__.py
🔇 Additional comments (5)
CHANGELOG.md (1)

5-7: LGTM!

Also applies to: 11-16

README.md (1)

2-2: LGTM!

Also applies to: 28-28, 307-307

docs/getting-started.md (1)

6-6: LGTM!

easycord/__init__.py (1)

19-19: LGTM!

pyproject.toml (1)

7-7: LGTM!

Also applies to: 51-52

Comment thread CHANGELOG.md Outdated
tee1339 added 2 commits July 25, 2026 17:07
…panel and log sends

- test_shared_helpers.py: 5 new tests for GuildLockManager._cleanup() — idle >7 day
  eviction (lines 170-171), held-lock immunity, recent-lock preservation, and the
  MAX_TRACKED_GUILDS cap eviction (lines 175-182)
- test_suggestions.py: 2 new tests for add_reaction Forbidden/HTTPException handler
  (lines 111-112) — confirms command completes and suggestion is stored despite failure
- test_giveaway_commands.py: 2 new tests for _end_giveaway send_safe winner
  announcement (lines 249-262) — winners path and no-entries path
- test_tickets_commands.py: 4 new tests — panel_message_id guard from send_safe
  (line 357), log channel send in _finish_close (lines 245-268), no-send when
  log_channel_id is None, no-send when channel is not a TextChannel
@rolling-codes
rolling-codes merged commit 397755b into main Jul 25, 2026
12 checks passed
@rolling-codes
rolling-codes deleted the refactor/dry-phase1-config-helpers branch July 25, 2026 21:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies documentation Improvements or additions to documentation enhancement New feature or request plugin release:minor tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants