A mute stops somebody typing, not only talking (GRYT-917) - #136
Merged
Conversation
`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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
server:mutehas takenexpiresInMinutessince timeouts landed. It writesserver_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:sendnever read it. Neither didrequireAuth, andsessionGategates 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
textMuteForreads the row and applies the expiry through the existingeffectiveModerationState, so a lapsed timeout is not a mute even though nothing sweeps the column.chat:sendandchat:editrefuse with a namedmutederror 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:typinggoes 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.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
requireAuthand before conversation access, so a muted member getsmutedrather 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
clientsInfofor 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
clientsInfoexcept 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 --noEmitclean, 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