Skip to content

Take the reporter's copy of an MLS message as a report (GRYT-1557) - #250

Merged
sivert-io merged 1 commit into
mainfrom
claude/GRYT-1557-mls-reports
Sep 28, 2026
Merged

sivert-io merged 1 commit into
mainfrom
claude/GRYT-1557-mls-reports

Conversation

@sivert-io

@sivert-io sivert-io commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

The server never has an MLS message, so chat:report couldn't take a report of one. It looked the id up, found nothing, and answered "Message not found". Decision 11 says the reporter's app sends its decrypted copy, marked unverified. The desktop and the phone already send it (client#715, mobile#272), and they offer Report on an MLS message only once server:info.mls.reports is true. This is the server half.

What changes

  • chat:report takes mls: { senderServerUserId, text }. With it, the server doesn't look the message up. It stores the copy as a report with unverified = 1, no attachments, and the sender's current nickname.
  • Checks before it's stored:
    • the conversation exists, and the reporter and the named sender are both members. Anything else gets "Message not found", the same answer as a DM that doesn't exist, so this can't be used to find out who talks to whom
    • the sender isn't the reporter
    • report_messages, the already-reported check and the rate limit, the same as any report. They run first, so a flood is refused before any lookup
    • text is at most 32,000 characters and messageId at most 64, the content format's own limits. Too long is refused, not cut, so a moderator never reads half a message
  • reports:list adds unverified: true to those cards, and only to those.
  • server:info.mls adds reports: true.

Behaviour that changes for normal reports too

  • The queue groups by conversation and message id, not message id alone. An MLS id is the sender's choice, so a made-up copy could reuse a real channel message's id and pile onto that report. For a server-made id, one message is one conversation, so nothing groups differently.
  • reports:resolve narrows approve and delete to the conversation the client names. Both apps already send it. Without it, it resolves by id like before.
  • delete only deletes, and broadcasts chat:deleted, when the server actually has the message. For an MLS copy there's nothing to delete. Before, the broadcast went to every socket on the server, which for an MLS copy would have named the DM to everybody. A normal message its author already deleted also gets no broadcast now. It had nothing to remove anyway.

Review-required

src/db/** is review-required, and this touches three files there:

  • connection.ts: ALTER TABLE reports ADD COLUMN unverified INTEGER NOT NULL DEFAULT 0, behind the usual hasColumn check. Every existing row reads as verified, which is right, since each one was copied from the server's own message.
  • reports.ts: writes and reads the column, and makes the grouping and resolve changes above.
  • interfaces.ts: unverified: boolean on ReportRecord.

The parts I'm least sure about:

  • Group DMs are allowed. The check is "both are members", not "the other one of two". Group DMs aren't on MLS yet, but when they are, anybody in the group can report anybody else in it, which seems right.
  • hasUserReportedMessage still matches on message id alone. The only way to hit that is reporting your own collision, so I left it.
  • The desktop's reports panel still says Delete "will permanently delete this reported message" on an unverified card. That's GRYT-1569.

Checked

  • yarn test (1,794 passing), yarn test:examples, yarn build, npx eslint . (no errors; the five warnings are in files this doesn't touch), and both check scripts.
  • mlsReports.test.ts, new, runs against a real SQLite database:
    • a copy is queued as unverified under the sender
    • a repeat is refused, and so is your own message
    • a reporter or sender outside the DM gets "Message not found", and so do a channel id and a DM that doesn't exist
    • a group member can report
    • malformed copies and over-long ids and texts are refused
    • report_messages is needed
    • the rate limit applies
    • a copy that reuses a real message's id gets its own card, and deleting it leaves the real report and deletes nothing
  • I broke the grouping key and the sender-membership check one at a time, and a test failed each time.
  • serverInfo.test.ts now expects reports: true.

Docs: Gryt-chat/docs#150, which should merge after this one.

Closes GRYT-1557. Verifiable reports, where a moderator can check the signature, are GRYT-1568.

🤖 Generated with Claude Code

The server never has an MLS message, so chat:report now takes
mls: { senderServerUserId, text } and stores it as an unverified report
(decision 11). The reporter and the sender both have to be in the
conversation, and it can't be your own message; otherwise it answers
"Message not found", like a DM that doesn't exist. Permission, the
already-reported check and the rate limit are the same as any report.

reports:list marks those cards unverified, and server:info.mls says
reports: true. The queue groups by conversation and message id, since
an MLS id is the sender's choice and could copy a real one, and
reports:resolve narrows to the conversation it's given. A delete only
deletes and broadcasts when the server has the message.

New column: reports.unverified, 0 on every existing row.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@sivert-io
sivert-io merged commit c39ea57 into main Sep 28, 2026
6 checks passed
@sivert-io
sivert-io deleted the claude/GRYT-1557-mls-reports branch September 28, 2026 23:12
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.

1 participant