refactor: DRY pass — GuildLockManager, send_safe, require_admin, EmbedBuilder#123
Conversation
…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.
…ts): adopt send_safe
…r for panel embed
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Sorry @rolling-codes, you have reached your weekly rate limit of 500000 diff characters.
Please try again later or upgrade to continue using Sourcery
|
Warning Review limit reached
Next review available in: 34 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (12)
📝 WalkthroughWalkthroughEasyCord 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 ChangesShared infrastructure and plugin migration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 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 |
🤖 Augment PR SummarySummary: Mechanical DRY refactor across bundled plugins to centralize common patterns (locks, error replies, safe sends, and embed creation) without intended behavior changes. Changes:
Technical Notes: Lock manager includes idle eviction to bound memory growth; typed ID helpers normalize persisted snowflake IDs back to 🤖 Was this summary useful? React with 👍 or 👎 |
There was a problem hiding this comment.
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 winUnconditional
panel_msg.idaccess crashes whensend_safereturnsNone.
send_safereturnsNoneon aForbidden/HTTPExceptionsend failure (per_shared/send_safecontract used elsewhere in this PR). Line 358-359 correctly guardspanel_msg is not Nonebefore storingpanel_message_id, but line 368'sself.bot.add_view(view, message_id=panel_msg.id)is unconditional — if the panel send fails, this raisesAttributeError: 'NoneType' object has no attribute 'id', which propagates out of the command and the finalctx.respond(...)never runs, leaving the user with no response even though the ticket thread/DB record was already created and saved.Compare with
verification_panelineasycord/plugins/verification.py, which correctly doesif message is None: await ctx.respond(...); returnbefore callingadd_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 winDiscord API calls executed while
self._locks/self._plugin._locksis held, across 5 sites in tickets.py.Each site interleaves a validation check with a Discord response call before releasing the per-guild
GuildLockManagerlock, holding it across a network round-trip. This repeats a pattern the coding guidelines explicitly forbid for per-guild locks, andverification.pyin 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 twointeraction.response.send_message(...)calls (lines 113-115, 119-121) so they execute after releasingself._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 theinteraction.response.send_message(...)calls at lines 144-146 and 151-153 in_on_close.easycord/plugins/tickets.py#L391-L411: moverespond_error(ctx, ...)(line 396) andctx.respond(...)(lines 401-403) outsideasync with self._locks.lock(guild_id):inticket_close.easycord/plugins/tickets.py#L434-L450: moverespond_error(ctx, ...)(line 439) andctx.respond(...)(lines 443-445) outside the lock inticket_claim.easycord/plugins/tickets.py#L470-L478: movectx.respond(...)(lines 474-477) outside the lock inticket_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 winDiscord I/O runs while the per-guild lock is held.
respond_error(ctx, ...)at line 317 executes (andreturns) insideasync with self._locks.lock(guild_id):. This performs a network call to Discord while still holding the guild lock, stalling every otherBirthdayPluginoperation on that guild for the duration of the request.economy.py's/dailyshows 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 winDiscord 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 andreturns insideasync with self._locks.lock(guild_id):(opened line 108). This performs a network call to Discord while holding the guild lock, blocking every otherReputationPluginoperation 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/dailyin 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 liftHardcoded plugin response strings instead of
ctx.t(...)across the migrated plugins. The coding guideline is explicit: "Never hardcode plugin response strings; retrieve localized text throughctx.t(...)."polls.py's newly-addedrespond_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 torespond_error/ctx.respondon 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 withctx.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 liftThese migrations bypass
ServerConfigStore’s atomic mutation contract. A separateGuildLockManagerdoes not serialize with writers using the store’s own lock; useServerConfigStore.mutatefor every read-modify-save and perform Discord responses after it returns.
easycord/plugins/giveaway.py#L91-L111: mutate giveaway entries throughServerConfigStore.mutate; send the ended response after mutation.easycord/plugins/giveaway.py#L203-L215: atomically select and persist winners throughServerConfigStore.mutate.easycord/plugins/giveaway.py#L309-L322: create the giveaway throughServerConfigStore.mutate.easycord/plugins/giveaway.py#L373-L386: atomically persist rerolled winners throughServerConfigStore.mutate.easycord/plugins/reminder.py#L147-L163: atomically mark the reminder complete throughServerConfigStore.mutate.easycord/plugins/reminder.py#L210-L228: atomically allocate the ID and append the reminder throughServerConfigStore.mutate.easycord/plugins/reminder.py#L288-L305: return the cancellation outcome fromServerConfigStore.mutate; callrespond_errorafter the mutation.easycord/plugins/scheduled_announcements.py#L152-L162: atomically advancenext_firethroughServerConfigStore.mutate.easycord/plugins/scheduled_announcements.py#L213-L230: atomically allocate and save the announcement throughServerConfigStore.mutate.easycord/plugins/scheduled_announcements.py#L293-L306: return whether removal occurred fromServerConfigStore.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
📒 Files selected for processing (35)
easycord/plugins/_shared.pyeasycord/plugins/ai_moderator.pyeasycord/plugins/auto_role.pyeasycord/plugins/birthday.pyeasycord/plugins/economy.pyeasycord/plugins/giveaway.pyeasycord/plugins/invite_tracker.pyeasycord/plugins/juicewrld.pyeasycord/plugins/levels.pyeasycord/plugins/member_logging.pyeasycord/plugins/moderation.pyeasycord/plugins/openclaw.pyeasycord/plugins/polls.pyeasycord/plugins/reminder.pyeasycord/plugins/reputation.pyeasycord/plugins/scheduled_announcements.pyeasycord/plugins/server_stats.pyeasycord/plugins/starboard.pyeasycord/plugins/suggestions.pyeasycord/plugins/tags.pyeasycord/plugins/tickets.pyeasycord/plugins/verification.pyeasycord/plugins/welcome.pyeasycord/plugins/word_filter.pytests/test_birthday.pytests/test_economy.pytests/test_giveaway_commands.pytests/test_plugin_logic.pytests/test_polls.pytests/test_reminder.pytests/test_server_stats.pytests/test_shared_helpers.pytests/test_stress.pytests/test_tickets_commands.pytests/test_verification.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (6)
**/*
📄 CodeRabbit inference engine (CLAUDE.md)
For any question about the codebase, query
graphifyagainstgraphify-out/graph.jsonbefore opening source, documentation, or context files; do not read the deprecated Wiki or rebuild the graph unless explicitly asked.
Files:
easycord/plugins/invite_tracker.pytests/test_plugin_logic.pytests/test_tickets_commands.pyeasycord/plugins/ai_moderator.pyeasycord/plugins/moderation.pytests/test_polls.pytests/test_giveaway_commands.pytests/test_shared_helpers.pyeasycord/plugins/starboard.pyeasycord/plugins/member_logging.pytests/test_stress.pytests/test_reminder.pytests/test_economy.pyeasycord/plugins/welcome.pyeasycord/plugins/polls.pyeasycord/plugins/reputation.pyeasycord/plugins/openclaw.pytests/test_birthday.pyeasycord/plugins/server_stats.pyeasycord/plugins/suggestions.pyeasycord/plugins/tags.pyeasycord/plugins/scheduled_announcements.pytests/test_server_stats.pyeasycord/plugins/verification.pyeasycord/plugins/birthday.pytests/test_verification.pyeasycord/plugins/levels.pyeasycord/plugins/_shared.pyeasycord/plugins/giveaway.pyeasycord/plugins/juicewrld.pyeasycord/plugins/word_filter.pyeasycord/plugins/auto_role.pyeasycord/plugins/economy.pyeasycord/plugins/reminder.pyeasycord/plugins/tickets.py
easycord/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
easycord/**/*.py: Treateasycord/__init__.pyas the stable public API. Import consumers fromeasycord, never from_-prefixed internal modules or helpers.
Usectx.userorctx.member, notctx.author; accessctx.is_adminas a property and never call it asctx.is_admin().
Store per-guild plugin state in the database layer using per-guild namespaced records, never on thePlugininstance itself.
Never hardcode plugin response strings; retrieve localized text throughctx.t(...).
Always await asynchronousToolLimitermethods, includingcheck_limit,reset_user, andreset_tool.
Require an explicitToolSafetyannotation when registering an@ai_tool; keepRESTRICTEDtools 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.
UseServerConfigStore.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 withSENDABLE_CHANNEL_TYPESusingisinstance; do not call.send()on an unnarrowed channel.
InitializeLevelsPlugin._cooldownssentinels tofloat("-inf"), not0.0, so the first message always passes.
sync_commands()must raise on removals unlessconfirm_removals=Trueis exp...
Files:
easycord/plugins/invite_tracker.pyeasycord/plugins/ai_moderator.pyeasycord/plugins/moderation.pyeasycord/plugins/starboard.pyeasycord/plugins/member_logging.pyeasycord/plugins/welcome.pyeasycord/plugins/polls.pyeasycord/plugins/reputation.pyeasycord/plugins/openclaw.pyeasycord/plugins/server_stats.pyeasycord/plugins/suggestions.pyeasycord/plugins/tags.pyeasycord/plugins/scheduled_announcements.pyeasycord/plugins/verification.pyeasycord/plugins/birthday.pyeasycord/plugins/levels.pyeasycord/plugins/_shared.pyeasycord/plugins/giveaway.pyeasycord/plugins/juicewrld.pyeasycord/plugins/word_filter.pyeasycord/plugins/auto_role.pyeasycord/plugins/economy.pyeasycord/plugins/reminder.pyeasycord/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.jsonwith/graphify; do not manually read source first.
Files:
easycord/plugins/invite_tracker.pytests/test_plugin_logic.pytests/test_tickets_commands.pyeasycord/plugins/ai_moderator.pyeasycord/plugins/moderation.pytests/test_polls.pytests/test_giveaway_commands.pytests/test_shared_helpers.pyeasycord/plugins/starboard.pyeasycord/plugins/member_logging.pytests/test_stress.pytests/test_reminder.pytests/test_economy.pyeasycord/plugins/welcome.pyeasycord/plugins/polls.pyeasycord/plugins/reputation.pyeasycord/plugins/openclaw.pytests/test_birthday.pyeasycord/plugins/server_stats.pyeasycord/plugins/suggestions.pyeasycord/plugins/tags.pyeasycord/plugins/scheduled_announcements.pytests/test_server_stats.pyeasycord/plugins/verification.pyeasycord/plugins/birthday.pytests/test_verification.pyeasycord/plugins/levels.pyeasycord/plugins/_shared.pyeasycord/plugins/giveaway.pyeasycord/plugins/juicewrld.pyeasycord/plugins/word_filter.pyeasycord/plugins/auto_role.pyeasycord/plugins/economy.pyeasycord/plugins/reminder.pyeasycord/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.pyeasycord/plugins/ai_moderator.pyeasycord/plugins/moderation.pyeasycord/plugins/starboard.pyeasycord/plugins/member_logging.pyeasycord/plugins/welcome.pyeasycord/plugins/polls.pyeasycord/plugins/reputation.pyeasycord/plugins/openclaw.pyeasycord/plugins/server_stats.pyeasycord/plugins/suggestions.pyeasycord/plugins/tags.pyeasycord/plugins/scheduled_announcements.pyeasycord/plugins/verification.pyeasycord/plugins/birthday.pyeasycord/plugins/levels.pyeasycord/plugins/_shared.pyeasycord/plugins/giveaway.pyeasycord/plugins/juicewrld.pyeasycord/plugins/word_filter.pyeasycord/plugins/auto_role.pyeasycord/plugins/economy.pyeasycord/plugins/reminder.pyeasycord/plugins/tickets.py
tests/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
tests/**/*.py: Useeasycord.testinghelpers such asinvoke,FakeContextBuilder, andPluginTestSuitefor command and plugin tests instead of requiring a live Discord connection.
When constructing plugins manually in tests, useMyPlugin.__new__, assignplugin._bot = bot, and then callPlugin.__init__(plugin); do not assignplugin.bot.
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.
Files:
tests/test_plugin_logic.pytests/test_tickets_commands.pytests/test_polls.pytests/test_giveaway_commands.pytests/test_shared_helpers.pytests/test_stress.pytests/test_reminder.pytests/test_economy.pytests/test_birthday.pytests/test_server_stats.pytests/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.pytests/test_tickets_commands.pytests/test_polls.pytests/test_giveaway_commands.pytests/test_shared_helpers.pytests/test_stress.pytests/test_reminder.pytests/test_economy.pytests/test_birthday.pytests/test_server_stats.pytests/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 andsend_safeadoption 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 ofctx.t(...)withdefault=fallback for the newrespond_errorcalls — 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 onrep_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_safeadoption in_archive_messageis 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 thesend_safe/respond_errormigration 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 insideasync with self._locks.lock(...)), and the DM-warningsend_safeuse 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
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
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
📒 Files selected for processing (5)
CHANGELOG.mdREADME.mddocs/getting-started.mdeasycord/__init__.pypyproject.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
graphifyagainstgraphify-out/graph.jsonbefore opening source, documentation, or context files; do not read the deprecated Wiki or rebuild the graph unless explicitly asked.
Files:
pyproject.tomldocs/getting-started.mdREADME.mdeasycord/__init__.pyCHANGELOG.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.jsonwith/graphify; do not manually read source first.
Files:
docs/getting-started.mdREADME.mdeasycord/__init__.pyCHANGELOG.md
easycord/**/*.py
📄 CodeRabbit inference engine (CLAUDE.md)
easycord/**/*.py: Treateasycord/__init__.pyas the stable public API. Import consumers fromeasycord, never from_-prefixed internal modules or helpers.
Usectx.userorctx.member, notctx.author; accessctx.is_adminas a property and never call it asctx.is_admin().
Store per-guild plugin state in the database layer using per-guild namespaced records, never on thePlugininstance itself.
Never hardcode plugin response strings; retrieve localized text throughctx.t(...).
Always await asynchronousToolLimitermethods, includingcheck_limit,reset_user, andreset_tool.
Require an explicitToolSafetyannotation when registering an@ai_tool; keepRESTRICTEDtools 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.
UseServerConfigStore.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 withSENDABLE_CHANNEL_TYPESusingisinstance; do not call.send()on an unnarrowed channel.
InitializeLevelsPlugin._cooldownssentinels tofloat("-inf"), not0.0, so the first message always passes.
sync_commands()must raise on removals unlessconfirm_removals=Trueis 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
…ance attribute collision
…s, stale docstring, unused import)
…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
Summary
GuildLockManagerintoeasycord/plugins/_shared.pyand migrated all 13 plugins that had hand-rolled_guild_lockdicts. Removed unusedasyncioimports from 4 plugins. Tests updated to use_locks.lock()throughout.send_safehelper at ~20 narrow-then-send sites across 12 plugins, removing ~30 lines of inlinetry/except discord.Forbidden/HTTPExceptionboilerplate.if not ctx.is_admin: respond_error; returnblocks with@slash(require_admin=True)onticket_setup,stats_setup,stats_teardown,verification_setup,verification_panel,verification_question.discord.Embed(...)construction toEmbedBuilderat opportunistic sites intickets.pyandverification.py.27 commits, all mechanical — no behaviour changes.
Test plan
ruff check --select E9,F63,F7,F82 .— cleanruff check .— advisory, no new errorspython scripts/verify_plugin_tests.py— all plugin thresholds metpytest tests/ -q— 1,813 passed (2 new decorator-assertion tests added forstats_teardownandverification_panel)_shared.pyGuildLockManagereviction logic still correctai_moderator._execute_actiongoverned path untouched