openvibers/openvibe.contracts Commits
Add Chat's internal read capabilities and firstchat to send-result (#36)
Changes
@@@ -4,6 +4,24 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) thfrom `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,the notes were in the tag and commit messages (`git tag -n1`).++## 0.95.0 — 2026-10-04++++**OpenVibe.Chat's internal read API** (plan T3, J4b): OpenVibe.Live stops reading its mirrored copy of Chat's tables.++Additive. Six new internal capabilities owned by chat (loopback, Chat service token, Live's principal today; never++granted to an app or delegated), added to `manifests/services/chat.json`:++ - `chat.stats.read` (`POST /internal/chat/stats`): `chat.stats-request@1` (`kind` site, user, stream or channel-top,++ an optional epoch-ms window, `limit` 1-50) → `chat.stats-result@1` (`messages`, `chatters`, `sounds`, `top_chatters`).++ - `chat.messages.read` (`GET /internal/chat/messages`) → `chat.messages-page@1` (`chat_messages` rows, `max_id`).++ - `chat.analysis.read` (`GET /internal/chat/timeline`) → `chat.timeline-result@1` (`buckets` of `{t, count}`, `max_id`).++ - `chat.moderation.queue.read` (`GET /internal/chat/moderation/pending-ip`, `…/relay-users`, `…/relay-users/:id`,++ `…/tts-override`) → `chat.moderation-queue-result@1`, one shape per route with Chat's `pending_ip_messages`,++ `hidden_relay_users` and `tts_voice_overrides` rows. `chat.moderation.read` is unchanged.++ - `chat.sounds.read` (`GET /internal/chat/sounds`) → `chat.sounds-result@1` (`count`, or the `channel_sounds` rows++ still without a Media asset).++ - `chat.sounds.write` (`POST /internal/chat/sounds/asset`): `chat.sound-asset-request@1` → `chat.ingress-ack@1`.++++`chat.send-result@1` 1.2.0: the `POST /internal/chat/messages` answer gains optional `first_chat` (boolean).++## 0.94.2 — 2026-10-04**`past_due` renewal grace on the billing events** (plan T5 step 11, s1). Additive: no field is required or removed.@@@ -4559,7 +4559,7 @@},{"id": "chat.send-result",-- "version": "1.1.0",++ "version": "1.2.0","owner": "chat","schema": "chat/send-result.v1.json","visibility": "first-party",@@@ -4638,6 +4638,69 @@"compatibility": "backward","status": "active"},++ {++ "id": "chat.stats-request",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/stats-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.stats-result",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/stats-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.messages-page",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/messages-page.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.timeline-result",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/timeline-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.moderation-queue-result",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/moderation-queue-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.sounds-result",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/sounds-result.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },++ {++ "id": "chat.sound-asset-request",++ "version": "1.0.0",++ "owner": "chat",++ "schema": "chat/sound-asset-request.v1.json",++ "visibility": "first-party",++ "compatibility": "backward",++ "status": "active"++ },{"id": "live.moderation.action","version": "1.0.0",nnew file mode 100644@@@ -0,0 +1,141 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/messages-page.v1.json",++ "title": "ChatMessagesPage",++ "description": "chat.messages-page@1: the answer of GET /internal/chat/messages on OpenVibe.Chat (capability chat.messages.read), which pages non-deleted chat_messages for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history. Query: one filter of channel_user_id, stream_id, user_id, anon_id, username or id (one message), a cursor on id (after_id ascending, before_id descending), limit, and tail=1 (no rows, only max_id). max_id is the highest id matching the filter, null when none, so a caller can resume after it. A failure is chat.ingress-ack@1's error.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "messages",++ "max_id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "messages": {++ "type": "array",++ "items": {++ "$ref": "#/$defs/message"++ }++ },++ "max_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ],++ "$defs": {++ "message": {++ "type": "object",++ "required": [++ "id",++ "user_id",++ "anon_id",++ "username",++ "message",++ "message_type",++ "is_global",++ "stream_id",++ "channel_user_id",++ "source_platform",++ "reply_to_id",++ "timestamp"++ ],++ "description": "One chat_messages row (Chat's columns, as stored).",++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "Live user id; null for an anonymous or relayed sender."++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ],++ "description": "'anon12345' for an anonymous sender."++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "message": {++ "type": "string"++ },++ "message_type": {++ "type": "string",++ "enum": [++ "chat",++ "system",++ "donation",++ "command",++ "tts",++ "channel-sound",++ "soundboard",++ "clip"++ ]++ },++ "is_global": {++ "type": "integer",++ "enum": [++ 0,++ 1++ ],++ "description": "1 for homepage global chat."++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The broadcaster whose channel room it is."++ },++ "source_platform": {++ "type": [++ "string",++ "null"++ ],++ "description": "The relay platform of a relayed line; null for OpenVibe chat."++ },++ "reply_to_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "timestamp": {++ "type": "string",++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it (ov_now())."++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,302 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/moderation-queue-result.v1.json",++ "title": "ChatModerationQueueResult",++ "description": "chat.moderation-queue-result@1: what OpenVibe.Chat's moderation queue reads answer (capability chat.moderation.queue.read), one shape per route, for OpenVibe.Live's moderator pages: GET /internal/chat/moderation/pending-ip?channel_id → { pending_ip } (a channel's messages held for IP approval, status pending, oldest first); GET /internal/chat/moderation/relay-users?channel_id → { relay_users } (relay names hidden or banned on the channel or everywhere, newest first); GET /internal/chat/moderation/relay-users/:id → { relay_user } (one, null when unknown); GET /internal/chat/moderation/tts-override?identity_key → { tts_override } (null when none). Internal: rows carry IP addresses. A failure is chat.ingress-ack@1's error.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "pending_ip"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "pending_ip": {++ "type": "array",++ "items": {++ "$ref": "#/$defs/pending_ip_message"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "relay_users"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "relay_users": {++ "type": "array",++ "items": {++ "$ref": "#/$defs/hidden_relay_user"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "relay_user"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "relay_user": {++ "anyOf": [++ {++ "$ref": "#/$defs/hidden_relay_user"++ },++ {++ "type": "null"++ }++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "tts_override"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "tts_override": {++ "anyOf": [++ {++ "$ref": "#/$defs/tts_voice_override"++ },++ {++ "type": "null"++ }++ ]++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ],++ "$defs": {++ "pending_ip_message": {++ "type": "object",++ "description": "One pending_ip_messages row.",++ "required": [++ "id",++ "channel_id",++ "stream_id",++ "ip_address",++ "user_id",++ "anon_id",++ "username",++ "message",++ "status",++ "reviewed_by",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "channel_id": {++ "type": "integer",++ "minimum": 1++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "ip_address": {++ "type": "string",++ "description": "The sender's address: internal, never shown outside moderation."++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "message": {++ "type": "string"++ },++ "status": {++ "type": "string",++ "enum": [++ "pending",++ "approved",++ "denied"++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."++ }++ }++ },++ "hidden_relay_user": {++ "type": "object",++ "description": "One hidden_relay_users row.",++ "required": [++ "id",++ "channel_id",++ "platform",++ "external_username",++ "action",++ "reason",++ "created_by",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "null hides the name on every channel."++ },++ "platform": {++ "type": "string"++ },++ "external_username": {++ "type": "string"++ },++ "action": {++ "type": "string",++ "enum": [++ "hide",++ "ban"++ ]++ },++ "reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "created_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "created_by_username": {++ "type": [++ "string",++ "null"++ ],++ "description": "The moderator's name from Chat's copy of the user, when known."++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."++ }++ }++ },++ "tts_voice_override": {++ "type": "object",++ "description": "One tts_voice_overrides row.",++ "required": [++ "identity_key",++ "voice",++ "pitch",++ "speed",++ "gap",++ "set_by",++ "updated_at"++ ],++ "properties": {++ "identity_key": {++ "type": "string",++ "minLength": 1,++ "description": "Lowercased identity the override applies to."++ },++ "voice": {++ "type": [++ "string",++ "null"++ ]++ },++ "pitch": {++ "type": [++ "integer",++ "null"++ ]++ },++ "speed": {++ "type": [++ "integer",++ "null"++ ]++ },++ "gap": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0++ },++ "set_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."++ }++ }++ }++ }++}@@@ -2,7 +2,7 @@"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/chat/send-result.v1.json","title": "ChatSendResult",-- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",++ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id, first_chat) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).","anyOf": [{"type": "object",@@@ -230,6 +230,10 @@"to_user_id": {"type": "integer","description": "DM only."++ },++ "first_chat": {++ "type": "boolean",++ "description": "Room only, since 1.2.0: true when this is the sender's first message in this channel's stream (Chat's stream_first_chats), so Live can welcome them without reading its own copy; absent on a DM and from an older Chat."}}},nnew file mode 100644@@@ -0,0 +1,31 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/sound-asset-request.v1.json",++ "title": "ChatSoundAssetRequest",++ "description": "chat.sound-asset-request@1: the body of POST /internal/chat/sounds/asset on OpenVibe.Chat (capability chat.sounds.write): after OpenVibe.Live's asset sync uploaded a channel sound to OpenVibe.Media, the asset is written back onto Chat's channel_sounds row id. Writing the same asset again is a no-op; an unknown id is 404. Answers chat.ingress-ack@1.",++ "type": "object",++ "required": [++ "id",++ "media_url",++ "media_asset_id"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1,++ "description": "channel_sounds.id"++ },++ "media_url": {++ "type": "string",++ "minLength": 1,++ "maxLength": 2048,++ "description": "The asset's OpenVibe.Media URL."++ },++ "media_asset_id": {++ "type": "integer",++ "minimum": 1,++ "description": "The OpenVibe.Media asset id."++ }++ }++}nnew file mode 100644@@@ -0,0 +1,104 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/sounds-result.v1.json",++ "title": "ChatSoundsResult",++ "description": "chat.sounds-result@1: the answer of GET /internal/chat/sounds on OpenVibe.Chat (capability chat.sounds.read). ?channel_owner_id answers { count } (a channel's custom sounds, for Live's channel readiness); ?pending_asset=1 answers { sounds } (rows with no media_asset_id yet, which Live's asset sync uploads to OpenVibe.Media and writes back with POST /internal/chat/sounds/asset). A failure is chat.ingress-ack@1's error.",++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "count": {++ "type": "integer",++ "minimum": 0++ },++ "sounds": {++ "type": "array",++ "items": {++ "$ref": "#/$defs/sound"++ }++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ],++ "$defs": {++ "sound": {++ "type": "object",++ "description": "One channel_sounds row.",++ "required": [++ "id",++ "channel_owner_id",++ "command",++ "url",++ "media_url",++ "media_asset_id"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "channel_owner_id": {++ "type": "integer",++ "minimum": 1++ },++ "command": {++ "type": "string",++ "description": "The trigger word (no leading '!'), lowercased: the sound's name."++ },++ "url": {++ "type": "string",++ "description": "Where Chat keeps the audio file (its on-disk path)."++ },++ "mime": {++ "type": [++ "string",++ "null"++ ]++ },++ "duration_seconds": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0++ },++ "created_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The uploader's Live user id."++ },++ "created_by_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "media_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "The file's OpenVibe.Media URL; null until Live's asset sync uploaded it."++ },++ "media_asset_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The OpenVibe.Media asset id; null until uploaded."++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,89 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/stats-request.v1.json",++ "title": "ChatStatsRequest",++ "description": "chat.stats-request@1: the body of POST /internal/chat/stats on OpenVibe.Chat (capability chat.stats.read): one count over chat_messages, for the stats OpenVibe.Live shows without reading its own copy of Chat's tables (home and SEO stats, admin dashboard, star of the day, VOD and recap cards, stream analytics, top chatters, AI slogans). kind site counts all of chat; user one person's messages (user_id); stream one stream's messages, chatters and soundboard plays (stream_id); channel-top the top chatters, of one stream (stream_id) or one channel (channel_user_id), or of all chat when neither is given. since and until bound chat_messages.timestamp, epoch ms, until exclusive. Deleted messages are never counted. Loopback only, service token.",++ "type": "object",++ "required": [++ "kind"++ ],++ "additionalProperties": false,++ "properties": {++ "kind": {++ "type": "string",++ "enum": [++ "site",++ "user",++ "stream",++ "channel-top"++ ]++ },++ "user_id": {++ "type": "integer",++ "minimum": 1,++ "description": "Live user id; required for kind user."++ },++ "stream_id": {++ "type": "integer",++ "minimum": 1,++ "description": "Live stream id; required for kind stream."++ },++ "channel_user_id": {++ "type": "integer",++ "minimum": 1,++ "description": "The broadcaster whose channel room it is (chat_messages.channel_user_id)."++ },++ "since": {++ "type": "integer",++ "minimum": 0,++ "description": "Count messages at or after this instant (epoch ms)."++ },++ "until": {++ "type": "integer",++ "minimum": 0,++ "description": "Count messages before this instant (epoch ms)."++ },++ "limit": {++ "type": "integer",++ "minimum": 1,++ "maximum": 50,++ "description": "How many top chatters (channel-top); Chat's default is 10."++ }++ },++ "allOf": [++ {++ "if": {++ "properties": {++ "kind": {++ "const": "user"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "user_id"++ ]++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "stream"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "stream_id"++ ]++ }++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,88 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/stats-result.v1.json",++ "title": "ChatStatsResult",++ "description": "chat.stats-result@1: the answer of POST /internal/chat/stats (chat.stats.read). site, user and stream answer messages (non-deleted chat_messages rows) and chatters (distinct COALESCE(user_id, anon_id, source_platform || username)); stream also answers sounds (soundboard plays); channel-top answers top_chatters, most messages first. A failure is chat.ingress-ack@1's error ({ ok: false, error }).",++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "messages": {++ "type": "integer",++ "minimum": 0++ },++ "chatters": {++ "type": "integer",++ "minimum": 0++ },++ "sounds": {++ "type": "integer",++ "minimum": 0,++ "description": "Soundboard plays (message_type soundboard)."++ },++ "top_chatters": {++ "type": "array",++ "maxItems": 50,++ "items": {++ "$ref": "#/$defs/chatter"++ }++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ],++ "$defs": {++ "chatter": {++ "type": "object",++ "required": [++ "user_id",++ "username",++ "display_name",++ "count"++ ],++ "properties": {++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "Live user id; null for an anonymous or relayed chatter, counted by name."++ },++ "username": {++ "type": "string"++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "description": "From Chat's copy of the user (ctx_users); null when unknown."++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ },++ "count": {++ "type": "integer",++ "minimum": 1,++ "description": "Messages in the window."++ }++ }++ }++ }++}nnew file mode 100644@@@ -0,0 +1,53 @@++{++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/timeline-result.v1.json",++ "title": "ChatTimelineResult",++ "description": "chat.timeline-result@1: the answer of GET /internal/chat/timeline on OpenVibe.Chat (capability chat.analysis.read): non-deleted chat_messages of one channel (channel_user_id) or stream (stream_id) counted per bucket between since and until (epoch ms), bucket_ms wide, for OpenVibe.Live's AI moments, chat spikes and activity. Empty buckets are left out; buckets are oldest first. max_id is the highest message id in the window, null when none. A failure is chat.ingress-ack@1's error.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "buckets",++ "max_id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "buckets": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "t",++ "count"++ ],++ "additionalProperties": false,++ "properties": {++ "t": {++ "type": "integer",++ "minimum": 0,++ "description": "Bucket start, epoch ms."++ },++ "count": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ "max_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,18 @@++{++ "messages": [++ {++ "id": 90211,++ "user_id": 512,++ "anon_id": null,++ "username": "pixelfox",++ "message": "that was clean",++ "message_type": "chat",++ "is_global": 0,++ "stream_id": 4211,++ "channel_user_id": 77,++ "source_platform": null,++ "reply_to_id": null,++ "timestamp": "2026-10-04 21:14:09"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,19 @@++{++ "messages": [++ {++ "id": 90211,++ "user_id": 512,++ "anon_id": null,++ "username": "pixelfox",++ "message": "that was clean",++ "message_type": "whisper",++ "is_global": 0,++ "stream_id": 4211,++ "channel_user_id": 77,++ "source_platform": null,++ "reply_to_id": null,++ "timestamp": "2026-10-04 21:14:09"++ }++ ],++ "max_id": 90211++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "messages": [],++ "max_id": null++}nnew file mode 100644@@@ -0,0 +1,33 @@++{++ "messages": [++ {++ "id": 90211,++ "user_id": 512,++ "anon_id": null,++ "username": "pixelfox",++ "message": "that was clean",++ "message_type": "chat",++ "is_global": 0,++ "stream_id": 4211,++ "channel_user_id": 77,++ "source_platform": null,++ "reply_to_id": null,++ "timestamp": "2026-10-04 21:14:09"++ },++ {++ "id": 90214,++ "user_id": null,++ "anon_id": "anon48213",++ "username": "anon48213",++ "message": "!airhorn",++ "message_type": "soundboard",++ "is_global": 0,++ "stream_id": 4211,++ "channel_user_id": 77,++ "source_platform": null,++ "reply_to_id": 90211,++ "timestamp": "2026-10-04 21:14:31"++ }++ ],++ "max_id": 90214++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "messages": [],++ "max_id": 90214++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "pending_ip": [++ {++ "id": 311,++ "channel_id": 77,++ "stream_id": 4211,++ "ip_address": "203.0.113.24",++ "user_id": null,++ "anon_id": "anon48213",++ "username": "anon48213",++ "message": "first time here",++ "status": "held",++ "reviewed_by": null,++ "created_at": "2026-10-04 21:02:40"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "relay_user": {++ "id": 58,++ "platform": "kick"++ }++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "relay_user": null,++ "tts_override": null++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "ok": false,++ "error": "channel_id required"++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "pending_ip": [++ {++ "id": 311,++ "channel_id": 77,++ "stream_id": 4211,++ "ip_address": "203.0.113.24",++ "user_id": null,++ "anon_id": "anon48213",++ "username": "anon48213",++ "message": "first time here",++ "status": "pending",++ "reviewed_by": null,++ "created_at": "2026-10-04 21:02:40"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "relay_user": null++}nnew file mode 100644@@@ -0,0 +1,26 @@++{++ "relay_users": [++ {++ "id": 58,++ "channel_id": 77,++ "platform": "kick",++ "external_username": "spam_bot_9",++ "action": "ban",++ "reason": "link spam",++ "created_by": 512,++ "created_by_username": "pixelfox",++ "created_at": "2026-10-03 18:40:00"++ },++ {++ "id": 12,++ "channel_id": null,++ "platform": "kick",++ "external_username": "spam_bot_9",++ "action": "hide",++ "reason": null,++ "created_by": 512,++ "created_by_username": null,++ "created_at": "2026-10-03 18:40:00"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,11 @@++{++ "tts_override": {++ "identity_key": "user:512",++ "voice": "en-US-Wavenet-D",++ "pitch": 2,++ "speed": 110,++ "gap": 0,++ "set_by": 77,++ "updated_at": "2026-10-01 09:12:55"++ }++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "ok": true,++ "id": 90215,++ "stream_id": 4211,++ "channel_user_id": 77,++ "first_chat": 1++}nnew file mode 100644@@@ -0,0 +1,7 @@++{++ "ok": true,++ "id": 90215,++ "stream_id": 4211,++ "channel_user_id": 77,++ "first_chat": true++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "id": 904,++ "media_url": "https://openvibe.media/a/31882/airhorn.mp3",++ "media_asset_id": 31882,++ "channel_owner_id": 77++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "id": 904,++ "media_url": "https://openvibe.media/a/31882/airhorn.mp3"++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "id": 904,++ "media_url": "https://openvibe.media/a/31882/airhorn.mp3",++ "media_asset_id": 31882++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "count": -2++}nnew file mode 100644@@@ -0,0 +1,15 @@++{++ "sounds": [++ {++ "id": 904,++ "channel_owner_id": 77,++ "command": "airhorn",++ "mime": "audio/mpeg",++ "duration_seconds": 2.4,++ "created_by": 512,++ "created_by_name": "pixelfox",++ "media_url": null,++ "media_asset_id": null++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "count": 14++}nnew file mode 100644@@@ -0,0 +1,16 @@++{++ "sounds": [++ {++ "id": 904,++ "channel_owner_id": 77,++ "command": "airhorn",++ "url": "/sounds/77/airhorn.mp3",++ "mime": "audio/mpeg",++ "duration_seconds": 2.4,++ "created_by": 512,++ "created_by_name": "pixelfox",++ "media_url": null,++ "media_asset_id": null++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,5 @@++{++ "kind": "channel-top",++ "stream_id": 4211,++ "limit": 51++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "kind": "global"++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "kind": "user",++ "since": 1759536000000++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "kind": "channel-top",++ "channel_user_id": 77,++ "since": 1758326400000,++ "limit": 20++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "kind": "site",++ "since": 1759536000000++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "kind": "stream",++ "stream_id": 4211++}nnew file mode 100644@@@ -0,0 +1,9 @@++{++ "top_chatters": [++ {++ "user_id": 512,++ "username": "pixelfox",++ "display_name": "PixelFox"++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,3 @@++{++ "messages": -1++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "ok": false,++ "error": "kind user needs user_id"++}nnew file mode 100644@@@ -0,0 +1,6 @@++{++ "ok": true,++ "messages": 1834,++ "chatters": 96,++ "sounds": 41++}nnew file mode 100644@@@ -0,0 +1,17 @@++{++ "ok": true,++ "top_chatters": [++ {++ "user_id": 512,++ "username": "pixelfox",++ "display_name": "PixelFox",++ "count": 214++ },++ {++ "user_id": null,++ "username": "kick_viewer",++ "display_name": null,++ "count": 37++ }++ ]++}nnew file mode 100644@@@ -0,0 +1,8 @@++{++ "buckets": [++ {++ "count": 12++ }++ ],++ "max_id": 90214++}nnew file mode 100644@@@ -0,0 +1,13 @@++{++ "buckets": [++ {++ "t": 1759612440000,++ "count": 12++ },++ {++ "t": 1759612500000,++ "count": 31++ }++ ],++ "max_id": 90214++}nnew file mode 100644@@@ -0,0 +1,4 @@++{++ "buckets": [],++ "max_id": null++}@@@ -49305,14 +49305,14 @@}},"chat.send-result": {-- "version": "1.1.0",++ "version": "1.2.0","owner": "chat","status": "active","schema": {"$schema": "https://json-schema.org/draft/2020-12/schema","$id": "https://openvibe.network/contracts/chat/send-result.v1.json","title": "ChatSendResult",-- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",++ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id, first_chat) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).","anyOf": [{"type": "object",@@@ -49540,6 +49540,10 @@"to_user_id": {"type": "integer","description": "DM only."++ },++ "first_chat": {++ "type": "boolean",++ "description": "Room only, since 1.2.0: true when this is the sender's first message in this channel's stream (Chat's stream_first_chats), so Live can welcome them without reading its own copy; absent on a DM and from an older Chat."}}},@@@ -51716,6 +51720,849 @@]}},++ "chat.stats-request": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/stats-request.v1.json",++ "title": "ChatStatsRequest",++ "description": "chat.stats-request@1: the body of POST /internal/chat/stats on OpenVibe.Chat (capability chat.stats.read): one count over chat_messages, for the stats OpenVibe.Live shows without reading its own copy of Chat's tables (home and SEO stats, admin dashboard, star of the day, VOD and recap cards, stream analytics, top chatters, AI slogans). kind site counts all of chat; user one person's messages (user_id); stream one stream's messages, chatters and soundboard plays (stream_id); channel-top the top chatters, of one stream (stream_id) or one channel (channel_user_id), or of all chat when neither is given. since and until bound chat_messages.timestamp, epoch ms, until exclusive. Deleted messages are never counted. Loopback only, service token.",++ "type": "object",++ "required": [++ "kind"++ ],++ "additionalProperties": false,++ "properties": {++ "kind": {++ "type": "string",++ "enum": [++ "site",++ "user",++ "stream",++ "channel-top"++ ]++ },++ "user_id": {++ "type": "integer",++ "minimum": 1,++ "description": "Live user id; required for kind user."++ },++ "stream_id": {++ "type": "integer",++ "minimum": 1,++ "description": "Live stream id; required for kind stream."++ },++ "channel_user_id": {++ "type": "integer",++ "minimum": 1,++ "description": "The broadcaster whose channel room it is (chat_messages.channel_user_id)."++ },++ "since": {++ "type": "integer",++ "minimum": 0,++ "description": "Count messages at or after this instant (epoch ms)."++ },++ "until": {++ "type": "integer",++ "minimum": 0,++ "description": "Count messages before this instant (epoch ms)."++ },++ "limit": {++ "type": "integer",++ "minimum": 1,++ "maximum": 50,++ "description": "How many top chatters (channel-top); Chat's default is 10."++ }++ },++ "allOf": [++ {++ "if": {++ "properties": {++ "kind": {++ "const": "user"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "user_id"++ ]++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "stream"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "stream_id"++ ]++ }++ }++ ]++ }++ },++ "chat.stats-result": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/stats-result.v1.json",++ "title": "ChatStatsResult",++ "description": "chat.stats-result@1: the answer of POST /internal/chat/stats (chat.stats.read). site, user and stream answer messages (non-deleted chat_messages rows) and chatters (distinct COALESCE(user_id, anon_id, source_platform || username)); stream also answers sounds (soundboard plays); channel-top answers top_chatters, most messages first. A failure is chat.ingress-ack@1's error ({ ok: false, error }).",++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "messages": {++ "type": "integer",++ "minimum": 0++ },++ "chatters": {++ "type": "integer",++ "minimum": 0++ },++ "sounds": {++ "type": "integer",++ "minimum": 0,++ "description": "Soundboard plays (message_type soundboard)."++ },++ "top_chatters": {++ "type": "array",++ "maxItems": 50,++ "items": {++ "$ref": "#/$defs/chatter"++ }++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ],++ "$defs": {++ "chatter": {++ "type": "object",++ "required": [++ "user_id",++ "username",++ "display_name",++ "count"++ ],++ "properties": {++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "Live user id; null for an anonymous or relayed chatter, counted by name."++ },++ "username": {++ "type": "string"++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "description": "From Chat's copy of the user (ctx_users); null when unknown."++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ },++ "count": {++ "type": "integer",++ "minimum": 1,++ "description": "Messages in the window."++ }++ }++ }++ }++ }++ },++ "chat.messages-page": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/messages-page.v1.json",++ "title": "ChatMessagesPage",++ "description": "chat.messages-page@1: the answer of GET /internal/chat/messages on OpenVibe.Chat (capability chat.messages.read), which pages non-deleted chat_messages for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history. Query: one filter of channel_user_id, stream_id, user_id, anon_id, username or id (one message), a cursor on id (after_id ascending, before_id descending), limit, and tail=1 (no rows, only max_id). max_id is the highest id matching the filter, null when none, so a caller can resume after it. A failure is chat.ingress-ack@1's error.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "messages",++ "max_id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "messages": {++ "type": "array",++ "items": {++ "$ref": "#/$defs/message"++ }++ },++ "max_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ],++ "$defs": {++ "message": {++ "type": "object",++ "required": [++ "id",++ "user_id",++ "anon_id",++ "username",++ "message",++ "message_type",++ "is_global",++ "stream_id",++ "channel_user_id",++ "source_platform",++ "reply_to_id",++ "timestamp"++ ],++ "description": "One chat_messages row (Chat's columns, as stored).",++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "Live user id; null for an anonymous or relayed sender."++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ],++ "description": "'anon12345' for an anonymous sender."++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "message": {++ "type": "string"++ },++ "message_type": {++ "type": "string",++ "enum": [++ "chat",++ "system",++ "donation",++ "command",++ "tts",++ "channel-sound",++ "soundboard",++ "clip"++ ]++ },++ "is_global": {++ "type": "integer",++ "enum": [++ 0,++ 1++ ],++ "description": "1 for homepage global chat."++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The broadcaster whose channel room it is."++ },++ "source_platform": {++ "type": [++ "string",++ "null"++ ],++ "description": "The relay platform of a relayed line; null for OpenVibe chat."++ },++ "reply_to_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "timestamp": {++ "type": "string",++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it (ov_now())."++ }++ }++ }++ }++ }++ },++ "chat.timeline-result": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/timeline-result.v1.json",++ "title": "ChatTimelineResult",++ "description": "chat.timeline-result@1: the answer of GET /internal/chat/timeline on OpenVibe.Chat (capability chat.analysis.read): non-deleted chat_messages of one channel (channel_user_id) or stream (stream_id) counted per bucket between since and until (epoch ms), bucket_ms wide, for OpenVibe.Live's AI moments, chat spikes and activity. Empty buckets are left out; buckets are oldest first. max_id is the highest message id in the window, null when none. A failure is chat.ingress-ack@1's error.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "buckets",++ "max_id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "buckets": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "t",++ "count"++ ],++ "additionalProperties": false,++ "properties": {++ "t": {++ "type": "integer",++ "minimum": 0,++ "description": "Bucket start, epoch ms."++ },++ "count": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ "max_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ]++ }++ },++ "chat.moderation-queue-result": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/moderation-queue-result.v1.json",++ "title": "ChatModerationQueueResult",++ "description": "chat.moderation-queue-result@1: what OpenVibe.Chat's moderation queue reads answer (capability chat.moderation.queue.read), one shape per route, for OpenVibe.Live's moderator pages: GET /internal/chat/moderation/pending-ip?channel_id → { pending_ip } (a channel's messages held for IP approval, status pending, oldest first); GET /internal/chat/moderation/relay-users?channel_id → { relay_users } (relay names hidden or banned on the channel or everywhere, newest first); GET /internal/chat/moderation/relay-users/:id → { relay_user } (one, null when unknown); GET /internal/chat/moderation/tts-override?identity_key → { tts_override } (null when none). Internal: rows carry IP addresses. A failure is chat.ingress-ack@1's error.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "pending_ip"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "pending_ip": {++ "type": "array",++ "items": {++ "$ref": "#/$defs/pending_ip_message"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "relay_users"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "relay_users": {++ "type": "array",++ "items": {++ "$ref": "#/$defs/hidden_relay_user"++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "relay_user"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "relay_user": {++ "anyOf": [++ {++ "$ref": "#/$defs/hidden_relay_user"++ },++ {++ "type": "null"++ }++ ]++ }++ }++ },++ {++ "type": "object",++ "required": [++ "tts_override"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "tts_override": {++ "anyOf": [++ {++ "$ref": "#/$defs/tts_voice_override"++ },++ {++ "type": "null"++ }++ ]++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ],++ "$defs": {++ "pending_ip_message": {++ "type": "object",++ "description": "One pending_ip_messages row.",++ "required": [++ "id",++ "channel_id",++ "stream_id",++ "ip_address",++ "user_id",++ "anon_id",++ "username",++ "message",++ "status",++ "reviewed_by",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "channel_id": {++ "type": "integer",++ "minimum": 1++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "ip_address": {++ "type": "string",++ "description": "The sender's address: internal, never shown outside moderation."++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ]++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "message": {++ "type": "string"++ },++ "status": {++ "type": "string",++ "enum": [++ "pending",++ "approved",++ "denied"++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."++ }++ }++ },++ "hidden_relay_user": {++ "type": "object",++ "description": "One hidden_relay_users row.",++ "required": [++ "id",++ "channel_id",++ "platform",++ "external_username",++ "action",++ "reason",++ "created_by",++ "created_at"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "null hides the name on every channel."++ },++ "platform": {++ "type": "string"++ },++ "external_username": {++ "type": "string"++ },++ "action": {++ "type": "string",++ "enum": [++ "hide",++ "ban"++ ]++ },++ "reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "created_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "created_by_username": {++ "type": [++ "string",++ "null"++ ],++ "description": "The moderator's name from Chat's copy of the user, when known."++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."++ }++ }++ },++ "tts_voice_override": {++ "type": "object",++ "description": "One tts_voice_overrides row.",++ "required": [++ "identity_key",++ "voice",++ "pitch",++ "speed",++ "gap",++ "set_by",++ "updated_at"++ ],++ "properties": {++ "identity_key": {++ "type": "string",++ "minLength": 1,++ "description": "Lowercased identity the override applies to."++ },++ "voice": {++ "type": [++ "string",++ "null"++ ]++ },++ "pitch": {++ "type": [++ "integer",++ "null"++ ]++ },++ "speed": {++ "type": [++ "integer",++ "null"++ ]++ },++ "gap": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0++ },++ "set_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."++ }++ }++ }++ }++ }++ },++ "chat.sounds-result": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/sounds-result.v1.json",++ "title": "ChatSoundsResult",++ "description": "chat.sounds-result@1: the answer of GET /internal/chat/sounds on OpenVibe.Chat (capability chat.sounds.read). ?channel_owner_id answers { count } (a channel's custom sounds, for Live's channel readiness); ?pending_asset=1 answers { sounds } (rows with no media_asset_id yet, which Live's asset sync uploads to OpenVibe.Media and writes back with POST /internal/chat/sounds/asset). A failure is chat.ingress-ack@1's error.",++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "count": {++ "type": "integer",++ "minimum": 0++ },++ "sounds": {++ "type": "array",++ "items": {++ "$ref": "#/$defs/sound"++ }++ }++ }++ },++ {++ "$ref": "ingress-ack.v1.json#/$defs/error"++ }++ ],++ "$defs": {++ "sound": {++ "type": "object",++ "description": "One channel_sounds row.",++ "required": [++ "id",++ "channel_owner_id",++ "command",++ "url",++ "media_url",++ "media_asset_id"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "channel_owner_id": {++ "type": "integer",++ "minimum": 1++ },++ "command": {++ "type": "string",++ "description": "The trigger word (no leading '!'), lowercased: the sound's name."++ },++ "url": {++ "type": "string",++ "description": "Where Chat keeps the audio file (its on-disk path)."++ },++ "mime": {++ "type": [++ "string",++ "null"++ ]++ },++ "duration_seconds": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0++ },++ "created_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The uploader's Live user id."++ },++ "created_by_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "media_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "The file's OpenVibe.Media URL; null until Live's asset sync uploaded it."++ },++ "media_asset_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The OpenVibe.Media asset id; null until uploaded."++ }++ }++ }++ }++ }++ },++ "chat.sound-asset-request": {++ "version": "1.0.0",++ "owner": "chat",++ "status": "active",++ "schema": {++ "$schema": "https://json-schema.org/draft/2020-12/schema",++ "$id": "https://openvibe.network/contracts/chat/sound-asset-request.v1.json",++ "title": "ChatSoundAssetRequest",++ "description": "chat.sound-asset-request@1: the body of POST /internal/chat/sounds/asset on OpenVibe.Chat (capability chat.sounds.write): after OpenVibe.Live's asset sync uploaded a channel sound to OpenVibe.Media, the asset is written back onto Chat's channel_sounds row id. Writing the same asset again is a no-op; an unknown id is 404. Answers chat.ingress-ack@1.",++ "type": "object",++ "required": [++ "id",++ "media_url",++ "media_asset_id"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1,++ "description": "channel_sounds.id"++ },++ "media_url": {++ "type": "string",++ "minLength": 1,++ "maxLength": 2048,++ "description": "The asset's OpenVibe.Media URL."++ },++ "media_asset_id": {++ "type": "integer",++ "minimum": 1,++ "description": "The OpenVibe.Media asset id."++ }++ }++ }++ },"live.moderation.action": {"version": "1.0.0","owner": "live",@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.AI API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Billing API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Blog API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Bot API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Chat API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -129,6 +129,50 @@}},"/internal/chat/messages": {++ "get": {++ "operationId": "get_internal_chat_messages",++ "summary": "Page OpenVibe.Chat's stored messages of one channel, stream, person, anonymous id or name, or read one by id, with a cursor on the message id and the latest id: for OpenVibe.Live's AI context, clip an",++ "description": "**chat.messages.read** (internal): Page OpenVibe.Chat's stored messages of one channel, stream, person, anonymous id or name, or read one by id, with a cursor on the message id and the latest id: for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "tags": [++ "chat.messages"++ ],++ "x-openvibe-capabilities": [++ "chat.messages.read"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "internal-read",++ "security": [++ {++ "openvibeToken": [++ "chat.messages.read"++ ]++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.messages-page.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ },"post": {"operationId": "post_internal_chat_messages","summary": "Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",@@@ -240,45 +284,35 @@}}},-- "/internal/live/calls": {-- "post": {-- "operationId": "post_internal_live_calls",-- "summary": "Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",-- "description": "**chat.message.send** (first-party): Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",++ "/internal/chat/moderation/pending-ip": {++ "get": {++ "operationId": "get_internal_chat_moderation_pending_ip",++ "summary": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's ",++ "description": "**chat.moderation.queue.read** (internal): Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.","tags": [-- "chat.message"++ "chat.moderation"],"x-openvibe-capabilities": [-- "chat.message.send"++ "chat.moderation.queue.read"],"x-openvibe-visibility": [-- "first-party"++ "internal"],-- "x-openvibe-quota-class": "chat-send",++ "x-openvibe-quota-class": "internal-read","security": [{"openvibeToken": [-- "chat.message.send"++ "chat.moderation.queue.read"]}],-- "requestBody": {-- "required": false,-- "content": {-- "application/json": {-- "schema": {-- "$ref": "#/components/schemas/chat.send-request.v1"-- }-- }-- }-- },"responses": {"2XX": {"description": "Success","content": {"application/json": {"schema": {-- "$ref": "#/components/schemas/chat.send-result.v1"++ "$ref": "#/components/schemas/chat.moderation-queue-result.v1"}}}@@@ -296,16 +330,16 @@}}},-- "/internal/moderation/channels/{channelId}": {++ "/internal/chat/moderation/relay-users": {"get": {-- "operationId": "get_internal_moderation_channels_channelId",-- "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",-- "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",++ "operationId": "get_internal_chat_moderation_relay_users",++ "summary": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's ",++ "description": "**chat.moderation.queue.read** (internal): Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.","tags": ["chat.moderation"],"x-openvibe-capabilities": [-- "chat.moderation.read"++ "chat.moderation.queue.read"],"x-openvibe-visibility": ["internal"@@@ -314,27 +348,17 @@"security": [{"openvibeToken": [-- "chat.moderation.read"++ "chat.moderation.queue.read"]}],-- "parameters": [-- {-- "name": "channelId",-- "in": "path",-- "required": true,-- "schema": {-- "type": "string"-- }-- }-- ],"responses": {"2XX": {"description": "Success","content": {"application/json": {"schema": {-- "$ref": "#/components/schemas/chat.channel-moderation-result.v1"++ "$ref": "#/components/schemas/chat.moderation-queue-result.v1"}}}@@@ -352,16 +376,16 @@}}},-- "/internal/moderation/channels/{channelId}/emote-count": {++ "/internal/chat/moderation/relay-users/{id}": {"get": {-- "operationId": "get_internal_moderation_channels_channelId_emote_count",-- "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",-- "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",++ "operationId": "get_internal_chat_moderation_relay_users_id",++ "summary": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's ",++ "description": "**chat.moderation.queue.read** (internal): Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.","tags": ["chat.moderation"],"x-openvibe-capabilities": [-- "chat.moderation.read"++ "chat.moderation.queue.read"],"x-openvibe-visibility": ["internal"@@@ -370,13 +394,13 @@"security": [{"openvibeToken": [-- "chat.moderation.read"++ "chat.moderation.queue.read"]}],"parameters": [{-- "name": "channelId",++ "name": "id","in": "path","required": true,"schema": {@@@ -390,7 +414,7 @@"content": {"application/json": {"schema": {-- "$ref": "#/components/schemas/chat.channel-moderation-result.v1"++ "$ref": "#/components/schemas/chat.moderation-queue-result.v1"}}}@@@ -408,16 +432,16 @@}}},-- "/internal/moderation/users/{userId}/channels": {++ "/internal/chat/moderation/tts-override": {"get": {-- "operationId": "get_internal_moderation_users_userId_channels",-- "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",-- "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",++ "operationId": "get_internal_chat_moderation_tts_override",++ "summary": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's ",++ "description": "**chat.moderation.queue.read** (internal): Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.","tags": ["chat.moderation"],"x-openvibe-capabilities": [-- "chat.moderation.read"++ "chat.moderation.queue.read"],"x-openvibe-visibility": ["internal"@@@ -426,19 +450,55 @@"security": [{"openvibeToken": [-- "chat.moderation.read"++ "chat.moderation.queue.read"]}],-- "parameters": [-- {-- "name": "userId",-- "in": "path",-- "required": true,-- "schema": {-- "type": "string"++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.moderation-queue-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }}}++ }++ }++ },++ "/internal/chat/sounds": {++ "get": {++ "operationId": "get_internal_chat_sounds",++ "summary": "Count a channel's custom sounds, or list the sounds not yet uploaded to OpenVibe.Media, from OpenVibe.Chat's channel_sounds: for OpenVibe.Live's channel readiness and its Media asset sync (plan T3, J4",++ "description": "**chat.sounds.read** (internal): Count a channel's custom sounds, or list the sounds not yet uploaded to OpenVibe.Media, from OpenVibe.Chat's channel_sounds: for OpenVibe.Live's channel readiness and its Media asset sync (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "tags": [++ "chat.sounds"++ ],++ "x-openvibe-capabilities": [++ "chat.sounds.read"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "internal-read",++ "security": [++ {++ "openvibeToken": [++ "chat.sounds.read"++ ]++ }],"responses": {"2XX": {@@@ -446,7 +506,7 @@"content": {"application/json": {"schema": {-- "$ref": "#/components/schemas/chat.channel-moderation-result.v1"++ "$ref": "#/components/schemas/chat.sounds-result.v1"}}}@@@ -463,62 +523,444 @@}}}-- }-- },-- "components": {-- "schemas": {-- "chat.channel-moderation-result.v1": {-- "title": "ChatChannelModerationResult",-- "description": "chat.channel-moderation-result@1: the answer of GET /internal/moderation/channels/:channelId on OpenVibe.Chat (capability chat.moderation.read): { ok: true, settings, moderator_ids } — one channel's moderation settings and the ids of the people who moderate it, for OpenVibe.Live's stream chat. settings carries every channel_moderation_settings column (Live's own defaults when the channel has no row yet, database.js getChannelModerationSettings); the switches are 0/1 as the table stores them. moderator_ids are Live user ids (channel_moderators.user_id), never names. A failure is { ok: false, error }; a caller without the grant gets 401 { ok: false }.",-- "anyOf": [++ },++ "/internal/chat/sounds/asset": {++ "post": {++ "operationId": "post_internal_chat_sounds_asset",++ "summary": "Write a channel sound's OpenVibe.Media asset (URL and asset id) back onto OpenVibe.Chat's channel_sounds row after OpenVibe.Live's asset sync uploaded the file (plan T3, J4b).",++ "description": "**chat.sounds.write** (internal): Write a channel sound's OpenVibe.Media asset (URL and asset id) back onto OpenVibe.Chat's channel_sounds row after OpenVibe.Live's asset sync uploaded the file (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "tags": [++ "chat.sounds"++ ],++ "x-openvibe-capabilities": [++ "chat.sounds.write"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "internal-write",++ "security": [{-- "type": "object",-- "required": [-- "ok",-- "settings",-- "moderator_ids"-- ],-- "properties": {-- "ok": {-- "const": true-- },-- "settings": {-- "$ref": "#/components/schemas/chat.channel-moderation-result.v1/$defs/settings"-- },-- "moderator_ids": {-- "type": "array",-- "description": "The channel's moderators, oldest first (channel_moderators.created_at); empty when it has none.",-- "items": {-- "type": "integer",-- "minimum": 1++ "openvibeToken": [++ "chat.sounds.write"++ ]++ }++ ],++ "requestBody": {++ "required": false,++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.sound-asset-request.v1"++ }++ }++ }++ },++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.ingress-ack.v1"}}}},-- {-- "type": "object",-- "required": [-- "ok",-- "error"-- ],-- "properties": {-- "ok": {-- "const": false-- },-- "error": {-- "type": "string"++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }}}}++ }++ }++ },++ "/internal/chat/stats": {++ "post": {++ "operationId": "post_internal_chat_stats",++ "summary": "Count OpenVibe.Chat's stored messages: all of chat, one person's, one stream's (messages, chatters, soundboard plays) or the top chatters of a stream, a channel or all chat, in an optional time window",++ "description": "**chat.stats.read** (internal): Count OpenVibe.Chat's stored messages: all of chat, one person's, one stream's (messages, chatters, soundboard plays) or the top chatters of a stream, a channel or all chat, in an optional time window — the stats OpenVibe.Live shows (home, SEO, admin, star of the day, VOD and recap cards, stream analytics, AI slogans) without reading its own copy of Chat's tables (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "tags": [++ "chat.stats"],-- "$defs": {-- "flag": {-- "type": "integer",-- "enum": [-- 0,-- 1-- ],-- "description": "A switch as the SQLite table stores it."++ "x-openvibe-capabilities": [++ "chat.stats.read"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "internal-read",++ "security": [++ {++ "openvibeToken": [++ "chat.stats.read"++ ]++ }++ ],++ "requestBody": {++ "required": false,++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.stats-request.v1"++ }++ }++ }++ },++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.stats-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/chat/timeline": {++ "get": {++ "operationId": "get_internal_chat_timeline",++ "summary": "Count a channel's or a stream's chat messages per time bucket, with the latest message id: the chat timeline behind OpenVibe.Live's AI moments, chat spikes and activity reads (plan T3, J4b).",++ "description": "**chat.analysis.read** (internal): Count a channel's or a stream's chat messages per time bucket, with the latest message id: the chat timeline behind OpenVibe.Live's AI moments, chat spikes and activity reads (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "tags": [++ "chat.analysis"++ ],++ "x-openvibe-capabilities": [++ "chat.analysis.read"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "internal-read",++ "security": [++ {++ "openvibeToken": [++ "chat.analysis.read"++ ]++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.timeline-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/live/calls": {++ "post": {++ "operationId": "post_internal_live_calls",++ "summary": "Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",++ "description": "**chat.message.send** (first-party): Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",++ "tags": [++ "chat.message"++ ],++ "x-openvibe-capabilities": [++ "chat.message.send"++ ],++ "x-openvibe-visibility": [++ "first-party"++ ],++ "x-openvibe-quota-class": "chat-send",++ "security": [++ {++ "openvibeToken": [++ "chat.message.send"++ ]++ }++ ],++ "requestBody": {++ "required": false,++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.send-request.v1"++ }++ }++ }++ },++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.send-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/moderation/channels/{channelId}": {++ "get": {++ "operationId": "get_internal_moderation_channels_channelId",++ "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",++ "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",++ "tags": [++ "chat.moderation"++ ],++ "x-openvibe-capabilities": [++ "chat.moderation.read"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "internal-read",++ "security": [++ {++ "openvibeToken": [++ "chat.moderation.read"++ ]++ }++ ],++ "parameters": [++ {++ "name": "channelId",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.channel-moderation-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/moderation/channels/{channelId}/emote-count": {++ "get": {++ "operationId": "get_internal_moderation_channels_channelId_emote_count",++ "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",++ "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",++ "tags": [++ "chat.moderation"++ ],++ "x-openvibe-capabilities": [++ "chat.moderation.read"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "internal-read",++ "security": [++ {++ "openvibeToken": [++ "chat.moderation.read"++ ]++ }++ ],++ "parameters": [++ {++ "name": "channelId",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.channel-moderation-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ },++ "/internal/moderation/users/{userId}/channels": {++ "get": {++ "operationId": "get_internal_moderation_users_userId_channels",++ "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",++ "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",++ "tags": [++ "chat.moderation"++ ],++ "x-openvibe-capabilities": [++ "chat.moderation.read"++ ],++ "x-openvibe-visibility": [++ "internal"++ ],++ "x-openvibe-quota-class": "internal-read",++ "security": [++ {++ "openvibeToken": [++ "chat.moderation.read"++ ]++ }++ ],++ "parameters": [++ {++ "name": "userId",++ "in": "path",++ "required": true,++ "schema": {++ "type": "string"++ }++ }++ ],++ "responses": {++ "2XX": {++ "description": "Success",++ "content": {++ "application/json": {++ "schema": {++ "$ref": "#/components/schemas/chat.channel-moderation-result.v1"++ }++ }++ }++ },++ "default": {++ "description": "An error (RFC 9457 problem details)",++ "content": {++ "application/problem+json": {++ "schema": {++ "$ref": "#/components/schemas/errors.problem.v1"++ }++ }++ }++ }++ }++ }++ }++ },++ "components": {++ "schemas": {++ "chat.channel-moderation-result.v1": {++ "title": "ChatChannelModerationResult",++ "description": "chat.channel-moderation-result@1: the answer of GET /internal/moderation/channels/:channelId on OpenVibe.Chat (capability chat.moderation.read): { ok: true, settings, moderator_ids } — one channel's moderation settings and the ids of the people who moderate it, for OpenVibe.Live's stream chat. settings carries every channel_moderation_settings column (Live's own defaults when the channel has no row yet, database.js getChannelModerationSettings); the switches are 0/1 as the table stores them. moderator_ids are Live user ids (channel_moderators.user_id), never names. A failure is { ok: false, error }; a caller without the grant gets 401 { ok: false }.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "ok",++ "settings",++ "moderator_ids"++ ],++ "properties": {++ "ok": {++ "const": true++ },++ "settings": {++ "$ref": "#/components/schemas/chat.channel-moderation-result.v1/$defs/settings"++ },++ "moderator_ids": {++ "type": "array",++ "description": "The channel's moderators, oldest first (channel_moderators.created_at); empty when it has none.",++ "items": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ {++ "type": "object",++ "required": [++ "ok",++ "error"++ ],++ "properties": {++ "ok": {++ "const": false++ },++ "error": {++ "type": "string"++ }++ }++ }++ ],++ "$defs": {++ "flag": {++ "type": "integer",++ "enum": [++ 0,++ 1++ ],++ "description": "A switch as the SQLite table stores it."},"settings": {"type": "object",@@@ -1276,39 +1718,146 @@"type": "object","properties": {"type": {-- "const": "vc-call-response"++ "const": "vc-call-response"++ }++ }++ },++ "then": {++ "type": "object",++ "required": [++ "status"++ ]++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "type": {++ "const": "vc-call-invite"++ }++ }++ },++ "then": {++ "not": {++ "type": "object",++ "required": [++ "status"++ ]++ }++ }++ }++ ]++ }++ },++ "allOf": [++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "vc-call-invite",++ "vc-call-response"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "user"++ }++ }++ }++ }++ },++ "else": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "not": {++ "const": "user"++ }++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "chat",++ "channel-sound",++ "redemption",++ "vibe-coding"++ ]++ }++ }++ }++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {++ "type": "object",++ "properties": {++ "kind": {++ "const": "stream"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "const": "alert"}}-- },-- "then": {-- "type": "object",-- "required": [-- "status"-- ]}-- },-- {-- "if": {++ }++ },++ "then": {++ "type": "object",++ "properties": {++ "target": {"type": "object","properties": {-- "type": {-- "const": "vc-call-invite"++ "kind": {++ "const": "channel"}}-- },-- "then": {-- "not": {-- "type": "object",-- "required": [-- "status"-- ]-- }}}-- ]-- }-- },-- "allOf": [++ }++ },{"if": {"type": "object",@@@ -1317,10 +1866,7 @@"type": "object","properties": {"type": {-- "enum": [-- "vc-call-invite",-- "vc-call-response"-- ]++ "const": "voice-channels"}}}@@@ -1333,343 +1879,678 @@"type": "object","properties": {"kind": {-- "const": "user"++ "const": "all"++ }++ }++ }++ }++ }++ },++ {++ "if": {++ "type": "object",++ "properties": {++ "frame": {++ "type": "object",++ "properties": {++ "type": {++ "enum": [++ "media_queue_update",++ "media_now_playing"++ ]}}}}},-- "else": {++ "then": {"type": "object","properties": {"target": {"type": "object","properties": {"kind": {-- "not": {-- "const": "user"-- }++ "const": "owner-streams"}}}}}++ }++ ]++ },++ "chat.ingress-ack.v1": {++ "title": "ChatIngressAck",++ "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",++ "$defs": {++ "error": {++ "type": "object",++ "required": [++ "ok",++ "error"++ ],++ "properties": {++ "ok": {++ "const": false++ },++ "error": {++ "type": "string",++ "description": "A human-readable reason."++ }++ }++ }++ },++ "oneOf": [++ {++ "type": "object",++ "required": [++ "ok"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ }++ }++ },++ {++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"++ }++ ]++ },++ "chat.invalidate-request.v1": {++ "title": "ChatInvalidateRequest",++ "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",++ "type": "object",++ "required": [++ "key"++ ],++ "additionalProperties": false,++ "properties": {++ "key": {++ "type": "string",++ "minLength": 1,++ "maxLength": 160,++ "pattern": "^[A-Za-z0-9:._-]+$",++ "description": "Idempotency key, scoped to the caller and to this route."++ },++ "user": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A person whose cached row is stale."++ },++ "user_data": {++ "type": "object",++ "required": [++ "id",++ "username"++ ],++ "additionalProperties": false,++ "description": "The person's new public fields, pushed to their sockets; id must equal user.",++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "username": {++ "type": "string",++ "minLength": 1,++ "maxLength": 120++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "minLength": 1,++ "maxLength": 120++ },++ "role": {++ "type": "string"++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ }++ }++ },++ "approvals": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A channel whose IP approvals changed."++ },++ "bans": {++ "type": [++ "boolean",++ "null"++ ],++ "description": "true: bans changed."++ },++ "channel": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "A channel whose cached settings changed."++ }++ },++ "dependentSchemas": {++ "user_data": {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ "anyOf": [++ {++ "type": "object",++ "required": [++ "user"++ ],++ "properties": {++ "user": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "approvals"++ ],++ "properties": {++ "approvals": {++ "type": "integer",++ "minimum": 1++ }++ }++ },++ {++ "type": "object",++ "required": [++ "channel"++ ],++ "properties": {++ "channel": {++ "type": "integer",++ "minimum": 1++ }++ }},{-- "if": {-- "type": "object",-- "properties": {-- "frame": {-- "type": "object",-- "properties": {-- "type": {-- "enum": [-- "chat",-- "channel-sound",-- "redemption",-- "vibe-coding"-- ]-- }-- }-- }-- }-- },-- "then": {-- "type": "object",-- "properties": {-- "target": {-- "type": "object",-- "properties": {-- "kind": {-- "const": "stream"-- }-- }-- }++ "type": "object",++ "required": [++ "bans"++ ],++ "properties": {++ "bans": {++ "const": true}}-- },++ }++ ]++ },++ "chat.messages-page.v1": {++ "title": "ChatMessagesPage",++ "description": "chat.messages-page@1: the answer of GET /internal/chat/messages on OpenVibe.Chat (capability chat.messages.read), which pages non-deleted chat_messages for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history. Query: one filter of channel_user_id, stream_id, user_id, anon_id, username or id (one message), a cursor on id (after_id ascending, before_id descending), limit, and tail=1 (no rows, only max_id). max_id is the highest id matching the filter, null when none, so a caller can resume after it. A failure is chat.ingress-ack@1's error.",++ "anyOf": [{-- "if": {-- "type": "object",-- "properties": {-- "frame": {-- "type": "object",-- "properties": {-- "type": {-- "const": "alert"-- }-- }-- }-- }-- },-- "then": {-- "type": "object",-- "properties": {-- "target": {-- "type": "object",-- "properties": {-- "kind": {-- "const": "channel"-- }-- }++ "type": "object",++ "required": [++ "messages",++ "max_id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "messages": {++ "type": "array",++ "items": {++ "$ref": "#/components/schemas/chat.messages-page.v1/$defs/message"}++ },++ "max_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1}}},{-- "if": {-- "type": "object",-- "properties": {-- "frame": {-- "type": "object",-- "properties": {-- "type": {-- "const": "voice-channels"-- }-- }-- }++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"++ }++ ],++ "$defs": {++ "message": {++ "type": "object",++ "required": [++ "id",++ "user_id",++ "anon_id",++ "username",++ "message",++ "message_type",++ "is_global",++ "stream_id",++ "channel_user_id",++ "source_platform",++ "reply_to_id",++ "timestamp"++ ],++ "description": "One chat_messages row (Chat's columns, as stored).",++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "Live user id; null for an anonymous or relayed sender."++ },++ "anon_id": {++ "type": [++ "string",++ "null"++ ],++ "description": "'anon12345' for an anonymous sender."++ },++ "username": {++ "type": [++ "string",++ "null"++ ]++ },++ "message": {++ "type": "string"++ },++ "message_type": {++ "type": "string",++ "enum": [++ "chat",++ "system",++ "donation",++ "command",++ "tts",++ "channel-sound",++ "soundboard",++ "clip"++ ]++ },++ "is_global": {++ "type": "integer",++ "enum": [++ 0,++ 1++ ],++ "description": "1 for homepage global chat."++ },++ "stream_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "channel_user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The broadcaster whose channel room it is."++ },++ "source_platform": {++ "type": [++ "string",++ "null"++ ],++ "description": "The relay platform of a relayed line; null for OpenVibe chat."++ },++ "reply_to_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "timestamp": {++ "type": "string",++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it (ov_now())."}-- },-- "then": {-- "type": "object",-- "properties": {-- "target": {-- "type": "object",-- "properties": {-- "kind": {-- "const": "all"-- }-- }++ }++ }++ }++ },++ "chat.moderation-queue-result.v1": {++ "title": "ChatModerationQueueResult",++ "description": "chat.moderation-queue-result@1: what OpenVibe.Chat's moderation queue reads answer (capability chat.moderation.queue.read), one shape per route, for OpenVibe.Live's moderator pages: GET /internal/chat/moderation/pending-ip?channel_id → { pending_ip } (a channel's messages held for IP approval, status pending, oldest first); GET /internal/chat/moderation/relay-users?channel_id → { relay_users } (relay names hidden or banned on the channel or everywhere, newest first); GET /internal/chat/moderation/relay-users/:id → { relay_user } (one, null when unknown); GET /internal/chat/moderation/tts-override?identity_key → { tts_override } (null when none). Internal: rows carry IP addresses. A failure is chat.ingress-ack@1's error.",++ "oneOf": [++ {++ "type": "object",++ "required": [++ "pending_ip"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "pending_ip": {++ "type": "array",++ "items": {++ "$ref": "#/components/schemas/chat.moderation-queue-result.v1/$defs/pending_ip_message"}}}},{-- "if": {-- "type": "object",-- "properties": {-- "frame": {-- "type": "object",-- "properties": {-- "type": {-- "enum": [-- "media_queue_update",-- "media_now_playing"-- ]-- }-- }-- }-- }-- },-- "then": {-- "type": "object",-- "properties": {-- "target": {-- "type": "object",-- "properties": {-- "kind": {-- "const": "owner-streams"-- }-- }++ "type": "object",++ "required": [++ "relay_users"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "relay_users": {++ "type": "array",++ "items": {++ "$ref": "#/components/schemas/chat.moderation-queue-result.v1/$defs/hidden_relay_user"}}}-- }-- ]-- },-- "chat.ingress-ack.v1": {-- "title": "ChatIngressAck",-- "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",-- "$defs": {-- "error": {++ },++ {"type": "object","required": [-- "ok",-- "error"++ "relay_user"],++ "additionalProperties": false,"properties": {"ok": {-- "const": false++ "const": true},-- "error": {-- "type": "string",-- "description": "A human-readable reason."++ "relay_user": {++ "anyOf": [++ {++ "$ref": "#/components/schemas/chat.moderation-queue-result.v1/$defs/hidden_relay_user"++ },++ {++ "type": "null"++ }++ ]}}-- }-- },-- "oneOf": [++ },{"type": "object","required": [-- "ok"++ "tts_override"],"additionalProperties": false,"properties": {"ok": {"const": true++ },++ "tts_override": {++ "anyOf": [++ {++ "$ref": "#/components/schemas/chat.moderation-queue-result.v1/$defs/tts_voice_override"++ },++ {++ "type": "null"++ }++ ]}}},{"$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"}-- ]-- },-- "chat.invalidate-request.v1": {-- "title": "ChatInvalidateRequest",-- "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",-- "type": "object",-- "required": [-- "key"],-- "additionalProperties": false,-- "properties": {-- "key": {-- "type": "string",-- "minLength": 1,-- "maxLength": 160,-- "pattern": "^[A-Za-z0-9:._-]+$",-- "description": "Idempotency key, scoped to the caller and to this route."-- },-- "user": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "A person whose cached row is stale."-- },-- "user_data": {++ "$defs": {++ "pending_ip_message": {"type": "object",++ "description": "One pending_ip_messages row.","required": ["id",-- "username"++ "channel_id",++ "stream_id",++ "ip_address",++ "user_id",++ "anon_id",++ "username",++ "message",++ "status",++ "reviewed_by",++ "created_at"],-- "additionalProperties": false,-- "description": "The person's new public fields, pushed to their sockets; id must equal user.","properties": {"id": {"type": "integer","minimum": 1},-- "username": {-- "type": "string",-- "minLength": 1,-- "maxLength": 120++ "channel_id": {++ "type": "integer",++ "minimum": 1},-- "display_name": {++ "stream_id": {"type": [-- "string",++ "integer","null"],-- "minLength": 1,-- "maxLength": 120++ "minimum": 1},-- "role": {-- "type": "string"++ "ip_address": {++ "type": "string",++ "description": "The sender's address: internal, never shown outside moderation."},-- "avatar_url": {++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "anon_id": {"type": ["string","null"]},-- "profile_color": {++ "username": {"type": ["string","null"]-- }-- }-- },-- "approvals": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "A channel whose IP approvals changed."-- },-- "bans": {-- "type": [-- "boolean",-- "null"-- ],-- "description": "true: bans changed."-- },-- "channel": {-- "type": [-- "integer",-- "null"-- ],-- "minimum": 1,-- "description": "A channel whose cached settings changed."-- }-- },-- "dependentSchemas": {-- "user_data": {-- "type": "object",-- "required": [-- "user"-- ],-- "properties": {-- "user": {-- "type": "integer",-- "minimum": 1-- }-- }-- }-- },-- "anyOf": [-- {-- "type": "object",-- "required": [-- "user"-- ],-- "properties": {-- "user": {-- "type": "integer",++ },++ "message": {++ "type": "string"++ },++ "status": {++ "type": "string",++ "enum": [++ "pending",++ "approved",++ "denied"++ ]++ },++ "reviewed_by": {++ "type": [++ "integer",++ "null"++ ],"minimum": 1++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."}}},-- {++ "hidden_relay_user": {"type": "object",++ "description": "One hidden_relay_users row.","required": [-- "approvals"++ "id",++ "channel_id",++ "platform",++ "external_username",++ "action",++ "reason",++ "created_by",++ "created_at"],"properties": {-- "approvals": {++ "id": {"type": "integer","minimum": 1-- }-- }-- },-- {-- "type": "object",-- "required": [-- "channel"-- ],-- "properties": {-- "channel": {-- "type": "integer",++ },++ "channel_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "null hides the name on every channel."++ },++ "platform": {++ "type": "string"++ },++ "external_username": {++ "type": "string"++ },++ "action": {++ "type": "string",++ "enum": [++ "hide",++ "ban"++ ]++ },++ "reason": {++ "type": [++ "string",++ "null"++ ]++ },++ "created_by": {++ "type": [++ "integer",++ "null"++ ],"minimum": 1++ },++ "created_by_username": {++ "type": [++ "string",++ "null"++ ],++ "description": "The moderator's name from Chat's copy of the user, when known."++ },++ "created_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."}}},-- {++ "tts_voice_override": {"type": "object",++ "description": "One tts_voice_overrides row.","required": [-- "bans"++ "identity_key",++ "voice",++ "pitch",++ "speed",++ "gap",++ "set_by",++ "updated_at"],"properties": {-- "bans": {-- "const": true++ "identity_key": {++ "type": "string",++ "minLength": 1,++ "description": "Lowercased identity the override applies to."++ },++ "voice": {++ "type": [++ "string",++ "null"++ ]++ },++ "pitch": {++ "type": [++ "integer",++ "null"++ ]++ },++ "speed": {++ "type": [++ "integer",++ "null"++ ]++ },++ "gap": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 0++ },++ "set_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1++ },++ "updated_at": {++ "type": [++ "string",++ "null"++ ],++ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."}}}-- ]++ }},"chat.moderation-request.v1": {"title": "ChatModerationRequest",@@@ -2854,7 +3735,7 @@},"chat.send-result.v1": {"title": "ChatSendResult",-- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",++ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id, first_chat) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).","anyOf": [{"type": "object",@@@ -3082,6 +3963,365 @@"to_user_id": {"type": "integer","description": "DM only."++ },++ "first_chat": {++ "type": "boolean",++ "description": "Room only, since 1.2.0: true when this is the sender's first message in this channel's stream (Chat's stream_first_chats), so Live can welcome them without reading its own copy; absent on a DM and from an older Chat."++ }++ }++ },++ {++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"++ }++ ]++ },++ "chat.sound-asset-request.v1": {++ "title": "ChatSoundAssetRequest",++ "description": "chat.sound-asset-request@1: the body of POST /internal/chat/sounds/asset on OpenVibe.Chat (capability chat.sounds.write): after OpenVibe.Live's asset sync uploaded a channel sound to OpenVibe.Media, the asset is written back onto Chat's channel_sounds row id. Writing the same asset again is a no-op; an unknown id is 404. Answers chat.ingress-ack@1.",++ "type": "object",++ "required": [++ "id",++ "media_url",++ "media_asset_id"++ ],++ "additionalProperties": false,++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1,++ "description": "channel_sounds.id"++ },++ "media_url": {++ "type": "string",++ "minLength": 1,++ "maxLength": 2048,++ "description": "The asset's OpenVibe.Media URL."++ },++ "media_asset_id": {++ "type": "integer",++ "minimum": 1,++ "description": "The OpenVibe.Media asset id."++ }++ }++ },++ "chat.sounds-result.v1": {++ "title": "ChatSoundsResult",++ "description": "chat.sounds-result@1: the answer of GET /internal/chat/sounds on OpenVibe.Chat (capability chat.sounds.read). ?channel_owner_id answers { count } (a channel's custom sounds, for Live's channel readiness); ?pending_asset=1 answers { sounds } (rows with no media_asset_id yet, which Live's asset sync uploads to OpenVibe.Media and writes back with POST /internal/chat/sounds/asset). A failure is chat.ingress-ack@1's error.",++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "count": {++ "type": "integer",++ "minimum": 0++ },++ "sounds": {++ "type": "array",++ "items": {++ "$ref": "#/components/schemas/chat.sounds-result.v1/$defs/sound"++ }++ }++ }++ },++ {++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"++ }++ ],++ "$defs": {++ "sound": {++ "type": "object",++ "description": "One channel_sounds row.",++ "required": [++ "id",++ "channel_owner_id",++ "command",++ "url",++ "media_url",++ "media_asset_id"++ ],++ "properties": {++ "id": {++ "type": "integer",++ "minimum": 1++ },++ "channel_owner_id": {++ "type": "integer",++ "minimum": 1++ },++ "command": {++ "type": "string",++ "description": "The trigger word (no leading '!'), lowercased: the sound's name."++ },++ "url": {++ "type": "string",++ "description": "Where Chat keeps the audio file (its on-disk path)."++ },++ "mime": {++ "type": [++ "string",++ "null"++ ]++ },++ "duration_seconds": {++ "type": [++ "number",++ "null"++ ],++ "minimum": 0++ },++ "created_by": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The uploader's Live user id."++ },++ "created_by_name": {++ "type": [++ "string",++ "null"++ ]++ },++ "media_url": {++ "type": [++ "string",++ "null"++ ],++ "description": "The file's OpenVibe.Media URL; null until Live's asset sync uploaded it."++ },++ "media_asset_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "The OpenVibe.Media asset id; null until uploaded."++ }++ }++ }++ }++ },++ "chat.stats-request.v1": {++ "title": "ChatStatsRequest",++ "description": "chat.stats-request@1: the body of POST /internal/chat/stats on OpenVibe.Chat (capability chat.stats.read): one count over chat_messages, for the stats OpenVibe.Live shows without reading its own copy of Chat's tables (home and SEO stats, admin dashboard, star of the day, VOD and recap cards, stream analytics, top chatters, AI slogans). kind site counts all of chat; user one person's messages (user_id); stream one stream's messages, chatters and soundboard plays (stream_id); channel-top the top chatters, of one stream (stream_id) or one channel (channel_user_id), or of all chat when neither is given. since and until bound chat_messages.timestamp, epoch ms, until exclusive. Deleted messages are never counted. Loopback only, service token.",++ "type": "object",++ "required": [++ "kind"++ ],++ "additionalProperties": false,++ "properties": {++ "kind": {++ "type": "string",++ "enum": [++ "site",++ "user",++ "stream",++ "channel-top"++ ]++ },++ "user_id": {++ "type": "integer",++ "minimum": 1,++ "description": "Live user id; required for kind user."++ },++ "stream_id": {++ "type": "integer",++ "minimum": 1,++ "description": "Live stream id; required for kind stream."++ },++ "channel_user_id": {++ "type": "integer",++ "minimum": 1,++ "description": "The broadcaster whose channel room it is (chat_messages.channel_user_id)."++ },++ "since": {++ "type": "integer",++ "minimum": 0,++ "description": "Count messages at or after this instant (epoch ms)."++ },++ "until": {++ "type": "integer",++ "minimum": 0,++ "description": "Count messages before this instant (epoch ms)."++ },++ "limit": {++ "type": "integer",++ "minimum": 1,++ "maximum": 50,++ "description": "How many top chatters (channel-top); Chat's default is 10."++ }++ },++ "allOf": [++ {++ "if": {++ "properties": {++ "kind": {++ "const": "user"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "user_id"++ ]++ }++ },++ {++ "if": {++ "properties": {++ "kind": {++ "const": "stream"++ }++ },++ "required": [++ "kind"++ ]++ },++ "then": {++ "required": [++ "stream_id"++ ]++ }++ }++ ]++ },++ "chat.stats-result.v1": {++ "title": "ChatStatsResult",++ "description": "chat.stats-result@1: the answer of POST /internal/chat/stats (chat.stats.read). site, user and stream answer messages (non-deleted chat_messages rows) and chatters (distinct COALESCE(user_id, anon_id, source_platform || username)); stream also answers sounds (soundboard plays); channel-top answers top_chatters, most messages first. A failure is chat.ingress-ack@1's error ({ ok: false, error }).",++ "anyOf": [++ {++ "type": "object",++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "messages": {++ "type": "integer",++ "minimum": 0++ },++ "chatters": {++ "type": "integer",++ "minimum": 0++ },++ "sounds": {++ "type": "integer",++ "minimum": 0,++ "description": "Soundboard plays (message_type soundboard)."++ },++ "top_chatters": {++ "type": "array",++ "maxItems": 50,++ "items": {++ "$ref": "#/components/schemas/chat.stats-result.v1/$defs/chatter"++ }++ }++ }++ },++ {++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"++ }++ ],++ "$defs": {++ "chatter": {++ "type": "object",++ "required": [++ "user_id",++ "username",++ "display_name",++ "count"++ ],++ "properties": {++ "user_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1,++ "description": "Live user id; null for an anonymous or relayed chatter, counted by name."++ },++ "username": {++ "type": "string"++ },++ "display_name": {++ "type": [++ "string",++ "null"++ ],++ "description": "From Chat's copy of the user (ctx_users); null when unknown."++ },++ "avatar_url": {++ "type": [++ "string",++ "null"++ ]++ },++ "profile_color": {++ "type": [++ "string",++ "null"++ ]++ },++ "count": {++ "type": "integer",++ "minimum": 1,++ "description": "Messages in the window."++ }++ }++ }++ }++ },++ "chat.timeline-result.v1": {++ "title": "ChatTimelineResult",++ "description": "chat.timeline-result@1: the answer of GET /internal/chat/timeline on OpenVibe.Chat (capability chat.analysis.read): non-deleted chat_messages of one channel (channel_user_id) or stream (stream_id) counted per bucket between since and until (epoch ms), bucket_ms wide, for OpenVibe.Live's AI moments, chat spikes and activity. Empty buckets are left out; buckets are oldest first. max_id is the highest message id in the window, null when none. A failure is chat.ingress-ack@1's error.",++ "anyOf": [++ {++ "type": "object",++ "required": [++ "buckets",++ "max_id"++ ],++ "additionalProperties": false,++ "properties": {++ "ok": {++ "const": true++ },++ "buckets": {++ "type": "array",++ "items": {++ "type": "object",++ "required": [++ "t",++ "count"++ ],++ "additionalProperties": false,++ "properties": {++ "t": {++ "type": "integer",++ "minimum": 0,++ "description": "Bucket start, epoch ms."++ },++ "count": {++ "type": "integer",++ "minimum": 1++ }++ }++ }++ },++ "max_id": {++ "type": [++ "integer",++ "null"++ ],++ "minimum": 1}}},@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Codes API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Community API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Coupons API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Deals API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Events API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Games API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Host API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -33,8 +33,8 @@"service": "chat","name": "OpenVibe.Chat","origin": "https://openvibe.chat",-- "operations": 8,-- "capabilities": 5++ "operations": 17,++ "capabilities": 11},{"service": "codes",@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Live API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Media API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Network API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.News API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenRe.Stream API",-- "version": "0.94.2",-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Reviews API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Search API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Sources API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Space API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tips API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Tools API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Trade API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.VIP API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -3,8 +3,8 @@"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema","info": {"title": "OpenVibe.Wiki API",-- "version": "0.94.2",-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",++ "version": "0.95.0",++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.","license": {"name": "MIT","identifier": "MIT"@@@ -47447,9 +47447,9 @@ export type ChatSendRequest =[k: string]: unknown | undefined;};--/** [email protected] (owner: chat) */++/** [email protected] (owner: chat) *//**-- * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).++ * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id, first_chat) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).*/export type ChatSendResult =| {@@@ -47561,6 +47561,10 @@ export type ChatSendResult =* DM only.*/to_user_id?: number;++ /**++ * Room only, since 1.2.0: true when this is the sender's first message in this channel's stream (Chat's stream_first_chats), so Live can welcome them without reading its own copy; absent on a DM and from an older Chat.++ */++ first_chat?: boolean;}| {ok: false;@@@ -48066,6 +48070,317 @@ export type ChatEmoteCountResult =error: string;};++/** [email protected] (owner: chat) */++/**++ * chat.stats-request@1: the body of POST /internal/chat/stats on OpenVibe.Chat (capability chat.stats.read): one count over chat_messages, for the stats OpenVibe.Live shows without reading its own copy of Chat's tables (home and SEO stats, admin dashboard, star of the day, VOD and recap cards, stream analytics, top chatters, AI slogans). kind site counts all of chat; user one person's messages (user_id); stream one stream's messages, chatters and soundboard plays (stream_id); channel-top the top chatters, of one stream (stream_id) or one channel (channel_user_id), or of all chat when neither is given. since and until bound chat_messages.timestamp, epoch ms, until exclusive. Deleted messages are never counted. Loopback only, service token.++ */++export type ChatStatsRequest = {++ [k: string]: unknown | undefined;++} & {++ kind: "site" | "user" | "stream" | "channel-top";++ /**++ * Live user id; required for kind user.++ */++ user_id?: number;++ /**++ * Live stream id; required for kind stream.++ */++ stream_id?: number;++ /**++ * The broadcaster whose channel room it is (chat_messages.channel_user_id).++ */++ channel_user_id?: number;++ /**++ * Count messages at or after this instant (epoch ms).++ */++ since?: number;++ /**++ * Count messages before this instant (epoch ms).++ */++ until?: number;++ /**++ * How many top chatters (channel-top); Chat's default is 10.++ */++ limit?: number;++};++++/** [email protected] (owner: chat) */++/**++ * chat.stats-result@1: the answer of POST /internal/chat/stats (chat.stats.read). site, user and stream answer messages (non-deleted chat_messages rows) and chatters (distinct COALESCE(user_id, anon_id, source_platform || username)); stream also answers sounds (soundboard plays); channel-top answers top_chatters, most messages first. A failure is chat.ingress-ack@1's error ({ ok: false, error }).++ */++export type ChatStatsResult =++ | {++ ok?: true;++ messages?: number;++ chatters?: number;++ /**++ * Soundboard plays (message_type soundboard).++ */++ sounds?: number;++ /**++ * @maxItems 50++ */++ top_chatters?: {++ /**++ * Live user id; null for an anonymous or relayed chatter, counted by name.++ */++ user_id: number | null;++ username: string;++ /**++ * From Chat's copy of the user (ctx_users); null when unknown.++ */++ display_name: string | null;++ avatar_url?: string | null;++ profile_color?: string | null;++ /**++ * Messages in the window.++ */++ count: number;++ }[];++ }++ | {++ ok: false;++ /**++ * A human-readable reason.++ */++ error: string;++ };++++/** [email protected] (owner: chat) */++/**++ * chat.messages-page@1: the answer of GET /internal/chat/messages on OpenVibe.Chat (capability chat.messages.read), which pages non-deleted chat_messages for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history. Query: one filter of channel_user_id, stream_id, user_id, anon_id, username or id (one message), a cursor on id (after_id ascending, before_id descending), limit, and tail=1 (no rows, only max_id). max_id is the highest id matching the filter, null when none, so a caller can resume after it. A failure is chat.ingress-ack@1's error.++ */++export type ChatMessagesPage =++ | {++ ok?: true;++ messages: {++ id: number;++ /**++ * Live user id; null for an anonymous or relayed sender.++ */++ user_id: number | null;++ /**++ * 'anon12345' for an anonymous sender.++ */++ anon_id: string | null;++ username: string | null;++ message: string;++ message_type: "chat" | "system" | "donation" | "command" | "tts" | "channel-sound" | "soundboard" | "clip";++ /**++ * 1 for homepage global chat.++ */++ is_global: 0 | 1;++ stream_id: number | null;++ /**++ * The broadcaster whose channel room it is.++ */++ channel_user_id: number | null;++ /**++ * The relay platform of a relayed line; null for OpenVibe chat.++ */++ source_platform: string | null;++ reply_to_id: number | null;++ /**++ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it (ov_now()).++ */++ timestamp: string;++ }[];++ max_id: number | null;++ }++ | {++ ok: false;++ /**++ * A human-readable reason.++ */++ error: string;++ };++++/** [email protected] (owner: chat) */++/**++ * chat.timeline-result@1: the answer of GET /internal/chat/timeline on OpenVibe.Chat (capability chat.analysis.read): non-deleted chat_messages of one channel (channel_user_id) or stream (stream_id) counted per bucket between since and until (epoch ms), bucket_ms wide, for OpenVibe.Live's AI moments, chat spikes and activity. Empty buckets are left out; buckets are oldest first. max_id is the highest message id in the window, null when none. A failure is chat.ingress-ack@1's error.++ */++export type ChatTimelineResult =++ | {++ ok?: true;++ buckets: {++ /**++ * Bucket start, epoch ms.++ */++ t: number;++ count: number;++ }[];++ max_id: number | null;++ }++ | {++ ok: false;++ /**++ * A human-readable reason.++ */++ error: string;++ };++++/** [email protected] (owner: chat) */++/**++ * chat.moderation-queue-result@1: what OpenVibe.Chat's moderation queue reads answer (capability chat.moderation.queue.read), one shape per route, for OpenVibe.Live's moderator pages: GET /internal/chat/moderation/pending-ip?channel_id → { pending_ip } (a channel's messages held for IP approval, status pending, oldest first); GET /internal/chat/moderation/relay-users?channel_id → { relay_users } (relay names hidden or banned on the channel or everywhere, newest first); GET /internal/chat/moderation/relay-users/:id → { relay_user } (one, null when unknown); GET /internal/chat/moderation/tts-override?identity_key → { tts_override } (null when none). Internal: rows carry IP addresses. A failure is chat.ingress-ack@1's error.++ */++export type ChatModerationQueueResult =++ | {++ ok?: true;++ pending_ip: {++ id: number;++ channel_id: number;++ stream_id: number | null;++ /**++ * The sender's address: internal, never shown outside moderation.++ */++ ip_address: string;++ user_id: number | null;++ anon_id: string | null;++ username: string | null;++ message: string;++ status: "pending" | "approved" | "denied";++ reviewed_by: number | null;++ /**++ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it.++ */++ created_at: string | null;++ }[];++ }++ | {++ ok?: true;++ relay_users: {++ id: number;++ /**++ * null hides the name on every channel.++ */++ channel_id: number | null;++ platform: string;++ external_username: string;++ action: "hide" | "ban";++ reason: string | null;++ created_by: number | null;++ /**++ * The moderator's name from Chat's copy of the user, when known.++ */++ created_by_username?: string | null;++ /**++ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it.++ */++ created_at: string | null;++ }[];++ }++ | {++ ok?: true;++ relay_user: {++ id: number;++ /**++ * null hides the name on every channel.++ */++ channel_id: number | null;++ platform: string;++ external_username: string;++ action: "hide" | "ban";++ reason: string | null;++ created_by: number | null;++ /**++ * The moderator's name from Chat's copy of the user, when known.++ */++ created_by_username?: string | null;++ /**++ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it.++ */++ created_at: string | null;++ } | null;++ }++ | {++ ok?: true;++ tts_override: {++ /**++ * Lowercased identity the override applies to.++ */++ identity_key: string;++ voice: string | null;++ pitch: number | null;++ speed: number | null;++ gap: number | null;++ set_by: number | null;++ /**++ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it.++ */++ updated_at: string | null;++ } | null;++ }++ | {++ ok: false;++ /**++ * A human-readable reason.++ */++ error: string;++ };++++/** [email protected] (owner: chat) */++/**++ * chat.sounds-result@1: the answer of GET /internal/chat/sounds on OpenVibe.Chat (capability chat.sounds.read). ?channel_owner_id answers { count } (a channel's custom sounds, for Live's channel readiness); ?pending_asset=1 answers { sounds } (rows with no media_asset_id yet, which Live's asset sync uploads to OpenVibe.Media and writes back with POST /internal/chat/sounds/asset). A failure is chat.ingress-ack@1's error.++ */++export type ChatSoundsResult =++ | {++ ok?: true;++ count?: number;++ sounds?: {++ id: number;++ channel_owner_id: number;++ /**++ * The trigger word (no leading '!'), lowercased: the sound's name.++ */++ command: string;++ /**++ * Where Chat keeps the audio file (its on-disk path).++ */++ url: string;++ mime?: string | null;++ duration_seconds?: number | null;++ /**++ * The uploader's Live user id.++ */++ created_by?: number | null;++ created_by_name?: string | null;++ /**++ * The file's OpenVibe.Media URL; null until Live's asset sync uploaded it.++ */++ media_url: string | null;++ /**++ * The OpenVibe.Media asset id; null until uploaded.++ */++ media_asset_id: number | null;++ }[];++ }++ | {++ ok: false;++ /**++ * A human-readable reason.++ */++ error: string;++ };++++/** [email protected] (owner: chat) */++/**++ * chat.sound-asset-request@1: the body of POST /internal/chat/sounds/asset on OpenVibe.Chat (capability chat.sounds.write): after OpenVibe.Live's asset sync uploaded a channel sound to OpenVibe.Media, the asset is written back onto Chat's channel_sounds row id. Writing the same asset again is a no-op; an unknown id is 404. Answers chat.ingress-ack@1.++ */++export interface ChatSoundAssetRequest {++ /**++ * channel_sounds.id++ */++ id: number;++ /**++ * The asset's OpenVibe.Media URL.++ */++ media_url: string;++ /**++ * The OpenVibe.Media asset id.++ */++ media_asset_id: number;++}++/** [email protected] (owner: live) *//*** live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "chat.analysis.read",++ "version": "1.0",++ "owner": "chat",++ "status": "active",++ "visibility": "internal",++ "description": "Count a channel's or a stream's chat messages per time bucket, with the latest message id: the chat timeline behind OpenVibe.Live's AI moments, chat spikes and activity reads (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "permissions": [++ "chat:read"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "internal-read",++ "events": [],++ "implementedBy": [++ "GET /internal/chat/timeline"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "chat.timeline-result@1"++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "chat.messages.read",++ "version": "1.0",++ "owner": "chat",++ "status": "active",++ "visibility": "internal",++ "description": "Page OpenVibe.Chat's stored messages of one channel, stream, person, anonymous id or name, or read one by id, with a cursor on the message id and the latest id: for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "permissions": [++ "chat:read"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "internal-read",++ "events": [],++ "implementedBy": [++ "GET /internal/chat/messages"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "chat.messages-page@1"++}nnew file mode 100644@@@ -0,0 +1,24 @@++{++ "id": "chat.moderation.queue.read",++ "version": "1.0",++ "owner": "chat",++ "status": "active",++ "visibility": "internal",++ "description": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "permissions": [++ "chat:read"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "internal-read",++ "events": [],++ "implementedBy": [++ "GET /internal/chat/moderation/pending-ip",++ "GET /internal/chat/moderation/relay-users",++ "GET /internal/chat/moderation/relay-users/:id",++ "GET /internal/chat/moderation/tts-override"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "chat.moderation-queue-result@1"++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "chat.sounds.read",++ "version": "1.0",++ "owner": "chat",++ "status": "active",++ "visibility": "internal",++ "description": "Count a channel's custom sounds, or list the sounds not yet uploaded to OpenVibe.Media, from OpenVibe.Chat's channel_sounds: for OpenVibe.Live's channel readiness and its Media asset sync (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "permissions": [++ "chat:read"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "internal-read",++ "events": [],++ "implementedBy": [++ "GET /internal/chat/sounds"++ ],++ "inputSchema": "common.no-body@1",++ "outputSchema": "chat.sounds-result@1"++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "chat.sounds.write",++ "version": "1.0",++ "owner": "chat",++ "status": "active",++ "visibility": "internal",++ "description": "Write a channel sound's OpenVibe.Media asset (URL and asset id) back onto OpenVibe.Chat's channel_sounds row after OpenVibe.Live's asset sync uploaded the file (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "permissions": [++ "chat:write"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "internal-write",++ "events": [],++ "implementedBy": [++ "POST /internal/chat/sounds/asset"++ ],++ "inputSchema": "chat.sound-asset-request@1",++ "outputSchema": "chat.ingress-ack@1"++}nnew file mode 100644@@@ -0,0 +1,21 @@++{++ "id": "chat.stats.read",++ "version": "1.0",++ "owner": "chat",++ "status": "active",++ "visibility": "internal",++ "description": "Count OpenVibe.Chat's stored messages: all of chat, one person's, one stream's (messages, chatters, soundboard plays) or the top chatters of a stream, a channel or all chat, in an optional time window — the stats OpenVibe.Live shows (home, SEO, admin, star of the day, VOD and recap cards, stream analytics, AI slogans) without reading its own copy of Chat's tables (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",++ "permissions": [++ "chat:read"++ ],++ "resourceConstraints": [++ "none"++ ],++ "quotaClass": "internal-read",++ "events": [],++ "implementedBy": [++ "POST /internal/chat/stats"++ ],++ "inputSchema": "chat.stats-request@1",++ "outputSchema": "chat.stats-result@1"++}@@@ -16,9 +16,15 @@"chat.event.publish","chat.live_bridge.write","chat.message.send",++ "chat.analysis.read",++ "chat.messages.read",++ "chat.moderation.queue.read","chat.moderation.read","chat.moderation.write",-- "chat.presence.read"++ "chat.presence.read",++ "chat.sounds.read",++ "chat.sounds.write",++ "chat.stats.read"],"eventsProduced": ["chat.message.created",@@@ -1,12 +1,12 @@{"name": "openvibe-contracts",-- "version": "0.94.2",++ "version": "0.95.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-contracts",-- "version": "0.94.2",++ "version": "0.95.0","license": "AGPL-3.0-only","dependencies": {"ajv": "^8.17.1",@@@ -1,6 +1,6 @@{"name": "openvibe-contracts",-- "version": "0.94.2",++ "version": "0.95.0","description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.","main": "index.js","types": "generated/typescript/index.d.ts",
@@ -4,6 +4,24 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
@@ -4,6 +4,24 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
the notes were in the tag and commit messages (`git tag -n1`).
+## 0.95.0 — 2026-10-04
+
+**OpenVibe.Chat's internal read API** (plan T3, J4b): OpenVibe.Live stops reading its mirrored copy of Chat's tables.
+Additive. Six new internal capabilities owned by chat (loopback, Chat service token, Live's principal today; never
+granted to an app or delegated), added to `manifests/services/chat.json`:
+ - `chat.stats.read` (`POST /internal/chat/stats`): `chat.stats-request@1` (`kind` site, user, stream or channel-top,
+ an optional epoch-ms window, `limit` 1-50) → `chat.stats-result@1` (`messages`, `chatters`, `sounds`, `top_chatters`).
+ - `chat.messages.read` (`GET /internal/chat/messages`) → `chat.messages-page@1` (`chat_messages` rows, `max_id`).
+ - `chat.analysis.read` (`GET /internal/chat/timeline`) → `chat.timeline-result@1` (`buckets` of `{t, count}`, `max_id`).
+ - `chat.moderation.queue.read` (`GET /internal/chat/moderation/pending-ip`, `…/relay-users`, `…/relay-users/:id`,
+ `…/tts-override`) → `chat.moderation-queue-result@1`, one shape per route with Chat's `pending_ip_messages`,
+ `hidden_relay_users` and `tts_voice_overrides` rows. `chat.moderation.read` is unchanged.
+ - `chat.sounds.read` (`GET /internal/chat/sounds`) → `chat.sounds-result@1` (`count`, or the `channel_sounds` rows
+ still without a Media asset).
+ - `chat.sounds.write` (`POST /internal/chat/sounds/asset`): `chat.sound-asset-request@1` → `chat.ingress-ack@1`.
+
+`chat.send-result@1` 1.2.0: the `POST /internal/chat/messages` answer gains optional `first_chat` (boolean).
+
## 0.94.2 — 2026-10-04
## 0.94.2 — 2026-10-04
**`past_due` renewal grace on the billing events** (plan T5 step 11, s1). Additive: no field is required or removed.
**`past_due` renewal grace on the billing events** (plan T5 step 11, s1). Additive: no field is required or removed.
@@ -4559,7 +4559,7 @@
@@ -4559,7 +4559,7 @@
},
},
{
{
"id": "chat.send-result",
"id": "chat.send-result",
- "version": "1.1.0",
+ "version": "1.2.0",
"owner": "chat",
"owner": "chat",
"schema": "chat/send-result.v1.json",
"schema": "chat/send-result.v1.json",
"visibility": "first-party",
"visibility": "first-party",
@@ -4638,6 +4638,69 @@
@@ -4638,6 +4638,69 @@
"compatibility": "backward",
"compatibility": "backward",
"status": "active"
"status": "active"
},
},
+ {
+ "id": "chat.stats-request",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/stats-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.stats-result",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/stats-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.messages-page",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/messages-page.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.timeline-result",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/timeline-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.moderation-queue-result",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/moderation-queue-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.sounds-result",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/sounds-result.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
+ {
+ "id": "chat.sound-asset-request",
+ "version": "1.0.0",
+ "owner": "chat",
+ "schema": "chat/sound-asset-request.v1.json",
+ "visibility": "first-party",
+ "compatibility": "backward",
+ "status": "active"
+ },
{
{
"id": "live.moderation.action",
"id": "live.moderation.action",
"version": "1.0.0",
"version": "1.0.0",
new file mode 100644
new file mode 100644
@@ -0,0 +1,141 @@
@@ -0,0 +1,141 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/messages-page.v1.json",
+ "title": "ChatMessagesPage",
+ "description": "chat.messages-page@1: the answer of GET /internal/chat/messages on OpenVibe.Chat (capability chat.messages.read), which pages non-deleted chat_messages for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history. Query: one filter of channel_user_id, stream_id, user_id, anon_id, username or id (one message), a cursor on id (after_id ascending, before_id descending), limit, and tail=1 (no rows, only max_id). max_id is the highest id matching the filter, null when none, so a caller can resume after it. A failure is chat.ingress-ack@1's error.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "messages",
+ "max_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/message"
+ }
+ },
+ "max_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ],
+ "$defs": {
+ "message": {
+ "type": "object",
+ "required": [
+ "id",
+ "user_id",
+ "anon_id",
+ "username",
+ "message",
+ "message_type",
+ "is_global",
+ "stream_id",
+ "channel_user_id",
+ "source_platform",
+ "reply_to_id",
+ "timestamp"
+ ],
+ "description": "One chat_messages row (Chat's columns, as stored).",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "Live user id; null for an anonymous or relayed sender."
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "'anon12345' for an anonymous sender."
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string"
+ },
+ "message_type": {
+ "type": "string",
+ "enum": [
+ "chat",
+ "system",
+ "donation",
+ "command",
+ "tts",
+ "channel-sound",
+ "soundboard",
+ "clip"
+ ]
+ },
+ "is_global": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "1 for homepage global chat."
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The broadcaster whose channel room it is."
+ },
+ "source_platform": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The relay platform of a relayed line; null for OpenVibe chat."
+ },
+ "reply_to_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "timestamp": {
+ "type": "string",
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it (ov_now())."
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,302 @@
@@ -0,0 +1,302 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/moderation-queue-result.v1.json",
+ "title": "ChatModerationQueueResult",
+ "description": "chat.moderation-queue-result@1: what OpenVibe.Chat's moderation queue reads answer (capability chat.moderation.queue.read), one shape per route, for OpenVibe.Live's moderator pages: GET /internal/chat/moderation/pending-ip?channel_id → { pending_ip } (a channel's messages held for IP approval, status pending, oldest first); GET /internal/chat/moderation/relay-users?channel_id → { relay_users } (relay names hidden or banned on the channel or everywhere, newest first); GET /internal/chat/moderation/relay-users/:id → { relay_user } (one, null when unknown); GET /internal/chat/moderation/tts-override?identity_key → { tts_override } (null when none). Internal: rows carry IP addresses. A failure is chat.ingress-ack@1's error.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "pending_ip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "pending_ip": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/pending_ip_message"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "relay_users"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "relay_users": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/hidden_relay_user"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "relay_user"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "relay_user": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/hidden_relay_user"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "tts_override"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "tts_override": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/tts_voice_override"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ],
+ "$defs": {
+ "pending_ip_message": {
+ "type": "object",
+ "description": "One pending_ip_messages row.",
+ "required": [
+ "id",
+ "channel_id",
+ "stream_id",
+ "ip_address",
+ "user_id",
+ "anon_id",
+ "username",
+ "message",
+ "status",
+ "reviewed_by",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "ip_address": {
+ "type": "string",
+ "description": "The sender's address: internal, never shown outside moderation."
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string",
+ "enum": [
+ "pending",
+ "approved",
+ "denied"
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."
+ }
+ }
+ },
+ "hidden_relay_user": {
+ "type": "object",
+ "description": "One hidden_relay_users row.",
+ "required": [
+ "id",
+ "channel_id",
+ "platform",
+ "external_username",
+ "action",
+ "reason",
+ "created_by",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "null hides the name on every channel."
+ },
+ "platform": {
+ "type": "string"
+ },
+ "external_username": {
+ "type": "string"
+ },
+ "action": {
+ "type": "string",
+ "enum": [
+ "hide",
+ "ban"
+ ]
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "created_by_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The moderator's name from Chat's copy of the user, when known."
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."
+ }
+ }
+ },
+ "tts_voice_override": {
+ "type": "object",
+ "description": "One tts_voice_overrides row.",
+ "required": [
+ "identity_key",
+ "voice",
+ "pitch",
+ "speed",
+ "gap",
+ "set_by",
+ "updated_at"
+ ],
+ "properties": {
+ "identity_key": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Lowercased identity the override applies to."
+ },
+ "voice": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "pitch": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "speed": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "gap": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "set_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."
+ }
+ }
+ }
+ }
+}
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
"$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
"title": "ChatSendResult",
"title": "ChatSendResult",
- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",
+ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id, first_chat) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
@@ -230,6 +230,10 @@
@@ -230,6 +230,10 @@
"to_user_id": {
"to_user_id": {
"type": "integer",
"type": "integer",
"description": "DM only."
"description": "DM only."
+ },
+ "first_chat": {
+ "type": "boolean",
+ "description": "Room only, since 1.2.0: true when this is the sender's first message in this channel's stream (Chat's stream_first_chats), so Live can welcome them without reading its own copy; absent on a DM and from an older Chat."
}
}
}
}
},
},
new file mode 100644
new file mode 100644
@@ -0,0 +1,31 @@
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/sound-asset-request.v1.json",
+ "title": "ChatSoundAssetRequest",
+ "description": "chat.sound-asset-request@1: the body of POST /internal/chat/sounds/asset on OpenVibe.Chat (capability chat.sounds.write): after OpenVibe.Live's asset sync uploaded a channel sound to OpenVibe.Media, the asset is written back onto Chat's channel_sounds row id. Writing the same asset again is a no-op; an unknown id is 404. Answers chat.ingress-ack@1.",
+ "type": "object",
+ "required": [
+ "id",
+ "media_url",
+ "media_asset_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "channel_sounds.id"
+ },
+ "media_url": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2048,
+ "description": "The asset's OpenVibe.Media URL."
+ },
+ "media_asset_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The OpenVibe.Media asset id."
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,104 @@
@@ -0,0 +1,104 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/sounds-result.v1.json",
+ "title": "ChatSoundsResult",
+ "description": "chat.sounds-result@1: the answer of GET /internal/chat/sounds on OpenVibe.Chat (capability chat.sounds.read). ?channel_owner_id answers { count } (a channel's custom sounds, for Live's channel readiness); ?pending_asset=1 answers { sounds } (rows with no media_asset_id yet, which Live's asset sync uploads to OpenVibe.Media and writes back with POST /internal/chat/sounds/asset). A failure is chat.ingress-ack@1's error.",
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "sounds": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/sound"
+ }
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ],
+ "$defs": {
+ "sound": {
+ "type": "object",
+ "description": "One channel_sounds row.",
+ "required": [
+ "id",
+ "channel_owner_id",
+ "command",
+ "url",
+ "media_url",
+ "media_asset_id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "channel_owner_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "command": {
+ "type": "string",
+ "description": "The trigger word (no leading '!'), lowercased: the sound's name."
+ },
+ "url": {
+ "type": "string",
+ "description": "Where Chat keeps the audio file (its on-disk path)."
+ },
+ "mime": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "duration_seconds": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "created_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The uploader's Live user id."
+ },
+ "created_by_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "media_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The file's OpenVibe.Media URL; null until Live's asset sync uploaded it."
+ },
+ "media_asset_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The OpenVibe.Media asset id; null until uploaded."
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,89 @@
@@ -0,0 +1,89 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/stats-request.v1.json",
+ "title": "ChatStatsRequest",
+ "description": "chat.stats-request@1: the body of POST /internal/chat/stats on OpenVibe.Chat (capability chat.stats.read): one count over chat_messages, for the stats OpenVibe.Live shows without reading its own copy of Chat's tables (home and SEO stats, admin dashboard, star of the day, VOD and recap cards, stream analytics, top chatters, AI slogans). kind site counts all of chat; user one person's messages (user_id); stream one stream's messages, chatters and soundboard plays (stream_id); channel-top the top chatters, of one stream (stream_id) or one channel (channel_user_id), or of all chat when neither is given. since and until bound chat_messages.timestamp, epoch ms, until exclusive. Deleted messages are never counted. Loopback only, service token.",
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "type": "string",
+ "enum": [
+ "site",
+ "user",
+ "stream",
+ "channel-top"
+ ]
+ },
+ "user_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Live user id; required for kind user."
+ },
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Live stream id; required for kind stream."
+ },
+ "channel_user_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The broadcaster whose channel room it is (chat_messages.channel_user_id)."
+ },
+ "since": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Count messages at or after this instant (epoch ms)."
+ },
+ "until": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Count messages before this instant (epoch ms)."
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 50,
+ "description": "How many top chatters (channel-top); Chat's default is 10."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "user"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "user_id"
+ ]
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "stream"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "stream_id"
+ ]
+ }
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/stats-result.v1.json",
+ "title": "ChatStatsResult",
+ "description": "chat.stats-result@1: the answer of POST /internal/chat/stats (chat.stats.read). site, user and stream answer messages (non-deleted chat_messages rows) and chatters (distinct COALESCE(user_id, anon_id, source_platform || username)); stream also answers sounds (soundboard plays); channel-top answers top_chatters, most messages first. A failure is chat.ingress-ack@1's error ({ ok: false, error }).",
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "messages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "chatters": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "sounds": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Soundboard plays (message_type soundboard)."
+ },
+ "top_chatters": {
+ "type": "array",
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/$defs/chatter"
+ }
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ],
+ "$defs": {
+ "chatter": {
+ "type": "object",
+ "required": [
+ "user_id",
+ "username",
+ "display_name",
+ "count"
+ ],
+ "properties": {
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "Live user id; null for an anonymous or relayed chatter, counted by name."
+ },
+ "username": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "From Chat's copy of the user (ctx_users); null when unknown."
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Messages in the window."
+ }
+ }
+ }
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,53 @@
@@ -0,0 +1,53 @@
+{
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/timeline-result.v1.json",
+ "title": "ChatTimelineResult",
+ "description": "chat.timeline-result@1: the answer of GET /internal/chat/timeline on OpenVibe.Chat (capability chat.analysis.read): non-deleted chat_messages of one channel (channel_user_id) or stream (stream_id) counted per bucket between since and until (epoch ms), bucket_ms wide, for OpenVibe.Live's AI moments, chat spikes and activity. Empty buckets are left out; buckets are oldest first. max_id is the highest message id in the window, null when none. A failure is chat.ingress-ack@1's error.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "buckets",
+ "max_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "buckets": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "t",
+ "count"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "t": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Bucket start, epoch ms."
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "max_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
+{
+ "messages": [
+ {
+ "id": 90211,
+ "user_id": 512,
+ "anon_id": null,
+ "username": "pixelfox",
+ "message": "that was clean",
+ "message_type": "chat",
+ "is_global": 0,
+ "stream_id": 4211,
+ "channel_user_id": 77,
+ "source_platform": null,
+ "reply_to_id": null,
+ "timestamp": "2026-10-04 21:14:09"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,19 @@
@@ -0,0 +1,19 @@
+{
+ "messages": [
+ {
+ "id": 90211,
+ "user_id": 512,
+ "anon_id": null,
+ "username": "pixelfox",
+ "message": "that was clean",
+ "message_type": "whisper",
+ "is_global": 0,
+ "stream_id": 4211,
+ "channel_user_id": 77,
+ "source_platform": null,
+ "reply_to_id": null,
+ "timestamp": "2026-10-04 21:14:09"
+ }
+ ],
+ "max_id": 90211
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "messages": [],
+ "max_id": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
+{
+ "messages": [
+ {
+ "id": 90211,
+ "user_id": 512,
+ "anon_id": null,
+ "username": "pixelfox",
+ "message": "that was clean",
+ "message_type": "chat",
+ "is_global": 0,
+ "stream_id": 4211,
+ "channel_user_id": 77,
+ "source_platform": null,
+ "reply_to_id": null,
+ "timestamp": "2026-10-04 21:14:09"
+ },
+ {
+ "id": 90214,
+ "user_id": null,
+ "anon_id": "anon48213",
+ "username": "anon48213",
+ "message": "!airhorn",
+ "message_type": "soundboard",
+ "is_global": 0,
+ "stream_id": 4211,
+ "channel_user_id": 77,
+ "source_platform": null,
+ "reply_to_id": 90211,
+ "timestamp": "2026-10-04 21:14:31"
+ }
+ ],
+ "max_id": 90214
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "messages": [],
+ "max_id": 90214
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "pending_ip": [
+ {
+ "id": 311,
+ "channel_id": 77,
+ "stream_id": 4211,
+ "ip_address": "203.0.113.24",
+ "user_id": null,
+ "anon_id": "anon48213",
+ "username": "anon48213",
+ "message": "first time here",
+ "status": "held",
+ "reviewed_by": null,
+ "created_at": "2026-10-04 21:02:40"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "relay_user": {
+ "id": 58,
+ "platform": "kick"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "relay_user": null,
+ "tts_override": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "ok": false,
+ "error": "channel_id required"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "pending_ip": [
+ {
+ "id": 311,
+ "channel_id": 77,
+ "stream_id": 4211,
+ "ip_address": "203.0.113.24",
+ "user_id": null,
+ "anon_id": "anon48213",
+ "username": "anon48213",
+ "message": "first time here",
+ "status": "pending",
+ "reviewed_by": null,
+ "created_at": "2026-10-04 21:02:40"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "relay_user": null
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,26 @@
@@ -0,0 +1,26 @@
+{
+ "relay_users": [
+ {
+ "id": 58,
+ "channel_id": 77,
+ "platform": "kick",
+ "external_username": "spam_bot_9",
+ "action": "ban",
+ "reason": "link spam",
+ "created_by": 512,
+ "created_by_username": "pixelfox",
+ "created_at": "2026-10-03 18:40:00"
+ },
+ {
+ "id": 12,
+ "channel_id": null,
+ "platform": "kick",
+ "external_username": "spam_bot_9",
+ "action": "hide",
+ "reason": null,
+ "created_by": 512,
+ "created_by_username": null,
+ "created_at": "2026-10-03 18:40:00"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,11 @@
@@ -0,0 +1,11 @@
+{
+ "tts_override": {
+ "identity_key": "user:512",
+ "voice": "en-US-Wavenet-D",
+ "pitch": 2,
+ "speed": 110,
+ "gap": 0,
+ "set_by": 77,
+ "updated_at": "2026-10-01 09:12:55"
+ }
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "ok": true,
+ "id": 90215,
+ "stream_id": 4211,
+ "channel_user_id": 77,
+ "first_chat": 1
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
+{
+ "ok": true,
+ "id": 90215,
+ "stream_id": 4211,
+ "channel_user_id": 77,
+ "first_chat": true
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "id": 904,
+ "media_url": "https://openvibe.media/a/31882/airhorn.mp3",
+ "media_asset_id": 31882,
+ "channel_owner_id": 77
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "id": 904,
+ "media_url": "https://openvibe.media/a/31882/airhorn.mp3"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "id": 904,
+ "media_url": "https://openvibe.media/a/31882/airhorn.mp3",
+ "media_asset_id": 31882
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "count": -2
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,15 @@
@@ -0,0 +1,15 @@
+{
+ "sounds": [
+ {
+ "id": 904,
+ "channel_owner_id": 77,
+ "command": "airhorn",
+ "mime": "audio/mpeg",
+ "duration_seconds": 2.4,
+ "created_by": 512,
+ "created_by_name": "pixelfox",
+ "media_url": null,
+ "media_asset_id": null
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "count": 14
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
+{
+ "sounds": [
+ {
+ "id": 904,
+ "channel_owner_id": 77,
+ "command": "airhorn",
+ "url": "/sounds/77/airhorn.mp3",
+ "mime": "audio/mpeg",
+ "duration_seconds": 2.4,
+ "created_by": 512,
+ "created_by_name": "pixelfox",
+ "media_url": null,
+ "media_asset_id": null
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
+{
+ "kind": "channel-top",
+ "stream_id": 4211,
+ "limit": 51
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "kind": "global"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "kind": "user",
+ "since": 1759536000000
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "kind": "channel-top",
+ "channel_user_id": 77,
+ "since": 1758326400000,
+ "limit": 20
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "kind": "site",
+ "since": 1759536000000
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "kind": "stream",
+ "stream_id": 4211
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
+{
+ "top_chatters": [
+ {
+ "user_id": 512,
+ "username": "pixelfox",
+ "display_name": "PixelFox"
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
+{
+ "messages": -1
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "ok": false,
+ "error": "kind user needs user_id"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
+{
+ "ok": true,
+ "messages": 1834,
+ "chatters": 96,
+ "sounds": 41
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,17 @@
@@ -0,0 +1,17 @@
+{
+ "ok": true,
+ "top_chatters": [
+ {
+ "user_id": 512,
+ "username": "pixelfox",
+ "display_name": "PixelFox",
+ "count": 214
+ },
+ {
+ "user_id": null,
+ "username": "kick_viewer",
+ "display_name": null,
+ "count": 37
+ }
+ ]
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,8 @@
@@ -0,0 +1,8 @@
+{
+ "buckets": [
+ {
+ "count": 12
+ }
+ ],
+ "max_id": 90214
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
+{
+ "buckets": [
+ {
+ "t": 1759612440000,
+ "count": 12
+ },
+ {
+ "t": 1759612500000,
+ "count": 31
+ }
+ ],
+ "max_id": 90214
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
+{
+ "buckets": [],
+ "max_id": null
+}
@@ -49305,14 +49305,14 @@
@@ -49305,14 +49305,14 @@
}
}
},
},
"chat.send-result": {
"chat.send-result": {
- "version": "1.1.0",
+ "version": "1.2.0",
"owner": "chat",
"owner": "chat",
"status": "active",
"status": "active",
"schema": {
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
"$id": "https://openvibe.network/contracts/chat/send-result.v1.json",
"title": "ChatSendResult",
"title": "ChatSendResult",
- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",
+ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id, first_chat) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
@@ -49540,6 +49540,10 @@
@@ -49540,6 +49540,10 @@
"to_user_id": {
"to_user_id": {
"type": "integer",
"type": "integer",
"description": "DM only."
"description": "DM only."
+ },
+ "first_chat": {
+ "type": "boolean",
+ "description": "Room only, since 1.2.0: true when this is the sender's first message in this channel's stream (Chat's stream_first_chats), so Live can welcome them without reading its own copy; absent on a DM and from an older Chat."
}
}
}
}
},
},
@@ -51716,6 +51720,849 @@
@@ -51716,6 +51720,849 @@
]
]
}
}
},
},
+ "chat.stats-request": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/stats-request.v1.json",
+ "title": "ChatStatsRequest",
+ "description": "chat.stats-request@1: the body of POST /internal/chat/stats on OpenVibe.Chat (capability chat.stats.read): one count over chat_messages, for the stats OpenVibe.Live shows without reading its own copy of Chat's tables (home and SEO stats, admin dashboard, star of the day, VOD and recap cards, stream analytics, top chatters, AI slogans). kind site counts all of chat; user one person's messages (user_id); stream one stream's messages, chatters and soundboard plays (stream_id); channel-top the top chatters, of one stream (stream_id) or one channel (channel_user_id), or of all chat when neither is given. since and until bound chat_messages.timestamp, epoch ms, until exclusive. Deleted messages are never counted. Loopback only, service token.",
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "type": "string",
+ "enum": [
+ "site",
+ "user",
+ "stream",
+ "channel-top"
+ ]
+ },
+ "user_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Live user id; required for kind user."
+ },
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Live stream id; required for kind stream."
+ },
+ "channel_user_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The broadcaster whose channel room it is (chat_messages.channel_user_id)."
+ },
+ "since": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Count messages at or after this instant (epoch ms)."
+ },
+ "until": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Count messages before this instant (epoch ms)."
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 50,
+ "description": "How many top chatters (channel-top); Chat's default is 10."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "user"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "user_id"
+ ]
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "stream"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "stream_id"
+ ]
+ }
+ }
+ ]
+ }
+ },
+ "chat.stats-result": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/stats-result.v1.json",
+ "title": "ChatStatsResult",
+ "description": "chat.stats-result@1: the answer of POST /internal/chat/stats (chat.stats.read). site, user and stream answer messages (non-deleted chat_messages rows) and chatters (distinct COALESCE(user_id, anon_id, source_platform || username)); stream also answers sounds (soundboard plays); channel-top answers top_chatters, most messages first. A failure is chat.ingress-ack@1's error ({ ok: false, error }).",
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "messages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "chatters": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "sounds": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Soundboard plays (message_type soundboard)."
+ },
+ "top_chatters": {
+ "type": "array",
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/$defs/chatter"
+ }
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ],
+ "$defs": {
+ "chatter": {
+ "type": "object",
+ "required": [
+ "user_id",
+ "username",
+ "display_name",
+ "count"
+ ],
+ "properties": {
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "Live user id; null for an anonymous or relayed chatter, counted by name."
+ },
+ "username": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "From Chat's copy of the user (ctx_users); null when unknown."
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Messages in the window."
+ }
+ }
+ }
+ }
+ }
+ },
+ "chat.messages-page": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/messages-page.v1.json",
+ "title": "ChatMessagesPage",
+ "description": "chat.messages-page@1: the answer of GET /internal/chat/messages on OpenVibe.Chat (capability chat.messages.read), which pages non-deleted chat_messages for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history. Query: one filter of channel_user_id, stream_id, user_id, anon_id, username or id (one message), a cursor on id (after_id ascending, before_id descending), limit, and tail=1 (no rows, only max_id). max_id is the highest id matching the filter, null when none, so a caller can resume after it. A failure is chat.ingress-ack@1's error.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "messages",
+ "max_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/message"
+ }
+ },
+ "max_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ],
+ "$defs": {
+ "message": {
+ "type": "object",
+ "required": [
+ "id",
+ "user_id",
+ "anon_id",
+ "username",
+ "message",
+ "message_type",
+ "is_global",
+ "stream_id",
+ "channel_user_id",
+ "source_platform",
+ "reply_to_id",
+ "timestamp"
+ ],
+ "description": "One chat_messages row (Chat's columns, as stored).",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "Live user id; null for an anonymous or relayed sender."
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "'anon12345' for an anonymous sender."
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string"
+ },
+ "message_type": {
+ "type": "string",
+ "enum": [
+ "chat",
+ "system",
+ "donation",
+ "command",
+ "tts",
+ "channel-sound",
+ "soundboard",
+ "clip"
+ ]
+ },
+ "is_global": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "1 for homepage global chat."
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The broadcaster whose channel room it is."
+ },
+ "source_platform": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The relay platform of a relayed line; null for OpenVibe chat."
+ },
+ "reply_to_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "timestamp": {
+ "type": "string",
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it (ov_now())."
+ }
+ }
+ }
+ }
+ }
+ },
+ "chat.timeline-result": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/timeline-result.v1.json",
+ "title": "ChatTimelineResult",
+ "description": "chat.timeline-result@1: the answer of GET /internal/chat/timeline on OpenVibe.Chat (capability chat.analysis.read): non-deleted chat_messages of one channel (channel_user_id) or stream (stream_id) counted per bucket between since and until (epoch ms), bucket_ms wide, for OpenVibe.Live's AI moments, chat spikes and activity. Empty buckets are left out; buckets are oldest first. max_id is the highest message id in the window, null when none. A failure is chat.ingress-ack@1's error.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "buckets",
+ "max_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "buckets": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "t",
+ "count"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "t": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Bucket start, epoch ms."
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "max_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ]
+ }
+ },
+ "chat.moderation-queue-result": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/moderation-queue-result.v1.json",
+ "title": "ChatModerationQueueResult",
+ "description": "chat.moderation-queue-result@1: what OpenVibe.Chat's moderation queue reads answer (capability chat.moderation.queue.read), one shape per route, for OpenVibe.Live's moderator pages: GET /internal/chat/moderation/pending-ip?channel_id → { pending_ip } (a channel's messages held for IP approval, status pending, oldest first); GET /internal/chat/moderation/relay-users?channel_id → { relay_users } (relay names hidden or banned on the channel or everywhere, newest first); GET /internal/chat/moderation/relay-users/:id → { relay_user } (one, null when unknown); GET /internal/chat/moderation/tts-override?identity_key → { tts_override } (null when none). Internal: rows carry IP addresses. A failure is chat.ingress-ack@1's error.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "pending_ip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "pending_ip": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/pending_ip_message"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "relay_users"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "relay_users": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/hidden_relay_user"
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "relay_user"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "relay_user": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/hidden_relay_user"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "tts_override"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "tts_override": {
+ "anyOf": [
+ {
+ "$ref": "#/$defs/tts_voice_override"
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ],
+ "$defs": {
+ "pending_ip_message": {
+ "type": "object",
+ "description": "One pending_ip_messages row.",
+ "required": [
+ "id",
+ "channel_id",
+ "stream_id",
+ "ip_address",
+ "user_id",
+ "anon_id",
+ "username",
+ "message",
+ "status",
+ "reviewed_by",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "ip_address": {
+ "type": "string",
+ "description": "The sender's address: internal, never shown outside moderation."
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string",
+ "enum": [
+ "pending",
+ "approved",
+ "denied"
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."
+ }
+ }
+ },
+ "hidden_relay_user": {
+ "type": "object",
+ "description": "One hidden_relay_users row.",
+ "required": [
+ "id",
+ "channel_id",
+ "platform",
+ "external_username",
+ "action",
+ "reason",
+ "created_by",
+ "created_at"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "null hides the name on every channel."
+ },
+ "platform": {
+ "type": "string"
+ },
+ "external_username": {
+ "type": "string"
+ },
+ "action": {
+ "type": "string",
+ "enum": [
+ "hide",
+ "ban"
+ ]
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "created_by_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The moderator's name from Chat's copy of the user, when known."
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."
+ }
+ }
+ },
+ "tts_voice_override": {
+ "type": "object",
+ "description": "One tts_voice_overrides row.",
+ "required": [
+ "identity_key",
+ "voice",
+ "pitch",
+ "speed",
+ "gap",
+ "set_by",
+ "updated_at"
+ ],
+ "properties": {
+ "identity_key": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Lowercased identity the override applies to."
+ },
+ "voice": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "pitch": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "speed": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "gap": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "set_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."
+ }
+ }
+ }
+ }
+ }
+ },
+ "chat.sounds-result": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/sounds-result.v1.json",
+ "title": "ChatSoundsResult",
+ "description": "chat.sounds-result@1: the answer of GET /internal/chat/sounds on OpenVibe.Chat (capability chat.sounds.read). ?channel_owner_id answers { count } (a channel's custom sounds, for Live's channel readiness); ?pending_asset=1 answers { sounds } (rows with no media_asset_id yet, which Live's asset sync uploads to OpenVibe.Media and writes back with POST /internal/chat/sounds/asset). A failure is chat.ingress-ack@1's error.",
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "sounds": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/sound"
+ }
+ }
+ }
+ },
+ {
+ "$ref": "ingress-ack.v1.json#/$defs/error"
+ }
+ ],
+ "$defs": {
+ "sound": {
+ "type": "object",
+ "description": "One channel_sounds row.",
+ "required": [
+ "id",
+ "channel_owner_id",
+ "command",
+ "url",
+ "media_url",
+ "media_asset_id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "channel_owner_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "command": {
+ "type": "string",
+ "description": "The trigger word (no leading '!'), lowercased: the sound's name."
+ },
+ "url": {
+ "type": "string",
+ "description": "Where Chat keeps the audio file (its on-disk path)."
+ },
+ "mime": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "duration_seconds": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "created_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The uploader's Live user id."
+ },
+ "created_by_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "media_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The file's OpenVibe.Media URL; null until Live's asset sync uploaded it."
+ },
+ "media_asset_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The OpenVibe.Media asset id; null until uploaded."
+ }
+ }
+ }
+ }
+ }
+ },
+ "chat.sound-asset-request": {
+ "version": "1.0.0",
+ "owner": "chat",
+ "status": "active",
+ "schema": {
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
+ "$id": "https://openvibe.network/contracts/chat/sound-asset-request.v1.json",
+ "title": "ChatSoundAssetRequest",
+ "description": "chat.sound-asset-request@1: the body of POST /internal/chat/sounds/asset on OpenVibe.Chat (capability chat.sounds.write): after OpenVibe.Live's asset sync uploaded a channel sound to OpenVibe.Media, the asset is written back onto Chat's channel_sounds row id. Writing the same asset again is a no-op; an unknown id is 404. Answers chat.ingress-ack@1.",
+ "type": "object",
+ "required": [
+ "id",
+ "media_url",
+ "media_asset_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "channel_sounds.id"
+ },
+ "media_url": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2048,
+ "description": "The asset's OpenVibe.Media URL."
+ },
+ "media_asset_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The OpenVibe.Media asset id."
+ }
+ }
+ }
+ },
"live.moderation.action": {
"live.moderation.action": {
"version": "1.0.0",
"version": "1.0.0",
"owner": "live",
"owner": "live",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.AI API",
"title": "OpenVibe.AI API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Billing API",
"title": "OpenVibe.Billing API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Blog API",
"title": "OpenVibe.Blog API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Bot API",
"title": "OpenVibe.Bot API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Chat API",
"title": "OpenVibe.Chat API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -129,6 +129,50 @@
@@ -129,6 +129,50 @@
}
}
},
},
"/internal/chat/messages": {
"/internal/chat/messages": {
+ "get": {
+ "operationId": "get_internal_chat_messages",
+ "summary": "Page OpenVibe.Chat's stored messages of one channel, stream, person, anonymous id or name, or read one by id, with a cursor on the message id and the latest id: for OpenVibe.Live's AI context, clip an",
+ "description": "**chat.messages.read** (internal): Page OpenVibe.Chat's stored messages of one channel, stream, person, anonymous id or name, or read one by id, with a cursor on the message id and the latest id: for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "tags": [
+ "chat.messages"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.messages.read"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "internal-read",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.messages.read"
+ ]
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.messages-page.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ },
"post": {
"post": {
"operationId": "post_internal_chat_messages",
"operationId": "post_internal_chat_messages",
"summary": "Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",
"summary": "Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",
@@ -240,45 +284,35 @@
@@ -240,45 +284,35 @@
}
}
}
}
},
},
- "/internal/live/calls": {
- "post": {
- "operationId": "post_internal_live_calls",
- "summary": "Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",
- "description": "**chat.message.send** (first-party): Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",
+ "/internal/chat/moderation/pending-ip": {
+ "get": {
+ "operationId": "get_internal_chat_moderation_pending_ip",
+ "summary": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's ",
+ "description": "**chat.moderation.queue.read** (internal): Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
"tags": [
"tags": [
- "chat.message"
+ "chat.moderation"
],
],
"x-openvibe-capabilities": [
"x-openvibe-capabilities": [
- "chat.message.send"
+ "chat.moderation.queue.read"
],
],
"x-openvibe-visibility": [
"x-openvibe-visibility": [
- "first-party"
+ "internal"
],
],
- "x-openvibe-quota-class": "chat-send",
+ "x-openvibe-quota-class": "internal-read",
"security": [
"security": [
{
{
"openvibeToken": [
"openvibeToken": [
- "chat.message.send"
+ "chat.moderation.queue.read"
]
]
}
}
],
],
- "requestBody": {
- "required": false,
- "content": {
- "application/json": {
- "schema": {
- "$ref": "#/components/schemas/chat.send-request.v1"
- }
- }
- }
- },
"responses": {
"responses": {
"2XX": {
"2XX": {
"description": "Success",
"description": "Success",
"content": {
"content": {
"application/json": {
"application/json": {
"schema": {
"schema": {
- "$ref": "#/components/schemas/chat.send-result.v1"
+ "$ref": "#/components/schemas/chat.moderation-queue-result.v1"
}
}
}
}
}
}
@@ -296,16 +330,16 @@
@@ -296,16 +330,16 @@
}
}
}
}
},
},
- "/internal/moderation/channels/{channelId}": {
+ "/internal/chat/moderation/relay-users": {
"get": {
"get": {
- "operationId": "get_internal_moderation_channels_channelId",
- "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",
- "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",
+ "operationId": "get_internal_chat_moderation_relay_users",
+ "summary": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's ",
+ "description": "**chat.moderation.queue.read** (internal): Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
"tags": [
"tags": [
"chat.moderation"
"chat.moderation"
],
],
"x-openvibe-capabilities": [
"x-openvibe-capabilities": [
- "chat.moderation.read"
+ "chat.moderation.queue.read"
],
],
"x-openvibe-visibility": [
"x-openvibe-visibility": [
"internal"
"internal"
@@ -314,27 +348,17 @@
@@ -314,27 +348,17 @@
"security": [
"security": [
{
{
"openvibeToken": [
"openvibeToken": [
- "chat.moderation.read"
+ "chat.moderation.queue.read"
]
]
}
}
],
],
- "parameters": [
- {
- "name": "channelId",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
- }
- }
- ],
"responses": {
"responses": {
"2XX": {
"2XX": {
"description": "Success",
"description": "Success",
"content": {
"content": {
"application/json": {
"application/json": {
"schema": {
"schema": {
- "$ref": "#/components/schemas/chat.channel-moderation-result.v1"
+ "$ref": "#/components/schemas/chat.moderation-queue-result.v1"
}
}
}
}
}
}
@@ -352,16 +376,16 @@
@@ -352,16 +376,16 @@
}
}
}
}
},
},
- "/internal/moderation/channels/{channelId}/emote-count": {
+ "/internal/chat/moderation/relay-users/{id}": {
"get": {
"get": {
- "operationId": "get_internal_moderation_channels_channelId_emote_count",
- "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",
- "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",
+ "operationId": "get_internal_chat_moderation_relay_users_id",
+ "summary": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's ",
+ "description": "**chat.moderation.queue.read** (internal): Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
"tags": [
"tags": [
"chat.moderation"
"chat.moderation"
],
],
"x-openvibe-capabilities": [
"x-openvibe-capabilities": [
- "chat.moderation.read"
+ "chat.moderation.queue.read"
],
],
"x-openvibe-visibility": [
"x-openvibe-visibility": [
"internal"
"internal"
@@ -370,13 +394,13 @@
@@ -370,13 +394,13 @@
"security": [
"security": [
{
{
"openvibeToken": [
"openvibeToken": [
- "chat.moderation.read"
+ "chat.moderation.queue.read"
]
]
}
}
],
],
"parameters": [
"parameters": [
{
{
- "name": "channelId",
+ "name": "id",
"in": "path",
"in": "path",
"required": true,
"required": true,
"schema": {
"schema": {
@@ -390,7 +414,7 @@
@@ -390,7 +414,7 @@
"content": {
"content": {
"application/json": {
"application/json": {
"schema": {
"schema": {
- "$ref": "#/components/schemas/chat.channel-moderation-result.v1"
+ "$ref": "#/components/schemas/chat.moderation-queue-result.v1"
}
}
}
}
}
}
@@ -408,16 +432,16 @@
@@ -408,16 +432,16 @@
}
}
}
}
},
},
- "/internal/moderation/users/{userId}/channels": {
+ "/internal/chat/moderation/tts-override": {
"get": {
"get": {
- "operationId": "get_internal_moderation_users_userId_channels",
- "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",
- "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",
+ "operationId": "get_internal_chat_moderation_tts_override",
+ "summary": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's ",
+ "description": "**chat.moderation.queue.read** (internal): Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
"tags": [
"tags": [
"chat.moderation"
"chat.moderation"
],
],
"x-openvibe-capabilities": [
"x-openvibe-capabilities": [
- "chat.moderation.read"
+ "chat.moderation.queue.read"
],
],
"x-openvibe-visibility": [
"x-openvibe-visibility": [
"internal"
"internal"
@@ -426,19 +450,55 @@
@@ -426,19 +450,55 @@
"security": [
"security": [
{
{
"openvibeToken": [
"openvibeToken": [
- "chat.moderation.read"
+ "chat.moderation.queue.read"
]
]
}
}
],
],
- "parameters": [
- {
- "name": "userId",
- "in": "path",
- "required": true,
- "schema": {
- "type": "string"
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.moderation-queue-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
}
}
}
}
+ }
+ }
+ },
+ "/internal/chat/sounds": {
+ "get": {
+ "operationId": "get_internal_chat_sounds",
+ "summary": "Count a channel's custom sounds, or list the sounds not yet uploaded to OpenVibe.Media, from OpenVibe.Chat's channel_sounds: for OpenVibe.Live's channel readiness and its Media asset sync (plan T3, J4",
+ "description": "**chat.sounds.read** (internal): Count a channel's custom sounds, or list the sounds not yet uploaded to OpenVibe.Media, from OpenVibe.Chat's channel_sounds: for OpenVibe.Live's channel readiness and its Media asset sync (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "tags": [
+ "chat.sounds"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.sounds.read"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "internal-read",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.sounds.read"
+ ]
+ }
],
],
"responses": {
"responses": {
"2XX": {
"2XX": {
@@ -446,7 +506,7 @@
@@ -446,7 +506,7 @@
"content": {
"content": {
"application/json": {
"application/json": {
"schema": {
"schema": {
- "$ref": "#/components/schemas/chat.channel-moderation-result.v1"
+ "$ref": "#/components/schemas/chat.sounds-result.v1"
}
}
}
}
}
}
@@ -463,62 +523,444 @@
@@ -463,62 +523,444 @@
}
}
}
}
}
}
- }
- },
- "components": {
- "schemas": {
- "chat.channel-moderation-result.v1": {
- "title": "ChatChannelModerationResult",
- "description": "chat.channel-moderation-result@1: the answer of GET /internal/moderation/channels/:channelId on OpenVibe.Chat (capability chat.moderation.read): { ok: true, settings, moderator_ids } — one channel's moderation settings and the ids of the people who moderate it, for OpenVibe.Live's stream chat. settings carries every channel_moderation_settings column (Live's own defaults when the channel has no row yet, database.js getChannelModerationSettings); the switches are 0/1 as the table stores them. moderator_ids are Live user ids (channel_moderators.user_id), never names. A failure is { ok: false, error }; a caller without the grant gets 401 { ok: false }.",
- "anyOf": [
+ },
+ "/internal/chat/sounds/asset": {
+ "post": {
+ "operationId": "post_internal_chat_sounds_asset",
+ "summary": "Write a channel sound's OpenVibe.Media asset (URL and asset id) back onto OpenVibe.Chat's channel_sounds row after OpenVibe.Live's asset sync uploaded the file (plan T3, J4b).",
+ "description": "**chat.sounds.write** (internal): Write a channel sound's OpenVibe.Media asset (URL and asset id) back onto OpenVibe.Chat's channel_sounds row after OpenVibe.Live's asset sync uploaded the file (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "tags": [
+ "chat.sounds"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.sounds.write"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "internal-write",
+ "security": [
{
{
- "type": "object",
- "required": [
- "ok",
- "settings",
- "moderator_ids"
- ],
- "properties": {
- "ok": {
- "const": true
- },
- "settings": {
- "$ref": "#/components/schemas/chat.channel-moderation-result.v1/$defs/settings"
- },
- "moderator_ids": {
- "type": "array",
- "description": "The channel's moderators, oldest first (channel_moderators.created_at); empty when it has none.",
- "items": {
- "type": "integer",
- "minimum": 1
+ "openvibeToken": [
+ "chat.sounds.write"
+ ]
+ }
+ ],
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.sound-asset-request.v1"
+ }
+ }
+ }
+ },
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1"
}
}
}
}
}
}
},
},
- {
- "type": "object",
- "required": [
- "ok",
- "error"
- ],
- "properties": {
- "ok": {
- "const": false
- },
- "error": {
- "type": "string"
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
}
}
}
}
}
}
+ }
+ }
+ },
+ "/internal/chat/stats": {
+ "post": {
+ "operationId": "post_internal_chat_stats",
+ "summary": "Count OpenVibe.Chat's stored messages: all of chat, one person's, one stream's (messages, chatters, soundboard plays) or the top chatters of a stream, a channel or all chat, in an optional time window",
+ "description": "**chat.stats.read** (internal): Count OpenVibe.Chat's stored messages: all of chat, one person's, one stream's (messages, chatters, soundboard plays) or the top chatters of a stream, a channel or all chat, in an optional time window — the stats OpenVibe.Live shows (home, SEO, admin, star of the day, VOD and recap cards, stream analytics, AI slogans) without reading its own copy of Chat's tables (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "tags": [
+ "chat.stats"
],
],
- "$defs": {
- "flag": {
- "type": "integer",
- "enum": [
- 0,
- 1
- ],
- "description": "A switch as the SQLite table stores it."
+ "x-openvibe-capabilities": [
+ "chat.stats.read"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "internal-read",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.stats.read"
+ ]
+ }
+ ],
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.stats-request.v1"
+ }
+ }
+ }
+ },
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.stats-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/chat/timeline": {
+ "get": {
+ "operationId": "get_internal_chat_timeline",
+ "summary": "Count a channel's or a stream's chat messages per time bucket, with the latest message id: the chat timeline behind OpenVibe.Live's AI moments, chat spikes and activity reads (plan T3, J4b).",
+ "description": "**chat.analysis.read** (internal): Count a channel's or a stream's chat messages per time bucket, with the latest message id: the chat timeline behind OpenVibe.Live's AI moments, chat spikes and activity reads (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "tags": [
+ "chat.analysis"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.analysis.read"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "internal-read",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.analysis.read"
+ ]
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.timeline-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/live/calls": {
+ "post": {
+ "operationId": "post_internal_live_calls",
+ "summary": "Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",
+ "description": "**chat.message.send** (first-party): Send a chat message to a room the actor is a member of (browsers over the chat socket; first-party services over Chat's service API).",
+ "tags": [
+ "chat.message"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.message.send"
+ ],
+ "x-openvibe-visibility": [
+ "first-party"
+ ],
+ "x-openvibe-quota-class": "chat-send",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.message.send"
+ ]
+ }
+ ],
+ "requestBody": {
+ "required": false,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.send-request.v1"
+ }
+ }
+ }
+ },
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.send-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/moderation/channels/{channelId}": {
+ "get": {
+ "operationId": "get_internal_moderation_channels_channelId",
+ "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",
+ "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",
+ "tags": [
+ "chat.moderation"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.moderation.read"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "internal-read",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.moderation.read"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "channelId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.channel-moderation-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/moderation/channels/{channelId}/emote-count": {
+ "get": {
+ "operationId": "get_internal_moderation_channels_channelId_emote_count",
+ "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",
+ "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",
+ "tags": [
+ "chat.moderation"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.moderation.read"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "internal-read",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.moderation.read"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "channelId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.channel-moderation-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/internal/moderation/users/{userId}/channels": {
+ "get": {
+ "operationId": "get_internal_moderation_users_userId_channels",
+ "summary": "Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.C",
+ "description": "**chat.moderation.read** (internal): Read one channel's moderation settings and moderator ids, the channels a person moderates, and a channel's custom emote count: how OpenVibe.Live reads what it needs from the six chat tables OpenVibe.Chat owns (plan T3). Loopback only, service token.",
+ "tags": [
+ "chat.moderation"
+ ],
+ "x-openvibe-capabilities": [
+ "chat.moderation.read"
+ ],
+ "x-openvibe-visibility": [
+ "internal"
+ ],
+ "x-openvibe-quota-class": "internal-read",
+ "security": [
+ {
+ "openvibeToken": [
+ "chat.moderation.read"
+ ]
+ }
+ ],
+ "parameters": [
+ {
+ "name": "userId",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "2XX": {
+ "description": "Success",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/chat.channel-moderation-result.v1"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "An error (RFC 9457 problem details)",
+ "content": {
+ "application/problem+json": {
+ "schema": {
+ "$ref": "#/components/schemas/errors.problem.v1"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "chat.channel-moderation-result.v1": {
+ "title": "ChatChannelModerationResult",
+ "description": "chat.channel-moderation-result@1: the answer of GET /internal/moderation/channels/:channelId on OpenVibe.Chat (capability chat.moderation.read): { ok: true, settings, moderator_ids } — one channel's moderation settings and the ids of the people who moderate it, for OpenVibe.Live's stream chat. settings carries every channel_moderation_settings column (Live's own defaults when the channel has no row yet, database.js getChannelModerationSettings); the switches are 0/1 as the table stores them. moderator_ids are Live user ids (channel_moderators.user_id), never names. A failure is { ok: false, error }; a caller without the grant gets 401 { ok: false }.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "settings",
+ "moderator_ids"
+ ],
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "settings": {
+ "$ref": "#/components/schemas/chat.channel-moderation-result.v1/$defs/settings"
+ },
+ "moderator_ids": {
+ "type": "array",
+ "description": "The channel's moderators, oldest first (channel_moderators.created_at); empty when it has none.",
+ "items": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "ok",
+ "error"
+ ],
+ "properties": {
+ "ok": {
+ "const": false
+ },
+ "error": {
+ "type": "string"
+ }
+ }
+ }
+ ],
+ "$defs": {
+ "flag": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "A switch as the SQLite table stores it."
},
},
"settings": {
"settings": {
"type": "object",
"type": "object",
@@ -1276,39 +1718,146 @@
@@ -1276,39 +1718,146 @@
"type": "object",
"type": "object",
"properties": {
"properties": {
"type": {
"type": {
- "const": "vc-call-response"
+ "const": "vc-call-response"
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "required": [
+ "status"
+ ]
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "vc-call-invite"
+ }
+ }
+ },
+ "then": {
+ "not": {
+ "type": "object",
+ "required": [
+ "status"
+ ]
+ }
+ }
+ }
+ ]
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "vc-call-invite",
+ "vc-call-response"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "user"
+ }
+ }
+ }
+ }
+ },
+ "else": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "not": {
+ "const": "user"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "chat",
+ "channel-sound",
+ "redemption",
+ "vibe-coding"
+ ]
+ }
+ }
+ }
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
+ "type": "object",
+ "properties": {
+ "kind": {
+ "const": "stream"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "const": "alert"
}
}
}
}
- },
- "then": {
- "type": "object",
- "required": [
- "status"
- ]
}
}
- },
- {
- "if": {
+ }
+ },
+ "then": {
+ "type": "object",
+ "properties": {
+ "target": {
"type": "object",
"type": "object",
"properties": {
"properties": {
- "type": {
- "const": "vc-call-invite"
+ "kind": {
+ "const": "channel"
}
}
}
}
- },
- "then": {
- "not": {
- "type": "object",
- "required": [
- "status"
- ]
- }
}
}
}
}
- ]
- }
- },
- "allOf": [
+ }
+ },
{
{
"if": {
"if": {
"type": "object",
"type": "object",
@@ -1317,10 +1866,7 @@
@@ -1317,10 +1866,7 @@
"type": "object",
"type": "object",
"properties": {
"properties": {
"type": {
"type": {
- "enum": [
- "vc-call-invite",
- "vc-call-response"
- ]
+ "const": "voice-channels"
}
}
}
}
}
}
@@ -1333,343 +1879,678 @@
@@ -1333,343 +1879,678 @@
"type": "object",
"type": "object",
"properties": {
"properties": {
"kind": {
"kind": {
- "const": "user"
+ "const": "all"
+ }
+ }
+ }
+ }
+ }
+ },
+ {
+ "if": {
+ "type": "object",
+ "properties": {
+ "frame": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "enum": [
+ "media_queue_update",
+ "media_now_playing"
+ ]
}
}
}
}
}
}
}
}
},
},
- "else": {
+ "then": {
"type": "object",
"type": "object",
"properties": {
"properties": {
"target": {
"target": {
"type": "object",
"type": "object",
"properties": {
"properties": {
"kind": {
"kind": {
- "not": {
- "const": "user"
- }
+ "const": "owner-streams"
}
}
}
}
}
}
}
}
}
}
+ }
+ ]
+ },
+ "chat.ingress-ack.v1": {
+ "title": "ChatIngressAck",
+ "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",
+ "$defs": {
+ "error": {
+ "type": "object",
+ "required": [
+ "ok",
+ "error"
+ ],
+ "properties": {
+ "ok": {
+ "const": false
+ },
+ "error": {
+ "type": "string",
+ "description": "A human-readable reason."
+ }
+ }
+ }
+ },
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "ok"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ }
+ }
+ },
+ {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
+ }
+ ]
+ },
+ "chat.invalidate-request.v1": {
+ "title": "ChatInvalidateRequest",
+ "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",
+ "type": "object",
+ "required": [
+ "key"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "key": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 160,
+ "pattern": "^[A-Za-z0-9:._-]+$",
+ "description": "Idempotency key, scoped to the caller and to this route."
+ },
+ "user": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A person whose cached row is stale."
+ },
+ "user_data": {
+ "type": "object",
+ "required": [
+ "id",
+ "username"
+ ],
+ "additionalProperties": false,
+ "description": "The person's new public fields, pushed to their sockets; id must equal user.",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "username": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "minLength": 1,
+ "maxLength": 120
+ },
+ "role": {
+ "type": "string"
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "approvals": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A channel whose IP approvals changed."
+ },
+ "bans": {
+ "type": [
+ "boolean",
+ "null"
+ ],
+ "description": "true: bans changed."
+ },
+ "channel": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "A channel whose cached settings changed."
+ }
+ },
+ "dependentSchemas": {
+ "user_data": {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "user"
+ ],
+ "properties": {
+ "user": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "approvals"
+ ],
+ "properties": {
+ "approvals": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ },
+ {
+ "type": "object",
+ "required": [
+ "channel"
+ ],
+ "properties": {
+ "channel": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
},
},
{
{
- "if": {
- "type": "object",
- "properties": {
- "frame": {
- "type": "object",
- "properties": {
- "type": {
- "enum": [
- "chat",
- "channel-sound",
- "redemption",
- "vibe-coding"
- ]
- }
- }
- }
- }
- },
- "then": {
- "type": "object",
- "properties": {
- "target": {
- "type": "object",
- "properties": {
- "kind": {
- "const": "stream"
- }
- }
- }
+ "type": "object",
+ "required": [
+ "bans"
+ ],
+ "properties": {
+ "bans": {
+ "const": true
}
}
}
}
- },
+ }
+ ]
+ },
+ "chat.messages-page.v1": {
+ "title": "ChatMessagesPage",
+ "description": "chat.messages-page@1: the answer of GET /internal/chat/messages on OpenVibe.Chat (capability chat.messages.read), which pages non-deleted chat_messages for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history. Query: one filter of channel_user_id, stream_id, user_id, anon_id, username or id (one message), a cursor on id (after_id ascending, before_id descending), limit, and tail=1 (no rows, only max_id). max_id is the highest id matching the filter, null when none, so a caller can resume after it. A failure is chat.ingress-ack@1's error.",
+ "anyOf": [
{
{
- "if": {
- "type": "object",
- "properties": {
- "frame": {
- "type": "object",
- "properties": {
- "type": {
- "const": "alert"
- }
- }
- }
- }
- },
- "then": {
- "type": "object",
- "properties": {
- "target": {
- "type": "object",
- "properties": {
- "kind": {
- "const": "channel"
- }
- }
+ "type": "object",
+ "required": [
+ "messages",
+ "max_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "messages": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/chat.messages-page.v1/$defs/message"
}
}
+ },
+ "max_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
}
}
}
}
},
},
{
{
- "if": {
- "type": "object",
- "properties": {
- "frame": {
- "type": "object",
- "properties": {
- "type": {
- "const": "voice-channels"
- }
- }
- }
+ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
+ }
+ ],
+ "$defs": {
+ "message": {
+ "type": "object",
+ "required": [
+ "id",
+ "user_id",
+ "anon_id",
+ "username",
+ "message",
+ "message_type",
+ "is_global",
+ "stream_id",
+ "channel_user_id",
+ "source_platform",
+ "reply_to_id",
+ "timestamp"
+ ],
+ "description": "One chat_messages row (Chat's columns, as stored).",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "Live user id; null for an anonymous or relayed sender."
+ },
+ "anon_id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "'anon12345' for an anonymous sender."
+ },
+ "username": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "message": {
+ "type": "string"
+ },
+ "message_type": {
+ "type": "string",
+ "enum": [
+ "chat",
+ "system",
+ "donation",
+ "command",
+ "tts",
+ "channel-sound",
+ "soundboard",
+ "clip"
+ ]
+ },
+ "is_global": {
+ "type": "integer",
+ "enum": [
+ 0,
+ 1
+ ],
+ "description": "1 for homepage global chat."
+ },
+ "stream_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "channel_user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The broadcaster whose channel room it is."
+ },
+ "source_platform": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The relay platform of a relayed line; null for OpenVibe chat."
+ },
+ "reply_to_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "timestamp": {
+ "type": "string",
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it (ov_now())."
}
}
- },
- "then": {
- "type": "object",
- "properties": {
- "target": {
- "type": "object",
- "properties": {
- "kind": {
- "const": "all"
- }
- }
+ }
+ }
+ }
+ },
+ "chat.moderation-queue-result.v1": {
+ "title": "ChatModerationQueueResult",
+ "description": "chat.moderation-queue-result@1: what OpenVibe.Chat's moderation queue reads answer (capability chat.moderation.queue.read), one shape per route, for OpenVibe.Live's moderator pages: GET /internal/chat/moderation/pending-ip?channel_id → { pending_ip } (a channel's messages held for IP approval, status pending, oldest first); GET /internal/chat/moderation/relay-users?channel_id → { relay_users } (relay names hidden or banned on the channel or everywhere, newest first); GET /internal/chat/moderation/relay-users/:id → { relay_user } (one, null when unknown); GET /internal/chat/moderation/tts-override?identity_key → { tts_override } (null when none). Internal: rows carry IP addresses. A failure is chat.ingress-ack@1's error.",
+ "oneOf": [
+ {
+ "type": "object",
+ "required": [
+ "pending_ip"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "pending_ip": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/chat.moderation-queue-result.v1/$defs/pending_ip_message"
}
}
}
}
}
}
},
},
{
{
- "if": {
- "type": "object",
- "properties": {
- "frame": {
- "type": "object",
- "properties": {
- "type": {
- "enum": [
- "media_queue_update",
- "media_now_playing"
- ]
- }
- }
- }
- }
- },
- "then": {
- "type": "object",
- "properties": {
- "target": {
- "type": "object",
- "properties": {
- "kind": {
- "const": "owner-streams"
- }
- }
+ "type": "object",
+ "required": [
+ "relay_users"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "relay_users": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/chat.moderation-queue-result.v1/$defs/hidden_relay_user"
}
}
}
}
}
}
- }
- ]
- },
- "chat.ingress-ack.v1": {
- "title": "ChatIngressAck",
- "description": "chat.ingress-ack@1: what OpenVibe.Chat's service ingress answers when there is nothing to return (POST /internal/chat/events, chat.event.publish; POST /internal/chat/invalidate, chat.cache.invalidate), and the error every /internal/chat route answers: 400 a body Chat refuses, 401/403 no service token or no grant for the capability, 403/404 a refused or unknown DM participant or stream, 409 an idempotency key reused with another body, 503 Chat unavailable or a delivery in progress (retry with the same key).",
- "$defs": {
- "error": {
+ },
+ {
"type": "object",
"type": "object",
"required": [
"required": [
- "ok",
- "error"
+ "relay_user"
],
],
+ "additionalProperties": false,
"properties": {
"properties": {
"ok": {
"ok": {
- "const": false
+ "const": true
},
},
- "error": {
- "type": "string",
- "description": "A human-readable reason."
+ "relay_user": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/chat.moderation-queue-result.v1/$defs/hidden_relay_user"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
}
}
}
- }
- },
- "oneOf": [
+ },
{
{
"type": "object",
"type": "object",
"required": [
"required": [
- "ok"
+ "tts_override"
],
],
"additionalProperties": false,
"additionalProperties": false,
"properties": {
"properties": {
"ok": {
"ok": {
"const": true
"const": true
+ },
+ "tts_override": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/chat.moderation-queue-result.v1/$defs/tts_voice_override"
+ },
+ {
+ "type": "null"
+ }
+ ]
}
}
}
}
},
},
{
{
"$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
"$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
}
}
- ]
- },
- "chat.invalidate-request.v1": {
- "title": "ChatInvalidateRequest",
- "description": "chat.invalidate-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/invalidate (chat.cache.invalidate) after it changed something Chat caches (a person, a channel, IP approvals, bans). A cache hint only: Chat drops and reloads the entry (with user_data, it also pushes the person's new name and look to their sockets). At least one hint is required. Applied once per (caller, key).",
- "type": "object",
- "required": [
- "key"
],
],
- "additionalProperties": false,
- "properties": {
- "key": {
- "type": "string",
- "minLength": 1,
- "maxLength": 160,
- "pattern": "^[A-Za-z0-9:._-]+$",
- "description": "Idempotency key, scoped to the caller and to this route."
- },
- "user": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "A person whose cached row is stale."
- },
- "user_data": {
+ "$defs": {
+ "pending_ip_message": {
"type": "object",
"type": "object",
+ "description": "One pending_ip_messages row.",
"required": [
"required": [
"id",
"id",
- "username"
+ "channel_id",
+ "stream_id",
+ "ip_address",
+ "user_id",
+ "anon_id",
+ "username",
+ "message",
+ "status",
+ "reviewed_by",
+ "created_at"
],
],
- "additionalProperties": false,
- "description": "The person's new public fields, pushed to their sockets; id must equal user.",
"properties": {
"properties": {
"id": {
"id": {
"type": "integer",
"type": "integer",
"minimum": 1
"minimum": 1
},
},
- "username": {
- "type": "string",
- "minLength": 1,
- "maxLength": 120
+ "channel_id": {
+ "type": "integer",
+ "minimum": 1
},
},
- "display_name": {
+ "stream_id": {
"type": [
"type": [
- "string",
+ "integer",
"null"
"null"
],
],
- "minLength": 1,
- "maxLength": 120
+ "minimum": 1
},
},
- "role": {
- "type": "string"
+ "ip_address": {
+ "type": "string",
+ "description": "The sender's address: internal, never shown outside moderation."
},
},
- "avatar_url": {
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "anon_id": {
"type": [
"type": [
"string",
"string",
"null"
"null"
]
]
},
},
- "profile_color": {
+ "username": {
"type": [
"type": [
"string",
"string",
"null"
"null"
]
]
- }
- }
- },
- "approvals": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "A channel whose IP approvals changed."
- },
- "bans": {
- "type": [
- "boolean",
- "null"
- ],
- "description": "true: bans changed."
- },
- "channel": {
- "type": [
- "integer",
- "null"
- ],
- "minimum": 1,
- "description": "A channel whose cached settings changed."
- }
- },
- "dependentSchemas": {
- "user_data": {
- "type": "object",
- "required": [
- "user"
- ],
- "properties": {
- "user": {
- "type": "integer",
- "minimum": 1
- }
- }
- }
- },
- "anyOf": [
- {
- "type": "object",
- "required": [
- "user"
- ],
- "properties": {
- "user": {
- "type": "integer",
+ },
+ "message": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string",
+ "enum": [
+ "pending",
+ "approved",
+ "denied"
+ ]
+ },
+ "reviewed_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
"minimum": 1
"minimum": 1
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."
}
}
}
}
},
},
- {
+ "hidden_relay_user": {
"type": "object",
"type": "object",
+ "description": "One hidden_relay_users row.",
"required": [
"required": [
- "approvals"
+ "id",
+ "channel_id",
+ "platform",
+ "external_username",
+ "action",
+ "reason",
+ "created_by",
+ "created_at"
],
],
"properties": {
"properties": {
- "approvals": {
+ "id": {
"type": "integer",
"type": "integer",
"minimum": 1
"minimum": 1
- }
- }
- },
- {
- "type": "object",
- "required": [
- "channel"
- ],
- "properties": {
- "channel": {
- "type": "integer",
+ },
+ "channel_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "null hides the name on every channel."
+ },
+ "platform": {
+ "type": "string"
+ },
+ "external_username": {
+ "type": "string"
+ },
+ "action": {
+ "type": "string",
+ "enum": [
+ "hide",
+ "ban"
+ ]
+ },
+ "reason": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "created_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
"minimum": 1
"minimum": 1
+ },
+ "created_by_username": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The moderator's name from Chat's copy of the user, when known."
+ },
+ "created_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."
}
}
}
}
},
},
- {
+ "tts_voice_override": {
"type": "object",
"type": "object",
+ "description": "One tts_voice_overrides row.",
"required": [
"required": [
- "bans"
+ "identity_key",
+ "voice",
+ "pitch",
+ "speed",
+ "gap",
+ "set_by",
+ "updated_at"
],
],
"properties": {
"properties": {
- "bans": {
- "const": true
+ "identity_key": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Lowercased identity the override applies to."
+ },
+ "voice": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "pitch": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "speed": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "gap": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "set_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
+ },
+ "updated_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it."
}
}
}
}
}
}
- ]
+ }
},
},
"chat.moderation-request.v1": {
"chat.moderation-request.v1": {
"title": "ChatModerationRequest",
"title": "ChatModerationRequest",
@@ -2854,7 +3735,7 @@
@@ -2854,7 +3735,7 @@
},
},
"chat.send-result.v1": {
"chat.send-result.v1": {
"title": "ChatSendResult",
"title": "ChatSendResult",
- "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",
+ "description": "chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id, first_chat) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).",
"anyOf": [
"anyOf": [
{
{
"type": "object",
"type": "object",
@@ -3082,6 +3963,365 @@
@@ -3082,6 +3963,365 @@
"to_user_id": {
"to_user_id": {
"type": "integer",
"type": "integer",
"description": "DM only."
"description": "DM only."
+ },
+ "first_chat": {
+ "type": "boolean",
+ "description": "Room only, since 1.2.0: true when this is the sender's first message in this channel's stream (Chat's stream_first_chats), so Live can welcome them without reading its own copy; absent on a DM and from an older Chat."
+ }
+ }
+ },
+ {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
+ }
+ ]
+ },
+ "chat.sound-asset-request.v1": {
+ "title": "ChatSoundAssetRequest",
+ "description": "chat.sound-asset-request@1: the body of POST /internal/chat/sounds/asset on OpenVibe.Chat (capability chat.sounds.write): after OpenVibe.Live's asset sync uploaded a channel sound to OpenVibe.Media, the asset is written back onto Chat's channel_sounds row id. Writing the same asset again is a no-op; an unknown id is 404. Answers chat.ingress-ack@1.",
+ "type": "object",
+ "required": [
+ "id",
+ "media_url",
+ "media_asset_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "channel_sounds.id"
+ },
+ "media_url": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2048,
+ "description": "The asset's OpenVibe.Media URL."
+ },
+ "media_asset_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The OpenVibe.Media asset id."
+ }
+ }
+ },
+ "chat.sounds-result.v1": {
+ "title": "ChatSoundsResult",
+ "description": "chat.sounds-result@1: the answer of GET /internal/chat/sounds on OpenVibe.Chat (capability chat.sounds.read). ?channel_owner_id answers { count } (a channel's custom sounds, for Live's channel readiness); ?pending_asset=1 answers { sounds } (rows with no media_asset_id yet, which Live's asset sync uploads to OpenVibe.Media and writes back with POST /internal/chat/sounds/asset). A failure is chat.ingress-ack@1's error.",
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "sounds": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/chat.sounds-result.v1/$defs/sound"
+ }
+ }
+ }
+ },
+ {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
+ }
+ ],
+ "$defs": {
+ "sound": {
+ "type": "object",
+ "description": "One channel_sounds row.",
+ "required": [
+ "id",
+ "channel_owner_id",
+ "command",
+ "url",
+ "media_url",
+ "media_asset_id"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "channel_owner_id": {
+ "type": "integer",
+ "minimum": 1
+ },
+ "command": {
+ "type": "string",
+ "description": "The trigger word (no leading '!'), lowercased: the sound's name."
+ },
+ "url": {
+ "type": "string",
+ "description": "Where Chat keeps the audio file (its on-disk path)."
+ },
+ "mime": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "duration_seconds": {
+ "type": [
+ "number",
+ "null"
+ ],
+ "minimum": 0
+ },
+ "created_by": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The uploader's Live user id."
+ },
+ "created_by_name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "media_url": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The file's OpenVibe.Media URL; null until Live's asset sync uploaded it."
+ },
+ "media_asset_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "The OpenVibe.Media asset id; null until uploaded."
+ }
+ }
+ }
+ }
+ },
+ "chat.stats-request.v1": {
+ "title": "ChatStatsRequest",
+ "description": "chat.stats-request@1: the body of POST /internal/chat/stats on OpenVibe.Chat (capability chat.stats.read): one count over chat_messages, for the stats OpenVibe.Live shows without reading its own copy of Chat's tables (home and SEO stats, admin dashboard, star of the day, VOD and recap cards, stream analytics, top chatters, AI slogans). kind site counts all of chat; user one person's messages (user_id); stream one stream's messages, chatters and soundboard plays (stream_id); channel-top the top chatters, of one stream (stream_id) or one channel (channel_user_id), or of all chat when neither is given. since and until bound chat_messages.timestamp, epoch ms, until exclusive. Deleted messages are never counted. Loopback only, service token.",
+ "type": "object",
+ "required": [
+ "kind"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "kind": {
+ "type": "string",
+ "enum": [
+ "site",
+ "user",
+ "stream",
+ "channel-top"
+ ]
+ },
+ "user_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Live user id; required for kind user."
+ },
+ "stream_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Live stream id; required for kind stream."
+ },
+ "channel_user_id": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "The broadcaster whose channel room it is (chat_messages.channel_user_id)."
+ },
+ "since": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Count messages at or after this instant (epoch ms)."
+ },
+ "until": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Count messages before this instant (epoch ms)."
+ },
+ "limit": {
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 50,
+ "description": "How many top chatters (channel-top); Chat's default is 10."
+ }
+ },
+ "allOf": [
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "user"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "user_id"
+ ]
+ }
+ },
+ {
+ "if": {
+ "properties": {
+ "kind": {
+ "const": "stream"
+ }
+ },
+ "required": [
+ "kind"
+ ]
+ },
+ "then": {
+ "required": [
+ "stream_id"
+ ]
+ }
+ }
+ ]
+ },
+ "chat.stats-result.v1": {
+ "title": "ChatStatsResult",
+ "description": "chat.stats-result@1: the answer of POST /internal/chat/stats (chat.stats.read). site, user and stream answer messages (non-deleted chat_messages rows) and chatters (distinct COALESCE(user_id, anon_id, source_platform || username)); stream also answers sounds (soundboard plays); channel-top answers top_chatters, most messages first. A failure is chat.ingress-ack@1's error ({ ok: false, error }).",
+ "anyOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "messages": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "chatters": {
+ "type": "integer",
+ "minimum": 0
+ },
+ "sounds": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Soundboard plays (message_type soundboard)."
+ },
+ "top_chatters": {
+ "type": "array",
+ "maxItems": 50,
+ "items": {
+ "$ref": "#/components/schemas/chat.stats-result.v1/$defs/chatter"
+ }
+ }
+ }
+ },
+ {
+ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
+ }
+ ],
+ "$defs": {
+ "chatter": {
+ "type": "object",
+ "required": [
+ "user_id",
+ "username",
+ "display_name",
+ "count"
+ ],
+ "properties": {
+ "user_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1,
+ "description": "Live user id; null for an anonymous or relayed chatter, counted by name."
+ },
+ "username": {
+ "type": "string"
+ },
+ "display_name": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "From Chat's copy of the user (ctx_users); null when unknown."
+ },
+ "avatar_url": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "profile_color": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 1,
+ "description": "Messages in the window."
+ }
+ }
+ }
+ }
+ },
+ "chat.timeline-result.v1": {
+ "title": "ChatTimelineResult",
+ "description": "chat.timeline-result@1: the answer of GET /internal/chat/timeline on OpenVibe.Chat (capability chat.analysis.read): non-deleted chat_messages of one channel (channel_user_id) or stream (stream_id) counted per bucket between since and until (epoch ms), bucket_ms wide, for OpenVibe.Live's AI moments, chat spikes and activity. Empty buckets are left out; buckets are oldest first. max_id is the highest message id in the window, null when none. A failure is chat.ingress-ack@1's error.",
+ "anyOf": [
+ {
+ "type": "object",
+ "required": [
+ "buckets",
+ "max_id"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "ok": {
+ "const": true
+ },
+ "buckets": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "t",
+ "count"
+ ],
+ "additionalProperties": false,
+ "properties": {
+ "t": {
+ "type": "integer",
+ "minimum": 0,
+ "description": "Bucket start, epoch ms."
+ },
+ "count": {
+ "type": "integer",
+ "minimum": 1
+ }
+ }
+ }
+ },
+ "max_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "minimum": 1
}
}
}
}
},
},
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Codes API",
"title": "OpenVibe.Codes API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Community API",
"title": "OpenVibe.Community API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Coupons API",
"title": "OpenVibe.Coupons API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Deals API",
"title": "OpenVibe.Deals API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Events API",
"title": "OpenVibe.Events API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Games API",
"title": "OpenVibe.Games API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Host API",
"title": "OpenVibe.Host API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -33,8 +33,8 @@
@@ -33,8 +33,8 @@
"service": "chat",
"service": "chat",
"name": "OpenVibe.Chat",
"name": "OpenVibe.Chat",
"origin": "https://openvibe.chat",
"origin": "https://openvibe.chat",
- "operations": 8,
- "capabilities": 5
+ "operations": 17,
+ "capabilities": 11
},
},
{
{
"service": "codes",
"service": "codes",
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Live API",
"title": "OpenVibe.Live API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Media API",
"title": "OpenVibe.Media API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Network API",
"title": "OpenVibe.Network API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.News API",
"title": "OpenVibe.News API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenRe.Stream API",
"title": "OpenRe.Stream API",
- "version": "0.94.2",
- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Reviews API",
"title": "OpenVibe.Reviews API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Search API",
"title": "OpenVibe.Search API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Sources API",
"title": "OpenVibe.Sources API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Space API",
"title": "OpenVibe.Space API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tips API",
"title": "OpenVibe.Tips API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Tools API",
"title": "OpenVibe.Tools API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Trade API",
"title": "OpenVibe.Trade API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.VIP API",
"title": "OpenVibe.VIP API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -3,8 +3,8 @@
@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"info": {
"title": "OpenVibe.Wiki API",
"title": "OpenVibe.Wiki API",
- "version": "0.94.2",
- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.94.2): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
+ "version": "0.95.0",
+ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.95.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"license": {
"name": "MIT",
"name": "MIT",
"identifier": "MIT"
"identifier": "MIT"
@@ -47447,9 +47447,9 @@ export type ChatSendRequest =
@@ -47447,9 +47447,9 @@ export type ChatSendRequest =
[k: string]: unknown | undefined;
[k: string]: unknown | undefined;
};
};
-/** [email protected] (owner: chat) */
+/** [email protected] (owner: chat) */
/**
/**
- * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).
+ * chat.send-result@1: what chat.message.send answers. On WS /ws/chat nothing is acknowledged directly: the stored line is broadcast as a chat frame to the room (the sender included; channel lines also reach the homepage global feed), and a refusal (banned, slow mode, held for IP approval, a channel rule, not saved) is a system or error frame to the sender alone. On POST /internal/live/calls, the batch result: one entry per op, in order, where a message op without chat.message.send is { ok: false, code: capability.denied }. On POST /internal/chat/messages: { ok: true, id } with the room (stream_id, channel_user_id, first_chat) or the DM (conversation_id, to_user_id), or chat.ingress-ack@1's error ({ ok: false, error }).
*/
*/
export type ChatSendResult =
export type ChatSendResult =
| {
| {
@@ -47561,6 +47561,10 @@ export type ChatSendResult =
@@ -47561,6 +47561,10 @@ export type ChatSendResult =
* DM only.
* DM only.
*/
*/
to_user_id?: number;
to_user_id?: number;
+ /**
+ * Room only, since 1.2.0: true when this is the sender's first message in this channel's stream (Chat's stream_first_chats), so Live can welcome them without reading its own copy; absent on a DM and from an older Chat.
+ */
+ first_chat?: boolean;
}
}
| {
| {
ok: false;
ok: false;
@@ -48066,6 +48070,317 @@ export type ChatEmoteCountResult =
@@ -48066,6 +48070,317 @@ export type ChatEmoteCountResult =
error: string;
error: string;
};
};
+/** [email protected] (owner: chat) */
+/**
+ * chat.stats-request@1: the body of POST /internal/chat/stats on OpenVibe.Chat (capability chat.stats.read): one count over chat_messages, for the stats OpenVibe.Live shows without reading its own copy of Chat's tables (home and SEO stats, admin dashboard, star of the day, VOD and recap cards, stream analytics, top chatters, AI slogans). kind site counts all of chat; user one person's messages (user_id); stream one stream's messages, chatters and soundboard plays (stream_id); channel-top the top chatters, of one stream (stream_id) or one channel (channel_user_id), or of all chat when neither is given. since and until bound chat_messages.timestamp, epoch ms, until exclusive. Deleted messages are never counted. Loopback only, service token.
+ */
+export type ChatStatsRequest = {
+ [k: string]: unknown | undefined;
+} & {
+ kind: "site" | "user" | "stream" | "channel-top";
+ /**
+ * Live user id; required for kind user.
+ */
+ user_id?: number;
+ /**
+ * Live stream id; required for kind stream.
+ */
+ stream_id?: number;
+ /**
+ * The broadcaster whose channel room it is (chat_messages.channel_user_id).
+ */
+ channel_user_id?: number;
+ /**
+ * Count messages at or after this instant (epoch ms).
+ */
+ since?: number;
+ /**
+ * Count messages before this instant (epoch ms).
+ */
+ until?: number;
+ /**
+ * How many top chatters (channel-top); Chat's default is 10.
+ */
+ limit?: number;
+};
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.stats-result@1: the answer of POST /internal/chat/stats (chat.stats.read). site, user and stream answer messages (non-deleted chat_messages rows) and chatters (distinct COALESCE(user_id, anon_id, source_platform || username)); stream also answers sounds (soundboard plays); channel-top answers top_chatters, most messages first. A failure is chat.ingress-ack@1's error ({ ok: false, error }).
+ */
+export type ChatStatsResult =
+ | {
+ ok?: true;
+ messages?: number;
+ chatters?: number;
+ /**
+ * Soundboard plays (message_type soundboard).
+ */
+ sounds?: number;
+ /**
+ * @maxItems 50
+ */
+ top_chatters?: {
+ /**
+ * Live user id; null for an anonymous or relayed chatter, counted by name.
+ */
+ user_id: number | null;
+ username: string;
+ /**
+ * From Chat's copy of the user (ctx_users); null when unknown.
+ */
+ display_name: string | null;
+ avatar_url?: string | null;
+ profile_color?: string | null;
+ /**
+ * Messages in the window.
+ */
+ count: number;
+ }[];
+ }
+ | {
+ ok: false;
+ /**
+ * A human-readable reason.
+ */
+ error: string;
+ };
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.messages-page@1: the answer of GET /internal/chat/messages on OpenVibe.Chat (capability chat.messages.read), which pages non-deleted chat_messages for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history. Query: one filter of channel_user_id, stream_id, user_id, anon_id, username or id (one message), a cursor on id (after_id ascending, before_id descending), limit, and tail=1 (no rows, only max_id). max_id is the highest id matching the filter, null when none, so a caller can resume after it. A failure is chat.ingress-ack@1's error.
+ */
+export type ChatMessagesPage =
+ | {
+ ok?: true;
+ messages: {
+ id: number;
+ /**
+ * Live user id; null for an anonymous or relayed sender.
+ */
+ user_id: number | null;
+ /**
+ * 'anon12345' for an anonymous sender.
+ */
+ anon_id: string | null;
+ username: string | null;
+ message: string;
+ message_type: "chat" | "system" | "donation" | "command" | "tts" | "channel-sound" | "soundboard" | "clip";
+ /**
+ * 1 for homepage global chat.
+ */
+ is_global: 0 | 1;
+ stream_id: number | null;
+ /**
+ * The broadcaster whose channel room it is.
+ */
+ channel_user_id: number | null;
+ /**
+ * The relay platform of a relayed line; null for OpenVibe chat.
+ */
+ source_platform: string | null;
+ reply_to_id: number | null;
+ /**
+ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it (ov_now()).
+ */
+ timestamp: string;
+ }[];
+ max_id: number | null;
+ }
+ | {
+ ok: false;
+ /**
+ * A human-readable reason.
+ */
+ error: string;
+ };
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.timeline-result@1: the answer of GET /internal/chat/timeline on OpenVibe.Chat (capability chat.analysis.read): non-deleted chat_messages of one channel (channel_user_id) or stream (stream_id) counted per bucket between since and until (epoch ms), bucket_ms wide, for OpenVibe.Live's AI moments, chat spikes and activity. Empty buckets are left out; buckets are oldest first. max_id is the highest message id in the window, null when none. A failure is chat.ingress-ack@1's error.
+ */
+export type ChatTimelineResult =
+ | {
+ ok?: true;
+ buckets: {
+ /**
+ * Bucket start, epoch ms.
+ */
+ t: number;
+ count: number;
+ }[];
+ max_id: number | null;
+ }
+ | {
+ ok: false;
+ /**
+ * A human-readable reason.
+ */
+ error: string;
+ };
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.moderation-queue-result@1: what OpenVibe.Chat's moderation queue reads answer (capability chat.moderation.queue.read), one shape per route, for OpenVibe.Live's moderator pages: GET /internal/chat/moderation/pending-ip?channel_id → { pending_ip } (a channel's messages held for IP approval, status pending, oldest first); GET /internal/chat/moderation/relay-users?channel_id → { relay_users } (relay names hidden or banned on the channel or everywhere, newest first); GET /internal/chat/moderation/relay-users/:id → { relay_user } (one, null when unknown); GET /internal/chat/moderation/tts-override?identity_key → { tts_override } (null when none). Internal: rows carry IP addresses. A failure is chat.ingress-ack@1's error.
+ */
+export type ChatModerationQueueResult =
+ | {
+ ok?: true;
+ pending_ip: {
+ id: number;
+ channel_id: number;
+ stream_id: number | null;
+ /**
+ * The sender's address: internal, never shown outside moderation.
+ */
+ ip_address: string;
+ user_id: number | null;
+ anon_id: string | null;
+ username: string | null;
+ message: string;
+ status: "pending" | "approved" | "denied";
+ reviewed_by: number | null;
+ /**
+ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it.
+ */
+ created_at: string | null;
+ }[];
+ }
+ | {
+ ok?: true;
+ relay_users: {
+ id: number;
+ /**
+ * null hides the name on every channel.
+ */
+ channel_id: number | null;
+ platform: string;
+ external_username: string;
+ action: "hide" | "ban";
+ reason: string | null;
+ created_by: number | null;
+ /**
+ * The moderator's name from Chat's copy of the user, when known.
+ */
+ created_by_username?: string | null;
+ /**
+ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it.
+ */
+ created_at: string | null;
+ }[];
+ }
+ | {
+ ok?: true;
+ relay_user: {
+ id: number;
+ /**
+ * null hides the name on every channel.
+ */
+ channel_id: number | null;
+ platform: string;
+ external_username: string;
+ action: "hide" | "ban";
+ reason: string | null;
+ created_by: number | null;
+ /**
+ * The moderator's name from Chat's copy of the user, when known.
+ */
+ created_by_username?: string | null;
+ /**
+ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it.
+ */
+ created_at: string | null;
+ } | null;
+ }
+ | {
+ ok?: true;
+ tts_override: {
+ /**
+ * Lowercased identity the override applies to.
+ */
+ identity_key: string;
+ voice: string | null;
+ pitch: number | null;
+ speed: number | null;
+ gap: number | null;
+ set_by: number | null;
+ /**
+ * UTC 'YYYY-MM-DD HH:MM:SS', as Chat stores it.
+ */
+ updated_at: string | null;
+ } | null;
+ }
+ | {
+ ok: false;
+ /**
+ * A human-readable reason.
+ */
+ error: string;
+ };
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.sounds-result@1: the answer of GET /internal/chat/sounds on OpenVibe.Chat (capability chat.sounds.read). ?channel_owner_id answers { count } (a channel's custom sounds, for Live's channel readiness); ?pending_asset=1 answers { sounds } (rows with no media_asset_id yet, which Live's asset sync uploads to OpenVibe.Media and writes back with POST /internal/chat/sounds/asset). A failure is chat.ingress-ack@1's error.
+ */
+export type ChatSoundsResult =
+ | {
+ ok?: true;
+ count?: number;
+ sounds?: {
+ id: number;
+ channel_owner_id: number;
+ /**
+ * The trigger word (no leading '!'), lowercased: the sound's name.
+ */
+ command: string;
+ /**
+ * Where Chat keeps the audio file (its on-disk path).
+ */
+ url: string;
+ mime?: string | null;
+ duration_seconds?: number | null;
+ /**
+ * The uploader's Live user id.
+ */
+ created_by?: number | null;
+ created_by_name?: string | null;
+ /**
+ * The file's OpenVibe.Media URL; null until Live's asset sync uploaded it.
+ */
+ media_url: string | null;
+ /**
+ * The OpenVibe.Media asset id; null until uploaded.
+ */
+ media_asset_id: number | null;
+ }[];
+ }
+ | {
+ ok: false;
+ /**
+ * A human-readable reason.
+ */
+ error: string;
+ };
+
+/** [email protected] (owner: chat) */
+/**
+ * chat.sound-asset-request@1: the body of POST /internal/chat/sounds/asset on OpenVibe.Chat (capability chat.sounds.write): after OpenVibe.Live's asset sync uploaded a channel sound to OpenVibe.Media, the asset is written back onto Chat's channel_sounds row id. Writing the same asset again is a no-op; an unknown id is 404. Answers chat.ingress-ack@1.
+ */
+export interface ChatSoundAssetRequest {
+ /**
+ * channel_sounds.id
+ */
+ id: number;
+ /**
+ * The asset's OpenVibe.Media URL.
+ */
+ media_url: string;
+ /**
+ * The OpenVibe.Media asset id.
+ */
+ media_asset_id: number;
+}
+
/** [email protected] (owner: live) */
/** [email protected] (owner: live) */
/**
/**
* live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).
* live.moderation.action v1 (OpenVibe.Live server/db/database.js logModerationAction; ADR-022). A staff or channel moderator action taken on OpenVibe.Live outside chat: site and global bans, IP bans, message deletes and purges from Live's admin panel, a stream force-ended, relay users hidden, channel moderators added or removed. Not a person tidying their own messages or configuring their own channel. Written to Live's outbox in the transaction that records the action. OpenVibe.Network keeps it in the moderation audit log. Envelope: subject { type: moderation_action, id: <action_id> }, visibility internal, actor the acting person when known. details is free-form per action (never secrets or message text beyond what moderators saw).
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "chat.analysis.read",
+ "version": "1.0",
+ "owner": "chat",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Count a channel's or a stream's chat messages per time bucket, with the latest message id: the chat timeline behind OpenVibe.Live's AI moments, chat spikes and activity reads (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "permissions": [
+ "chat:read"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "internal-read",
+ "events": [],
+ "implementedBy": [
+ "GET /internal/chat/timeline"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "chat.timeline-result@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "chat.messages.read",
+ "version": "1.0",
+ "owner": "chat",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Page OpenVibe.Chat's stored messages of one channel, stream, person, anonymous id or name, or read one by id, with a cursor on the message id and the latest id: for OpenVibe.Live's AI context, clip and viewer jobs and its moderator search and history (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "permissions": [
+ "chat:read"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "internal-read",
+ "events": [],
+ "implementedBy": [
+ "GET /internal/chat/messages"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "chat.messages-page@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
+{
+ "id": "chat.moderation.queue.read",
+ "version": "1.0",
+ "owner": "chat",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Read OpenVibe.Chat's moderation queues for OpenVibe.Live's moderator pages: a channel's messages held for IP approval, the relay names hidden or banned on a channel (or one by id), and one identity's TTS voice override (plan T3, J4b). Internal: rows carry IP addresses. chat.moderation.read stays the channel settings and moderators read. Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "permissions": [
+ "chat:read"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "internal-read",
+ "events": [],
+ "implementedBy": [
+ "GET /internal/chat/moderation/pending-ip",
+ "GET /internal/chat/moderation/relay-users",
+ "GET /internal/chat/moderation/relay-users/:id",
+ "GET /internal/chat/moderation/tts-override"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "chat.moderation-queue-result@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "chat.sounds.read",
+ "version": "1.0",
+ "owner": "chat",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Count a channel's custom sounds, or list the sounds not yet uploaded to OpenVibe.Media, from OpenVibe.Chat's channel_sounds: for OpenVibe.Live's channel readiness and its Media asset sync (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "permissions": [
+ "chat:read"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "internal-read",
+ "events": [],
+ "implementedBy": [
+ "GET /internal/chat/sounds"
+ ],
+ "inputSchema": "common.no-body@1",
+ "outputSchema": "chat.sounds-result@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "chat.sounds.write",
+ "version": "1.0",
+ "owner": "chat",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Write a channel sound's OpenVibe.Media asset (URL and asset id) back onto OpenVibe.Chat's channel_sounds row after OpenVibe.Live's asset sync uploaded the file (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "permissions": [
+ "chat:write"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "internal-write",
+ "events": [],
+ "implementedBy": [
+ "POST /internal/chat/sounds/asset"
+ ],
+ "inputSchema": "chat.sound-asset-request@1",
+ "outputSchema": "chat.ingress-ack@1"
+}
new file mode 100644
new file mode 100644
@@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
+{
+ "id": "chat.stats.read",
+ "version": "1.0",
+ "owner": "chat",
+ "status": "active",
+ "visibility": "internal",
+ "description": "Count OpenVibe.Chat's stored messages: all of chat, one person's, one stream's (messages, chatters, soundboard plays) or the top chatters of a stream, a channel or all chat, in an optional time window — the stats OpenVibe.Live shows (home, SEO, admin, star of the day, VOD and recap cards, stream analytics, AI slogans) without reading its own copy of Chat's tables (plan T3, J4b). Loopback only, Chat service token (Live's principal today); never granted to an app or delegated to an agent.",
+ "permissions": [
+ "chat:read"
+ ],
+ "resourceConstraints": [
+ "none"
+ ],
+ "quotaClass": "internal-read",
+ "events": [],
+ "implementedBy": [
+ "POST /internal/chat/stats"
+ ],
+ "inputSchema": "chat.stats-request@1",
+ "outputSchema": "chat.stats-result@1"
+}
@@ -16,9 +16,15 @@
@@ -16,9 +16,15 @@
"chat.event.publish",
"chat.event.publish",
"chat.live_bridge.write",
"chat.live_bridge.write",
"chat.message.send",
"chat.message.send",
+ "chat.analysis.read",
+ "chat.messages.read",
+ "chat.moderation.queue.read",
"chat.moderation.read",
"chat.moderation.read",
"chat.moderation.write",
"chat.moderation.write",
- "chat.presence.read"
+ "chat.presence.read",
+ "chat.sounds.read",
+ "chat.sounds.write",
+ "chat.stats.read"
],
],
"eventsProduced": [
"eventsProduced": [
"chat.message.created",
"chat.message.created",
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.94.2",
+ "version": "0.95.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.94.2",
+ "version": "0.95.0",
"license": "AGPL-3.0-only",
"license": "AGPL-3.0-only",
"dependencies": {
"dependencies": {
"ajv": "^8.17.1",
"ajv": "^8.17.1",
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-contracts",
"name": "openvibe-contracts",
- "version": "0.94.2",
+ "version": "0.95.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"main": "index.js",
"types": "generated/typescript/index.d.ts",
"types": "generated/typescript/index.d.ts",