Skip to content

feature: edit or delete your own chat messages for 10 minutes - #442

Merged
lukepolo merged 5 commits into
mainfrom
feature/chat-self-edit
Sep 29, 2026
Merged

lukepolo merged 5 commits into
mainfrom
feature/chat-self-edit

Conversation

@lukepolo

@lukepolo lukepolo commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Players can edit or delete their own website messages for 10 minutes, and every group-room edit is audited so abuse can't be edited away.

  • lobby:edit swaps the message in one compare-and-set Lua script that keeps its exact expiry and never brings back a deleted or expired message
  • Authors can self-delete within the window (audited in group rooms); a gag blocks editing but not deleting; moderator deletes are unchanged
  • New chat_message_edits audit (previous and new text), written before the swap; a resend after a lost redis reply counts as applied
  • DMs: direct_messages.edited_at and guarded single-statement edit/delete on the DB clock; deleting a conversation's only message takes it off both rails
  • The draft-to-match move is one Lua script, and bell previews (read and collapsed rows too, never retracted ones) show the edit
  • New socket events: lobby:edit {type, id, messageId, message, requestId?} in, :edited {id, message, edited_at} out; new window_closed error

Merge/deploy: stacked on #438; run yarn hasura:metadata after merge. Pairs with 5stackgg/web#622.

Tests: a test fails without each fix: the edit ack carrying the stored edited_at, an empty edit answered invalid, previews on read and collapsed rows, the DM only-message rail cleanup, kept TTL, no resurrection, the resend receipt, audit before swap, the discarded audit of a failed swap, the notification catch-up, and (new test) the atomic draft move. Based on DEAFCS ee3ce335e c76bba393

@lukepolo

Copy link
Copy Markdown
Contributor Author

Edit ack now carries what the server stored: chat:ack {requestId, messageId, action: "edit", message, edited_at}. edited_at is an ISO string, and it has the same value as the :edited broadcast (the Postgres now() for DMs, the api clock for rooms). message is the stored text. web#622 can take edited_at from the ack rather than stamping the browser clock. It should fall back to the browser clock when the field is missing (older api). Send and delete acks are unchanged. Commit 9a7fd09, cascaded into #445 and #449 as merges.

@lukepolo
lukepolo force-pushed the feature/chat-moderation branch from 60d2f76 to af7a1ca Compare September 29, 2026 00:49
Base automatically changed from feature/chat-moderation to main September 29, 2026 00:49
The author of a website message (source "web") can edit or delete it for
10 minutes (ChatService.SELF_SERVICE_WINDOW_MS, 5s of pod clock skew
allowed). Nobody edits another player's message, administrators included;
moderator deletes at any age stay as they were.

- lobby:edit { type, id, messageId, message, requestId? } broadcasts
  lobby:<type>:<id>:edited { id, message, edited_at }, acks/errors with
  action "edit", and is never relayed to the game server
- room edits are one compare-and-set Lua script against the value that was
  read and checked: HSET drops a field's TTL, so the exact HPEXPIRETIME is
  put back with HPEXPIREAT, and an expired or deleted field is never
  written back
- a gag blocks editing in group rooms but not deleting your own message; an
  author's own delete in a group room is audited like a moderator's
- DMs: direct_messages.edited_at (migration 1888000000200); edit and delete
  are single statements guarded on author, room and created_at on the
  database clock; not audited; history returns edited_at
- unread, live bell rows get the edited preview; rows a delete already
  retracted stay blank, and an edit or delete that lands while a message's
  rows are still being written is applied once they are
- new chat:error code window_closed
…c draft move

- updateChatMessagePreview rewrites every row for the message except a
  retracted (blank) one: read rows stay in the bell for a week and a
  recipient can restore a collapsed one, so both kept the text the author
  took back. `message <> ''` still stops an edit racing a delete from
  writing text back, including when it waits on the retraction's row lock
- migrateLobbyMessages moves a draft's messages in one Lua script, so an
  edit or a self-delete in the draft room can no longer land on a copy that
  is thrown away (edit lost) or written back (deleted message returns)
- lobby:edit answers an empty or non-string edit with chat:error invalid
  instead of leaving the client waiting
- deleting the only message of a DM conversation takes it off both rails,
  so the recipient is not left with an empty tab from the sender
- tests: the edit/delete race asserts the refusal code, the DM catch-up test
  deletes through the service, a retraction holding the row lock mid-edit
An author could post abuse and edit it into something harmless within the
10 minute window, and nothing of the original survived. Editing and then
deleting kept only the edited text in chat_message_deletions.

- chat_message_edits (message, room, author, previous and new text, when
  the message was sent, when it was edited), one row per group-room edit.
  DMs are never audited, the same as deletions.
- The row is written before the redis compare-and-set, the way a deletion
  is audited before its HDEL, so a failed audit write refuses the edit. A
  swap that finds the message gone or changed deletes its row again; a swap
  that fails outright keeps it, since it may have applied.
- The swap leaves a short-lived receipt keyed on its audit row, so an
  ioredis resend after a lost reply answers 1 even if the message was
  deleted or edited again in between, instead of reading as a failed swap
  and discarding the audit of an edit that happened.
- Hasura: moderator reads every row outside the organizers' room,
  match_organizer and up read everything; no insert/update/delete.
With the move made of separate calls, an author's delete in the draft room
landed on a copy that was written back into the match, and an edit landed
on one that was thrown away.
@lukepolo
lukepolo force-pushed the feature/chat-self-edit branch from aa3e4cb to 5715f46 Compare September 29, 2026 00:51
@lukepolo
lukepolo merged commit 0731b07 into main Sep 29, 2026
2 checks passed
@lukepolo
lukepolo deleted the feature/chat-self-edit branch September 29, 2026 00:51
lukepolo added a commit that referenced this pull request Sep 29, 2026
…erialized DM toggles

- The room toggle script takes a receipt key per toggle: ioredis resends a
  command whose reply was lost to a reconnect, and a toggle run twice
  undid itself. A resend now answers with the current state instead.
- A gagged player can still take back a reaction they gave, the way #442
  lets them delete their own message; only adding one is refused.
- DM toggles lock the message row first (FOR NO KEY UPDATE) and toggle in a
  fresh statement, so racing toggles on an absent row cancel out as they
  do in rooms instead of both inserting, and a delete can no longer land
  between finding the message and writing the reaction.
- Reactions are always handed out in ChatService.REACTIONS order; the Lua
  table and jsonb each had their own.
- Index direct_message_reactions (steam_id) for the players cascade.
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