Skip to content

Who may send as Petto is checked first; every DM says where it is from and can be reported to the team; priority error log and self-healing tickets; Pinterest no longer repeats an empty card - #399

Merged
liamt8d merged 6 commits into
mainfrom
fix/send-permissions
Nov 2, 2026

Conversation

@liamt8d

@liamt8d liamt8d commented Nov 1, 2026 •

Copy link
Copy Markdown
Collaborator

No version change: the notes are under [Unreleased]. No new slash command (no deploy-commands.js needed). This PR changes the database (a new table dm_reports, created by schema.sql on start): back up the database before deploying. Set these environment variables (see "To configure" below).

1. send and the permission checks

A normal member was seen making Petto talk with send. The routes could not be made to post for a plain member in the tests, so this hardens every place where a missing or unreadable permission could be read as "allowed", makes the refusals private and adds a test that tries every route as a plain member.

  • send (src/commands/config/send.js): the check is the first thing execute does. The member must be a real guild member with Manage Messages in the server (strict has(...) === true; anything unreadable is a no), then View, Send and Manage Messages in the target channel (permissionsFor null is a no). Every refusal is private (the defer is ephemeral too) and names the permission.
  • Prefix (checkDefaultPermission): a member whose permissions cannot be read used to make it throw silently; now it denies with a card. Umbrellas (checkPermission, autocomplete): strict has(needed) === true.
  • New scripts/check-send-permissions.js (real PermissionsBitField, channel overwrites, the real prefix and slash pipelines): !send, petto send, @Petto send, !message send, !msen, /message send... for eight kinds of member, 7173 command words, 783 umbrella subcommands and 1654 slash subcommands; it fails on the old code. check-member-access.js has an honest permissionsFor fake now.
  • To know: /message is open at Discord (a whole command is gated, never one subcommand), so /message send is listed to every member and refuses them privately; a prefix refusal is a public reply; standalone slash commands (/honeypot add, /partnerconfig welcome, /logs add, /verify setup, /webhook create, /channel lock|unlock|topic) rely on Discord's default_member_permissions and run for everybody only if a server opens them in Integrations; custom commands (including the say template) run before !restrictcommand, !disablecommand and permission levels.

2. Every DM from Petto says who it is from, and can be reported to the Petto team

  • One helper (src/utils/dmOrigin.js, sendDm) for every DM sent to a user (sanctions, join DM, form results, tickets, appeals, giveaways, verification, levels, backups, confessions, setup and team notices; User#send and GuildMember#send are also wrapped as a net). A test fails if a raw DM send appears outside the helper.
  • Each DM starts with a grey -# From <server> line and ends with three small icon buttons: info (public facts about the server and why the DM was sent, and how to stop it where there is a real setting), delete (deletes that DM) and report. It works for plain text, embeds and Components V2 messages, and never drops the message to make room.
  • Report opens a form (category, 20 to 1000 characters of details, "Delete this DM after reporting") and goes only to the Petto team, never to the server's moderators: a card in the team's channel with defanged text, the server, the kind of DM, the reporter and a "Mark handled" button; it is always stored in the new table dm_reports. 3 reports an hour per person, one per message. Only the DM's recipient can use the buttons and they refuse inside a server.

3. Priority error log and self-healing tickets

  • Two logs. The existing log channel is now the general log (warnings, rate limits, Redis, expected Discord errors). New priority channel: PETTO_PRIORITY_ERRORS_CHANNEL_ID; it pings PETTO_PRIORITY_PING_USER_IDS (default: the first developer id) and optionally PETTO_PRIORITY_PING_ROLE_ID, at most once per fingerprint per 30 minutes and 5 pings an hour; nothing else can be pinged. Repeats of the same error edit one card (a count) instead of posting again.
  • Triage (src/utils/errorTriage.js, a tested table with an English explanation per code): expected errors (10062 Unknown interaction, 10003 Unknown Channel, 10008, 10007, 40060, 50001/50013 from a server's setup, 50007, 429, 5xx, network and Redis timeouts, database connection loss) go to the general log as warnings; everything else (bugs, database errors, unknown codes, "Petto sent something wrong") is priority. The priority card has what it probably means, server/channel/user, command, count, the top stack frames, secrets redacted, and a "Copy this to investigate" block to paste when asking for help.
  • Tickets fix themselves (src/utils/safeTicketAction.js, used by every ticket button, menu, form and rating handler): acknowledge first, a channel or message that is already gone counts as done and the bookkeeping is finished, a late interaction is a warning and the work still completes, one retry on transient failures, a friendly message on a final failure plus a priority error, and a ticket is never left half deleted (transcript first, then close, then delete). The ticket job also heals open tickets whose channel is gone and panels whose message is gone.

4. Pinterest posted an empty card again and again

The public RSS of a board can contain a ghost pin (empty title, empty picture, invalid date) whose id changes on every read, so it was new every 15 minutes. Dateless items are no longer "fresh for ever" (reddit, rss, pinterest, mastodon, tumblr, letterboxd), what was seen is marked before sending (a failure after a send cannot repost), a pin without a picture asks Pinterest's public pin info for numeric ids and is not posted as an empty card if there is still none (one warning per feed per hour, no content), and at most 3 new items per feed per pass are posted.

To configure

  • PETTO_DM_REPORTS_CHANNEL_ID: the team's channel for DM reports (without it they are only stored in dm_reports and a warning is logged).
  • PETTO_PRIORITY_ERRORS_CHANNEL_ID, PETTO_PRIORITY_PING_USER_IDS, optional PETTO_PRIORITY_PING_ROLE_ID (the old error channel variable is unchanged and is now the general log).
  • Check that pe_info and pe_delete exist as application emojis (the ids come from the voicemaster buttons).

Checks

New: check-send-permissions.js, check-dm-origin.js, check-error-triage.js; extended: check-feeds.js (the real engine twice over real-shaped Pinterest data), check-member-access.js, check-error-log.js, check-send.js and others. npm run check and npm run types:check pass. Nothing was run against real Discord, Pinterest or a production database: the look of the grey line and buttons in DMs, the modal checkbox, V2 priority cards, real button clicks in DMs and the live pin info endpoint are unverified.

…and deny when a permission cannot be read

`send` now needs Manage Messages in the server (what its default permission
means) plus View, Send and Manage Messages in the target channel, found on a
real member of the server. The check runs first, for `!send`, `petto send`,
`@Petto send`, `!message send`, its short alias and `/message send`, and the
refusal is private (ephemeral for the slash command) and names the permission.
A member with no readable permissions, a raw interaction member or a channel
that cannot answer is refused. The prefix handler and the umbrella gate use the
same strict yes-or-no instead of throwing or `?? false` shortcuts.

New scripts/check-send-permissions.js (real PermissionsBitField, computed
channel overwrites) runs every route with members that have nothing, only Send
Messages, Manage Messages in one channel only, Manage Messages in the server and
Administrator, with levels, restrictions and a moderation role that must only
add limits, server aliases, every command word and every umbrella subcommand as
a plain member. check-member-access.js now fakes channel permissions honestly.

Signed-off-by: Liamt8dd <juliancamilparratrujillo@gmail.com>
…o report it to the Petto team

Each direct message Petto starts now begins with a small grey line (From <server>, or From Petto when no server is behind it) and ends with one row of three icon buttons in Petto's emojis: about the message, delete it and report it.

- utils/dmOrigin.js: sendDm(target, payload, { guild, kind }) is the one place that adds the line and the row, for plain text, embeds and Components V2 (the line is the first component, the row the last), keeping the message when a limit is reached (5 rows, 2000 characters, 40 components, 4000 characters of text); server names are escaped and clipped to 60 characters. User#send and GuildMember#send carry the same step as a net.
- Every user-directed send in the code (join, sanctions, automod, escalation, anti-scam, honeypot, forms, appeals, tickets, giveaways, verification, levels, suggestions, report updates, setup, backups, confessions, custom commands, embed buttons, owner notices) goes through sendDm; the setup messages keep their line when redrawn after a language change.
- interactions/dmOrigin.js: info answers with what is public about the server and why the message was sent; delete removes that message; report opens a form (notice, "does NOT go to the server's moderators", reason, details of 20 to 1000 characters, delete-after switch) in en, es and pt-BR. The buttons only work in the person's own chat with Petto, on messages of Petto, and refuse inside a server.
- utils/dmReports.js and db/dmReports.js: the report is stored (table dm_reports, one per message, 3 an hour per person) and sent as a card to the Petto team in PETTO_DM_REPORTS_CHANNEL_ID with a Mark handled button; without a channel it waits in the table and a warning is logged. It never goes to the server's moderators.
- scripts/check-dm-origin.js (last of the check script) covers the shapes, limits, escaping, handlers, form, rate limit, delivery, fallback, languages and that no direct message goes around the helper.

Signed-off-by: Liamt8dd <juliancamilparratrujillo@gmail.com>
…and a failure after a send never reposts

A Pinterest board can list a pin that no longer exists: no picture, no
title, no date, and a new short address on every reading. Its id was new
on every look, so every 15 minutes it was posted as an empty card.

- Pinterest: a pin with no picture asks the public pin information once
  (numeric ids only, 6 s, answer kept, paced); with still no picture the
  engine does not post it, marks it as seen, and logs it once an hour for
  each feed (ids only).
- Engine: what a look found is marked as seen before the first send, and
  kept in memory when the database cannot write it; a failure after a
  send can no longer cause a repost. A live message that cannot be saved
  is not counted as a failure to post. Many new items at once are cut to
  the newest three and counted in one log line.
- Items with no date (Reddit, Pinterest, RSS, Tumblr, Mastodon,
  Letterboxd) are as old as the first time they were seen, so they are
  never fresh for ever.
- The test of a feed no longer picks a pin without a picture.
- Checks: the engine over a fake store with the shape of a real board,
  failing writes, the cap of three, the pin information, and ids with
  special characters through the real SQL.

Signed-off-by: Liamt8dd <juliancamilparratrujillo@gmail.com>
Comment thread scripts/check-feeds.js Fixed
Comment thread scripts/check-feeds.js Fixed
The existing error channel is now the general log: warnings, rate limits,
Redis and the errors Petto expects (an interaction that expired, a channel
or message that was already deleted, a missing permission of a server, a
network or Discord hiccup) arrive there as warnings with a line that says
what they mean, and never ping anybody. A new priority log
(PETTO_PRIORITY_ERRORS_CHANNEL_ID) gets every other error, as a card with
what it probably means, where it happened, a count for the last ten
minutes, the top stack frames with secrets removed and a "Copy this to
investigate" block. Repeats edit the card. It pings
PETTO_PRIORITY_PING_USER_IDS (default the first developer, else the owner)
and optionally PETTO_PRIORITY_PING_ROLE_ID: one ping per problem every 30
minutes and 5 an hour, and no text of an error can mention anybody.

The triage (utils/errorTriage.js) is a table of Discord codes, network and
database errors and message patterns, with fingerprints that group repeats.

Every ticket button, menu, form and rating goes through one safety net
(utils/safeTicketAction.js): it answers Discord first, a second press is
harmless, a channel or message that is already gone counts as done and
finishes the bookkeeping, an expired interaction is a warning and the work
still finishes, a timeout or rate limit is retried once, and a real failure
tells the member kindly, leaves the ticket safe and reaches the priority
log. Deleting a ticket saves its transcript first and is not done if that
fails. The channel sweep also forgets the message of a panel whose channel
is gone and reports what it healed. Expired interactions on any command or
button are warnings now.

Signed-off-by: Liamt8dd <juliancamilparratrujillo@gmail.com>
…ons branch

Signed-off-by: Liamt8dd <juliancamilparratrujillo@gmail.com>
@liamt8d liamt8d changed the title Check who may send as Petto before anything is sent, on every route, and deny when a permission cannot be read Who may send as Petto is checked first; every DM says where it is from and can be reported to the team; priority error log and self-healing tickets; Pinterest no longer repeats an empty card Nov 1, 2026
Signed-off-by: Liamt8dd <juliancamilparratrujillo@gmail.com>
@liamt8d
liamt8d merged commit 201a053 into main Nov 2, 2026
4 checks passed
@liamt8d
liamt8d deleted the fix/send-permissions branch November 2, 2026 00:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants