From 2cb263542e51a664977d505887cd16e1b6c3aae3 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Mon, 26 Jan 2026 15:20:17 +0530 Subject: [PATCH] feat(06-01): document complete chat API surface - Map REST endpoints (create, index) with request/response formats - Document WebSocket Protocol Buffer messages - Capture database schema and ActiveRecord model - Identify security, validation, and access control - Document data flow and routing patterns - Note missing functionality (read/unread tracking) Co-Authored-By: Claude Sonnet 4.5 --- .planning/codebase/CHAT_API.md | 798 +++++++++++++++++++++++++++++++++ 1 file changed, 798 insertions(+) create mode 100644 .planning/codebase/CHAT_API.md diff --git a/.planning/codebase/CHAT_API.md b/.planning/codebase/CHAT_API.md new file mode 100644 index 000000000..0edf3316d --- /dev/null +++ b/.planning/codebase/CHAT_API.md @@ -0,0 +1,798 @@ +# Chat API Surface Documentation + +**Purpose:** Document all chat-related API endpoints, WebSocket messages, database models, and data structures. + +**Date:** 2026-01-26 +**Phase:** 06-session-chat-research-design +**Plan:** 01 + +--- + +## Overview + +The chat system uses a **hybrid architecture**: +- **REST API** for sending messages and fetching history +- **WebSocket (Protocol Buffers)** for real-time message delivery +- **No jamClient methods** - chat is purely web-based (no native client integration) + +**Supported Channels:** +- `global` - All online users (disabled for school users) +- `session` - Users in a specific music session +- `lesson` - Direct messages between teacher and student in a lesson + +--- + +## REST API Endpoints + +### Base Path +All endpoints are under `/api/chat` (configured in `web/config/routes.rb`) + +### Authentication +All endpoints require: +- `before_filter :api_signed_in_user` - valid session/token +- `before_filter :check_session` - validates session/lesson access + +--- + +### 1. Create Chat Message + +**Endpoint:** `POST /api/chat` + +**Controller:** `ApiChatsController#create` +**File:** `/web/app/controllers/api_chats_controller.rb` + +**Request Body:** +```json +{ + "message": "Hello world", + "channel": "session", + "music_session": "session-uuid", + "lesson_session": "lesson-uuid", + "client_id": "client-uuid", + "target_user": "user-uuid" +} +``` + +**Parameters:** +- `message` (string, required) - Message text (1-255 chars, profanity filtered) +- `channel` (string, required) - Channel type: `"global"`, `"session"`, `"lesson"` +- `music_session` (string, conditional) - Required if `channel="session"` +- `lesson_session` (string, conditional) - Required if `channel="lesson"` +- `client_id` (string, optional) - Client UUID for routing (excludes sender from broadcast) +- `target_user` (string, conditional) - Required if `channel="lesson"` (recipient user ID) + +**Response (Success 200):** +```json +{ + "id": "msg-uuid", + "message": "Hello world", + "user_id": "user-uuid", + "session_id": null, + "created_at": "2026-01-26T12:00:00.000Z", + "channel": "session", + "lesson_session_id": null, + "purpose": null, + "user": { + "name": "John Doe" + }, + "music_notation": null, + "claimed_recording": null +} +``` + +**Response (Error 404):** +```json +{ + "error": "specified session not found" +} +``` + +**Response (Error 403):** +```json +{ + "error": "not allowed to join the specified session" +} +``` + +**Response (Error 422):** +```json +{ + "errors": { + "message": ["is too short (minimum is 1 character)"], + "user": ["Global chat is disabled for school users"] + } +} +``` + +**Business Logic (from `ChatMessage.create`):** +1. Validates message length (1-255 chars) and profanity +2. HTML sanitizes message content (strict mode) +3. For lesson channel: + - Auto-determines source_user and target_user from lesson + - Sets unread flags: `teacher_unread_messages` or `student_unread_messages` + - Sends email notification if target user offline +4. Saves message to database +5. Broadcasts via WebSocket (see WebSocket section) + +**Access Control:** +- Global channel: Blocked for school users (`user.school_id` present) +- Session channel: Must be participant in session (`music_session.access?(user)`) +- Lesson channel: Must be teacher or student in lesson (`lesson_session.access?(user)`) +- School protection: Can only chat with users from same school (unless platform instructor) + +--- + +### 2. Get Chat History + +**Endpoint:** `GET /api/chat` + +**Controller:** `ApiChatsController#index` +**File:** `/web/app/controllers/api_chats_controller.rb` + +**Query Parameters:** +``` +GET /api/chat?channel=session&music_session=session-uuid&limit=20&page=0 +``` + +**Parameters:** +- `channel` (string, required) - Channel type: `"global"`, `"session"`, `"lesson"` +- `music_session` (string, conditional) - Required if `channel="session"` +- `lesson_session` (string, conditional) - Required if `channel="lesson"` +- `limit` (integer, optional) - Number of messages to fetch (default: 20) +- `page` (integer, optional) - Page number (default: 0) +- `start` (integer, optional) - Offset cursor for pagination + +**Response (Success 200):** +```json +{ + "next": 20, + "chats": [ + { + "id": "msg-uuid", + "message": "Hello world", + "user_id": "user-uuid", + "session_id": null, + "created_at": "2026-01-26T12:00:00.000Z", + "channel": "session", + "lesson_session_id": null, + "purpose": null, + "user": { + "name": "John Doe" + }, + "music_notation": null, + "claimed_recording": null + } + ] +} +``` + +**Response Fields:** +- `next` (integer|null) - Cursor for next page, or `null` if no more messages +- `chats` (array) - Array of chat messages (newest first, due to `default_scope` order) + +**Business Logic (from `ChatMessage.index`):** +1. Queries by channel +2. If `music_session` param: filters by `music_session_id` +3. If `lesson_session` param: filters by `lesson_session_id` +4. Orders by `created_at DESC` (newest first) +5. Includes associated `user` records (eager loading) +6. Returns pagination cursor if more messages available + +**Access Control:** +- Same as create endpoint (validates session/lesson access) +- Global channel: Returns empty array for school users + +--- + +## WebSocket Messages + +### Protocol +- Uses **Protocol Buffers** (defined in `pb/src/client_container.proto`) +- Routed through `websocket-gateway` service +- Published via RabbitMQ to appropriate targets + +### Message Type: CHAT_MESSAGE + +**Protocol Buffer Definition:** +```protobuf +message ChatMessage { + optional string sender_name = 1; + optional string sender_id = 2; + optional string msg = 3; + optional string msg_id = 4; + optional string created_at = 5; + optional string channel = 6; + optional string lesson_session_id = 7; + optional string purpose = 8; + optional string attachment_id = 9; + optional string attachment_type = 10; + optional string attachment_name = 11; +} +``` + +**JSON Equivalent (for jam-ui WebSocket handling):** +```json +{ + "sender_name": "John Doe", + "sender_id": "user-uuid", + "msg": "Hello world", + "msg_id": "msg-uuid", + "created_at": "2026-01-26T12:00:00.000Z", + "channel": "session", + "lesson_session_id": null, + "purpose": null, + "attachment_id": null, + "attachment_type": null, + "attachment_name": null +} +``` + +**Field Descriptions:** +- `sender_name` - Display name of sender +- `sender_id` - User UUID of sender +- `msg` - Message text content +- `msg_id` - Unique message ID (for deduplication) +- `created_at` - ISO 8601 timestamp +- `channel` - Channel type: `"global"`, `"session"`, `"lesson"` +- `lesson_session_id` - Lesson UUID (if lesson channel) +- `purpose` - Optional purpose: `"Notation File"`, `"Audio File"`, `"JamKazam Recording"`, `"Video Uploaded"`, etc. +- `attachment_id` - ID of attached resource (notation, recording, video) +- `attachment_type` - Type: `"notation"`, `"audio"`, `"recording"`, `"video"` +- `attachment_name` - Display name of attachment + +--- + +### Message Routing (from `ChatMessage.send_chat_msg`) + +**Session Channel:** +```ruby +@@mq_router.server_publish_to_session(music_session, msg, sender = {:client_id => client_id}) +``` +- Broadcasts to all users in the session +- Excludes sender (by client_id) +- Route target: `session:{session_id}` + +**Global Channel:** +```ruby +@@mq_router.publish_to_active_clients(msg) +``` +- Broadcasts to all active (online) clients +- Route target: `__ALL_ACTIVE_CLIENTS__` + +**Lobby Channel:** +```ruby +@@mq_router.publish_to_active_clients(msg) +``` +- Same as global (TODO: should filter to lobby users only) +- Route target: `__ALL_ACTIVE_CLIENTS__` + +**Lesson Channel:** +```ruby +@@mq_router.publish_to_user(target_user.id, msg, sender = {:client_id => client_id}) +@@mq_router.publish_to_user(user.id, msg, sender = {:client_id => client_id}) +``` +- Sends to both teacher and student +- Route target: `user:{user_id}` + +--- + +### Client-Side WebSocket Handling + +**Registration (legacy pattern):** +```javascript +context.JK.JamServer.registerMessageCallback( + context.JK.MessageType.CHAT_MESSAGE, + function (header, payload) { + // Handle incoming chat message + ChatActions.msgReceived(payload); + } +); +``` + +**jam-ui Integration:** +- Uses `JamServer.js` helper for WebSocket management +- Messages routed through Redux middleware (similar to MIXER_CHANGES, JAM_TRACK_CHANGES) +- Should subscribe/unsubscribe based on session state + +--- + +## Database Model + +### Table: chat_messages + +**Schema File:** `/ruby/db/schema.rb` + +**Table Definition:** +```sql +CREATE TABLE chat_messages ( + id VARCHAR(64) PRIMARY KEY DEFAULT uuid_generate_v4(), + user_id VARCHAR(64) REFERENCES users(id), + music_session_id VARCHAR(64) REFERENCES music_sessions(id), + message TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + channel VARCHAR(128) NOT NULL DEFAULT 'session', + target_user_id VARCHAR(64) REFERENCES users(id), + lesson_session_id VARCHAR(64) REFERENCES lesson_sessions(id) ON DELETE CASCADE, + purpose VARCHAR(200), + music_notation_id VARCHAR(64) REFERENCES music_notations(id), + claimed_recording_id VARCHAR(64) REFERENCES claimed_recordings(id) +); +``` + +**Indexes:** +```sql +CREATE INDEX chat_messages_idx_channels ON chat_messages(channel); +CREATE INDEX chat_messages_idx_created_at ON chat_messages(created_at); +CREATE INDEX chat_messages_idx_music_session_id ON chat_messages(music_session_id); +``` + +**Columns:** +- `id` - UUID primary key +- `user_id` - Sender user ID (FK to users) +- `music_session_id` - Session ID (FK to music_sessions, nullable) +- `message` - Message text (TEXT, NOT NULL) +- `created_at` - Timestamp (default: current_timestamp) +- `channel` - Channel type (default: "session") +- `target_user_id` - Recipient user ID for direct messages (FK to users, nullable) +- `lesson_session_id` - Lesson ID (FK to lesson_sessions, nullable, CASCADE delete) +- `purpose` - Optional purpose/type (VARCHAR 200) +- `music_notation_id` - Attached notation file (FK to music_notations, nullable) +- `claimed_recording_id` - Attached recording (FK to claimed_recordings, nullable) + +--- + +### ActiveRecord Model + +**File:** `/ruby/lib/jam_ruby/models/chat_message.rb` + +**Class:** `JamRuby::ChatMessage` + +**Associations:** +```ruby +belongs_to :user +belongs_to :music_session +belongs_to :target_user, class_name: "JamRuby::User" +belongs_to :lesson_session, class_name: "JamRuby::LessonSession" +belongs_to :music_notation, class_name: "JamRuby::MusicNotation" +belongs_to :claimed_recording, class_name: "JamRuby::ClaimedRecording" +``` + +**Validations:** +```ruby +validates :user, presence: true +validates :message, length: {minimum: 1, maximum: 255}, no_profanity: true +validate :same_school_protection +validate :no_global_for_schools +``` + +**Default Scope:** +```ruby +default_scope { order('created_at DESC') } +``` +Note: Messages are ordered newest-first by default! + +**HTML Sanitization:** +```ruby +html_sanitize strict: [:message] +``` +Strips all HTML tags from message content. + +--- + +### Constants + +**Channels:** +```ruby +CHANNEL_SESSION = 'session' +CHANNEL_LESSON = 'lesson' +CHANNEL_LOBBY = 'lobby' +``` +Note: `global` channel is used but not defined as constant. + +--- + +## Related Models + +### lesson_sessions Table + +**Unread Message Tracking (for lesson chat):** +```sql +ALTER TABLE lesson_sessions ADD COLUMN teacher_unread_messages BOOLEAN DEFAULT FALSE NOT NULL; +ALTER TABLE lesson_sessions ADD COLUMN student_unread_messages BOOLEAN DEFAULT FALSE NOT NULL; +``` + +**Business Logic:** +- When student sends message: sets `teacher_unread_messages = true` +- When teacher sends message: sets `student_unread_messages = true` +- Flags should be cleared when user opens lesson chat (not implemented in code yet) + +**Email Notifications:** +- If target user is offline (`!target.online?`) and message is present +- Sends email via `UserMailer.lesson_chat(chat_msg).deliver_now` + +--- + +## Data Flow Diagrams + +### Create Message Flow + +``` +Client (jam-ui) + ↓ POST /api/chat +ApiChatsController#create + ↓ +ChatMessage.create(user, session, message, channel, client_id, target_user, lesson) + ↓ Validates + Sanitizes + ↓ Saves to DB +ChatMessage.send_chat_msg(...) + ↓ +MessageFactory.chat_message(...) → Protocol Buffer + ↓ +MQRouter.publish_to_* + ↓ RabbitMQ +WebSocket Gateway + ↓ WebSocket +All Clients (in channel) + ↓ +JamServer.handleMessage(CHAT_MESSAGE) + ↓ +ChatActions.msgReceived(payload) + ↓ +Redux Store Update + ↓ +UI Re-render +``` + +### Get History Flow + +``` +Client (jam-ui) + ↓ GET /api/chat?channel=session&music_session=123 +ApiChatsController#index + ↓ +ChatMessage.index(user, params) + ↓ Queries DB (with pagination) + ↓ Includes user records + ↓ Returns {chats: [...], next: cursor} +Rabl Template (api_chats/index.rabl) + ↓ JSON serialization +Client receives chat history + ↓ +Merges with Redux store + ↓ +UI displays messages +``` + +--- + +## Example API Calls + +### Create Session Message + +**Request:** +```bash +POST /api/chat +Content-Type: application/json +Cookie: remember_token=... + +{ + "message": "Great jam everyone!", + "channel": "session", + "music_session": "abc123", + "client_id": "client-uuid-456" +} +``` + +**Response:** +```json +{ + "id": "msg-789", + "message": "Great jam everyone!", + "user_id": "user-123", + "session_id": null, + "created_at": "2026-01-26T15:30:00.000Z", + "channel": "session", + "lesson_session_id": null, + "purpose": null, + "user": { + "name": "John Doe" + }, + "music_notation": null, + "claimed_recording": null +} +``` + +**WebSocket Broadcast (to all session participants):** +```json +{ + "type": "CHAT_MESSAGE", + "chat_message": { + "sender_name": "John Doe", + "sender_id": "user-123", + "msg": "Great jam everyone!", + "msg_id": "msg-789", + "created_at": "2026-01-26T15:30:00.000Z", + "channel": "session", + "lesson_session_id": null, + "purpose": null + } +} +``` + +--- + +### Get Session History + +**Request:** +```bash +GET /api/chat?channel=session&music_session=abc123&limit=20&page=0 +Cookie: remember_token=... +``` + +**Response:** +```json +{ + "next": 20, + "chats": [ + { + "id": "msg-789", + "message": "Great jam everyone!", + "user_id": "user-123", + "session_id": null, + "created_at": "2026-01-26T15:30:00.000Z", + "channel": "session", + "lesson_session_id": null, + "purpose": null, + "user": { + "name": "John Doe" + }, + "music_notation": null, + "claimed_recording": null + }, + { + "id": "msg-788", + "message": "Let's start!", + "user_id": "user-456", + "session_id": null, + "created_at": "2026-01-26T15:29:00.000Z", + "channel": "session", + "lesson_session_id": null, + "purpose": null, + "user": { + "name": "Jane Smith" + }, + "music_notation": null, + "claimed_recording": null + } + ] +} +``` + +**Pagination:** +- If `next` is not null: More messages available +- Next request: `GET /api/chat?...&start=20` + +--- + +### Create Global Message + +**Request:** +```bash +POST /api/chat +Content-Type: application/json +Cookie: remember_token=... + +{ + "message": "Anyone want to jam?", + "channel": "global", + "client_id": "client-uuid-456" +} +``` + +**Response:** +```json +{ + "id": "msg-999", + "message": "Anyone want to jam?", + "user_id": "user-123", + "session_id": null, + "created_at": "2026-01-26T15:31:00.000Z", + "channel": "global", + "lesson_session_id": null, + "purpose": null, + "user": { + "name": "John Doe" + }, + "music_notation": null, + "claimed_recording": null +} +``` + +**WebSocket Broadcast (to all active clients):** +```json +{ + "type": "CHAT_MESSAGE", + "chat_message": { + "sender_name": "John Doe", + "sender_id": "user-123", + "msg": "Anyone want to jam?", + "msg_id": "msg-999", + "created_at": "2026-01-26T15:31:00.000Z", + "channel": "global", + "lesson_session_id": null, + "purpose": null + } +} +``` + +--- + +## Missing Functionality + +### Read/Unread Tracking + +**Current State:** +- ✅ Exists for **lesson chat**: `teacher_unread_messages`, `student_unread_messages` boolean flags +- ❌ Does NOT exist for **session chat** or **global chat** +- ❌ No per-message read receipts +- ❌ No persistent unread counts + +**What Needs to Be Added (Phase 6):** + +1. **Database Schema Changes:** + - Add `last_read_at` timestamp to track when user last viewed channel + - OR: Add `unread_count` cache column per user/channel + - OR: Add `message_reads` join table for per-message tracking (overkill for MVP) + +2. **API Endpoints:** + - `PUT /api/chat/mark_read` - Mark messages as read for current user/channel + - `GET /api/chat/unread_count` - Get unread count per channel + +3. **WebSocket Messages:** + - Broadcast read receipts to other participants (optional, Phase 11) + +4. **Business Logic:** + - Increment unread count when message received (if user not viewing chat) + - Reset unread count when user opens chat window + - Show badge on chat button with unread count + +**Implementation Strategy (to be designed in Phase 6 Plan 2):** +- Use localStorage for client-side unread tracking (simple, no schema changes) +- OR: Use server-side tracking with new `chat_message_reads` table (more robust) +- Preference: Server-side for cross-device consistency + +--- + +### File Attachments + +**Current State:** +- ✅ Lesson chat supports attachments (`music_notation`, `claimed_recording`) +- ❌ Session chat does NOT support attachments +- ❌ No UI in jam-ui for attaching files + +**Out of Scope for Phase 6:** +- File attachment functionality deferred to next milestone +- Can be added later without breaking changes (fields already exist) + +--- + +## jamClient Methods + +**Conclusion:** Chat is purely web-based. No native client (C++) methods exist. + +**Relevant jamClient Calls:** +- `jamClient.UserAttention(true)` - Triggers system notification when message received (if chat hidden) + +**Note:** In jam-ui, we won't use legacy jamClient directly. Instead, use `JamServer.js` for WebSocket and browser Notification API for desktop notifications. + +--- + +## Security & Validation + +### Message Validation +- Length: 1-255 characters +- Profanity filter: `no_profanity: true` validation +- HTML sanitization: Strips all tags (`html_sanitize strict: [:message]`) + +### Access Control +- **Global Channel:** + - Blocked for school users (`user.school_id` present) + - All non-school users can send/receive +- **Session Channel:** + - Must be participant: `music_session.access?(current_user)` + - Validated via `check_session` before_filter +- **Lesson Channel:** + - Must be teacher or student: `lesson_session.access?(current_user)` + - Auto-routes messages between teacher/student + - School protection: Can only chat with same school (unless platform instructor) + +### Rate Limiting +- **NOT IMPLEMENTED** in current API +- Consider adding in Phase 11 (error handling) + +--- + +## Performance Considerations + +### Database Indexes +- ✅ Indexed: `channel`, `created_at`, `music_session_id` +- ❌ NOT indexed: `lesson_session_id` (should add if lesson chat heavily used) +- ❌ NOT indexed: `user_id` (consider adding for user history queries) + +### Query Patterns +- Default scope orders by `created_at DESC` - always scans newest messages first +- Pagination uses offset (not cursor) - can be slow for deep pages +- Eager loads `user` records - prevents N+1 queries + +### Message Retention +- Global channel: Client-side limit of 100 messages (`max_global_msgs`) +- Session/lesson: No limit in database (messages persist forever) +- **Consider:** Add TTL or archiving for old messages (future optimization) + +--- + +## Integration Checklist for jam-ui + +### Phase 7 (Data Layer): +- [ ] Create Redux slice for chat messages (`chatSlice.js`) +- [ ] Add API client methods: `fetchChatHistory()`, `sendChatMessage()` +- [ ] Add WebSocket message handler for `CHAT_MESSAGE` type +- [ ] Implement message deduplication (by `msg_id`) +- [ ] Add read/unread tracking logic + +### Phase 8 (UI Components): +- [ ] Create `JKSessionChatWindow` component +- [ ] Render message list with timestamps +- [ ] Add message composition area (textarea + send button) +- [ ] Implement auto-scroll on new messages +- [ ] Add loading states (fetching history) + +### Phase 9 (State Management): +- [ ] Wire up Redux actions (send, receive, fetch history) +- [ ] Integrate WebSocket subscription +- [ ] Handle session join/leave (activate channel) +- [ ] Sync unread counts with Redux state + +### Phase 10 (Testing): +- [ ] Unit tests: API client, Redux reducers +- [ ] Integration tests: Message send/receive flow +- [ ] E2E tests: Complete chat workflow + +### Phase 11 (Error Handling): +- [ ] Handle API errors (404, 403, 422) +- [ ] Handle WebSocket disconnection +- [ ] Retry failed message sends +- [ ] Show error messages to user + +--- + +## Summary + +**REST API:** +- ✅ Simple 2-endpoint design (create, index) +- ✅ Channel-based routing (global, session, lesson) +- ✅ Access control enforced +- ❌ No read/unread tracking for session chat +- ❌ No rate limiting + +**WebSocket:** +- ✅ Real-time delivery via Protocol Buffers +- ✅ Efficient routing (session, user, broadcast) +- ✅ Attachment metadata included + +**Database:** +- ✅ Single table for all channels +- ✅ Foreign keys for associations +- ✅ Indexes for common queries +- ❌ No read tracking columns + +**Missing Features:** +- Read/unread tracking (session/global) +- Rate limiting +- Message deletion/editing +- File attachments for session chat + +**Next Steps:** +- Analyze React patterns in jam-ui (CHAT_REACT_PATTERNS.md) +- Design Redux state structure (Phase 6 Plan 2) +- Design read/unread tracking strategy (Phase 6 Plan 2)