Skip to content

A mute stops somebody typing, not only talking (GRYT-917) - #136

Merged
sivert-io merged 1 commit into
mainfrom
claude/GRYT-917-text-mute
Sep 5, 2026
Merged

A mute stops somebody typing, not only talking (GRYT-917)#136
sivert-io merged 1 commit into
mainfrom
claude/GRYT-917-text-mute

Conversation

@sivert-io

Copy link
Copy Markdown
Member

server:mute has taken expiresInMinutes since timeouts landed. It writes server_mute_expires_at, survives a reconnect, tells the client when it lifts, and the member list draws the flag. It only ever covered voice.

chat:send never read it. Neither did requireAuth, and sessionGate gates on banned and membership rather than on muted. So a moderator muted somebody who was spamming, the room watched them show as muted, and they carried on posting.

What changed

textMuteFor reads the row and applies the expiry through the existing effectiveModerationState, so a lapsed timeout is not a mute even though nothing sweeps the column.

  • chat:send and chat:edit refuse with a named muted error carrying the expiry. An edit is the other way to put new text in a channel — four characters becoming four million is already why the length cap is checked there.
  • chat:typing goes quiet. A muted member showing as about to speak is odd, and it reads off the row that handler already fetches rather than through a second query.
  • Reactions are deliberately left alone. A reaction is not text, spamming them is RL_REACT's job, and folding it in here would be a scope decision made in passing.

What to look at

The mute check sits after requireAuth and before conversation access, so a muted member gets muted rather than a channel-permission answer. That ordering is a choice: it means the refusal does not depend on whether they could have posted there anyway, which I think is right but is the line to disagree with.

It reads the user row on every send, on the path that already does several reads. If that turns out to matter, the flag is on clientsInfo for connected sockets and could be cached there — I did not, because the in-memory flag is what went stale before and the row is the source of truth the mute handler's own comment points at.

Verification

Six tests, driven through the handlers rather than through textMuteFor — the deciding was never wrong, nothing on the way to a message asked, and a test of the helper would have passed against the broken build.

I checked they mean something by reverting the three call sites and re-running: four of the six fail, and the two that pass are the negative cases (a lapsed timeout, and a member who is not muted). The typing one was vacuous on the first attempt — the audience is everybody in clientsInfo except the typist, so with one client it emitted nothing either way — and is now paired with the unmuted case to prove the harness reaches the emit.

742 tests pass, tsc --noEmit clean, eslint has no errors.

Follow-ups, both opened: GRYT-918 slow mode, GRYT-919 auto-mute on repeated rate-limit trips, which needs this to land first or the mute it applies does nothing.

🤖 Generated with Claude Code

`server:mute` has taken `expiresInMinutes` since timeouts landed. It writes
`server_mute_expires_at`, survives a reconnect, tells the client when it lifts,
and the member list draws the flag. It only ever covered voice.

`chat:send` never read it. Neither did `requireAuth`, and `sessionGate` gates on
banned and membership rather than on muted. So a moderator muted somebody who
was spamming, the room watched them show as muted, and they carried on posting.

`textMuteFor` reads the row and applies the expiry through the existing
`effectiveModerationState`, so a lapsed timeout is not a mute even though
nothing sweeps the column. `chat:send` and `chat:edit` refuse with a named
`muted` error carrying the expiry — an edit is the other way to put new text in
a channel, and four characters becoming four million is already why the length
cap is checked there.

`chat:typing` goes quiet too. A muted member showing as about to speak is odd,
and it is read off the row that handler already fetches rather than through a
second query.

Reactions are deliberately left alone. A reaction is not text, spamming them is
`RL_REACT`'s job, and adding it here would be a scope decision made in passing.

Six tests, driven through the handlers rather than through `textMuteFor` —
the deciding was never wrong, nothing on the way to a message asked, and a test
of the helper would have passed against the broken build. Checked by reverting
the three call sites: four of the six fail, and the two that do not are the
negative cases.

742 tests pass, tsc clean, eslint has no errors.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sivert-io
sivert-io merged commit 6e2d70e into main Sep 5, 2026
2 checks passed
@sivert-io
sivert-io deleted the claude/GRYT-917-text-mute branch September 5, 2026 16:18
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