ryOS ryOS / Docs
GitHub Launch

Rooms API

Endpoints for creating, listing, and managing chat rooms.

Overview

Chat rooms are either public (visible to all users) or private (visible only to members). Users can create, join, leave, and delete rooms.

Endpoint Summary

MethodEndpointDescriptionAuth
GET/api/roomsList rooms (public + private membership)No
POST/api/roomsCreate roomYes
GET/api/rooms/{id}Get room by IDNo
DELETE/api/rooms/{id}Delete roomYes
POST/api/rooms/{id}/joinJoin roomYes
POST/api/rooms/{id}/leaveLeave roomYes
GET/api/rooms/{id}/usersList active usersNo
POST/api/rooms/{id}/typingBroadcast typing indicatorYes

Endpoints

List Rooms

Get all public rooms and private rooms visible to the authenticated user (if provided).

GET /api/rooms?username=alice
Query Parameters:
ParameterTypeRequiredDescription
usernamestringNoLegacy compatibility query param (visibility still comes from auth token when present)
Response (200):
{
  "rooms": [
    {
      "id": "general",
      "name": "General Chat",
      "type": "public",
      "createdAt": 1704067200000,
      "userCount": 15
    },
    {
      "id": "abc123",
      "name": "Private Room",
      "type": "private",
      "members": ["alice", "bob"],
      "createdAt": 1704153600000,
      "userCount": 2
    }
  ]
}

Create Room

Create a new chat room. Requires authentication.

Notes:

  • Public room creation is admin-only (ryo).
  • Private room creation requires at least one member; creator is auto-included.

POST /api/rooms
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json

{
  "type": "private",
  "members": ["alice", "bob"],
  "name": "Project Discussion"  // optional
}
Request Body:
FieldTypeRequiredDescription
typestringYes"public" or "private"
membersstring[]For privateList of usernames
namestringPublicPublic room name (private names are generated)
Response (200):
{
  "room": {
    "id": "abc123def456",
    "name": "@alice, @bob",
    "type": "private",
    "members": ["alice", "bob"],
    "createdAt": 1704067200000,
    "userCount": 2
  }
}

Get Room

Get details for a specific room.

GET /api/rooms/{id}
Response (200):
{
  "room": {
    "id": "general",
    "name": "General Chat",
    "type": "public",
    "createdAt": 1704067200000,
    "userCount": 15
  }
}

Delete Room

Delete a room.

  • Public rooms: admin-only (ryo).
  • Private rooms: members can "delete" by leaving; room is removed automatically when <= 1 members remain.

DELETE /api/rooms/{id}
Authorization: Bearer {token}
X-Username: alice
Response (200):
{
  "success": true
}

Join Room

Join a room to participate in chat.

POST /api/rooms/{id}/join
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json

{
  "username": "alice"
}
Response (200):
{
  "success": true
}

Leave Room

Leave a room.

POST /api/rooms/{id}/leave
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json

{
  "username": "alice"
}
Response (200):
{
  "success": true
}

List Active Users

Get list of users currently active in a room.

GET /api/rooms/{id}/users
Response (200):
{
  "users": [
    {
      "username": "alice",
      "joinedAt": 1704067200000
    },
    {
      "username": "bob",
      "joinedAt": 1704153600000
    }
  ]
}

Typing Indicator

Broadcast a transient typing indicator to room members over the realtime channel. Requires authentication and write access to the room (members for private rooms).

POST /api/rooms/{id}/typing
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json

{
  "isTyping": true
}
Request Body:
FieldTypeRequiredDescription
isTypingbooleanNoWhether the user is typing (defaults to true; only false clears it)
Response (200):
{
  "success": true
}

Nothing is persisted; the indicator is delivered as a realtime broadcast only.

Room Types

Public Rooms

  • Visible to all users in room listings
  • Anyone can join without invitation
  • Default room type for general discussions

Private Rooms

  • Only visible to members
  • Created with a specific member list
  • Ideal for direct messages or group chats

Listen Together Sessions

"Listen Together" lets users share synchronized music/video playback. One member is the host (session owner) and one connection is the DJ (the playback device whose state everyone syncs to). Sessions support both authenticated members and anonymous listeners (joined via an anonymousId). Connections are disambiguated by an optional clientInstanceId so the same user can join from multiple devices. All mutations broadcast over the session's realtime channel.

Endpoint Summary

MethodEndpointDescriptionAuth
GET/api/listen/sessionsList active sessionsNo
POST/api/listen/sessionsCreate a sessionYes
GET/api/listen/sessions/{id}Get session stateNo
POST/api/listen/sessions/{id}/joinJoin a sessionYes
POST/api/listen/sessions/{id}/leaveLeave a sessionYes
POST/api/listen/sessions/{id}/syncSync playback state (DJ only)Yes
POST/api/listen/sessions/{id}/assign-djAssign the playback device (host only)Yes
POST/api/listen/sessions/{id}/transfer-hostTransfer session ownership (host only)Yes
POST/api/listen/sessions/{id}/remote-commandSend a playback intent to the DJYes
POST/api/listen/sessions/{id}/reactionSend an emoji reactionYes

*join/leave accept either an authenticated user or an anonymous listener (anonymousId in the body, no auth). Providing both is a 400.

Session Object

interface ListenSession {
  id: string;
  hostUsername: string;
  hostClientInstanceId?: string;
  djUsername: string;
  djClientInstanceId?: string;
  createdAt: number;
  currentTrackId: string | null;
  currentTrackMeta: { title: string; artist?: string; cover?: string; coverColor?: string } | null;
  isPlaying: boolean;
  positionMs: number;
  lastSyncAt: number;
  users: Array<{ username: string; joinedAt: number; isOnline: boolean; clientInstanceId?: string }>;
  anonymousListeners?: Array<{ anonymousId: string; joinedAt: number }>;
}

List / Create Sessions

GET /api/listen/sessions

Returns lightweight summaries (stale sessions older than 30 minutes are filtered out), sorted by listener count then recency:

{
  "sessions": [
    {
      "id": "abc123",
      "hostUsername": "alice",
      "djUsername": "alice",
      "createdAt": 1704067200000,
      "currentTrackMeta": { "title": "Song", "artist": "Artist" },
      "isPlaying": true,
      "listenerCount": 3
    }
  ]
}
POST /api/listen/sessions
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json

{ "clientInstanceId": "device-1" }

The creator becomes both host and DJ. Response (201): { "session": ListenSession }.

Get Session

GET /api/listen/sessions/{id}
Response (200): { "session": ListenSession }, or 404 if the session does not exist.

Join / Leave

POST /api/listen/sessions/{id}/join
Authorization: Bearer {token}
X-Username: bob
Content-Type: application/json

{ "clientInstanceId": "device-2" }

Anonymous variant (no auth): { "anonymousId": "anon-xyz" }. Response (200): { "session": ListenSession } for authenticated joins. leave returns { "success": true } (with session when the user remains absent but the session lives on). When the host leaves or the last member leaves, the session ends and a session-ended broadcast is sent.

Sync Playback (DJ only)

POST /api/listen/sessions/{id}/sync
Authorization: Bearer {token}
X-Username: alice
Content-Type: application/json

{
  "clientInstanceId": "device-1",
  "state": {
    "currentTrackId": "dQw4w9WgXcQ",
    "currentTrackMeta": { "title": "Song", "artist": "Artist" },
    "isPlaying": true,
    "positionMs": 42000,
    "djUsername": "alice",
    "djClientInstanceId": "device-1"
  }
}

Only the current DJ connection may sync. state.isPlaying (boolean) and state.positionMs (number) are required. The state may also hand off the DJ role to another active member. Response (200): { "success": true }.

Assign DJ / Transfer Host (host only)

POST /api/listen/sessions/{id}/assign-dj
{ "nextDjUsername": "bob", "nextDjClientInstanceId": "device-2" }

POST /api/listen/sessions/{id}/transfer-host
{ "nextHostUsername": "bob", "nextHostClientInstanceId": "device-2" }

Both require the caller to be the current host connection and the target to be an active session member. Response (200): { "success": true, "session": ListenSession }.

Remote Command

Non-DJ members submit playback intents; the DJ client applies them and re-syncs.

POST /api/listen/sessions/{id}/remote-command
{ "action": "play" | "pause" | "seek" | "next" | "previous" | "playTrack", "positionMs": 1000, "trackId": "...", "trackMeta": { ... } }
seek requires positionMs; playTrack requires trackId. The DJ connection itself may not send remote commands. Response (200): { "success": true }.

Reaction

POST /api/listen/sessions/{id}/reaction
{ "emoji": "🎉" }

Requires a logged-in member; emoji must be ≤ 8 characters. Response (200): { "success": true }.

IRC Servers

ryOS can bridge chat rooms to public IRC networks. Admins register IRC server configs; any authenticated user can browse the channels a registered server advertises.

Endpoint Summary

MethodEndpointDescriptionAuth
GET/api/irc/serversList registered IRC serversNo
POST/api/irc/serversRegister an IRC serverAdmin (ryo)
DELETE/api/irc/servers/{id}Remove an IRC serverAdmin (ryo)
GET/api/irc/servers/{id}/channelsList channels advertised by a serverYes

List / Create Servers

GET /api/irc/servers
{
  "servers": [
    { "id": "abc", "label": "Pieter", "host": "irc.pieter.com", "port": 6667, "tls": false, "createdAt": 1704067200000 }
  ]
}
POST /api/irc/servers
Authorization: Bearer {token}
X-Username: ryo
Content-Type: application/json

{ "label": "Libera", "host": "irc.libera.chat", "port": 6697, "tls": true }
Response (201): { "server": IrcServer }.

Delete Server

DELETE /api/irc/servers/{id} (admin only). The built-in default irc.pieter.com server cannot be deleted (400). Response (200): { "success": true }.

List Channels

GET /api/irc/servers/{id}/channels?limit=500&timeoutMs=15000

Runs an IRC LIST against the configured server via the bridge. limit (1–2000, default 500) and timeoutMs (1000–30000, default 15000) are optional.

{
  "server": { "id": "abc", "label": "Pieter", "host": "irc.pieter.com", "port": 6667, "tls": false },
  "channels": [
    { "channel": "#pieter", "numUsers": 42, "topic": "welcome" }
  ],
  "truncated": false
}

Returns 503 when the IRC bridge is disabled in the current environment.

Error Responses

StatusErrorDescription
400Invalid requestMissing or invalid parameters
401Authentication requiredToken missing or invalid
403ForbiddenNot authorized to perform action (admin/member checks)
404Room not foundRoom ID doesn't exist
429Rate limit exceededToo many requests

Related