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