Fix/bug fixes#76
Merged
Merged
Conversation
I had Claude Fable generate a bug report. I will now work to resolve each bug.
…stored
The on_raw_message_edit try/finally removed the stored message after
every edit (including early-return paths), so subsequent edit/delete
logs found no record ('Contents Unretrievable'). The upsert already
keeps the record current; the finally-block deletion was leftover logic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✓ Verified by cypress-exe as a legitimate issue that was properly
resolved
…n place properties aliased the cached dict, so the first to_embed() replaced the color string with a nextcord.Color object; a second call on the same instance then crashed in get_discord_color_from_string (color.lower()). This aborted the rest of a guild's birthday run when 2+ members shared a birthday. Copy the dict before converting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
… not an exact minute Each guild previously computed its own 'now' and fired only on exact HH:MM equality with the stored runtime. At scale the guild loop takes minutes, so guilds processed after the matching minute silently never fired (B1); off-grid legacy runtimes and slightly-late runs could never match either (B3). The scheduler now passes its cycle-start time into check_and_run_birthday_actions, which fires when the runtime falls in [cycle start floored to the 15-min grid, +15 min). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
…e guild The throttle branch did sleep-then-continue, dropping guild i from the cycle entirely; a guild skipped during its matching window missed its birthdays for the day. Now it sleeps and then processes the guild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
…only guild.owner With startup chunking disabled, guild.owner is frequently None. This made owner permission checks deny actual owners, silently dropped owner error/warning DMs, rendered @owner as 'Unknown', and crashed the fallback join DM. Adds utils.get_guild_owner() (owner_id + cached fetch fallback) and uses owner_id comparisons where an object isn't needed. Also hardens the admin /view-server owner line against an uncached owner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human-review notes:** Changes in utils.apply_generic_replacements() does not use the new utils.get_guild_owner() function because it's a synchronous best-effort function. In the future, I'll update the docs to specify that some fields may return "Unknown" for uncached members.
…ooter nextcord's Embed.fields/.footer accessors return throwaway EmbedProxy objects, so the previous in-place proxy writes were silently discarded — placeholders like [age]/@mention in embed fields or the footer were never replaced. Replacements now rebuild fields via clear_fields()/add_field() and set the footer via set_footer(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a potential issue that was properly resolved **Human Review Notes:** This wasn't really an accessible issue since InfiniBot currently doesn't support footer or field customization for custom embeds. BUT, it might in the future, and this is a good change.
… fields Field-wise comparison rejected entries seconds old across an hour boundary (13:59 vs 14:01) and accepted entries up to 59 minutes old within the same hour (also mis-accepting Discord's stacked audit entries that keep their original created_at). Now: abs(now - created_at) <= 5 minutes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Well that was embarrassing... Mb. Good change.
…idates the target log_member_update previously grabbed the single latest audit entry of any type and attributed nickname/role/timeout changes to its user — crediting the wrong actor and logging InfiniBot's own role grants as admin actions. Each change type now looks up the newest matching-action entry targeting the updated member; when none is found (or it isn't fresh), the log says 'Someone' instead of guessing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
…rgeting the leaver log_member_removal accepted any fresh kick/ban in the last 5 audit entries, so a member leaving voluntarily shortly after someone else was kicked or banned was logged as kicked/banned themselves. Now requires entry.target.id == member.id. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
Long edited messages (1025-2000 chars) are relayed verbatim into the log channel, and the bot allows @everyone mentions globally — so editing a long message containing @everyone/@here/role mentions made InfiniBot ping them in the log channel. Mention parsing is now disabled on those sends. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
…bidden handler The coroutine was created but never awaited, so owners were never told why default roles failed (and a 'never awaited' warning was emitted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Woops.
nextcord's DMChannel has no purge(), so the command raised AttributeError (swallowed by LogIfFailure) every time and had never worked. It now walks recent DM history and deletes the bot's most recent message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Verified to be an issue in production. Can't believe I did that.
guild.get_role() returns None for deleted roles; passing None to add_roles/remove_roles raised AttributeError (only Forbidden was caught), killing the button instead of applying the remaining valid roles. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
…ess window The old check used timedelta.seconds (wraps at 24h, so day-old entries could read as fresh) with a 1-second window that was always exceeded by the 1-second audit-log sleep already performed upstream — so 'they edited their own nick' was essentially never detected. Now uses total_seconds() with a 10s window and validates the audit entry targets the member. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
log_member_update chunked any unchunked guild before logging, re- creating the gateway member-chunk flood that disabling startup chunking was meant to remove. before/after are already populated (the event only fires for cached members), and the audit-log lookup doesn't need the member cache. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✗ Incomplete change; see notes below: **Human Review Notes:** The paradigm of this change is correct, but it actually breaks all member-update events since this function requires a cached user to work. After Claude finishes its commits, I'm going to change the on_member_update() function into on_raw_member_update(), correcting these issues.
…top_role crash O3: the hierarchy check dereferenced relevant_member.top_role, which raises AttributeError when the message author is an uncached nextcord.User; compare against the fetched Member instead. Button loads are now also isolated so one failing button can't abort the whole menu build. O2: menu opens no longer pay two REST round-trips — the member fetch is skipped when a Member object is already in hand, and the already-banned check (fetch_ban) is deferred to the button press, which shows an 'Already Banned' notice instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a potential issue that was properly resolved
…lues json.loads + packet['status'] raised on any non-JSON stored value, propagating out of property reads (e.g. birthdays_profile.runtime inside the birthday loop, aborting that guild's run). No live write path produces such values today (verified PLAUSIBLE-UNCERTAIN), but the getter now falls back to the raw number or UNSET/None instead of raising. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a potential issue that was properly resolved **Human Review Notes:** I'm not sure this is a real bug, since I don't think my prod db has any legacy values that would trigger this. But, regardless, this is a good change in case I do.
daily_leveling_maintenance and daily_moderation_maintenance fetched each DB-listed member individually (REST call per uncached member per guild at 3am). They now resolve members via a new utils.get_members_batch() — member cache first, then gateway query_members in batches of 100 — and skip the run (rather than mass-deleting rows) if the gateway query fails. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Claude wanted to change src/features/leveling.py:251 to reference the pre-decremented points value. Technically, other db cleanup code would resolve that, but I believe that would have been a regression, for the reason outlined in the comment that I added.
…eanup B13: the WAL checkpoint, optimize_database (which time.sleep()s between tables), and message-log cleanup ran synchronously on the event loop at 09:00 UTC, freezing all interactions for the duration. They now run via asyncio.to_thread. The orphaned-guild phase stays on the loop because it depends on a per-connection SQLite TEMP table across serial queries. B14: orphaned-guild cleanup treated any guild missing from bot.guilds as orphaned; during shard reconnects/outages that set is incomplete, so it could wipe live servers' data. The phase is now skipped unless the bot is fully ready, has guilds, and none are flagged unavailable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
Every get_configs()/get_global_kill_status() call constructed a fresh GlobalSetting, whose init re-synced every leaf key — and each membership check/read deep-copied the entire parsed JSON file (multiple times per lookup via __contains__ + __getitem__). On the per-message and per-guild hot paths this was a deep-copy storm. - JSONFile._get_data() now returns the cached dict for read-only paths and deep-copies only for write paths (mutable=True); __getitem__ copies just the returned value so callers still can't mutate the cache. - get_configs()/get_global_kill_status() return module-level singletons, keyed by file_manager.base_path so tests that repoint the config directory get fresh instances. 10k hot-path reads: ~0.03s. Full test suite passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Took me a minute to understand, but after tracing the code paths, I agree that this works and is a good change.
…resolved A deleted/hidden birthday channel silently dropped the message (only the Forbidden path warned). The owner now gets the same style of DM warning. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Good change.
The daily setup_logging() call used the INFO default, discarding the logging.log-level from config that was applied at boot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Again: Woops, mb.
…f crashing With a fresh message_delete audit entry but an uncached message, entry.target (and in theory entry.user) can be None; the subsequent .id/.mention derefs raised AttributeError, which LogIfFailure swallowed — so no delete log was produced at all. Unresolvable actors now downgrade to the non-attributed log path, and a None author on a DB- sourced record falls back to the generic description. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a plausible issue that was properly resolved
… failed author fetch
L10: the upsert's conflict path wrote SQLite CURRENT_TIMESTAMP (naive,
different format) while inserts bound an aware ISO datetime — skewing
string-based retention comparisons and fromisoformat() parsing. The
update now binds an aware UTC timestamp from Python (keeping the bump-
on-edit semantics). The message_to_message_record_type fallback
timestamp is also aware now.
L12: a failed MessageRecord.fetch('author') in on_raw_message_delete
propagated (aborting the delete log), and log_raw_message_delete deref'd
message.author.id unguarded. The fetch is now isolated, and the
InfiniBot self-check falls back to the record's author_id when author is
None.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✓ Verified by cypress-exe as a legitimate issue that was properly
resolved
MessageRecord always defines .guild (None until fetched), so the generic 'has guild attr and it is None' check returned False for every record — silently dropping any store/cache of a MessageRecord without override_checks=True. Records are now validated by guild_id. (Latent: no current caller hits this, per verification.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** This was a sly one. Good catch.
…mption The on_message path exempts administrators and members holding a configured admin_role_ids role, but the edit path calls check_and_trigger_profanity_moderation_for_message directly, which only checked guild_permissions.administrator — so admin-role members were punished for edits but not for original posts. The role exemption now lives inside the shared check (still bypassed by skip_admin_check=True, whose caller performs both checks itself). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** It's fine for now, but I don't love how the codepath here is so messy, where we need this check inside check_and_trigger_profanity_moderation_for_message().
Strikes were stored with naive local datetime.now() but expiry parses last_strike as UTC (parse_datetime_string), skewing strike expiry by the host's UTC offset — and daily maintenance already wrote aware UTC, leaving records inconsistent. Both write sites now use aware UTC. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Yes... and, the container that runs this is UTC, so these bugs never manifested. Still good fixes.
… managed-message lookup When the target message was gone, load/load_buttons sent an error response but setup continued and called interaction.response.edit_message — raising InteractionResponded (O5). And managed_messages[message_id] KeyError'd when the message was no longer cataloged (O6). load now returns a success bool that setup honors, and an uncataloged message gets a proper 'Message Not Editable' response. Applied to the embed, reaction-role, and role-message editors alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> ✓ Verified by cypress-exe as a possible issue that was properly resolved
A crash mid-write previously left malformed JSON, which ensure_existence
then 'repaired' by renaming to .bak and recreating {} — silently
resetting all global-kill flags and admin IDs. Writes now go to a temp
file, fsync, then os.replace over the target.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
✓ Verified by cypress-exe a plausible issue that was properly resolved
**Human Review Notes:**
I'm not sure how important this really was... I think it would have been
fine, but sure—the change doesn't make anything worse.
`run_raw_reaction_add` resolved the reacting member (REST fetch on cache miss), the channel, and the message for *every* reaction in *every* guild, and only then asked whether the message was even InfiniBot's. On a busy bot that multiplied REST traffic for reactions the bot could never act on. Reordered so every disqualifying check runs first: the message is fetched, then author, embed presence, and the "react"-prefixed field are checked, and only a message that really is a reaction role goes on to resolve the member. Also added a cheap up-front skip for the bot's own reactions — InfiniBot seeds its own reaction-role messages, so those events were previously paying for a full resolution every time. The nested conditions the checks replace are flattened out; control flow is equivalent, since a non-matching message previously just fell through to `if action:` with action None. The unguarded `message.remove_reaction(...)` on the success path is now wrapped in try/except Forbidden, matching the guarded call already present on the role-missing path; without Manage Messages it was raising into the default event error handler. Not doing the suggested LRU of known non-reaction-role message IDs: the message fetch is now the only pre-filter cost, and a cache of that shape would need invalidation when a reaction role is created or deleted. The DB-backed check at the top of the function stays commented out — per the existing note, it would break pre-database-storage reaction roles. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
`BirthdaysView.setup` ran `server.birthdays_profile.runtime = server.birthdays_profile.runtime or "00:00:00"`, and UNSET_VALUE is falsy, so merely *opening* Dashboard -> Birthdays persisted a runtime of midnight. The scheduler arms birthdays for any runtime that is not UNSET (birthdays.py:99), so an admin who only looked at the page started getting birthday messages fired at 00:00 UTC without ever choosing a time. The "You must set a message time" warning two lines below was consequently dead — the value it tested had just been assigned. The page now reads the runtime without writing it, renders "Not set" when it is unset, and the warning tests that same value so it displays as intended. Setting a time through the Set Message Time modal still persists normally, and that modal already tolerates an unset runtime via its existing except branch. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
`check_and_run_autoban_for_member` caught Forbidden (and every other exception) from `member.ban()` and then fell straight through to `server.autobans.delete(member.id)`, returning True. So a single attempt made without Ban Members permission destroyed the autoban entry permanently — the member stayed in the guild and would never be autobanned again, even once the permission was granted. The True return also told `on_member_join` the member had been removed, so their welcome message and default roles were skipped despite them still being present. Now tracks whether the ban actually succeeded: on failure the entry is retained so the autoban retries on the next join, and the function returns False so the join is processed as a normal one. The owner notification for the missing permission is unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
`server.moderation_strikes.get(member_id, 0)` returns the row dataclass, not an int, so `default=(level == self.member_strikes)` compared an int against a dataclass and was never true. The strike-level dropdown opened with nothing preselected for exactly the members who had strikes — the only members it is ever opened for. Now reads `.strikes` off the record, falling back to 0 when the member has no row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Good catch.
…tor (DASH3) Dashboard strike edits wrote `last_strike=datetime.datetime.now()` — naive local time — while every other writer (moderation.py:389, 398, 1022) stores UTC-aware. `parse_datetime_string` assumes a naive value is already UTC, so on a host whose local timezone is not UTC the strike- expiry math shifted by the offset. Demoed locally: a naive "2026-07-18 23:51" was read back as 23:51 UTC when the true UTC instant was 05:51 the next day, a 6-hour error. Latent in production, where the container runs UTC, but wrong anywhere else. Also switched the cosmetic embed timestamps the report lists alongside it: join/leave messages (join_leave_messages.py:52, 112) and the two moderation embeds (moderation.py:586, 656). Verified the UTC-aware string round-trips through `parse_datetime_string` unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a consistency error that was properly resolved **Human Review Notes:** Again, would never manifest because the container is UTC, but it's still a good change.
…el is selectable (DASH4) `SelectView` raises ValueError on an empty options list. Both channel- select pages build their options by filtering on `utils.check_text_channel_permissions`, so a guild where InfiniBot cannot view/send in any text channel produced an empty list and the generic error embed. The profanity Admin-Channel page is force- redirected to whenever the channel is UNSET, so an admin in that state could hit the error immediately on opening the dashboard, with nothing telling them what was wrong. Adds `show_no_selectable_channels`, which renders the specific cause and the fix (grant View Channel + Send Messages somewhere) with a Back button routed through the existing cancel path. Both pages now check for empty options before constructing the SelectView. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** I moved the show_no_selectable_channels function into components/ui_components. Claude initially put it in features/dashboard.py.
`LevelRewardsView`'s LevelModal called `int(self.input.value)` with no validation, unlike its sibling modals (MaxStrikes, StrikeExpireTime, and the other Choose Level modal at :2133), so any non-numeric entry raised ValueError and surfaced as the generic modal error. Now rejects non-digits and enforces the 1-9999 range the field's own placeholder advertises, reporting it the same way the sibling modal does. Verified against empty, whitespace, alphabetic, decimal, negative, zero, and out-of-range inputs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
The daily cleanup tested `member_level_info.points`, the pre-decrement
value, so a member whose points reached 0 on this run kept their row
until the next day's run. Now tests the freshly computed `_points`.
Investigated the human note asking whether member levels are being
updated at all. They are — this was checked by running
`daily_leveling_maintenance` against a real SQLite database with three
members (well above zero, exactly reaching zero, already at zero):
before run 1: {1: 100, 2: 5, 3: 0} after run 1: {1: 95, 2: 0} <-
member 2 zeroed but row retained after run 2: {1: 90} <-
removed a day late
Points are decremented and persisted correctly on every run, and
`server.member_levels.edit(...)` writes through as expected, so the "not
updating at all" concern does not hold. The only defect is the delayed
row deletion. With the fix the same scenario gives {1: 95} after run 1 —
member 2 is removed on the run that zeroes them, and the already-zero
member 3 is still removed as before.
Row deletion stays ordered after `process_level_change`, so level
rewards are still revoked using the member's real 0-point state before
the row goes away.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✓ Verified by cypress-exe as a legitimate issue that was properly
resolved
**Human Review Notes:**
Duh, yeah. Of course it decrements. I missed that somehow.
…mbeds (A1) A deleted message carrying exactly 9 embeds produced a 10-embed log message (1 log embed + 9 attached). Clicking "Show More" appended an 11th, so `edit_message` returned HTTP 400 and the moderator got the generic view error. Per the report's stated solution, the button is now withheld when the log message would already be at the limit. Traced across attachment counts 0-12: only the 9-embed case withholds it, and no case can reach 11. The callback is also guarded. `ShowMoreButton` is a persistent view keyed on the `show_more` custom_id, so log messages sent before this change still carry a live button; those now get an ephemeral explanation instead of a 400. Introduces `MAX_EMBEDS_PER_MESSAGE` rather than repeating the bare 10. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was mostly properly resolved **Human Review Notes:** I agree with the later report down the line that says that MAX_EMBEDS_PER_MESSAGE should be abstracted everywhere. Other than that, I like this change.
`random.choice(all_jokes)` raises IndexError on an empty list — which happens when jokes.json fails to load — and the `if joke is None` guard sitting after it could never fire, since `random.choice` either returns an element or raises. Replaced with an emptiness check before the call, reporting the condition to the user ephemerally and logging the cause. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
…g (R3)
`option.split("=")[1][4:-1]` assumed the value was exactly " <@&ID>"
with one leading space. Replaced both call sites with a `<@&(\d+)>`
regex via a new `extract_role_id` helper.
Worth noting the failure was worse than the report states. Two spaces
raises and lands in the generic "You formatted that wrong" path as
described, but *no* space does not fail — the slice silently drops the
leading digit and returns a different, valid-looking role ID:
"👍=<@&123456789012345678>" -> 23456789012345678
That only surfaced as an error because the truncated ID matched no role
in the guild. Had it collided with a real role, the reaction role would
have been created against the wrong one, silently. The regex now returns
the same ID for every spacing variant, and returns None on genuinely
malformed input, which routes into the existing error message.
Also switched the emoji side to `split("=", 1)` with a strip, so a role
mention containing "=" can't split incorrectly and extra spacing doesn't
leak into the emoji.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✓ Verified by cypress-exe as a legitimate issue that was properly
resolved
The opt-in/opt-out confirmations told users to run `/opt-into-dms` and `/opt-out-of-dms`, but the commands are registered as `opt_into_dms` and `opt_out_of_dms` (bot.py:362, 374), so neither name existed and the instruction was a dead end. These two strings were the only dashed references; every other mention across the codebase already used the underscore form. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
nextcord dispatches `on_ready` again whenever a session is invalidated and re-established, and the handler had no notion of having already run. Every re-fire re-ran `init_views`, refetched the emoji cache, re-sent the "InfiniBot is online!" startup notification, and logged a STARTUP COMPLETE line whose duration was measured against the *original* `startup_time`, so the reported elapsed time and guilds/sec were nonsense. Adds a `startup_completed` flag. `start_scheduler()` still runs on every fire (it already handles restart); everything after it is now skipped on a re-fire, with a log line marking the reconnect. Confirmed against the installed nextcord 3.2.0 that skipping `init_views` is safe: `ConnectionState.parse_ready` calls `self.clear(views=False)` (state.py:1265) and the AutoSharded `parse_ready` doesn't clear at all, so the persistent view store survives a re-ready. This also matches the verifier's correction that the view-store *accumulation* sub-claim was refuted — ViewStore keys by (component_type, message_id, custom_id) and overwrites — so the re-registration was wasteful rather than leaky. The stale-duration logging and the duplicate startup notification are the substantive fixes here. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Good change. This was an issue I've experienced in prod.
… autoban modal (X3) The autoban add-modal read `self.id_input.value` and passed the `str` straight to `utils.get_member`. `Guild.get_member` is a `self._members.get(user_id)` dict lookup keyed by int, so the cache lookup always missed and every check paid a REST fetch. The negative `failed_member_fetches` cache is keyed by `(guild.id, user_id)` and missed for the same reason, so even repeat lookups of a nonexistent user re-fetched. Per the human note, the hardening went into `get_member` itself rather than only the call site: it now coerces `user_id` with `int()` up front, so a string ID hits both caches normally, and returns None with a warning for input that isn't a valid ID at all instead of failing deeper in. Verified that both `123` and `"123"` resolve from the member cache without any REST fetch, and that `"abc"`/`None` return None. The call site converts once now, so the downstream `int(user_id)` calls in the ban-list comparison and the autoban record are dropped as redundant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
Implemented prod release scripts that handle db backup and container tagging. - release_prod.bash — DB backup & container tag - rollback_prod.bash — Container restore (no db restore!) - backup_prod_db.bash — Uses sqlite or system copy fallback to backup db Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude Fable 5 did one last pass of the earlier changes to see if there were any bugs in those. It found a few things. I might fix them real quick before pushing this branch, but they're not critical. Co-Authored-By: Claude Fable 5 & Sonnet 5 <noreply@anthropic.com>
check_and_run_autoban_for_member's return value was overloaded to mean both "was this member banned" and "should join processing be suppressed". When the ban call failed (usually missing Ban Members), returning False caused on_member_join to fall through to trigger_join_message and add_roles_for_new_member — publicly welcoming and auto-roling the exact member an admin flagged for an autoban. Now returns True whenever the member matched an autoban entry, regardless of whether the ban call succeeded. The entry is still retained on failure so the autoban retries once the permission is granted. Also updated the error message sent to the server owner, explaining the situation in improved quality. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved
… (FR2)
A filtered word containing a regex metacharacter (e.g. "(" or "[") made
re.compile raise inside _get_compiled_profanity_patterns.
functools.lru_cache does not cache exceptions, so the bad word re-raised
on every message in that guild, and LogIfFailure swallowed it — silently
disabling profanity moderation for the whole guild with no error
surfaced anywhere.
The literal portion of each word is now re.escape'd, with the boundary
markers stripped beforehand and the supported wildcards (* and ?)
re-introduced after, so admin-supplied metacharacters match literally
instead of breaking compilation. The shipped default word list uses only
? and " markers, so its behavior is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✓ Verified by cypress-exe as a legitimate issue that was properly
resolved
For guilds on the default word list — the common case, since the storage
optimization stores [] to mean "using the defaults" — every moderated
message hit read_txt_to_list("default_profane_words.txt"), an uncached
disk read and parse, before the compiled-pattern cache was even
consulted. That read dominated the per-message cost the pattern cache
was meant to remove.
Adds read_txt_to_list_cached in file_manager (lru_cache keyed on file
name plus the current base path, cleared by update_base_path so tests
and the event harness still see their own files) and uses it for the
default word list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✓ Verified by cypress-exe as a legitimate issue that was properly
resolved
Every other blocking phase of daily_database_maintenance (checkpoint, optimize_database, stored_messages.cleanup_db) is offloaded via asyncio.to_thread; the orphaned-guild cleanup was still called directly on the loop, freezing the bot for its duration. It originally had to run inline because it shared a pooled connection with a SQLite TEMP table, but the D1 fix moved it onto pinned_connection, which takes its own connection and is safe in a worker thread. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** I don't 100% understand this, but the AI is very confident and it seems like something it would know.
…FR5) The autoban-add flow paginated the guild's entire ban list to decide whether the target was already banned, on the premise that nextcord can't check a ban by ID. It can: guild.fetch_ban is one REST call that raises NotFound when the user isn't banned, and it's already used in options_menu/ban_button.py. On a server with thousands of bans, adding one autoban cost seconds of paginated REST calls. Unexpected failures are now logged instead of silently swallowed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** Yes, this change *does* work. I tested it in Discord. Much better.
…flow (FR6) create_reaction_role built `roles` by filtering the parsed role IDs against interaction.guild.roles, silently dropping any role deleted since validation, while `emojis` was built independently from the raw option string with no filtering. A role deleted mid-list — reachable because the flow waits on a modal for up to five minutes after validation — desynced the two lists, so every entry after the gap got the wrong emoji. Both lists are now built in one pass, so a dropped role drops its emoji with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a legitimate issue that was properly resolved **Human Review Notes:** This bug would have been really rare, but it's good to fix anyway.
SelectView raised ValueError when handed an empty options list, so every page that could produce one had to guard for it itself — DASH4 added handling at the two channel-select pages, and roughly five other call sites carry their own ad-hoc guards. Any new page that forgets is a generic error embed. SelectView now accepts an empty list and renders empty_state_message with a Back button routed through the existing cancel path (return_command still receives None), instead of raising. The select and its pagination are dropped in that state since Discord rejects a zero-option select. Type errors on options still raise. Existing call-site guards are left in place — their messages are more specific than any generic default — but they're now a nicety rather than the only thing standing between a page and a crash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a nicety that this properly implements **Human Review Notes:** I'm not sure how much we needed this, but sure. It can't hurt.
… types (FR8) RoleMessageButton_Single.View and RoleMessageButton_Multiple.View were near-clones, so the R2 fix (field index as the option value plus an option_roles map) had to be pasted into both — and any future change to that encoding would too, with nothing catching a one-sided edit. Both now subclass RoleMessageSelectView, which carries the shared option building and callback; the subclasses only declare their select custom_id, placeholder, and whether multiple options may be chosen. Two incidental alignments from the merge: the single-choice variant picks up the multi-choice variant's NotFound guard around edit_message, and the multi-choice max_values no longer goes to 0 when a role message has no fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a good refactor that was properly implemented. **Human Review Notes:** Looks right.
<@&id> parsing was reimplemented five times — reaction_roles.extract_role_id (added by R3), the raw-reaction get_role helper, both role-message option parsers, and the reaction-role editor — so a fix in one, like R3's spacing-independent parsing, had to be rediscovered in the others. Adds utils.extract_role_id / utils.extract_role_ids and routes all five sites through them. The editor's variant accepted a bare ID with no mention wrapper, so extract_role_id keeps that behind an allow_bare_id flag. Two sites get incidental hardening from the shared parser: the raw-reaction get_role no longer raises ValueError on a malformed mention like "<@&abc>", and it now tolerates surrounding whitespace as R3 intended. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a good refactor that was properly implemented **Human Review Notes:** This all looks correct and it's a good fix for consistency.
…R10) The deleted-message log still used bare literals (> 9, "8/", [0:8], >= 10) for the embed-count limit right beside the MAX_EMBEDS_PER_MESSAGE constant A1 introduced, so a change to the limit would leave them silently out of sync. MAX_ATTACHED_EMBEDS and TRUNCATED_ATTACHED_EMBEDS now express the two derived counts — how many of the message's embeds fit alongside the log embed, and how many are attached once truncation leaves room for "Show More" — and both call sites use them. Values are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> ✓ Verified by cypress-exe as a good refactor that was properly implemented **Human Review Notes:** I'm not crazy about the MAX_ATTACHED_EMBEDS and TRUNCATED_ATTACHED_EMBEDS constants at the toplevel, but I think they're better than inlining it.
…d.get_channel guild.get_channel only checks the local cache, so channels that fell out of cache (or were never cached) were silently treated as missing across logging, moderation, leveling, birthdays, join/leave messages, join-to-create VCs, and admin broadcast. This led to real bugs and possible data loss on prod. utils.get_channel falls back to an API fetch (with failed-fetch caching) so these lookups behave correctly again. Also fixes a copy/paste typo in the Leave Messages embed title in the dashboard.
Link had a stray .md extension, unlike every other help-docs link in the file, which would 404 on the Jekyll-hosted GitHub Pages site.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR includes over 100 bug fixes. Issues were found and classified by Claude Fable. Fable and Opus then resolved each issue, and committed. All changes were individually reviewed and edited by me. Literally—all of them. It took a while, but I'm confident that this isn't a regression anywhere.
At the end, there are a few general refactors too, but most of this PR is bug fixes.
Type of Change
How Has This Been Tested?
All tests pass, and a thorough testing Discord bot pass was conducted, testing every feature, and trying to stress edge-cases.
Checklist
./documentation(if applicable)../run_tests.bashpasses locally.