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 <noreply@anthropic.com>
This commit is contained in:
Nuwan 2026-01-26 15:20:17 +05:30
parent 940009c22d
commit 2cb263542e
1 changed files with 798 additions and 0 deletions

View File

@ -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)