All notable changes to EasyCord are documented here. See Semantic Versioning for version numbering.
- Lifecycle hooks are now real bot API —
Botinitializesbot.hooks, slash command execution firesbefore_command/after_command, and plugin load/unload paths fireon_plugin_load/on_plugin_unload, matching the documentedHookRegistryworkflow. - Slash group lifecycle cleanup — unloading a
SlashGroupnow removes cooldown registry entries for nested group commands, preventing stale callback/plugin state from surviving hot reloads. - Slash group dependency checks — grouped plugins now honor
requiresdeclarations the same way normal plugins do, failing fast withPluginDependencyErrorwhen dependencies are missing. - Mixin command registration — decorated methods inherited from plain Python mixins are discovered again, preserving the pre-v5.61 plugin composition pattern for slash commands, events, components, modals, and subscriptions.
- Tags plugin grouping and mention safety —
TagsPluginnow registers as the documented/tagslash-command group and disables mentions when serving tag text. - Plugin slash decorator parity — plugin
@slash(...)now accepts and propagatesnsfw,allowed_contexts, andallowed_installs, matching the directBot.slash(...)API. - Ticket panel send failures —
TicketsPlugin.ticket_open()no longer crashes when the panel message cannot be sent; it records the ticket and skips persistent view registration without a message ID. - Levels config cache invalidation — rank and role-reward config mutations now invalidate the in-memory config cache immediately.
- Atomic config helper update —
ConfigHelpers.update_atomic()now routes throughServerConfigStore.mutate()instead of a separate load/save sequence. - Documentation accuracy sweep — refreshed release docs, README snippets, built-in plugin command names, AI setup examples, event bus behavior, command sync examples, and troubleshooting guidance to match the current v5.61 API.
- Added regression coverage for bot hook firing, grouped plugin dependency errors, grouped cooldown cleanup, mixin-decorated command discovery, slash decorator metadata propagation, ticket panel
Nonehandling, tag mention suppression, levels cache invalidation, and config helper atomic updates. - Full suite: 1871 passing.
- Wheel:
easycord-5.61.1-py3-none-any.whl - Source:
easycord-5.61.1.tar.gz—releases/download/v5.61.1/easycord-5.61.1.tar.gz
easycord try <target> <command>CLI — run a registered slash command offline (no token, no network) through the existingtesting.invoke()harness. Supports--set key=valueargument coercion, context flags (--user/--guild/--dm/--no-admin), and--jsonoutput. Exits non-zero on an unknown command (listing available names) or when the command raises, so it doubles as a scriptable smoke check. Documented in the developer-toolkit guide, and backed by a newformat_try_result()formatter (#128)PluginConfigHelpermixin — drop-in atomic per-guild config CRUD for plugins:config_get,config_set,config_update,config_delete,config_mutate, andconfig_clear. Every write routes throughServerConfigStore.mutate, so operations share the store's single per-guild lock domain — no separate lock to manage, and the section defaults to the plugin name (#129)- Pre-composed decorator stacks — five decorators that collapse the common
@slash+ permissions + cooldown + ephemeral boilerplate into one line:slash_admin_command(admin-only),slash_management_command(defaults tomanage_guild),slash_mod_command(moderation perms),slash_user_command(public, guild-only), andslash_with_confirm(marks a destructive command for actx.confirm()flow) (#129) TaskManager/TimerManagerlifecycle helpers — track and cancel background work without hand-rolled dicts:TaskManagersupportsstart_once(dedup by name),start_recurring(fixed-interval loop), andcancel_all;TimerManagerschedules one-shot delayed callbacks with optional per-guild grouping andcancel_timer/cancel_guild/cancel_all(#129)discord_errors()middleware — a central, type-aware complement tocatch_errors(): catchesdiscord.Forbidden,NotFound, andHTTPExceptionand replies with localized, type-specific ephemeral messages. Register it aftercatch_errors()so it handles Discord errors first whilecatch_errors()remains the generic fallback; non-Discord errors propagate untouched (#130)
server_statsconfig writes now route throughServerConfigStore.mutateinstead of a plugin-ownedGuildLockManager+ manualload/save, consolidating on the store's single lock domain (behavior-preserving). Serves as the reference pattern for migrating the remaining plugins (#130)- Shared plugin helpers — refinements to
easycord/plugins/_shared.py(require_guild,respond_error) that collapse repeated guild-guard and ephemeral-error-reply patterns across the bundled plugins
slash_mod_commandpermission name — requiredmoderate_members(the real discord.py timeout permission) instead of the nonexistenttimeout_members. Because permissions are enforced viagetattr(guild_permissions, name, False), the old entry was silently always-false, breaking the intended gate for any command built on the stack (#131)TaskManager.trackcleanup race — a task that finished after a replacement was registered under the same name could evict the replacement, leaving it untracked and uncancellable; the done-callback now removes a task only if it is still the one registered under that name (#129)- Starboard resilience — updating an existing starboard post now catches
discord.Forbidden/HTTPExceptionand logs instead of letting the handler crash when the bot lacks permission or the post is gone - Suggestions resilience — adding the up/down vote reactions now catches
discord.Forbidden/HTTPExceptionand logs, so a missing-permission case no longer breaks suggestion creation - Removed three unused imports in
_decorator_stacks.py(cooldown,require_permissions,describe) flagged by CodeQL (#131)
- New command-level suites for the giveaway (
test_giveaway_commands.py) and tickets (test_tickets_commands.py) plugins, plus expanded coverage for suggestions and the shared plugin helpers — roughly 900 new lines of tests overall (including the CLI, middleware, and plugin-helper suites from #128–#130). Full suite: 1865 passing
- CLAUDE.md context framework — restored truncated context and added a troubleshooting section, a documentation-freshness framework, an
AGENTS.mdlink, and stronger invariants for contributors and agents - CI workflow refresh — updates to the CodeQL, release-drafter, stale, and Claude Code/review workflows
- Wheel:
easycord-5.61.0-py3-none-any.whl - Source:
easycord-5.61.0.tar.gz—releases/download/v5.61.0/easycord-5.61.0.tar.gz
GuildLockManager— extracted the per-guildasyncio.Lockdict + eviction logic from 14 plugins into a single shared class ineasycord/plugins/_shared.py. All plugins (auto_role,birthday,economy,giveaway,juicewrld,polls,reminder,reputation,scheduled_announcements,server_stats,tags,tickets,verification,word_filter) now share one implementation with idle-eviction after 7 days and a MAX_TRACKED_GUILDS=5000 hard cap. Removes ~250 lines of copy-pasted boilerplate.send_safeadoption — replaced ~20 bareawait channel.send(...)calls and inlinetry/except discord.Forbidden/HTTPExceptionblocks across 14 plugins with the existingsend_safehelper fromeasycord/helpers/channel.py. Affected:ai_moderator,birthday,giveaway,invite_tracker,levels,member_logging,moderation,reminder,scheduled_announcements,starboard,suggestions,tickets,verification,word_filter.@slash(require_admin=True)— replaced 6 inlineif not ctx.is_admin: respond_error; returnblocks with therequire_admin=Truedecorator parameter onticket_setup,stats_setup,stats_teardown,verification_setup,verification_panel,verification_question. The decorator gate now rejects non-admins before the command body runs.EmbedBuilderadoption — migrateddiscord.Embed(...)construction toEmbedBuilderat opportunistic sites:_ticket_embed()and the close log embed intickets.py;_build_panel_embed()inverification.py.
- Wheel:
easycord-5.60.0-py3-none-any.whl - Source:
easycord-5.60.0.tar.gz—releases/download/v5.60.0/easycord-5.60.0.tar.gz
- Docs accuracy audit — verified every concrete claim across 10 documentation files against current source; fixed stale plugin counts (29→30), stale method names (
get_record→get), wrong constructor params, wrong type names (discord.Channel→discord.abc.GuildChannel), wrong class relationship (AIPlugin/OpenClaudePluginalias→subclass), and stale CI pin (checkout@v4→@v7) (#116, #117) HookRegistry.unregister()— new public method to deregister hook callbacks; returnsFalseinstead of raising when the callback is not registered, making repeated plugin unloads safe (#117)
- Full coverage for
HookRegistry.unregister(): removal, idempotency, invalid hook name, selective removal (#118)
- Release metadata — GitHub Release
v5.58.0tracks the expected artifacts:easycord-5.58.0-py3-none-any.whlandreleases/download/v5.58.0/easycord-5.58.0.tar.gz.
- Server setup templates (
ServerSetupPlugin) — new opt-in plugin with a/setup-servercommand that previews and applies preset server layouts: categories, text/voice channels, roles, role-level permissions, and per-channel permission overwrites. Four templates ship:gaming,community,study,creator. Application is additive only (existing items are skipped by Discord-normalized name, never modified or deleted) and gated behind an ephemeral preview with Apply/Cancel confirmation. Role permissions are clamped to what the bot can grant; per-item Discord failures are reported in the summary without aborting the run. Each successful run is recorded per guild (template, timestamp, invoker, created IDs). New guide:docs/server-setup.md.
- Release metadata — GitHub Release
v5.57.0tracks the expected artifacts:easycord-5.57.0-py3-none-any.whlandreleases/download/v5.57.0/easycord-5.57.0.tar.gz.
ServerConfigStorein-memory cache — per-guild config is cached after the first disk read. Subsequentload()calls return the cached copy without touching disk.save(),mutate(), anddelete()keep the cache coherent. Negative caching (None) prevents repeated disk misses for guilds with no config file.LevelsPlugin— leaderboard caching —/leaderboardresults are cached for 5 minutes per guild. Invalidated by/give_xpand/reset_xp.LevelsPlugin— XP multipliers — new/set_xp_multiplier <multiplier> [duration_minutes]command. Multiplier applies to all organic XP gains for the duration (persisted to config).LevelsPlugin— level-up DM toggle — new/toggle_level_dmcommand. When enabled, the bot DMs the user on level-up in addition to the channel announcement. DM failures (Forbidden) are logged but do not crash the event handler.LevelsPlugin— bulk XP reset — new/reset_xp <member>command. Zeroes a member's XP and level atomically and invalidates the leaderboard cache.
decorators.pyinternal cleanup —component()andmodal()now share a private_dual_api_decorator()helper;user_command()andmessage_command()share_context_menu_decorator(). No public API changes.EconomyPluginlock eviction — per-guild balance locks are now tracked with a creation timestamp and evicted after 7 days of idleness (or when the pool exceeds 5 000 guilds). Prevents unbounded memory growth on high-guild bots.EconomyPluginatomic transfer —/transfernow uses a single_transfer()helper that reads and writes both balances under one lock acquisition, making the operation all-or-nothing.- Release metadata — GitHub Release
v5.56.0tracks the expected artifacts:easycord-5.56.0-py3-none-any.whlandreleases/download/v5.56.0/easycord-5.56.0.tar.gz.
- JuiceWRLD Plugin (
JuiceWRLDPlugin) — new built-in plugin integrating the formerjuice-wrld-finderproject. Slash commands:/jw_search,/jw_song,/jw_era,/jw_random,/jw_add_song,/jw_reindex. AI tools:search_juicewrld,get_song_details. Event bus publishing on every command. Background 6-hour API sync task. - Optional
use_external_api=Truemode queriesjuicewrldapi.comvia the officialjuicewrld-api-wrapperPyPI package (no API key required):/jw_search— parallel local + API search with three-bucket comparison embed (✅ both sources, 🗄️ local-only, 🌐 API-only)/jw_random— falls back to API when local catalog is empty/jw_song— API fallback embed (orange) when local ID not found; event still published/jw_era— supplements local era results with API category results; API-only orange embed when era not in local catalogsearch_juicewrldAI tool — merges local + API-only results in one text responseget_song_detailsAI tool — API fallback when local ID not found
- MEGA folder URL fallback (
mega_folder_urlconstructor param) as last-resort song link. - Three-level URL resolution per song: official URL → MEGA file → MEGA folder.
expose_mega_linksflag redacts MEGA URLs in public servers (default:False).expose_api_download_linksflag showsapi_download_urlfield in/jw_song(default:False).- Event bus integration: publishes 7 events (
juicewrld.searched,.song_viewed,.era_browsed,.random_played,.song_added,.reindexed,.api_synced); subscribes to 3 for internal logging and stale-index detection.
- Config-schema phase 2 (PR #88): Edge-case guards and migration repair
ConfigSchema.apply()now resets non-integer_vstamps and records the correction- Forward-version sections (
_v > schema.version) pass through unchanged with warning - Missing migration steps logged as warnings; sections still stamp to target version
_vedge-case test coverage: 3 new unit tests for non-int, forward version, and gap detection
- BUG-A (CRITICAL): Vacuous
okflag in_doctor_report— was alwaysTruewhen--fix-configsused, masking failure-to-heal scenarios. Now correctly reflects healing success. - BUG-C (HIGH): Missing migration step no longer silently stamps
_vas fully migrated. Now logs warning and allows manual plugin author correction before next upgrade. - BUG-D (MEDIUM):
--fix-configswithout bot target now exits with error instead of silent no-op. - BUG-F (MEDIUM): Added help text warning that bot must be stopped before
--fix-configsto avoid concurrent write loss. - BUG-J,K (MEDIUM):
PluginConfigManagerfalsy-check bugs fixed withis Noneguards (update()andset_default()now preserve falsy-but-valid values like{},[],False,0). - Hygiene: Removed unnecessary lambda wrappers in
_bot_commands.py(3 locations, 2 fewer lines per site). - Hygiene: Hoisted inline
import loggingto module-level intest_config_schema.py.
- New tests for
ConfigSchema.apply()edge cases:test_apply_resets_non_int_v_and_migrates— validates non-int_vreset and migration chaintest_apply_warns_on_missing_migration_step— confirms warning logged +_vstill stampedtest_apply_ignores_forward_version— verifies forward-version section passthrough
- Codecov: 85.71% patch coverage; 2 missing lines in CLI error paths (acceptable).
Verified 12 CodeQL alerts:
- ALERT-141: FALSE POSITIVE (
except ValueError: passcorrectly typed) - ALERT-72–80,104,142: FALSE POSITIVES (cyclic imports with TYPE_CHECKING guards; architectural pattern)
- ALERT-134: FALSE POSITIVE (Protocol
...stub idiom) - ALERT-82–86: FIXED (unnecessary lambda wrappers removed)
- ALERT-67–71,101–103,135–138: No new violations in this diff
tests/test_bot_permissions_adoption.py— 16 regression tests pinningbot_permissionsdenial/allow behaviour and B-021 structural guard (config-setter commands must not declarebot_permissions)tests/test_cli_scaffold.py— CLI scaffold collision regression teststests/test_cooldown_cleanup.py— 10 tests for bot-level cooldown sweep (expiry,_COOLDOWN_MAX_ENTRIESoverflow eviction, plugin lifecycle cleanup)tests/test_event_bus.pyextensions — EventBus observability teststests/test_p1_bug_sweep.py— regression net for B-007 / B-015 / B-016
- B-007:
InviteTrackerPlugin._invite_cachenow pruned onguild_remove - B-015:
LevelsPlugin._grant_level_rewardexception narrowed todiscord.HTTPException(wasForbidden-only) - B-016:
auto_role._on_member_joinpost-sleepadd_rolesnow catches fulldiscord.HTTPExceptionhierarchy - Cooldown sweep consolidated to single bot-level
_cooldown_cleanup_loop; per-callback sweep task removed; hard size cap (_COOLDOWN_MAX_ENTRIES=50_000) added with oldest-bucket eviction; cooldown registry entry now cleaned up on plugin unload viaremove_plugin - CLI scaffold collision when plugin slug matched an existing directory
Total: 1513 (was 1438 in v5.52.0)
Plugin dependency declarations (easycord/plugin.py, easycord/_bot_plugins.py):
Plugin.requires: tuple[str, ...]class attribute — declare plugin load-order requirements.bot.add_plugin()raisesPluginDependencyError(RuntimeError)with.missingand.plugin_classattrs when a required plugin isn't loaded yet.PluginDependencyErroris exported fromeasycord.
Analytics middleware (easycord/middleware.py):
AnalyticsStoredataclass tracks invocation counts per(command_name, guild_id).analytics_middleware(store=None)factory — attach tobot.use()to start collecting.- Auto-wires the store to
bot._analytics_storewhenbot.use()detects the_analytics_storeattribute on the returned middleware. bot.command_stats(guild_id=None)queries aggregate or per-guild command counts.AnalyticsStoreandanalytics_middlewareare exported fromeasycord.
Per-guild plugin feature flags (easycord/_bot_plugins.py, easycord/_command_callbacks.py):
bot.disable_plugin(name, guild_id)— silently blocks all commands from a plugin in a specific guild.bot.enable_plugin(name, guild_id)— re-enables a plugin for a guild.bot.is_plugin_enabled(name, guild_id)— query current state (defaultTrue).- Disabled commands return an ephemeral "This feature is disabled in this server." response; DM invocations are unaffected.
tests/test_plugin_power_pack.py— 24 tests covering dependency declarations, flag methods,PluginDependencyErrorattributes, and end-to-end dispatch guard (integration tests using realBot+invoke()).tests/test_middleware.py— 11 new tests forAnalyticsStoreandanalytics_middleware.- 1438 tests total.
OpenClaw Optional member access (easycord/plugins/openclaw.py):
- Added
assert ctx.guild is not Noneguards in guild-only commands (lines 91, 119, 153, 164, 187) to narrowOptional[Guild]access. - Added
assert self.orchestrator is not Nonebefore accessingstrategy(line 225). - Added early return when
sourceregistry isNone(line 284) before accessing_tools.
Scheduled announcements loop resilience (easycord/plugins/scheduled_announcements.py):
- Wrapped
ch.send()in try/except to catchdiscord.Forbiddenanddiscord.HTTPException. - Loop now logs the error and continues on send failure instead of terminating permanently.
Context channel Optional access (giveaway.py:300, polls.py:304, reminder.py:209):
- Added guards:
if ctx.channel is None: returnbefore accessingctx.channel.idin slash commands.
Tickets button view guild guard (easycord/plugins/tickets.py):
- Added early return if
interaction.guild is Nonein the button callback (line 95). - Persistent views can receive DM interactions; now handled gracefully.
Three live plugin bugs (cherry-pick c60c8b6):
- birthday.py: Fixed
_days_untilyear-advance logic (Feb 29 crash on year boundary). - tickets.py: Fixed
oldest_first=Falsein transcript history to show messages in chronological order. - levels.py: Extracted
_grant_level_rewardmethod;/give_xpnow uses it for role rewards.
Suggestions plugin cleanup (easycord/plugins/suggestions.py):
- Removed unused
self.suggestion_counter = {}field (dead code, real counter lives in persistent config).
Starboard disabled by missing config key (easycord/plugins/starboard.py, B-018):
cfg.get("enabled")without a default treated a missing key as disabled — a guild that only ran/starboard_channelhad a starboard that never fired. Nowcfg.get("enabled", True)in both reaction handlers and the config display. The same pattern in five sibling plugins was audited and verified benign (B-019, closed).
- Public API exports:
SENDABLE_CHANNEL_TYPES,EventBus, andHookRegistryare now importable fromeasycord.
- Extended test coverage for plugin fixes; flat >=20-test-per-plugin CI floor (was complex >=20 / simple >=8); 1335 tests total (up from 1301).
Interaction component TTL boundary (easycord/registry.py):
InteractionRegistry._entry_activetreated an entry as active whileexpires_at >= now, so a component registered withttl=0(whoseexpires_atequals its registration time) still resolved as active when looked up within the same clock tick. The check is now strict —expires_at > now— so a component is inactive at and after its expiry instant. This makesresolve_componentdeterministic across platforms: the off-by-one was latent on fine-grained clocks (Linux CI) but surfaced on coarse-resolution clocks (Windowstime.time(), ~15 ms), where registration and resolution land in the same tick and a just-expired component was wrongly returned.
AIModeratorPlugin governance (easycord/plugins/ai_moderator.py):
- The live
on_messagemoderation path now routes destructive actions through the governed_execute_actionhelper. Previously that helper — which holds the per-user rate limiters and Discord error handling — was defined but never called; the live path used inline calls instead. auto_deleteno longer performs an unguardedmessage.delete(); a failed delete (race / missing permission) is caught rather than escaping into the event dispatcher.- Warnings now go through the per-user rate limiter that was previously bypassed.
- Removed the unreachable
timeout/mutebranches from_execute_action(dead code —mutecreated a role with no permission overwrites and would not have muted anyone). - Behavior change: a warning is now posted in-channel (rate-limited) instead of a best-effort DM, matching the governed action path.
Documentation drift:
docs/builtin-plugins.md: removed/purgefromModerationPlugin— the command is not implemented.context/architecture.md: corrected the OpenClaw slash command names to the registered/openclaw,/openclaw-task,/openclaw-status,/openclaw-stop,/openclaw-history(previously listed as/openclaw_task//openclaw_stop).
- Added
tests/test_ai_moderator.py(12 tests): auto-delete guarding (success +Forbidden/HTTPException), warn rate-limiting, dispatch guards (bot author, disabled guild, below-threshold), the notify-only review embed, and malformed/invalid model-output resilience.
EventBus (easycord/event_bus.py) — async pub/sub between plugins:
bot.event_bus.subscribe(event, callback)— register sync or async listenersbot.event_bus.unsubscribe(event, callback)— remove a listenerbot.event_bus.publish(event, **kwargs)— fire an event; exceptions are isolated per listener
HookRegistry (easycord/hooks.py) — lifecycle hooks for bot internals:
- Four built-in hooks:
before_command,after_command,on_plugin_load,on_plugin_unload bot.hooks.register(hook_name, callback)— register sync or async callbacksbot.hooks.fire(hook_name, **kwargs)— await all callbacks in registration order
@deprecated / @version_introduced decorators (easycord/decorators.py) (docs):
@deprecated("5.50.0", replacement="new_name")emitsDeprecationWarningat call time with a migration hint@version_introduced("5.50.0")annotates when a function was added (no runtime cost)- Both set introspectable
__deprecated__/__version_introduced__attributes on the wrapped function
PluginTestSuite (easycord/testing.py) (docs):
- Base class for plugin unit tests — wires up a
Botinstance with no Discord connection make_plugin(PluginClass),invoke_command,invoke_autocomplete,invoke_component,invoke_modal,invoke_user_command,invoke_message_commandassert_last_response(ctx, text)— content assertion helperFakeContextBuilder— fluent builder for locale, roles, admin, DM, and guild contexts
Hot-reload with on_reload() lifecycle (docs):
Plugin.on_reload()fires on the new instance after a successful hot-reload swap- Use it to migrate in-memory state that can't be reconstructed from
__init__alone - Poll interval: 1 s → 3 s; plugins requiring
__init__args are skipped gracefully with an error log
Command registration validation:
ValueErrorraised at registration time for: name > 32 chars or not matching[-_a-z0-9], description > 100 chars, > 25 options, > 25 choices per option- Error messages include the constraint, the actual value, and the command name
Bot permission validator (docs):
- At
on_ready, logs a WARNING per command if the bot lacks a required Discord permission in any joined guild - Includes guild name, guild ID, and command name in the message
Provider fallback metrics — AI provider attempts log at DEBUG (try), DEBUG (success), WARNING (provider failure + exception type), ERROR (all exhausted)
Database backend in /health — embed now shows the configured backend (sqlite or memory), connection status, and round-trip latency
pyrightconfig.json — standard-mode Pyright configuration at repo root for plugin authors
format_number: O(n²)list.insert(0, …)in thousands-grouping replaced withlist.append+"".join(reversed(parts))→ O(n)- Conversation summarization: silent
except Exception: passreplaced withlogger.warning(…)— failures visible in logs - Hot-reload poll: 1 s → 3 s
- Birthday plugin: untracked
asyncio.create_taskfor role removal — tasks now held in_role_tasksand cancelled onon_unload asyncio.iscoroutinefunction(deprecated in Python 3.16) replaced withinspect.iscoroutinefunctioninEventBusandHookRegistry- CodeQL "statement has no effect" in
test_hot_reload.pyresolved database.py:cast(DatabaseBackend, …)eliminates Pyright errors onos.getenv()return value- Pyright:
# type: ignorenarrowed to specific error codes; baredictgenerics replaced with typed equivalents - Release-drafter workflow:
paths-ignorefor version-bump files prevents redundant draft-release updates onmain
1,169 tests total (up from ~900). New test files: test_event_bus.py, test_hooks.py, test_hot_reload.py, test_command_registration.py, test_cooldown_cleanup.py, test_deprecation.py, test_health.py, test_orchestrator.py, test_permission_validator.py, test_plugin_test_suite.py, test_new_decorators.py. Patch coverage: 74% → 82%.
Four new guides: Event Bus, Lifecycle Hooks, Deprecation Helpers, Testing Commands.
TranslatePlugin — new /translate slash command backed by Google Translate (via deep-translator, no API key required):
text— content to translatelanguages—"source to target"pair (e.g."French to English","auto to Spanish"); blank to auto-translate into the invoking user's Discord locale- Translation runs in a thread executor (non-blocking); missing package or network failure returns an ephemeral error
Google Translate → LocalizationManager (easycord/helpers/google_translate.py):
make_google_auto_translator()— returns a callback forLocalizationManager(auto_translator=...)so missing-key lookups are translated on-the-fly instead of falling back to the default locale's English stringsGoogleTranslateTranslator(app_commands.Translator)— discord.py's official translator protocol; translates command names to all supported Discord locales at sync time
Localized command names — command names and descriptions now wrapped in locale_str() at registration time:
bot.use_google_translate()installsGoogleTranslateTranslatoron the command tree- After
sync_commands(), Discord shows each user the command in their own language (e.g./traduirefor French users,/übersetzenfor German users) - Interaction payload always carries the canonical name — no routing changes needed
New optional extra:
pip install "easycord[translate]" # pulls in deep-translator
pip install -e ".[dev]" # dev installs include it automatically_parse_languages: empty source or target after the" to "separator now correctly falls back to the user's Discord locale (was hardcoded to"english")_parse_languages: padding trick fixes edge cases wherestr.strip()removes the spaces that form the separator (" to English"and"French to ")asyncio.get_running_loop()replaces deprecatedasyncio.get_event_loop()inTranslatePlugin.translate
Module splits — internal modules broken into focused sub-modules for easier navigation:
_command_callbacks.py—build_slash_callback/build_context_menu_callbackwith full guild/permission/cooldown/premium guards_command_registration.py—register_slash,register_context_menu,inject_choices,autocomplete_options_plugin_scanner.py—scan_plugin_methodsauto-wires@slash/@ondecorated plugin methods_i18n_locale.py— locale normalisation, OS-locale detection, BCP 47 validation, fallback chain builders_i18n_diagnostics.py—DiagnosticModeenum andLocalizationDiagnosticsfor missing-key and placeholder tracking_i18n_validation.py—TranslationValidationReportfor per-locale completeness auditing
PollsPlugin persistence — polls now survive bot restarts:
- Vote state and remaining time are stored per-guild via
ServerConfigStore on_readyre-registers views and resumes countdown timers for all active polls- Deterministic
custom_idvalues (poll:vote:{message_id}:{option_index}) allow views to reconnect after restart
- Prompt injection (
ai_moderator.py): user-controlledmessage.contentandmessage.author.nameare now delimited with XML tags in the LLM prompt, preventing crafted messages from shifting model instructions - 12 Pyright type errors in
ai_moderator.py: unguardedctx.guildaccess narrowed withassert ctx.guild is not None(guild-only handlers) andif ctx.guild is None: return False(_execute_action) - Poll restore isolation (
polls.py): a single malformed poll entry no longer aborts restoration of all polls in a guild — each entry is wrapped in its owntry/except - Cooldown dict growth (
_command_callbacks.py): expired bucket keys are pruned after filtering, preventing unbounded accumulation for inactive users - Pylance type errors (
helpers/tools.py): replacedall()truthiness guard withisinstancechecks so Pylance narrowsname/descriptiontostrandsafetytoToolSafety
CLAUDE.mdexpanded with architecture quick-reference, testing patterns, channel send safety guide, and key invariants
tests/test_server_stats.py: explained the bareexcept (asyncio.CancelledError, Exception)around background-task teardown intest_setup_creates_channels— it's load-bearing (swallows theCancelledErrorraised by awaiting a just-cancelled task), not dead code.tests/test_word_filter.py: removed an unusedctx2fromtest_guilds_isolated— the test only ever exercisesctx1; guild 2's isolation is verified by reading its config store directly.
SecurityLabPlugin — educational security demonstration tool for Discord bot developers:
- 7 slash commands demonstrating real attack vectors: stored injection, input overflow, ReDoS, prompt injection, phantom permission gates, flood attacks
- Each demo shows the attack in action, explains why it works, and provides a code-based defense
- Requires
manage_guildpermission (admin-only) to prevent misuse
Security utilities (easycord.security):
escape_mentions()— sanitizes@everyone/@hereto prevent accidental pingstruncate()— hard-caps text length with ellipsissafe_regex()— runs regex with timeout protection against ReDoSstrip_injection_prefixes()— removes common prompt-injection openers
easycord/plugins/tags.py: Addedsuper().__init__()call toTagsPlugin.__init__to properly invoke parent class initializationeasycord/plugins/word_filter.py: Added explanatory comments on bareexceptclauses for clarityallowed_contexts/allowed_installsraisedAttributeErrorat runtime — discord.py 2.7.1 defines two distinct classes namedAppCommandContext/AppInstallationType: the slot-based ones underdiscord.app_commands.*(used byInteraction.contextand command registration) and an unrelated ArrayFlags-based pair re-exported at top-leveldiscord.*.@slash,@user_command,@message_command,SlashGroup, andctx.app_contextnow use thediscord.app_commandsversions.easycord/_bot_guild.py:send_webhookbuilds its forwarded kwargs explicitly instead of passingNoneinto discord.py's MISSING-sentinel API.
- Internal:
_bot_commands.py/_bot_events.py/_bot_guild.py/_bot_plugins.pymixins now declare their composedBotattribute surface via aTYPE_CHECKING-only_bot_base.pybase, eliminating 12py/unsafe-cyclic-importstatic-analysis false positives. No behavior change.
8 new community plugins:
- BirthdayPlugin — per-user birthday registry with daily midnight announcements and optional birthday role assignment
- ReminderPlugin — personal reminders with flexible duration syntax (
30m,2h) and pending-reminder list - VerificationPlugin — button-based or modal-question member verification that grants a configured role on success
- ServerStatsPlugin — live stat voice channels (
📊 Members,🟢 Online,💎 Boosts) updated every 10 minutes - ScheduledAnnouncementsPlugin — recurring scheduled announcements posted to any text channel on a configurable interval
- ReputationPlugin — community reputation points with 24-hour per-giver cooldown, leaderboard, and admin reset
- WordFilterPlugin — configurable word blocklist with delete/warn/both action modes and per-role exemptions
- AutoRolePlugin — automatic role assignment on member join with optional delay for bot-verification windows
context_builder.py:getattr(cmd, 'description', None)guardsContextMenucommands that lack adescriptionattribute, preventingAttributeErrorat runtime.i18n.py:_metricsannotation updated fromdict[str, int]todict[str, Any]—locale_frequencyvalue is a nesteddict, not anint._chain_cachekey type corrected fromstrtotuple(keys are(str|None, str|None, bool)tuples).plugins/invite_tracker.py:invite.usesnarrowed withor 0in two places (int | None→int).channel.sendguarded withisinstance(channel, (TextChannel, Thread, VoiceChannel, StageChannel))before calling.send()to prevent calls on non-sendable channel types.plugins/member_logging.py: Sameisinstancenarrowing applied beforechannel.send().
- Added 110 new tests across three new test files:
tests/test_new_stress.py(19 tests) — concurrency/load stress forrate_limit,ConversationMemory, andLocalizationManager.tests/test_plugins_new.py(54 tests) — unit tests for 8 previously-untested plugins: starboard, suggestions, reaction_roles, moderation, polls, tags, invite_tracker, member_logging.tests/test_core_gaps.py(38 tests) — unit tests for zero-coverage core modules:EmbedCard, formatters,ContextBuilder,SlashGroup,SecurityManager,FrameworkManager,AuditLog.
- Total test count: 744.
- https://github.com/rolling-codes/EasyCord/releases/download/v5.44.3/easycord-5.44.3-py3-none-any.whl
- https://github.com/rolling-codes/EasyCord/releases/download/v5.44.3/easycord-5.44.3.tar.gz
ToolLimiter._cleanup_usageraisedKeyErrorwheneverMAX_TRACKED_ENTRIES(10 000) was exceeded: the cleanup path deletedself._usage[key[0]](anint) instead ofself._usage[key](a(user_id, tool_name)tuple). Now deletes the correct key.EconomyPlugin._cleanup_old_lockscould evict a lock while it was still acquired: the 7-day age threshold was measured from creation time, never refreshed on subsequent calls, so an active guild's lock could be removed and replaced with a fresh unacquired one — silently bypassing per-guild write serialization. Fixed by refreshing the last-used timestamp on every_balance_lock()access and guarding removal candidates withnot lock.locked().progress_bar()inLevelsPluginreturned a string longer thanwidthwhen the supplied XP exceeded the next-level ceiling. Addedmin(width, max(0, …))clamp on thefilledcount.Range(min=5, max=3)constructed silently and only raised a confusingValidationErrorat call time. Added__post_init__toRangethat raisesValueErrorimmediately whenmin > max.BaseContext.respond()andBaseContext.dm()passedembed=Noneandcontentas a positional argument to discord.py overloaded functions, causing PylancereportArgumentTypeerrors. Both methods now build amsg_kwargsdict and skipcontent/embedkeys whenNone.BaseContext.send_embed()used**({"timestamp": ts} if ts is not None else {})which confused Pylance's narrowing. Replaced withtimestamp=tsdirectly (discord.Embed.__init__acceptsOptional[datetime]).BaseContext.forward()passeddiscord.abc.Messageablewhere discord.py's stub expectsMessageableChannel. Added# type: ignore[arg-type]— the runtime guard already narrowsNonebefore the call.LevelsPluginrole-reward path usedhasattr(message.author, "add_roles")which Pylance cannot narrow. Replaced withisinstance(message.author, discord.Member).
- Added 94 new tests in
tests/test_stress.pycovering the four fixed bugs (regression guards), levels-XP math invariants, all validators, andConversationMemoryedge cases. Total test count: 634.
- https://github.com/rolling-codes/EasyCord/releases/download/v5.44.2/easycord-5.44.2-py3-none-any.whl
- https://github.com/rolling-codes/EasyCord/releases/download/v5.44.2/easycord-5.44.2.tar.gz
- Economy: transfers now load once, mutate both balances in memory, and persist with a single save under the per-guild lock, so a failed write can never leave a half-applied transfer (no lost currency).
- Economy:
/dailyrecords its outcome under the lock and replies only after releasing it, so Discord response latency no longer stalls the guild;_get_configis now a pure read that cannot clobber a concurrent balance update. - Plugin type-safety:
guild_onlyhandlers assertctx.guild/ctx.user,suggestionsnarrows the target channel toTextChannel/Threadbefore sending, andreaction_rolesguardsself.bot.userbefore reading its id — clearing the outstanding PylancereportOptionalMemberAccess/reportAttributeAccessIssueerrors. - Starboard: removed duplicate archived-message helpers and fixed a misplaced slash import.
- Realigned in-repo version metadata (
pyproject.toml,easycord.__version__, README badge/links, anddocs/getting-started.md) with the published release line, which had drifted while still reporting5.43.0.
- Public API:
PluginConfigManageris now exported fromeasycord.pluginsso code outside the package no longer imports the private_config_managermodule.
- https://github.com/rolling-codes/EasyCord/releases/download/v5.44.1/easycord-5.44.1-py3-none-any.whl
- https://github.com/rolling-codes/EasyCord/releases/download/v5.44.1/easycord-5.44.1.tar.gz
- Added
easycord.plugin_creatoras a public Python API for generating in-project plugins and reusable package plugins. - Added plugin manifests with schema version
1, validation helpers, and entry-point discovery through theeasycord.pluginsgroup. - Added CLI wrappers for plugin authoring:
easycord plugin create,easycord plugin check, andeasycord plugin discover. - Added
docs/plugin-authoring.mdand refreshed developer toolkit/getting-started docs for plugin manifests, package discovery, and local-safe scaffold defaults.
- Default config-driven bots to local SQLite storage when no database backend is configured.
- Keep generated runnable bot scaffolds local-safe with command sync disabled; generated tests continue to use memory storage.
python scripts/check_release_metadata.py- passed.pytest -o cache_dir=.pytest_cache_codex tests/- 534 passed.python -m compileall -q easycord tests scripts- passed.
- Added
scripts/check_release_metadata.pyto enforce a singlepyproject.tomlversion acrosseasycord.__version__, README release links, CHANGELOG headings, project URLs, and release asset names. - Added release metadata tests and wired the checker into GitHub Actions before the pytest run.
- Cleaned
MANIFEST.inso source distributions keep the public library, docs, examples, and context notes while excluding local caches, release prep folders, workflow files, scripts, tests, and contributor-only development files.
python scripts/check_release_metadata.py- passed.pytest -o cache_dir=.pytest_cache_codex tests/- 517 passed.python -m compileall -q easycord tests scripts- passed.
- Updated the runtime dependency to
discord.py>=2.7.1,<3and verified current app-command context and install metadata support. - Added non-SQL memory database startup paths via
db_backend="memory",database=MemoryDatabase(), andEASYCORD_DB_BACKEND=memory. - Updated generated starter templates to use the memory database where persistence is unnecessary.
- Closed SQLite test fixtures cleanly to remove delayed
ResourceWarningnoise under strict warning checks. - Stabilized level-up tests on fresh CI runners by resetting XP cooldowns with an expired sentinel.
- Repaired the i18n performance regression workflow by adding the benchmark script it expects and aligning baseline cache paths.
- Added release-readiness coverage for the real GitHub wheel and source distribution asset names.
- Python 3.11.9 via
py -3.11. discord.py 2.7.1in.venv311.ruff check easycord tests --select E9,F63,F7,F82- passed.pytest tests/- 515 passed.scripts/benchmark_i18n.py- passed under thresholds.python -m build- passed.- Earlier environment checks also passed:
pytest tests/ -W error::ResourceWarning,python -X tracemalloc=10 -m pytest tests/ -W always::ResourceWarning,ruff check .,compileall,git diff --check, and CodeRabbit review with 0 issues.
- Stable JSON output contracts for
easycord doctor --json,easycord inspect --json, andeasycord sync-plan --json. - Project scaffold templates via
easycord new --template minimal|plugin|ai|database; the defaultplugintemplate preserves v5.3 behavior. - Actionable doctor diagnostics with machine-readable
code,severity, andfixfields while preserving existingname,ok, anddetailfields. FakeContextBuilderfor fluent offline command test setup.- End-to-end developer toolkit docs showing project creation, diagnostics, inspection, sync planning, and offline tests.
- Offline AI tool safety audits via
easycord audit-tools,audit_tool_registry(), andformat_tool_audit(). easycord doctornow surfaces anai.tools_auditcheck for bots with registered AI tools.easycord new --list-templatesfor discovering scaffold options.easycord audit-tools --fail-on-warningsfor CI-friendly local AI safety gates.FakeContextBuilder.with_roles()for offline role-gated command and tool tests.
- Existing CLI commands, flags, formatter exports, and testing helpers remain available.
- CLI commands remain dependency-free and avoid live Discord side effects by default.
- Runtime dependency floor is
discord.py>=2.7.1,<3; SQLite remains available, whileMemoryDatabase,db_backend="memory", andEASYCORD_DB_BACKEND=memoryprovide non-SQL startup paths for tests and ephemeral bots.
pytest tests/python -m compileall -q easycord tests
- Dependency-free
easycordCLI withnew,inspect,sync-plan,doctor, andtest-templatecommands. - Project scaffolding for a runnable bot, starter plugin,
.env.example, project metadata, and pytest coverage. easycord doctor [module:bot]for local setup diagnostics, token checks, dependency checks, and optional bot import validation.- Developer formatters:
format_interaction_inventory(),format_sync_plan(), andformat_doctor_report(). - Offline testing helpers for context menus, components, and modals via
invoke_user_command(),invoke_message_command(),invoke_component(), andinvoke_modal(). - Developer toolkit documentation.
- CLI commands avoid live Discord side effects by default.
sync-planonly compares local state with manually supplied remote names.
pytest tests/python -m compileall -q easycord tests
- Centralized
InteractionRegistryfor slash commands, context menus, components, modals, and autocomplete callbacks. - Command sync planning with dry-run support, duplicate detection, and safer destructive-sync handling via
bot.plan_command_sync()andbot.sync_commands(dry_run=True). - Dynamic component routing with typed route parameters, e.g.,
@component("ticket:close:{ticket_id:int}"). - Autocomplete callback registration and testing support via
@autocomplete. @slash_commandas a compatibility alias for@slash.- Reusable option validators:
Duration,URL,Snowflake,Range,Regex, andChoiceSet. - Telemetry: Global
/healthcommand now includes real-time telemetry: API latency, event loop latency (congestion monitoring), resident memory usage (via optionalpsutil), active thread counts, and plugin versions. - Memory Safety: Added memory-pruning to
LevelsPluginXP cooldown cache and pagination toTagsPlugintag list to prevent resource exhaustion and API limit errors.
- Refactored Plugin instance tracking to use unique
_instance_idvalues instead of class names, preventing state cross-pollution. - Updated
InteractionRegistryto compare structural segments of dynamic component patterns for collision detection. - Bumped minimum required
discord.pyversion to>=2.7.1,<3for current app-command context and installation support. - Standardized all bundled plugins to use
ctx.respond()instead of deprecatedctx.send_embed_from_dict().
- Core Stability: Fixed a critical infinite recursion bug in
LocalizationManagerwhen reporting missing keys inSTRICTmode. - Command Registration: Fixed global
/healthcommand not being registered in the tree. - Plugin Resilience: Resolved
StarboardPluginduplicate archival bug and missing configuration slash commands. - Config Handling: Fixed
BotConfig.from_env()syntax error and logic gap wherelog_leveloverrides were ignored. - Bug Fixes: Unified error pipeline: exceptions from components, modals, and autocomplete now route through
plugin.on_errorthenbot.on_error. - Autocomplete failures now return an empty list instead of bubbling exceptions.
- Task cancellation during plugin unload is now handled as a normal lifecycle event.
- Fixed legacy component-prefix matches bypassing plugin-scoped error handlers.
- Fixed choice validators crashing on mixed-type choice sets.
discord.py >= 2.7.1,<3is now required.InteractionRegistryreplacesCommandTreeas the authoritative internal metadata store.
- Interaction Registry: Access registered metadata via
bot.registryinstead of inspectingbot.treedirectly for EasyCord-specific logic. - Plugin IDs: Plugins now use
_instance_id(e.g.,MyPlugin_12345) for registration. If you relied on the class name for reloading, use the new ID or class name (fallback supported). - Dynamic Routes: Ensure dynamic component patterns do not overlap. The registry now performs strict shape-based collision detection.
- Autocomplete: Signatures are now validated at registration. Ensure callbacks accept
(ctx, current, options)or(current).
pytest tests/-> 472 passed.
- Config-driven startup via
BotConfig.from_env()andBotConfig.from_file(). easycord.testing.FakeContextandeasycord.testing.invoke()for unit-testing.- Command guards:
@cooldown,@require_permissions,@install_type, and@premium_required. Context.send()as a compatibility alias forContext.respond().
BotConfig.build_bot()now correctly honorsdb_backend="memory".BotConfig.from_file()precedence: Env -> File -> Explicit.- Guild-scoped command sync via
BotConfig.guild_id. - Discord user-install context metadata for current
discord.pyversions.
pytest tests/-> 461 passed.
LevelsPluginXP cooldown sentinel changed from0.0tofloat("-inf")to fix first-message blocking on new runners.
OpenClawPluginfor autonomous AI agent tasks.
LevelsPluginrole reward assignment usinghasattr(author, "add_roles")for better compatibility.- Orchestrator handling of empty string responses from AI providers.
ToolRegistryrole check crash in DM contexts.
pytest tests/-> 411 passed.
- Production-stable release with Python 3.13 support.
- Lazy-loaded AI providers exposed directly from
easycord. - Advanced
@ai_toolmetadata (safety, gates, limits).
FallbackStrategyprovider rotation logic.ctx.is_adminaccessed as property instead of method.ToolLimiterasync execution and locking.asyncio.get_event_loop()deprecation fixes.
pytest tests/-> 352 passed.