Skip to content

Releases: PlexiOSS/Popplio

v1.3.1

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 16 Aug 06:06
0fa4ea5

Added

  • Arcadia's SearchEntitys panel action now supports Pack, Team, and
    User in addition to the existing Bot/Server previously any other
    target type 501'd. Backs the admin Search page now covering every
    entity type and its respective staff actions instead of just bots and
    servers. New PartialPack/PartialTeam/PartialUser variants added to
    the PartialEntity wire union (additive existing Bot/Server
    consumers are unaffected).
  • GET /users/{id} now returns user_servers, the servers owned by any
    team the user is on (mirroring the existing user_bots/user_packs
    resolution). Servers have no direct owner column team ownership is
    the only path, same as GetOwnedBy's server branch. Public user
    profiles previously had no way to show a user's servers at all.
  • The staff bot gained guild moderation commands /kick, /ban,
    /timeout, and /warn (new moderate_guild/warn_users permissions),
    plus self-serve /kb, /ticket, and /staffinfo for pointing users at
    the Knowledge Base, ticket support, and the staff hierarchy without
    retyping the same links. Every moderation command refuses to act on a
    target who is themselves staff at a rank equal to or more senior than
    the caller's own (perms.LoadStaff(...).Rank()) staff_positions is
    the same hierarchy Discord role assignments already sync into via
    StaffResync, so this is one hierarchy check, not a separate Discord
    one and a separate Omniplex one.

Fixed

  • Two Discord embeds in routes/staff/endpoints/manage_app/route.go
    ("Application Approved"/"Application Denied") still linked to the
    deprecated SvelteKit panel (Sites.Panel + /panel/apps) after the
    equivalent submission-time embed was already fixed to point at
    Omniplex's /admin/applications — these two were missed in that pass.
  • StaffResync (arcadia/tasks/staffresync.go) inserted a new row into
    staff_members before checking whether that user had a users row yet.
    staff_members.user_id has a foreign key into users, so any staff
    member who held a staff role in Discord but had never actually logged
    into Omniplex made every resync run fail with a staff_members_user_id_fkey
    violation, repeating on every scheduled run until fixed. The
    ensure-users row exists check now runs before the staff_members
    insert/update instead of after it.

v1.3.0

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 16 Aug 05:23
1f3570d

Added

  • Certification approval (reviewLogicCert/reviewLogicCertServer in
    apps/logic.go) now automatically grants BotDeveloper/
    CertifiedDeveloper roles to a certified bot's owner(s), or every
    member of a certified server's owning team, provided they're already in
    the main guild — previously only the bot's own CertBot role was
    granted, and owners had to know to run ibb!getbotroles themselves.
  • GET /list/stats gained total_banned_users and total_vote_banned_bots,
    aggregate COUNT(*) queries over the existing banned/vote_banned
    columns, for the Moderation Transparency page's new "Platform safety"
    section. Public, no PII — same pattern as the existing report-stats
    endpoint.

Fixed

  • A Discord API failure while granting a bot's own CertBot role during
    certification (e.g. the bot not currently being in the server) used to
    hard-fail the entire review, leaving the application stuck "pending"
    even though bots.type had already been committed as certified
    separately. It's now logged as a warning instead of aborting the review.
  • GetOwnedBy (arcadia/impls/entities.go) only checked team ownership
    for bots, silently missing bots owned directly — meaning a direct owner
    got "you don't own any bots" from /getbotroles even when they
    genuinely did. Added the missing OR owner = $1 branch.
  • /staff/tickets?open=true was 500ing: 8 legacy ticket rows had
    messages stored as a JSON object instead of an array, and
    pgx.RowToStructByName fails the whole result-set scan on a single
    row's type mismatch. Normalized the affected rows; create_ticket
    already always writes a real array, so this was legacy data, not a
    recurring bug.

v1.2.2

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 15 Aug 23:03
1c93a48

Fixed

  • arcadia/panel/ops_proxy.go's popplioStaff proxy (backs the new
    Applications admin page) rejected every request with a misleading
    "Path must start with /" error in production, even for a
    correctly-formed path like /staff/apps. Root cause was in
    safeJoinPopplio (arcadia/panel/paths.go): beyond the real security
    boundary (same scheme+host as Popplio's own API base), it also enforced
    that the resolved path stay under the same path prefix as the
    configured base URL which rejects any legitimate root-level target
    whenever that base URL has a non-root path component. That check added
    no security beyond the origin check and only broke valid callers, so
    it's removed. Also stopped collapsing every safeJoinPopplio error into
    the same fixed string the real error now surfaces, so a future failure
    here is diagnosable instead of misleading.
  • notifications.PushNotification's NoSave field was inverted from its
    own name/doc comment (if notif.NoSave { INSERT } persisted only when
    told not to save). In effect, every "normal" alert (push-subscribe
    confirmation, reminder-set confirmation, payment-failure alerts) never
    reached a user's in-app alert inbox, only ever firing as a transient
    push notification the one caller that explicitly opted out of saving
    (vote_reminders.go, "spammy, fills up the db quickly") was the only
    alert type that persisted. Condition is now if !notif.NoSave, matching
    what the field has always been named and documented to mean.
  • The "New Application" Discord embed (routes/apps/endpoints/create_app)
    linked to the old SvelteKit panel (Sites.Panel + /panel/apps),
    superseded by Omniplex's own /admin/applications link updated.

Added

  • GET /staff/tickets every ticket platform-wide, gated on the existing
    view_tickets staff permission (optional ?open=true|false filter,
    paginated). Staff could already view/reply/close/reopen any ticket via
    the existing owner-or-staff checks on get_ticket/
    create_ticket_message/patch_ticket, but had no way to find a ticket
    ID to act on in the first place this closes that gap. Auth follows the
    same normal-user-session + in-handler permission check as the other
    ticket routes, not the legacy staffpanel__authchain system the
    Applications page uses.
  • A user-facing confirmation alert (now that PushNotification actually
    persists them) at three points that previously gave zero in-app
    feedback on success a purchase completing (GivePerks, alongside the
    existing staff-only mod-log post), a shop item purchase, and a vote
    credit redemption. All three are best-effort: the underlying change is
    already committed by the time the alert is sent, so a failed alert logs
    a warning rather than turning into an error response for something that
    actually succeeded.
  • A new report being filed now posts to the staff-only StaffLogs
    Discord channel (target, type, reason no reporter identity, consistent
    with reporter identity being staff-panel-only everywhere else). The
    equivalent "new application submitted" post already existed
    (create_app posts to the Apps channel with an @Apps role ping) confirmed by reading the handler directly, not assumed.

v1.2.1

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 15 Aug 21:52
635a5ab

Changed

  • The Friday-Sunday double-vote weekend bonus (votes.GetDoubleVote) now
    pins its day-of-week check to UTC explicitly instead of relying on
    time.Now()'s implicit host-local timezone, so the boundary is the same
    instant regardless of what timezone the process happens to run in. Also
    added VoteInfo.WeekendBonus, a per-entity flag reporting whether the
    bonus is actively boosting that entity's per_user/vote_time right
    now — false for premium bots/servers even on a bonus weekend, since
    their flat premium cooldown already applies instead. Nothing in the API
    previously told callers when the bonus was live.
  • Certification requirements loosened and diversified. The old rule
    required a bot to clear servers ≥100 and unique clicks ≥30 both,
    no exceptions. It's now an OR across three lowered bars (servers ≥50,
    unique clicks ≥15, or votes ≥50, the last one is new), plus a new but
    lenient 3-day minimum listed age. A bot excelling in one metric no
    longer gets rejected for not excelling in all of them.
  • Servers can now be certified too a new "Server Certification"
    position on /apps (extraLogicCertServer/reviewLogicCertServer in
    apps/logic.go), using the same OR-of-three-metrics rule scaled to
    server stats (members ≥100 in place of bot servers ≥50). New
    request_server_certification permission. The "Certified" badge
    Omniplex has always been able to render for servers had no backend path
    that ever set it until now.
  • Premium and Shop now work for servers, not just bots:
    • CreatePerkData/PerkData gained a for_type field ("bot" or
      "server", defaults to "bot" if omitted) so Stripe/PayPal checkout
      and the booster-offer redemption can target either.
      servers.premium has been a real, displayed column with no purchase
      path behind it since it was added; now there is one.
    • Shop purchases (POST /{target_type}/{target_id}/shop/purchase) and
      all five benefit effects (routes/shop/assets/benefits.go) now
      branch on target type between bots/servers both tables carry
      identical benefit columns.
    • servers gained the same boosted_until/featured_until/
      supporter_badge/vote_blitz_until columns bots already had
      (exp/serverbenefits.sql), plus the matching read-side effects:
      boosted-first sort in GET /servers/@all, a featured category in
      GET /servers/@index, and a vote-blitz cooldown halving in
      EntityVoteInfo's "server" case.

Added

  • GET /list/stats now includes total_pending_bots and
    total_denied_bots, so consumers can show a real approved/certified/
    pending/denied breakdown instead of inferring it from total_bots minus
    the listed count.
  • GET /staff/shop-purchases the same shop-purchase data
    GET /{target_type}/{target_id}/shop/purchases already exposes
    publicly one entity at a time, but platform-wide and staff-gated
    (view_shop) for abuse/fraud monitoring. No frontend consumes this yet
    since the Arcadia panel UI isn't part of this repo it's ready for
    whenever that side wires it up.
  • A standalone support ticket system. The existing tickets table/Ticket
    type were entirely Discord-channel-shaped (channel_id, enc_key) with
    no creation path anywhere in the codebase, not in the API, not in the
    Discord bot nothing has ever created a ticket in this codebase. Rather
    than build the Discord-integration side, tickets are now a plain web
    feature reusing the same table (channel_id left "", enc_key left
    null): GET /tickets/topics (a small hardcoded topic catalogue, same
    convention as apps.Apps), POST/GET /users/{id}/tickets,
    POST /tickets/{id}/messages, and PATCH /tickets/{id} to close/reopen
    (closing is open to the author or staff; reopening is staff-only, via a
    new manage_tickets permission). New message IDs are synthesized
    Discord-format snowflakes (disgoorg/snowflake's New) purely so
    GET /tickets/{id}'s existing snowflake.Parse-based timestamp
    decoding keeps working unchanged for old and new tickets alike.
  • Shop purchases actually do something now. shop_items/shop_item_benefits
    have had full staff CRUD via the Arcadia panel for a while, but nothing
    ever spent an entity's earned vote credits on one or defined what a
    benefit's effect even was. New:
    • POST /{target_type}/{target_id}/shop/purchase (bots only for now,
      gated on a new buy_shop_items entity permission) spends credits
      oldest-batch-first across entity_vote_redeem_logs, logs the purchase
      to a new shop_purchases table, and applies every benefit ID on the
      item that Popplio recognizes.
    • Five recognized benefit IDs, each with a real effect:
      premium_days (extends the bot's premium period, identical to the
      Stripe/PayPal path), priority_boost (new boosted_until column,
      sorts first in /bots/@all's default order while active),
      featured_slot (new featured_until column, surfaces the bot in a
      new featured category on /bots/@index), supporter_badge (new
      permanent supporter_badge flag), and vote_blitz (new
      vote_blitz_until column, halves EntityVoteInfo's vote-time
      cooldown while active). Unrecognized benefit IDs no-op rather than
      error, so staff can still catalogue purely descriptive/future
      benefits without breaking a purchase but an item with zero
      recognized benefits is rejected at purchase time rather than silently
      spending credits for nothing.
    • GET /{target_type}/{target_id}/shop/purchases purchase history,
      public, same transparency level as the existing vote-credit logs.
    • exp/shopbenefits.sql (schema: 4 new bots columns + the
      shop_purchases table) and exp/shopbenefits_seed.sql (optional
      starter catalog rows for the 5 benefits) the seed is just a
      starting point; the same rows can be created through the Arcadia
      panel instead.

Changed

  • Omniplex is now owned by NodeByte LTD. Remaining "Infinity Bot List" /
    "Infinity Development" copy left over from the old brand — application
    question text, the staff-denial DM, webhook docs, the RSS feed title
    and copyright line, the auth-log embed footer, and the !delete
    bot-command copy now reads "Omniplex" / "NodeByte LTD".

Fixed

  • The Gold premium plan granted ~365 hours (~15 days) of premium instead
    of a year TimePeriod was set in raw days while GivePerks applies
    it as hours. Bronze/Silver were already correct; Gold now multiplies by
    24 like they do.
  • POST /users/{id}/redeem-payment-offer?code=BOOSTPREMIUM granted the
    perk successfully but then always fell through to a final 400 Invalid offer code response regardless — no caller could ever see it succeed.
    It also never stamped last_booster_claim, so the "once every 30 days"
    cooldown could never actually engage. Both are fixed: a successful
    redemption now returns 204 and updates the claim timestamp.
  • tickets.user_id had no foreign key constraint to users(user_id) at
    all, just a plain column — so the account data-export/deletion pipeline
    (POST /users/{id}/data, routes/users/endpoints/create_data_task)
    silently skipped every ticket a user had ever filed. The walker
    (ddr_task.go) auto-includes any table with a real FK into an
    already-registered root (users/teams), so the fix is schema-only:
    exp/ticketuserfkey.sql adds the constraint NOT VALID (4 legacy
    tickets reference since-deleted accounts; NOT VALID enforces it for
    all new/updated rows without deleting or nulling that history). No Go
    changes needed — confirmed via a direct pg_constraint check against
    the dev DB that tickets are now walked correctly.

v1.2.0

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 14 Aug 22:13
bb71607

Added

  • Packs are no longer bots-only: a new pack_type column (bot | server
    | emoji, immutable after creation) generalizes the existing BotPack
    type, and a new pack_emojis table backs a genuinely new capability —
    user-curated emoji packs, each emoji its own durably-uploaded asset (not
    a live reference into a server's synced emoji list, so a pack keeps
    working even if the source server stops syncing or leaves). Server packs
    reuse the Servers []string field that already existed on BotPack but
    was never wired to any route or UI. add_pack/patch_pack validate
    content per type (bot packs need bots, server packs need servers,
    emoji packs need emojis, capped at 50), get_all_packs gained an
    optional ?pack_type= filter, and a new edit_packs entity permission
    (teams.GetEntityPerms's new "pack" case, single-owner only — no team
    fallback) lets the existing generic upload-permission-check flow cover
    pack emoji uploads the same way it already covers bot/server banners.
  • A generic content-report system (popplio/reports, new routes/reports
    package), built alongside the pack generalization above to give users a
    way to flag a pack (or, later, any votable entity) for e.g. a license
    violation on an emoji pack. PUT /users/{uid}/{target_type}/{target_id}/reports
    mirrors the votes router's exact URL shape and target-type handling.
    Reports are keyed (target_type, target_id), same convention as
    entity_votes; a partial unique index allows only one open report per
    reporter per target, and a per-user daily cap (10) limits spamming many
    different targets. Reporter identity is never exposed outside the staff
    panel — the public API never returns it. Reviewed exclusively through a
    new Arcadia RPC (UpdateReports/ReportAction, following
    PartnerAction's exact discriminated-union codec pattern) gated on a new
    review_reports staff permission; there is deliberately no public
    listing/review route, matching how Blog/Partners never got one either.
    Config/DB note: three new one-off migrations to apply —
    exp/packtype.sql, exp/packemojis.sql, exp/reports.sql.
  • GET /bots/@all and GET /servers/@all gained an optional
    ?sort=trending param, ranking by net votes (upvotes minus downvotes) in
    the last 7 days instead of newest-first, and returning only entities with
    at least one vote in that window. New composite index
    entity_votes_target_created_idx (exp/entityvotesidx.sql) backs the
    underlying grouped query — entity_votes had no index at all before
    this, so trending would otherwise have been a full table scan.
  • GET /reports/stats: a new, deliberate exception to the reports
    system's "no public read-back" design — anonymized counts of reports
    grouped by reason/status only (no report IDs, no target identity, no
    reporter identity), for a public moderation-transparency page.
  • GET /servers/@emojis: a new paginated endpoint returning only
    server_id/name/avatar/emojis/stickers for servers with
    show_emojis = true. IndexServer (what @all returns) excludes
    emoji/sticker data entirely, so a cross-server emoji/sticker browse page
    had no way to bulk-fetch this without N+1 calls to GET /servers/{id}
    before this.

v1.1.0

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 13 Aug 08:35
167666a

Added

  • Infernoplex the standalone Rust Discord server-tracking bot has been
    ported into Popplio's own binary as a new infernoplex/ package, the same
    treatment Arcadia got earlier. main.go now 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 (cmdStats version/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 (update config.yaml before deploying): a new
    infernoplex: block with client_id/client_secret plus per-environment
    prefix/server_port/token (same Differs[T] staging/prod/beta/dev
    pattern used elsewhere) — see config.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
    dead HCaptchaInfo scaffolding in types/vote.go (which was never wired
    to anything) with something actually enforced. GET /votes/captcha/challenge issues a signed, stateless hashcash-style
    challenge (find a nonce so sha256(salt+":"+nonce) has N leading zero
    bits); PUT .../votes now requires a solved challenge in the request body
    for bot/server votes unless the entity has opted out via the existing
    captcha_opt_out setting. Challenges are HMAC-signed with the new
    captcha.hmac_secret config 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 (update config.yaml before deploying): a new
    captcha: block with a per-environment hmac_secret (same Differs[T]
    pattern used elsewhere) — see config.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 /analytics embed title (and its frozen conformance string),
    and the fallback SEO description on GET .../teams/{id}/seo when 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.Say builds the
    embed itself, so this is a change of container rather than of wording —
    every string frozen in arcadia/conformance is 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,
    and Ctx.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 through Ctx,
    build the same shape by hand (modalReply in
    arcadia/bot/interactions.go). TestRepliesAreEmbeds walks the package's
    AST for any MessageCreate that sets Content and fails if one appears.
  • A second pass over the same files, this time pulling out the repetition
    rather than only moving it. In arcadia/rpc: modLogReason builds the
    mod-log embed nine handlers were each building by hand (title,
    description, one Reason field, footer, colour), reasonField covers the
    four multi-field embeds that keep their own shape, and
    guardBot/guardUser replace the ten copies of "reject an over-long
    reason, then check the target exists". certifyAdd went from 47 lines to
    20 this way, and review.go split into claim.go and verdict.go once
    it had. In arcadia/panel: authorize replaces the ten copies of the
    twelve-line checkAuth + resolvedPerms preamble, and ops_core.go
    (608) split into ops_auth, ops_hello, ops_queue, ops_rpc,
    ops_search and ops_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/conformance finds 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, since arcadia/dbconform PREPAREs every string literal it can
    find against a real database. And the five steps of StaffResync are
    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) into staffmgmt.go (the role model, the authority rules, the
    lookups), staffroles.go, staffperms.go and staffrender.go;
    arcadia/bot/commands.go (697) into commands.go (the registry and the
    two shared RPC helpers) plus help.go, invites.go, stats.go and
    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.go where the shop operations that also use them can find
    them; and arcadia/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, and routes/'s one-package-per-endpoint layout is
    left alone since uapi requires 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.rpcPermissions groups 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, plus dispatch.go
    for the method-to-handler switch and audit.go for the
    staff_general_logs write. core.go keeps the Execute pipeline 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,
    and arcadia/conformance scans the whole package rather than one file,
    so it pins the embed strings exactly as before. (review.go was later
    split further into claim.go/verdict.go — see the dedup-pass bullet
    above; arcadia/CONFORMANCE.md's file references still say review.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's OnGuildsReady handler). 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_cdn purged 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 other exp/
    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's Naevis struct (added alongside Infernoplex as an
    apparent placeholder for a second bot) is dead code it's never
    referenced from the top-level Config struct despite its fields being
    tagged validate:"required", and config.yaml.sample's `n...
Read more

Staff Tooling Update & Bug Fixes

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 05 Aug 07:16
1709b0c

Changed

  • Every Differs[T] config key (DB tokens, site URLs, etc.) previously
    required both a staging and a prod value to be set regardless of
    which environment a given box actually runs — a current-env: prod box
    was rejected at startup for a missing staging value it would never
    read, and vice versa. ValidateDiffers now only requires whichever value
    Parse() will actually resolve for CurrentEnv (prod needs prod,
    staging needs staging; beta/dev still accept either their own
    value or a staging fallback, unchanged), so a config file only needs to
    fill in what the box it's deployed to actually uses.
  • Staff roles and permissions can now be managed interactively from the staff
    bot: /staffroles edit [role] and /staffperms edit <user> open a select
    menu editor (arcadia/bot/permeditor.go) with a role picker, a category
    picker and a multi-select of that category's permissions, preselected to
    what the role or member currently holds — ticking grants, unticking revokes,
    and everything outside the open category is carried through untouched.
    Dangerous permissions are marked ⚠️ and ones the caller cannot manage 🔒.
    The existing one-at-a-time grant/revoke subcommands are unchanged and
    still work; both paths share the same rank check and perms.CheckPatch
    rule, and the editor re-checks both at the moment of the write rather than
    only when it opened, since a session lives for ten minutes. Every render
    reloads the target from the database, so two people editing the same role
    see each other's changes instead of saving a stale picture over them.
    Alongside the menus are buttons: "Grant all"/"Revoke all" for the open
    category (which leave permissions the caller cannot manage exactly as they
    are, so one locked permission doesn't make the button useless), "Pick
    another role" to switch targets without closing, and "Close". edit is
    registered as the first subcommand of both commands, since Discord lists
    them in registration order and never lets the parent command
    (/staffroles on its own) be invoked at all.

Fixed

  • DELETE /teams/{tid}/members/{mid} had its last-owner safety check
    inverted (introduced by the kittycat→internal perms package refactor):
    it fired when the member being removed was not an owner instead of when
    they were, so removing any regular member from a team with only one owner
    (the common case) 400'd with "There needs to be one other global owner
    before you can remove yourself from owner" — while actually removing the
    team's last real owner sailed through with no check at all, the exact
    case this was meant to prevent. Condition un-inverted.
  • Every staff bot slash command appeared twice in every server. The bot
    registers its commands per guild (arcadia/bot.SyncCommands), but the
    application still carried global registrations of the same commands from an
    earlier deployment, and Discord lists a global command alongside a guild
    command of the same name rather than letting the guild copy take its place.
    SyncCommands now finishes by deleting the global registration of any
    command it registers per guild (pruneGlobalCommands), so the duplicates
    clear themselves on the next sync (startup, or /register). Global
    commands whose names the bot does not register are left alone and only
    logged as a warning, since they belong to something else sharing the
    application.
  • The server/team auth types were never registered as OpenAPI security
    schemes (only User/Bot were, via docs.AddSecuritySchema in
    main.go) even though Authorize() has always fully supported them —
    every one of the 41 operations requiring server or team auth
    (PUT /bots, PUT /servers, both PATCH .../settings endpoints,
    reviews, sessions, etc.) referenced a security scheme name absent from
    components.securitySchemes. Harmless to the API itself, but any tool
    that resolves the requirement against registered schemes crashes outright
    on the unresolved reference — including the docs site's OpenAPI reference
    pages (fumadocs-openapi's APIPage, which throws
    Cannot read properties of undefined (reading 'type')). Registered both
    (docs.AddSecuritySchema("server", ...) / ("team", ...), lowercase to
    match AuthTypeMap's self-mapping for these two types).
  • Presence still never actually got set even after 1.0.0's fix, now logging
    error while setting presence err="no gateway configured" from inside
    OnGuildsReady instead of right on startup — that fix only addressed the
    timing, not the actual cause: Popplio runs sharded (OpenShardManager),
    and Discord.SetPresence only ever checks disgo's single-gateway field
    (populated by OpenGateway, not OpenShardManager), so it returns
    ErrNoGateway unconditionally on a sharded bot regardless of readiness.
    OnGuildsReady also fires once per shard, not once globally. Now uses
    Discord.SetPresenceForShard(ctx, event.ShardID(), ...) instead.
  • POST /auth/test ("Test Auth") 500ed on every call that reached an actual
    authorization check — api.Authorize reads PERMISSION_CHECK_KEY out of
    the route's ExtData unconditionally, but the synthetic uapi.Route{}
    this endpoint builds to call it never set ExtData at all, so any request
    with a syntactically valid token failed with a 500
    (permissionCheck not found in route.ExtData) instead of returning
    whether the token is actually valid. Only requests with a token that
    failed even earlier (nonexistent in api_sessions) ever got a real
    response (401). Now sets a no-op PermissionCheck (NeededPermission
    always returns nil), since this endpoint has no permission model of its
    own to enforce — it's purely "is this token valid for this target."

Removed

  • The use_borealis staff permission. Borealis was removed from the platform
    during the port (arcadia/CONFORMANCE.md D11a — the arcadia.borealis_url
    config key, the client and the Approve call to it are all long gone), so
    the permission has gated nothing since and only added a line to
    /permissions and a row to every permission picker. exp/rewrite/flatperms.sql
    now lists the old borealis.* in retired_perm (dropped on purpose)
    instead of mapping it onto use_borealis, and
    exp/rewrite/remove_borealis_perm.sql strips it from
    staff_positions.perms, staff_members.perm_overrides and
    staff_disciplinary_types.perm_limits for databases the old migration
    already ran against. That cleanup is needed rather than cosmetic: the
    permission model deliberately keeps names it does not declare, since they
    may belong to another service, so use_borealis would otherwise sit in
    those columns for good and show up under "Other services".

Security

  • Bot accounts can no longer hold staff permissions at all — not through a
    staff role, not through a direct grant, and not through arcadia.owners
    (perms.ErrBotAccount). Previously nothing stopped one: StaffResync
    walks every member of the staff server and creates a staff_members row
    for anyone holding a position's Discord role, and it never looked at
    whether that member was a bot, so giving a bot a staff role in Discord
    handed it that role's permissions — including through the panel session
    and RPC paths, which only ever asked what the row said. A bot is a token
    that can be handed to another program, which is exactly what the staff
    model's accountability assumes cannot happen, and nothing needs it: the
    staff bot and the panel both act under a staff member's identity, never
    their own. Enforced on both sides:
    • Reads: perms.StaffGrants carries a BotAccount flag, joined in from
      dovewing's user cache by LoadStaff at no extra cost, and Resolve()
      returns nothing and Rank() returns NoRank when it is set. The panel's
      session check (impls.CheckAuthInsecure), its login
      (ops_authorize.go) and its member view (impls.GetStaffMember, whose
      additory disciplinaries could otherwise add permissions on top of an
      empty set) all apply the same rule. These paths stay database-only, so
      they keep working when Discord does not.
    • Writes: perms.RejectBotAccount resolves through dovewing all the way
      to Discord if the account has never been seen, and fails closed if it
      cannot tell. StaffResync now skips bot members entirely, which also
      means an existing bot's staff row is cleaned up by the same pass that
      handles members who left; the panel's editMember and the staff bot's
      /staffperms grant/revoke/edit refuse a bot target outright.

v1.0.0

Choose a tag to compare

@CodeMeAPixel CodeMeAPixel released this 05 Aug 00:48
ece706d

Added

  • current-env now also accepts beta, a fourth environment alongside
    staging/prod/dev. Every Differs[T] config key gains an optional
    beta value (config.Differs[T].Beta), consulted only when current-env
    is beta and falling back to staging when unset — same mechanism as
    dev's override, but without dev's relaxed Staging/Prod requirement:
    beta is validated exactly like staging/prod (ValidateDiffers),
    since it's a real running deployment rather than a personal machine. In
    practice this means most config (DB, tokens, etc.) can stay shared with
    staging, and only keys that genuinely differ per deployment — like
    sites.frontend — need an explicit beta: value.

  • bgtasks package: a new home for Popplio's own periodic background jobs,
    separate from arcadia/tasks (the staff bot's jobs, which only run when
    Arcadia is configured) so core platform features don't depend on staff
    tooling being set up. First job: bot_uptime_check, which periodically
    records whether every listed bot is currently online in the main server
    into bots.uptime/total_uptime/uptime_last_checked. These columns
    have existed since the Rust port but were never actually written to —
    Arcadia's old uptime checker (src/tasks/__toberewritten/uptime.rs)
    didn't even compile against the serenity version it was last touched
    against, and was explicitly never ported (see arcadia/CONFORMANCE.md).
    Reads presence straight from Popplio's own gateway cache (it already
    requests the Presence intent) rather than Infernoplex, which deliberately
    never requests it.

  • servers.avatar: servers previously had no icon anywhere (index listing,
    detail page, or the staff panel's server search all showed a blank/
    initials fallback) — the old cache-server subsystem used to synthesize
    this from its own CDN cache, and nothing replaced it after that was
    retired (exp/remove_cache_servers.sql). Populated once at Add Server
    time from the invite resolution already done there, and kept fresh
    afterward by Infernoplex's serversync task, which now also syncs every
    listed server's icon (not just opted-in ones' emojis/stickers) from its
    gateway cache. Requires the new servers.avatar column
    (exp/add_servers_avatar.sql, needs to be applied manually against the
    database like other exp/ scripts).

  • Webhooks gained a new hmac_auth mode (hmac_auth on
    POST/PATCH .../webhooks): the payload is sent as plain JSON with an
    X-Webhook-Signature: sha256=<hex hmac> header, the same shape GitHub and
    Stripe webhooks already use. It's now the recommended mode for new
    webhooks the previous default ("splashtail": AES-GCM encrypted body,
    nonce-chained double HMAC across two headers) required implementing
    decryption just to verify a delivery, not just a signature check.
    Existing webhooks are unaffected: hmac_auth defaults to off and the
    splashtail/simple_auth protocols are unchanged and fully supported
    this only adds a third option, it doesn't remove or alter the other two.
    Requires the new webhooks.hmac_auth column
    (exp/webhookhmacauth.sql, needs to be applied manually against the
    database like other exp/ scripts).

  • current-env now also accepts dev, a third environment alongside
    staging/prod. Every Differs[T] config key (config/config.go) gains
    an optional dev value, only consulted when current-env is dev, and
    only used if actually set — an unset dev value falls back to staging,
    so no existing config.yaml needs to change. Lets a local checkout run
    against things like a personal Discord bot application
    (discord_auth.token, arcadia.token) without touching the real staging
    config. discord_auth.token (Popplio's own bot token) is now itself a
    Differs[string] rather than a single flat value, so it can differ across
    environments the same way Arcadia's staff bot token already could.
    Anything gated to "real production" (Paypal live vs sandbox API base,
    Arcadia's background tasks, the staff bot's guild-member-join
    announcements, the staging-sensitive-permission gate) now treats dev the
    same as staging rather than falling through to production behavior.

  • PUT /servers add a server to the list directly from a Discord invite
    link. Resolves the guild via the invite (the tracking bot does not need to
    already be in the server), rejects duplicates and blacklisted vanities,
    and auto-creates an owning team the same way bot submission already does
    (or attaches to an existing team the submitter has bot.add-equivalent
    permission on).

  • Packs can now include servers alongside bots: a servers column,
    resolution into full IndexServer objects, and matching validation on
    both POST /packs (create) and PATCH /packs/{url} (edit). A pack must
    contain at least one bot or server between the two fields.

  • Bots can self-report presence (online/idle/dnd/offline) via
    POST /bots/stats, alongside the existing server/shard/user stats. The
    reported value is folded into the resolved user.status returned
    everywhere a bot's info appears, since most bots don't share a guild with
    the tracking bot for a real gateway presence to be read from.

  • Bots with no explicit self-reported status but a real track record of
    posting stats (a nonzero server count from a stats post within the last
    24 hours) are now shown as online rather than falling back to
    dovewing's almost-always-offline gateway-derived status.

  • GET /servers/meta?invite=... resolves a Discord invite to a preview of
    the server it points to (name, icon, member counts, and whether it's
    already listed) without adding anything — lets a client show what's about
    to be submitted before Add Server is actually called. Shares its invite
    resolution logic with PUT /servers via a new ResolveInvite helper.

  • Servers can opt in to showing their custom emojis and stickers on their
    listing page via a new show_emojis setting (PATCH /servers/{id}/settings).
    GET /servers/{id} now includes emojis/stickers/emojis_synced_at,
    always empty unless the owner has opted in. The actual snapshot is synced
    periodically by the tracking bot (Infernoplex), not fetched live per
    request, and requires the bot to currently be a member of the server —
    Popplio itself never talks to Discord for this.

  • GET /servers/meta now also reports bot_present/bot_invite_url by
    asking Infernoplex's Sorbet API whether the tracking bot is currently a
    member of the resolved guild, via a new CheckBotGuildPresence helper.
    Best-effort: any failure to reach Infernoplex is treated as "not present"
    rather than failing the request.

Changed

  • Bots now support downvotes, matching servers/teams/packs
    (votes.EntityVoteInfo no longer hardcodes SupportsDownvotes = false for
    the bot target type).
  • meta.popplio_proxy now defaults to https://gateway.nodebyte.host/proxy/discord
    (the shared parent-company gateway), replacing the old local
    http://127.0.0.1:3219 twilight-http-proxy convention. Both Popplio's own
    bot client (state.Setup) and Arcadia's separate staff bot
    (arcadia/dclient) now route their REST traffic through it via
    rest.WithURL/rest.WithHTTPClient (state.ProxyRestOpts). Since that
    gateway authenticates every request with its own shared bot credential by
    default, each client sends its own token via an X-Upstream-Authorization
    header instead, which the gateway forwards as the real Authorization
    header sent to Discord — so Popplio and Arcadia's staff bot each keep
    their own distinct bot identity rather than both authenticating as
    whichever bot the gateway holds.
  • EntityGetVoteCount (used by nearly every bot/server/team/user/pack
    detail and list endpoint) now counts up- and down-votes in a single query
    with FILTER, instead of two separate COUNT(*) round trips.
  • Bot/server index resolution (ResolveIndexBot/ResolveIndexServer,
    called by GET /bots/@all, GET /servers/@all, search, random, the bots
    index, packs, team entities, and user profiles) now resolves every row in
    a page concurrently via errgroup instead of one row at a time — each
    row's dovewing/vanity/vote lookups are independent, so a page of results
    no longer pays for them sequentially.
  • GET /list/current-status now issues both the Instatus and UptimeRobot
    requests with the request's own context and a bounded client timeout,
    instead of an unbounded http.Get/http.NewRequest that could hang the
    handler indefinitely if the upstream stalled.
  • Deduplicated the page query-parameter parsing copy-pasted across nine
    endpoints (each with a slightly different error response for the same
    invalid-page case) into a shared pagination.Parse helper.
  • DELETE /users/{uid}/packs/{id} and PATCH /users/{uid}/packs/{id} each
    folded two sequential "does the pack exist" / "who owns it" queries into
    one.
  • The generic error bodies returned when a failure carries no specific
    message of its own (constants/constants.go — 404s, 400s, 403s, 401s,
    500s, 405s, and missing-body errors) were all a "Slow down, bucko!" joke
    string. Replaced with plain, professional messages that actually describe
    the failure.

Fixed

  • PUT /servers and PUT /bots still wrote the legacy wildcard string
    global.* into a new team's team_members.flags when creating the
    owner's membership, instead of the flat model's owner permission
    (perms.EntityOwner). exp/rewrite/flatperms.sql converts this
    correctly for existing rows, but every server/bot added after running
    that migration created a team whose owner held a permission string the
    flat permission checker doesn't recognize as anything — silently locking
    them out of man...
Read more