Changelog

Every notable change to Percy, newest first. Currently running v2.34.0.

v2.34.0 latest

2026-11-05

Added

  • Percy now waits out an internet outage: it holds at startup until the network and database are reachable, and background tasks pick up again once the connection returns.
  • Music rebuilds from its saved session after an outage, once Discord and Lavalink are both back.

Changed

  • Outages log a few short lines instead of repeated tracebacks.
  • The Docker image starts faster, stops cleanly on docker stop, and rebuilds in seconds after a version bump.
  • The dashboard container also stops cleanly on docker stop.

v2.33.0

2026-11-05

Changed

  • Percy now requires Python 3.15. Self-hosters on 3.13 or 3.14 must upgrade before updating.

v2.32.1

2026-11-04

Fixed

  • The music app's "Popular in this server" and "Trending on Percy" shelves no longer shrink to one track in a quiet week: under 5 tracks, they look back 30 days instead of 7.

v2.32.0

2026-11-03

Added

  • 9 new jobs on the career ladder, including two above Astronaut: Startup Founder and President.
  • work has five new puzzles, questions about your own job, and new shift events.
  • New search spots and postmemes formats.

Changed

  • Earning commands, claims, quest completions and chat drops answer in one small card, with varied result lines per job and activity. work, search and postmemes update the same message from task to result.
  • The economy hub, shop, inventory and trade cards are more compact. The career page shows the rungs around your job; job list shows them all.
  • A full music progress bar ends on the indicator emoji.

v2.31.1

2026-11-03

Changed

  • Economy progress bars (quests, career, prestige) use Percy's progress bar emojis instead of text blocks.
  • Music progress bars only look empty or full at exactly 0% and 100%.
  • Music volume is capped at 100%, so relative changes, the panel and saved volumes can no longer go past it.

Fixed

  • The volume command showed raw emoji text instead of its progress bar.
  • A negative volume could be set with a relative change such as -15.

v2.31.0

2026-11-03

Changed

  • A mistyped command only gets a "Did you mean...?" suggestion when exactly one command is within a typo of it (one slip, two for long names); anything vaguer gets no reply.
  • List flags convert each value once, so lists of members or channels work.

Fixed

  • Event errors were not reported for most events, and events without arguments crashed the error handler.
  • Error replies failed when the command message had already been deleted.
  • The "Status History" button on user info charted whoever clicked instead of the member shown; presence charts crashed on statuses that are not tracked.
  • Confirm prompts no longer show "This interaction failed" after a click, and timed-out cards now disable their buttons too.
  • Slash unban with an unknown user shows the error instead of a crash message.
  • Server info no longer fails on guild features Percy does not know yet.
  • A rare race could drop a timer (tempban, reminder) that was firing while the timer loop restarted.

Removed

  • AI command routing of unmatched commands, and its dashboard toggle.
  • Unused paginator search and other dead framework code.

v2.30.0

2026-11-03

Added

  • Poker: the hand-strength analysis is one compact table (every hand x every street) on a shared 0-100% scale, with the most likely hand per street highlighted and a "Likely at least" row showing the strongest hand held at least half the time.
  • Charts can carry a subtitle; the stats charts use it for their totals and time window.

Changed

  • All charts (stats, presence, heatmap, equalizer) use a quieter modern style: one sans font, sentence-case titles, a faint hairline grid, flat bars with rounded ends and values labelled only where they matter.
  • Quote images wrap long text properly, shrink long quotes to fit and use the dashboard colours.
  • Rank cards and quote images render about twice as fast.
  • Internal folder restructure (clients, services by domain, router models, cog packages); no behaviour change.

Fixed

  • Embed colours taken from emojis and avatars with transparency were always black.
  • The rank card's "of N members" undercounted on servers whose members were not all loaded.
  • Colour swatch captions were slightly off-centre and long colour names ran off the image.

Removed

  • Unused rendering code and assets, and the pilmoji and fonttools dependencies.

v2.29.0

2026-11-03

Added

  • Dashboard: the command manager can limit a command to chosen channels ("Only in…", like config channel enable after a server-wide disable), toggle this server's custom commands, ignore every text channel at once or clear the whole ignore list, and pick a channel to ignore from a list.
  • Internal API: /commands/public and the guild /commands list carry each command's help section (section, section_order); the guild list also returns enabled_in, custom commands (custom: true), and POST /commands/toggle accepts allow, POST /plonks the actions all and clear.

Changed

  • The public commands page and the dashboard's command manager group commands by the same sections as help in Discord. The command manager is rebuilt as a grouped list that shows each command's effective state, including commands switched off through their parent group.
  • Dashboard: config commands are no longer listed in the command manager, since they cannot be disabled in Discord either.
  • Developers: the command/group decorators and @<group>.command/@<group>.group declare their keyword arguments and overload on hybrid, so type checkers and editors resolve hybrid groups and subcommands precisely and complete decorator arguments.

Fixed

  • Help: usage examples set on a command group (group(examples=...)) are shown instead of being silently dropped.

v2.28.0

2026-11-02

Added

  • Help menu: large categories are split into sections with a "Jump to a section..." select, and every help page has a Search button that finds commands by name, alias or description across all categories.
  • Internal API: GET /music/trending also returns charts, Deezer's key-less worldwide chart (cached 1 h, empty when Deezer is unreachable).

Changed

  • The voice, nickname, channel-mute and sticky-role commands are now part of the Moderation category, and ask moved from the removed Automation category to Misc. Command rules that target cog:Restrictions or cog:Automation must be re-created for cog:Moderation or cog:Misc.

Fixed

  • Music: enabling autoplay no longer restarts the song that is playing.

v2.27.0

2026-11-02

Added

  • Moderation: warnings are point-weighted (warn --points) and can use reason presets; the warn policy thresholds act on the point total.
  • Moderation: voicekick, voiceban/voiceunban, nick, nickfreeze/nickunfreeze and per-channel channelmute/channelunmute, also available from the dashboard's member actions.
  • Moderation: sticky roles that are restored when a member rejoins, plus moderation settings: DM the member on an action, require a reason, re-invite on unban and a default ban message-delete window.
  • A custom reply for disabled commands, set per server.
  • Command permission rules: config rule ... allows or denies a command per role, channel or member, and config explain shows which rule decided the outcome. The dashboard's new Rules & Custom page manages them.
  • Custom commands and aliases: typed arguments, flags, permissions, cooldowns and random responses, created in Discord through an interactive Components V2 manager behind bare cc or on the dashboard's Rules & Custom page.
  • Member reports: report and a Report Message context menu send a report to the server's report channel with moderator action buttons. A report channel must be configured first.
  • Trivia: custom trivia lists (trivialist, imported from JSON) with free-text rounds and a leaderboard.
  • Music: playlists can be exported and imported.
  • Internal API: /command-rules and /custom-commands (including aliases) routers, the new member actions, and moderation-settings fields on the guild config.

Changed

  • Breaking for self-hosters: the 74 migration files are consolidated into 14 feature bundles (V1-V14). Existing databases cannot be upgraded in place: start from a fresh database (with the pg_trgm extension), run poetry run python main.py db init, and re-import any data you need to keep.
  • Banning without an explicit message-delete window now uses the server's default instead of a fixed 0 days.
  • Personal-data deletion (settings remove-personal-data) now covers every table that stores a user id, enforced by a CI contract test.

Fixed

  • Commands with optional text before flags no longer drop the flags (for example warn --points 3).
  • Rejoining members now get their restored restrictions re-applied.
  • A report whose action fails releases its claim so it can be retried.
  • Custom trivia: one custom round per channel, malformed deeply nested JSON is rejected, and played questions are not repeated.

v2.26.0

2026-11-02

Changed

  • Internal API: 131 of 218 routes now declare pydantic response models (guild, users, content, music, profile, gallery, economy, leveling, moderation, members, notifications, stats, webhooks, backup templates, role menus, starboard, watchlist, releases, comics, news). The JSON on the wire is unchanged and every model is pinned by a round-trip test; the dashboard's client types are generated from the resulting OpenAPI schema. Deliberately untyped: ok-only mutations, file/binary/stream responses, open-ended backup documents.

Fixed

  • Moderation cases now carry pardoned, so the dashboard's Pardon button only shows for active warns.
  • Comic series with no stored name no longer fail the series endpoints.

v2.25.0

2026-11-01

Added

  • Internal API: GET /guilds/{id}/events — a per-guild server-sent event stream (music, case, stats topics plus any percy_event), fed by an in-process EventHub with bounded drop-oldest queues so a slow listener never blocks the bot. The dashboard uses it for live updates instead of polling.
  • Internal API: typed response models for the analytics endpoints, and a checked-in OpenAPI schema the dashboard's client types are generated from.
  • CI: a client/API contract test (tests/test_api_contract.py) that fails when the dashboard client calls a route the API lacks.
  • Description to emoji stats commands.
  • Description to prefix remove command.
  • Economy: items can carry a picture (shop add/shop edit image:, image_url on the internal API), shown as the item card's thumbnail ahead of a custom emoji. Only permanent https:// links are accepted; Discord attachment links are refused because they expire. Migration V68 adds the column.
  • Economy: item cards show where a drop is found and a Market section — units in circulation, holders, and open market listings with their lowest and average price.

Changed

  • Modlog: warn cases use 🚩 instead of ⚠, which Discord drew as a white outline.
  • Modlog: cases/warnings mark pardoned warns with a Pardoned tag.

Fixed

  • appinfo no longer fails with KeyError: 'owner': the application endpoint it reads omits the owner (and the bot fields for apps without a bot user), which are now filled with defaults.
  • Components V2 views (paginators, modlog cases, leaderboards, ...) no longer ping the users they list: user mentions are now off by default. Direct invites (fight, tictactoe, coinflip duels, trade) still ping the invited member.
  • Uptime stats that are delivered through internal api were derived through a wrong attribute.

Changed

  • The Percy dashboard now lives in this repo at dashboard/ (Rust workspace with percy-client), with its own CI workflow.

Removed

  • Highlights (highlight …) and autoresponders (autoresponder …/ar), with their internal API endpoints (/highlights, /autoresponders) and the autoresponders backup section. Older backups that still carry it import fine; the section is ignored. The highlights and autoresponders tables are left in place (no migration drops them).

v2.24.0

2026-10-29

Added

  • AniList: anilist staff and anilist studio searches, and anilist anime airing [days] (episodes airing in the next 1-7 days).
  • AniList: anilist anime seasonal takes an optional season and year.
  • Releases: anime is a third subscribable medium. Follow an airing or upcoming show (or a studio) from /subscribe or the new Follow button on AniList cards, and new episodes arrive in the same hourly DM digest. Episodes are read live from AniList; migration V67 widens the subscription media check. Discord-only for now: the dashboard does not list anime subscriptions yet.
  • AniList: every card has a select that opens related titles (sequels, prequels, ...), AniList's recommendations, or the works a character, person or studio is known for; Back walks the trail.

Changed

  • The AniList client moved to app/clients/anilist.py, shared by the AniList and Releases cogs.
  • AniList results are one Components V2 message: a paged list with a select that opens a card in place (the embed paginator is gone).
  • AniList queries are cached for 5 minutes, so repeat lookups do not spend AniList's rate limit.
  • AniList autocomplete refreshes every 6 hours and covers trending plus all-time popular titles; anime search no longer suggests manga titles.
  • anilist anime seasonal lists the whole season instead of the first 3 titles.

Removed

  • AniList account linking (anilist link/unlink/profile/favourites/list/update and the card's list buttons). Stored tokens are dropped by migration V66, and ANILIST_CLIENT_ID/ANILIST_CLIENT_SECRET are no longer read.
  • linked_accounts from the personal data export.

Fixed

  • Docs: an unreachable inventory is retried twice (after 2 and 5 minutes) and then given up with one error naming the URL, instead of six retries that restarted on every refresh.
  • Docs: docs set on an existing package replaces its inventory and cancels the pending retry for the old URL, instead of merging into the old symbols and retrying the stale link.
  • Docs: docs delete removes any configured package (or alias), including one whose inventory never loaded, which used to fail with "not available".

v2.23.2

2026-10-29

Removed

  • Unused i18n scaffolding (app/i18n, bot.i18n): it was loaded on startup but never used, and shipped no translations.

v2.23.1

2026-10-29

Security

  • Internal API: channel and role ids in the guild config PATCH/batch, economy item roles, leveling reward roles, multipliers, blacklist, level-up channel and AI channel overrides must belong to the path guild (any snowflake was stored); assigned roles must sit below Percy's top role. Unchanged saved values still pass.
  • Internal API: request bodies and path ids are bounded (lengths, batch size 50, multiplier 100, snowflake/int ranges) instead of reaching asyncpg as 500s; Discord and hoster error texts are logged, not returned.
  • Gallery uploads are capped at 10 files and 10 MB per file.
  • Outbound webhooks use the shared check_destination SSRF guard, which also refuses IPv4-mapped IPv6 private addresses.
  • News: feed link and image URLs are stored only when http(s), so a javascript: link cannot reach a rendered href.
  • AI: a NaN/inf confidence is rejected and confidence is clamped to [0, 1] (a "nan" answer passed every threshold).
  • Captcha solutions use secrets instead of random.
  • klappstuhl.me hoster client no longer sends its "unconfigured" placeholder as a bearer token.
  • Timers: _kwargs_filter refuses non-identifier kwarg names before they reach SQL.
  • Commands restricted with @guilds(...) no longer run in DMs.
  • Internal API: POST /moderation/ignore (add) only accepts a channel, role or member of the path guild.
  • Error replies never ping: they echo user input such as a bad <@id> argument.

Fixed

  • Economy: amounts are bounded (1–10^15, set-money 0–10^15, lottery ticket price ≤ 1e9) instead of overflowing bigint; prestige re-checks level and balance after the confirmation.
  • Games: slots credits winnings before the reveal animation, so a deleted message no longer loses the payout; two simultaneous Russian roulette tables no longer overwrite each other; a deleted roulette or poker message no longer leaves the table stuck; poker background tasks are kept referenced; the Wordle played-set no longer grows forever.
  • Releases: a digest cancelled mid-send hands its delivery claims back.
  • Reminders: a reminder created from a reply is length-checked like typed text (long replies were silently dropped at fire time).
  • Music: forceskip and back confirmation texts.
  • Core: voice-gated commands in DMs, ctx.confirm(author_id=...) (broke solved), a timed-out disambiguation, error replies and help text over Discord's length limits, empty text paginators, the page-jump modal, time parsing overflow, shorten_number ≥ 1e15 and get_asset_url(None).
  • Rendering: resize_to_limit saves PNG/JPEG/WEBP after resizing and no longer loops on unsupported formats; series charts with fewer labels than values.
  • News: an out-of-range feed date no longer raises out of the parser.
  • Audit log: a missing reason shows "No reason provided." and long reasons are truncated; the ignored typo in config.
  • Internal API: a failed audit-log flags write no longer corrupts the cached flags.
  • Economy: prestige checks and resets in one transaction, so two simultaneous prestiges or a spend during the confirmation can no longer both pass.
  • Games: Tower, Slots, Higher/Lower, Mines, Crash, Plinko, Keno, Scratch, Dice, Coinflip and Russian Roulette refund the stake when the game message cannot be sent.
  • Music: the queue holds at most 1000 tracks; playlists past that are trimmed.
  • Giveaways: a failed setup deletes the "Preparing Giveaway..." card and the half-created row.
  • Internal API: changing a log/alert channel creates the new webhook before deleting the old one, so a failed create keeps logging working; leveling preset roles created before a Discord failure are still registered.
  • format_fields no longer raises on an empty mapping.

Removed

  • Unused helpers quick_token_sort_ratio, partial_token_sort_ratio and ScheduledTaskProtocol, the duplicate SSRF check in the webhooks cog, and the unused find_nth_occurrence, ensure_future_time, EconomyRepository.increment_prestige and FlagMeta.inject.

v2.23.0

2026-10-28

Security

  • Internal API: POST /rolemenus/{menu_id}/roles requires user_id; a missing actor no longer skips the role-hierarchy check.

Changed

  • Tests: every test times out after 60 s (pytest-timeout), so a blocking test fails with a traceback instead of freezing the run.

Changed

  • Leveling: rank and leaderboard order by level, then XP (was message count / XP within the level), and include voice-only and level set members.

Fixed

  • Internal API: PATCH /members/{uid}/roles refuses to change the roles of a member who outranks the moderator.
  • Economy: two simultaneous daily/weekly/monthly claims could both pay out; lottery tickets bought as the lottery ended lost the cash (now refunded); the jackpot was not paid when the announcement channel was gone; a negative debit is refused.
  • Leveling: reward roles stack when role stacking is on; reaching exactly the XP threshold levels up; negative XP can no longer drop below level 0; special level-up messages match their level; level config unignore no longer errors; level-up messages cannot ping @everyone or roles; a closed DM no longer raises from the message listener; no role edit when nothing changed.
  • Internal API: the leveling config PATCH validates factor, base, gain bounds and XP values (bad values broke XP on every message); level roles are bounded to 0–500.
  • Music API: back with no history, a loop mode outside 0–2 and a non-finite volume/seek return 400 instead of ok or 500.
  • News: GET /news?since=…Z no longer 500s; followed stories lead page 1 only instead of every page; a feed with a mis-declared charset updates its status instead of staying stale.
  • Releases: unsubscribe shows the subject's name instead of the lowercased key; a blank subject_value is a 400 instead of a follow that never matches.
  • Games: a lone or lopsided horse-race winner gets at least their stake back; tic-tac-toe shows the winner past the daily reward cap instead of "It's a tie!"; a timed-out roulette bet modal no longer errors.
  • Tags: an all-digit name no longer 500s dashboard create/import (new all-digit names are refused, as they read back as tag IDs).
  • Highlights: a whitespace-only trigger is refused instead of matching every message.
  • Starboard: a whitespace-only emoji is a 400 instead of saving an empty emoji.
  • Warn policy: escalation counts are capped at 1000 instead of 500ing past the integer column.
  • Economy: search/postmemes fines and admin remove_money are single guarded updates (the fine clamps at your cash); a deleted lottery's old timer no longer ends the next one early; GET /economy/balances reports the real total and PATCH /economy/balances/{uid} returns 400 for an out-of-range user id.
  • Leveling: level set stores within-level XP (was total XP, which inflated the level); PATCH /leveling/users/{uid} with a level resets within-level XP; message/voice XP gains are atomic so concurrent gains no longer lose XP; leaderboard total is the real count.
  • Reminders: a monthly repeat keeps its original day (Jan 31 -> Feb 28 -> Mar 31, not 28); older timers behave as before.
  • Polls: the dashboard edit refuses adding/removing options once votes exist (it relabelled options and dropped the last one's votes).
  • Giveaways: winners (end and reroll) must still meet the role / account-age / join-age requirements.
  • Notifications: request cancel on an expired request says it expired instead of "already pending".
  • Role menus: a member can remove an elevated role they already hold; only adding is blocked.
  • Blackjack: stats profit includes the insurance payout.
  • Releases: the dispatcher reads every recent comic/news row, so a burst over 500 no longer drops older undispatched ones.

v2.22.3

2026-10-28

Fixed

  • Startup logged a critical "Setup task failed" when copying global slash commands into the test guild exceeded Discord's 100-per-guild limit; the copy is now skipped with a warning.

v2.22.2

2026-10-28

Changed

  • settings data export is sent as gzip-compressed JSON (percy-data-<id>.json.gz), so large exports fit Discord's attachment limit.

v2.22.1

2026-10-28

Fixed

  • Internal API: GET /giveaways returns required_role_id, min_account_days and min_join_days, so the dashboard can show entry requirements.

v2.22.0

2026-10-28

Added

  • Poker: run it twice for heads-up all-ins before the river. Both players vote Yes/No (20 s, timeout or any No runs it once); each pot is split per board and both boards show in the result and hand history.
  • Internal API: warn policy (GET/PATCH /warn-policy, PUT/DELETE /warn-policy/thresholds/{warn_count}) and POST /cases/{case_index}/pardon, sharing their validation with the warnpolicy command.

Fixed

  • Games: a hot reload during a horse race or roulette spin could pay the stakes twice (restart refund plus the old race); the payout now claims the persisted stakes first.
  • Discord status feed: every concurrent incident now keeps its own card and keeps updating, instead of an older one going silent once a newer one was posted.
  • Poker: a straddle could pass the turn to a folded or sitting-out seat and stall the table; players who left the hand could rebuy and lose the chips; the host leaving no longer leaves the table without a host; a double "Next Round" click no longer auto-starts the hand.
  • Blackjack: a 21 on a split hand loses to a dealer blackjack instead of pushing.
  • Polls: ending a poll without Manage Threads still updates the poll card. Poll numbers no longer repeat after a poll is deleted; a failed image upload cancels creation instead of posting the poll without its image.
  • Moderation: the AI-alert Ban button cancels a pending tempban; its Warn button refuses bots; lockdown for no longer sets a timer when no channel was locked; two simultaneous warns can no longer trigger the same warn threshold twice.
  • Releases: digest items over the cap were marked delivered but never sent; news beyond the first five stories could starve.
  • Reminders: a reminder whose channel is gone is sent by DM instead of being dropped.
  • Stats: command usage logged during a save was lost; background loops stopped for good on non-connection errors.
  • settings data export: an export too large to DM now reports the failure.
  • "tonight"/"morning" crashed on the last day of a month.
  • V65__dstatus_incidents.sql: discord_incident_posts (one posted message per guild and incident), backfilled from discord_incidents.

Versioning follows Semantic Versioning. The source of this page is CHANGELOG.md.