v1.1.0
·
61 commits
to production
since this release
Added
- Infernoplex the standalone Rust Discord server-tracking bot has been
ported into Popplio's own binary as a newinfernoplex/package, the same
treatment Arcadia got earlier.main.gonow starts it right alongside
Arcadia (infernoplex.Start(state.Context), stopped with the same 30s
grace period on shutdown) instead of it running as a separate service.
The port covers everything the Rust bot did: a guided multi-step server
setup wizard (infernoplex/bot/setup.go), invite creation/resolution
(infernoplex/invite), a server-info push command gated on "Edit
Servers" (cmdUpdate), a vote leaderboard (cmdLeaderboard), a
bot-stats command (cmdStatsversion/Go version/git commit/env, mirrors
Arcadia's/info), and background tasks for server/emoji/sticker sync and
team-member cleanup (infernoplex/tasks). It also runs its own small
internal HTTP API, "Sorbet" (infernoplex/sorbet), structured the same
way as Arcadia's panel dispatch. The standalone Rust Infernoplex service
is superseded by this and should be decommissioned.
Config shape change (updateconfig.yamlbefore deploying): a new
infernoplex:block withclient_id/client_secretplus per-environment
prefix/server_port/token(sameDiffers[T]staging/prod/beta/dev
pattern used elsewhere) — seeconfig.yaml.sample. - Infernoplex's leaderboard command now replies with a "No Votes Yet" embed
instead of an empty/broken one when a server has zero votes. - Self-hosted proof-of-work vote captcha (
popplio/captcha), replacing the
deadHCaptchaInfoscaffolding intypes/vote.go(which was never wired
to anything) with something actually enforced.GET /votes/captcha/challengeissues a signed, stateless hashcash-style
challenge (find a nonce sosha256(salt+":"+nonce)has N leading zero
bits);PUT .../votesnow requires a solved challenge in the request body
for bot/server votes unless the entity has opted out via the existing
captcha_opt_outsetting. Challenges are HMAC-signed with the new
captcha.hmac_secretconfig value so they can't be forged, and each
solved challenge is single-use (consumed in Redis on first successful
verification) so a solve can't be replayed across multiple votes. No
third-party captcha provider involved — the whole protocol lives in
popplio/captcha.
Config shape change (updateconfig.yamlbefore deploying): a new
captcha:block with a per-environmenthmac_secret(sameDiffers[T]
pattern used elsewhere) — seeconfig.yaml.sample. Generate one with e.g.
openssl rand -hex 32; rotating it invalidates all outstanding
challenges.
Changed
- Rebranded "Infinity List" → "Omniplex" across every remaining user-facing
string that still had the old name: the MFA issuer shown in a staff
member's authenticator app on re-enrollment (arcadia/panel/mfa.go), the
staff bot's/analyticsembed title (and its frozen conformance string),
and the fallback SEO description onGET .../teams/{id}/seowhen a team
has no custom short description ("View the team X on Omniplex"). A few
doc comments got the same treatment with no functional effect. - Every reply the staff bot makes is now an embed, including one-liners
(the "Isabelle" rewrite, #43). A bare content message is indistinguishable
from a staff member talking, which matters in the staff server where the
bot's answers and the conversation share a channel.Ctx.Saybuilds the
embed itself, so this is a change of container rather than of wording —
every string frozen inarcadia/conformanceis untouched and still
asserted. Two coloured variants went in alongside it:Ctx.Fail(red) for
the command guards, the panic handler and the "there was an error" paths,
andCtx.Ok(green) for the 16 replies that report something having
worked, so a refusal is visibly different from an answer without either
having to say so. The modal driver and the permission editor's ephemeral
refusals, which answer through the interaction rather than throughCtx,
build the same shape by hand (modalReplyin
arcadia/bot/interactions.go).TestRepliesAreEmbedswalks the package's
AST for anyMessageCreatethat setsContentand fails if one appears. - A second pass over the same files, this time pulling out the repetition
rather than only moving it. Inarcadia/rpc:modLogReasonbuilds the
mod-log embed nine handlers were each building by hand (title,
description, one Reason field, footer, colour),reasonFieldcovers the
four multi-field embeds that keep their own shape, and
guardBot/guardUserreplace the ten copies of "reject an over-long
reason, then check the target exists".certifyAddwent from 47 lines to
20 this way, andreview.gosplit intoclaim.goandverdict.goonce
it had. Inarcadia/panel:authorizereplaces the ten copies of the
twelve-linecheckAuth+resolvedPermspreamble, andops_core.go
(608) split intoops_auth,ops_hello,ops_queue,ops_rpc,
ops_searchandops_proxy.arcadia/tasks/staffresync.go(579) split
into the resync itself, its reporting and its Discord role mirroring.
What was deliberately not factored out: the frozen embed and error
strings stay written out at their call sites, because
arcadia/conformancefinds them by scanning the source for the literal —
a helper that formatted them would pass its own tests while quietly
removing that check. For the same reason the SQL stays literal at each
call site, sincearcadia/dbconformPREPAREs every string literal it can
find against a real database. And the five steps ofStaffResyncare
left inline: they share a transaction and a working set that each step
narrows, so splitting them would make an ordering that is load-bearing
look optional. - The five files that had grown past the point of being navigable are split
by what they do, with no behaviour change:arcadia/bot/staffroles.go
(1137) intostaffmgmt.go(the role model, the authority rules, the
lookups),staffroles.go,staffperms.goandstaffrender.go;
arcadia/bot/commands.go(697) intocommands.go(the registry and the
two shared RPC helpers) plushelp.go,invites.go,stats.goand
staffops.go;arcadia/panel/ops_shop.go(861) into one file per shop
concern (tiers, items, benefits, coupons, whitelist);
arcadia/panel/ops_staff.go(759) into positions, members and
disciplinaries, with its two shared existence checks moved to
ops_query.gowhere the shop operations that also use them can find
them; andarcadia/bot/permeditor.go(858) into
session/apply/render/util. Each new file opens with what it covers and
what is non-obvious about that area. All of it is code movement verified
line-for-line against the original; nothing in the repo's directory
structure changed, androutes/'s one-package-per-endpoint layout is
left alone sinceuapirequires it. arcadia/rpc/methods.go(878 lines, every RPC action in one file) is
split into one file per group of actions, grouped exactly the way
types.rpcPermissionsgroups them — so the file an action lives in is
the same question as which permission gates it:review.go(claim,
unclaim, approve, deny, unverify),certify.go,transfer.go,
forceremove.go,premium.go,votes.go,apps.go, plusdispatch.go
for the method-to-handler switch andaudit.gofor the
staff_general_logswrite.core.gokeeps theExecutepipeline and
the shared guards, and its package doc now carries the map of where
things live and the note that every mod-log embed is reproduced verbatim
from the Rust original (that note used to sit above the dispatcher and
said "every embed below", which the split would have made a lie). Pure
code movement: every moved line is byte-identical to what it replaced,
andarcadia/conformancescans the whole package rather than one file,
so it pins the embed strings exactly as before. (review.gowas later
split further intoclaim.go/verdict.go— see the dedup-pass bullet
above;arcadia/CONFORMANCE.md's file references still sayreview.go
in a few spots and need updating to match, see Known Issues below.)- The Dev Team staff application no longer requires or mentions Rust
(description and two questions updated to reflect Go/TypeScript only);
the QAQC application track was removed entirely. Consistent with Arcadia
and now Infernoplex both being fully off Rust.
Security
- Only the prod instance now sets the main Discord bot's gateway presence
(state.go'sOnGuildsReadyhandler). Staging/beta/dev instances still
connect and function normally, they just no longer call
SetPresenceForShard, so a non-prod checkout — misconfigured shared
token or otherwise — can never overwrite what the public bot's profile
shows as its "Watching" activity.
Removed
- Five retired permissions —
view_shop,manage_shop,
manage_bot_whitelist,view_cdn,manage_cdnpurged from every
stored permission array (staff_positions.perms,
staff_members.perm_overrides,staff_disciplinary_types.perm_limits)
via a new one-off migration,exp/rewrite/remove_broken_perms.sql
(needs to be applied manually against the database like otherexp/
scripts).
Known issues found during this pass, not yet fixed
- Infernoplex's new "No Votes Yet" message has a typo: "Unfortuently, your
server has no votes at this time." config/config.go'sNaevisstruct (added alongsideInfernoplexas an
apparent placeholder for a second bot) is dead code it's never
referenced from the top-levelConfigstruct despite its fields being
taggedvalidate:"required", andconfig.yaml.sample'snaevis:section
was already removed. Safe to delete outright, or finish wiring it up if
Naevis is still planned.arcadia/CONFORMANCE.mdreferences aarcadia/rpc/review.goin a few
places (issues #6, #9, #10) that doesn't exist the file isverdict.go.
Looks like a stale rename from drafting the Isabelle split.