feat(messaging): friend-gated direct messages#8
Merged
Conversation
Implements the Direct Friend Messaging follow-up. Design choice: persisted
friend-to-friend direct messages (not ephemeral room whispers), since room chat
already covers same-room talk and DMs add private, cross-room, offline-capable
messaging.
Protocol:
- New dm.send client message ({ toUserId, text }) and dm.message server message
({ id, fromUserId, toUserId, text, sentAt }); text is sanitized and bounded.
Server:
- DirectMessageService gates every send and history read on a mutual-friendship
check, persists to a new direct_messages table (migration 0009, with a no-self
CHECK and conversation indexes), and the WS handler publishes dm.message to the
recipient's and sender's per-user topics (user:<id>) for realtime delivery.
- Each socket subscribes to its user topic on open. Sends are rate-limited via a
new `dm` token bucket. GET /friends/:id/messages returns friend-gated, bounded,
oldest-first conversation history.
Client:
- A "Message" action on each friend opens a DirectMessagePanel (history loaded via
HTTP, live messages appended over the socket, own vs. incoming alignment).
- Game routes dm.message to onDirectMessage and exposes sendDirectMessage.
Tests/verification:
- Protocol, DirectMessageService, Drizzle store, friend areFriends, WS handler,
router history, and client (DMClient + panel) unit tests; a real-Postgres
integration test; and a two-browser end-to-end test confirming send, realtime
delivery, persisted history, friend-gating, and message alignment.
Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Summary
Implements the Direct Friend Messaging follow-up from
FOLLOW_UPS.md.Design choice (the follow-up asked to define it): persisted friend-to-friend direct messages, not ephemeral room whispers — room chat already covers same-room talk, so DMs add private, cross-room, offline-capable messaging. Privacy: only mutual friends can message each other or read history.
Protocol
dm.sendclient message ({ toUserId, text }) anddm.messageserver message ({ id, fromUserId, toUserId, text, sentAt }). Text is sanitized + length-bounded by the schema.Server
DirectMessageServicegates every send and history read on a mutual-friendship check (FriendStore.areFriends), persists to a newdirect_messagestable (migration0009, with a no-selfCHECKand conversation indexes).dm.messageto the recipient's and sender's per-user topic (user:<id>) for realtime delivery to every open tab; each socket subscribes to its user topic on open.dmtoken bucket.GET /friends/:id/messagesreturns friend-gated, bounded, oldest-first history.Client
DirectMessagePanel— history loaded over HTTP, live messages appended over the socket, own vs. incoming alignment, styled to match the app.Gameroutesdm.messagetoonDirectMessageand exposessendDirectMessage.Test plan
bun run lint✅ ·bun run typecheck✅bun test✅ 272 pass / 1 skip ·bun run coverage:check✅ 91.6% (critical dirs ≥81%)RUN_DB_TESTS=1(real Postgres) ✅ 277 pass incl. a DM-store integration test (round-trip + bidirectional conversation + the no-self CHECK)dm.sendis rejected withNOT_FRIENDS.🤖 Generated with Claude Code