openvibers/openvibe.contracts Commits

Add Chat's internal read capabilities and firstchat to send-result (#36)

OP OpenVibers committed 4 Oct 2026 · 392832084e · 84 files, +4186 −434

local checks ✓ 3/3 · CI ✓ 3/3 · review SHIP by DeepSeek harness (QUALITY=high)

https://github.com/OpenVibers/OpenVibe.Contracts/pull/36

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 7a17eba9..c4b9043e 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -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,
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.
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex c7b9b873..4082fc8a 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -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",
ddiff --git a/contracts/chat/messages-page.v1.json b/contracts/chat/messages-page.v1.json
nnew file mode 100644
iindex 00000000..bbce1099
---- /dev/null
++++ b/contracts/chat/messages-page.v1.json
@@@ -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())."
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/chat/moderation-queue-result.v1.json b/contracts/chat/moderation-queue-result.v1.json
nnew file mode 100644
iindex 00000000..c04ea08d
---- /dev/null
++++ b/contracts/chat/moderation-queue-result.v1.json
@@@ -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."
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/chat/send-result.v1.json b/contracts/chat/send-result.v1.json
iindex 3d33118b..c114854f 100644
---- a/contracts/chat/send-result.v1.json
++++ b/contracts/chat/send-result.v1.json
@@@ -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."
}
}
},
ddiff --git a/contracts/chat/sound-asset-request.v1.json b/contracts/chat/sound-asset-request.v1.json
nnew file mode 100644
iindex 00000000..77953776
---- /dev/null
++++ b/contracts/chat/sound-asset-request.v1.json
@@@ -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."
++ }
++ }
++}
ddiff --git a/contracts/chat/sounds-result.v1.json b/contracts/chat/sounds-result.v1.json
nnew file mode 100644
iindex 00000000..a2811fda
---- /dev/null
++++ b/contracts/chat/sounds-result.v1.json
@@@ -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."
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/chat/stats-request.v1.json b/contracts/chat/stats-request.v1.json
nnew file mode 100644
iindex 00000000..3d2adf87
---- /dev/null
++++ b/contracts/chat/stats-request.v1.json
@@@ -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"
++ ]
++ }
++ }
++ ]
++}
ddiff --git a/contracts/chat/stats-result.v1.json b/contracts/chat/stats-result.v1.json
nnew file mode 100644
iindex 00000000..f411492c
---- /dev/null
++++ b/contracts/chat/stats-result.v1.json
@@@ -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."
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/chat/timeline-result.v1.json b/contracts/chat/timeline-result.v1.json
nnew file mode 100644
iindex 00000000..feb42a06
---- /dev/null
++++ b/contracts/chat/timeline-result.v1.json
@@@ -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"
++ }
++ ]
++}
ddiff --git a/fixtures/chat.messages-page/invalid/missing-max-id.json b/fixtures/chat.messages-page/invalid/missing-max-id.json
nnew file mode 100644
iindex 00000000..f19dec77
---- /dev/null
++++ b/fixtures/chat.messages-page/invalid/missing-max-id.json
@@@ -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"
++ }
++ ]
++}
ddiff --git a/fixtures/chat.messages-page/invalid/unknown-message-type.json b/fixtures/chat.messages-page/invalid/unknown-message-type.json
nnew file mode 100644
iindex 00000000..02c09014
---- /dev/null
++++ b/fixtures/chat.messages-page/invalid/unknown-message-type.json
@@@ -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
++}
ddiff --git a/fixtures/chat.messages-page/valid/empty.json b/fixtures/chat.messages-page/valid/empty.json
nnew file mode 100644
iindex 00000000..b8e19de2
---- /dev/null
++++ b/fixtures/chat.messages-page/valid/empty.json
@@@ -0,0 +1,4 @@
++{
++ "messages": [],
++ "max_id": null
++}
ddiff --git a/fixtures/chat.messages-page/valid/page.json b/fixtures/chat.messages-page/valid/page.json
nnew file mode 100644
iindex 00000000..7d02d1b8
---- /dev/null
++++ b/fixtures/chat.messages-page/valid/page.json
@@@ -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
++}
ddiff --git a/fixtures/chat.messages-page/valid/tail.json b/fixtures/chat.messages-page/valid/tail.json
nnew file mode 100644
iindex 00000000..53bfb343
---- /dev/null
++++ b/fixtures/chat.messages-page/valid/tail.json
@@@ -0,0 +1,4 @@
++{
++ "messages": [],
++ "max_id": 90214
++}
ddiff --git a/fixtures/chat.moderation-queue-result/invalid/bad-status.json b/fixtures/chat.moderation-queue-result/invalid/bad-status.json
nnew file mode 100644
iindex 00000000..35c78951
---- /dev/null
++++ b/fixtures/chat.moderation-queue-result/invalid/bad-status.json
@@@ -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"
++ }
++ ]
++}
ddiff --git a/fixtures/chat.moderation-queue-result/invalid/row-missing-columns.json b/fixtures/chat.moderation-queue-result/invalid/row-missing-columns.json
nnew file mode 100644
iindex 00000000..1ed1f214
---- /dev/null
++++ b/fixtures/chat.moderation-queue-result/invalid/row-missing-columns.json
@@@ -0,0 +1,6 @@
++{
++ "relay_user": {
++ "id": 58,
++ "platform": "kick"
++ }
++}
ddiff --git a/fixtures/chat.moderation-queue-result/invalid/two-shapes.json b/fixtures/chat.moderation-queue-result/invalid/two-shapes.json
nnew file mode 100644
iindex 00000000..aba76a38
---- /dev/null
++++ b/fixtures/chat.moderation-queue-result/invalid/two-shapes.json
@@@ -0,0 +1,4 @@
++{
++ "relay_user": null,
++ "tts_override": null
++}
ddiff --git a/fixtures/chat.moderation-queue-result/valid/error.json b/fixtures/chat.moderation-queue-result/valid/error.json
nnew file mode 100644
iindex 00000000..2d4b2ef1
---- /dev/null
++++ b/fixtures/chat.moderation-queue-result/valid/error.json
@@@ -0,0 +1,4 @@
++{
++ "ok": false,
++ "error": "channel_id required"
++}
ddiff --git a/fixtures/chat.moderation-queue-result/valid/pending-ip.json b/fixtures/chat.moderation-queue-result/valid/pending-ip.json
nnew file mode 100644
iindex 00000000..1089846e
---- /dev/null
++++ b/fixtures/chat.moderation-queue-result/valid/pending-ip.json
@@@ -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"
++ }
++ ]
++}
ddiff --git a/fixtures/chat.moderation-queue-result/valid/relay-user-unknown.json b/fixtures/chat.moderation-queue-result/valid/relay-user-unknown.json
nnew file mode 100644
iindex 00000000..fc9a69e6
---- /dev/null
++++ b/fixtures/chat.moderation-queue-result/valid/relay-user-unknown.json
@@@ -0,0 +1,3 @@
++{
++ "relay_user": null
++}
ddiff --git a/fixtures/chat.moderation-queue-result/valid/relay-users.json b/fixtures/chat.moderation-queue-result/valid/relay-users.json
nnew file mode 100644
iindex 00000000..db3d895d
---- /dev/null
++++ b/fixtures/chat.moderation-queue-result/valid/relay-users.json
@@@ -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"
++ }
++ ]
++}
ddiff --git a/fixtures/chat.moderation-queue-result/valid/tts-override.json b/fixtures/chat.moderation-queue-result/valid/tts-override.json
nnew file mode 100644
iindex 00000000..00e6ca43
---- /dev/null
++++ b/fixtures/chat.moderation-queue-result/valid/tts-override.json
@@@ -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"
++ }
++}
ddiff --git a/fixtures/chat.send-result/invalid/first-chat-not-boolean.json b/fixtures/chat.send-result/invalid/first-chat-not-boolean.json
nnew file mode 100644
iindex 00000000..d55042a3
---- /dev/null
++++ b/fixtures/chat.send-result/invalid/first-chat-not-boolean.json
@@@ -0,0 +1,7 @@
++{
++ "ok": true,
++ "id": 90215,
++ "stream_id": 4211,
++ "channel_user_id": 77,
++ "first_chat": 1
++}
ddiff --git a/fixtures/chat.send-result/valid/ingress-first-chat.json b/fixtures/chat.send-result/valid/ingress-first-chat.json
nnew file mode 100644
iindex 00000000..4006fa25
---- /dev/null
++++ b/fixtures/chat.send-result/valid/ingress-first-chat.json
@@@ -0,0 +1,7 @@
++{
++ "ok": true,
++ "id": 90215,
++ "stream_id": 4211,
++ "channel_user_id": 77,
++ "first_chat": true
++}
ddiff --git a/fixtures/chat.sound-asset-request/invalid/extra-field.json b/fixtures/chat.sound-asset-request/invalid/extra-field.json
nnew file mode 100644
iindex 00000000..8837b884
---- /dev/null
++++ b/fixtures/chat.sound-asset-request/invalid/extra-field.json
@@@ -0,0 +1,6 @@
++{
++ "id": 904,
++ "media_url": "https://openvibe.media/a/31882/airhorn.mp3",
++ "media_asset_id": 31882,
++ "channel_owner_id": 77
++}
ddiff --git a/fixtures/chat.sound-asset-request/invalid/missing-asset-id.json b/fixtures/chat.sound-asset-request/invalid/missing-asset-id.json
nnew file mode 100644
iindex 00000000..1acc4b76
---- /dev/null
++++ b/fixtures/chat.sound-asset-request/invalid/missing-asset-id.json
@@@ -0,0 +1,4 @@
++{
++ "id": 904,
++ "media_url": "https://openvibe.media/a/31882/airhorn.mp3"
++}
ddiff --git a/fixtures/chat.sound-asset-request/valid/asset.json b/fixtures/chat.sound-asset-request/valid/asset.json
nnew file mode 100644
iindex 00000000..b23555f3
---- /dev/null
++++ b/fixtures/chat.sound-asset-request/valid/asset.json
@@@ -0,0 +1,5 @@
++{
++ "id": 904,
++ "media_url": "https://openvibe.media/a/31882/airhorn.mp3",
++ "media_asset_id": 31882
++}
ddiff --git a/fixtures/chat.sounds-result/invalid/negative-count.json b/fixtures/chat.sounds-result/invalid/negative-count.json
nnew file mode 100644
iindex 00000000..5e10926c
---- /dev/null
++++ b/fixtures/chat.sounds-result/invalid/negative-count.json
@@@ -0,0 +1,3 @@
++{
++ "count": -2
++}
ddiff --git a/fixtures/chat.sounds-result/invalid/sound-without-url.json b/fixtures/chat.sounds-result/invalid/sound-without-url.json
nnew file mode 100644
iindex 00000000..633050e9
---- /dev/null
++++ b/fixtures/chat.sounds-result/invalid/sound-without-url.json
@@@ -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
++ }
++ ]
++}
ddiff --git a/fixtures/chat.sounds-result/valid/count.json b/fixtures/chat.sounds-result/valid/count.json
nnew file mode 100644
iindex 00000000..fc681a4e
---- /dev/null
++++ b/fixtures/chat.sounds-result/valid/count.json
@@@ -0,0 +1,3 @@
++{
++ "count": 14
++}
ddiff --git a/fixtures/chat.sounds-result/valid/pending-asset.json b/fixtures/chat.sounds-result/valid/pending-asset.json
nnew file mode 100644
iindex 00000000..350303fc
---- /dev/null
++++ b/fixtures/chat.sounds-result/valid/pending-asset.json
@@@ -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
++ }
++ ]
++}
ddiff --git a/fixtures/chat.stats-request/invalid/limit-too-high.json b/fixtures/chat.stats-request/invalid/limit-too-high.json
nnew file mode 100644
iindex 00000000..2de1b563
---- /dev/null
++++ b/fixtures/chat.stats-request/invalid/limit-too-high.json
@@@ -0,0 +1,5 @@
++{
++ "kind": "channel-top",
++ "stream_id": 4211,
++ "limit": 51
++}
ddiff --git a/fixtures/chat.stats-request/invalid/unknown-kind.json b/fixtures/chat.stats-request/invalid/unknown-kind.json
nnew file mode 100644
iindex 00000000..8fc6680c
---- /dev/null
++++ b/fixtures/chat.stats-request/invalid/unknown-kind.json
@@@ -0,0 +1,3 @@
++{
++ "kind": "global"
++}
ddiff --git a/fixtures/chat.stats-request/invalid/user-without-id.json b/fixtures/chat.stats-request/invalid/user-without-id.json
nnew file mode 100644
iindex 00000000..3557a1ba
---- /dev/null
++++ b/fixtures/chat.stats-request/invalid/user-without-id.json
@@@ -0,0 +1,4 @@
++{
++ "kind": "user",
++ "since": 1759536000000
++}
ddiff --git a/fixtures/chat.stats-request/valid/channel-top.json b/fixtures/chat.stats-request/valid/channel-top.json
nnew file mode 100644
iindex 00000000..b8da421b
---- /dev/null
++++ b/fixtures/chat.stats-request/valid/channel-top.json
@@@ -0,0 +1,6 @@
++{
++ "kind": "channel-top",
++ "channel_user_id": 77,
++ "since": 1758326400000,
++ "limit": 20
++}
ddiff --git a/fixtures/chat.stats-request/valid/site-since.json b/fixtures/chat.stats-request/valid/site-since.json
nnew file mode 100644
iindex 00000000..1c69e44a
---- /dev/null
++++ b/fixtures/chat.stats-request/valid/site-since.json
@@@ -0,0 +1,4 @@
++{
++ "kind": "site",
++ "since": 1759536000000
++}
ddiff --git a/fixtures/chat.stats-request/valid/stream.json b/fixtures/chat.stats-request/valid/stream.json
nnew file mode 100644
iindex 00000000..4d78bdb5
---- /dev/null
++++ b/fixtures/chat.stats-request/valid/stream.json
@@@ -0,0 +1,4 @@
++{
++ "kind": "stream",
++ "stream_id": 4211
++}
ddiff --git a/fixtures/chat.stats-result/invalid/chatter-without-count.json b/fixtures/chat.stats-result/invalid/chatter-without-count.json
nnew file mode 100644
iindex 00000000..dd4ed18d
---- /dev/null
++++ b/fixtures/chat.stats-result/invalid/chatter-without-count.json
@@@ -0,0 +1,9 @@
++{
++ "top_chatters": [
++ {
++ "user_id": 512,
++ "username": "pixelfox",
++ "display_name": "PixelFox"
++ }
++ ]
++}
ddiff --git a/fixtures/chat.stats-result/invalid/negative-count.json b/fixtures/chat.stats-result/invalid/negative-count.json
nnew file mode 100644
iindex 00000000..408ef5eb
---- /dev/null
++++ b/fixtures/chat.stats-result/invalid/negative-count.json
@@@ -0,0 +1,3 @@
++{
++ "messages": -1
++}
ddiff --git a/fixtures/chat.stats-result/valid/error.json b/fixtures/chat.stats-result/valid/error.json
nnew file mode 100644
iindex 00000000..b1c33a86
---- /dev/null
++++ b/fixtures/chat.stats-result/valid/error.json
@@@ -0,0 +1,4 @@
++{
++ "ok": false,
++ "error": "kind user needs user_id"
++}
ddiff --git a/fixtures/chat.stats-result/valid/stream.json b/fixtures/chat.stats-result/valid/stream.json
nnew file mode 100644
iindex 00000000..5073dc2d
---- /dev/null
++++ b/fixtures/chat.stats-result/valid/stream.json
@@@ -0,0 +1,6 @@
++{
++ "ok": true,
++ "messages": 1834,
++ "chatters": 96,
++ "sounds": 41
++}
ddiff --git a/fixtures/chat.stats-result/valid/top-chatters.json b/fixtures/chat.stats-result/valid/top-chatters.json
nnew file mode 100644
iindex 00000000..a1a5d152
---- /dev/null
++++ b/fixtures/chat.stats-result/valid/top-chatters.json
@@@ -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
++ }
++ ]
++}
ddiff --git a/fixtures/chat.timeline-result/invalid/bucket-without-t.json b/fixtures/chat.timeline-result/invalid/bucket-without-t.json
nnew file mode 100644
iindex 00000000..f0c30a77
---- /dev/null
++++ b/fixtures/chat.timeline-result/invalid/bucket-without-t.json
@@@ -0,0 +1,8 @@
++{
++ "buckets": [
++ {
++ "count": 12
++ }
++ ],
++ "max_id": 90214
++}
ddiff --git a/fixtures/chat.timeline-result/valid/buckets.json b/fixtures/chat.timeline-result/valid/buckets.json
nnew file mode 100644
iindex 00000000..416918ba
---- /dev/null
++++ b/fixtures/chat.timeline-result/valid/buckets.json
@@@ -0,0 +1,13 @@
++{
++ "buckets": [
++ {
++ "t": 1759612440000,
++ "count": 12
++ },
++ {
++ "t": 1759612500000,
++ "count": 31
++ }
++ ],
++ "max_id": 90214
++}
ddiff --git a/fixtures/chat.timeline-result/valid/quiet.json b/fixtures/chat.timeline-result/valid/quiet.json
nnew file mode 100644
iindex 00000000..96f38673
---- /dev/null
++++ b/fixtures/chat.timeline-result/valid/quiet.json
@@@ -0,0 +1,4 @@
++{
++ "buckets": [],
++ "max_id": null
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 56c8c47b..9d3367d7 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -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",
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex edeed018..45136892 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -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"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex 61baf646..2bf5b572 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -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"
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex 3c3d969d..2483a3d2 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -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"
ddiff --git a/generated/openapi/bot.json b/generated/openapi/bot.json
iindex 4efef3a5..c7d28193 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -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"
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 2fc52c6d..87178bba 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -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
}
}
},
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 8a7fe5da..399ba68d 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -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"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 98ffd89d..a7fa2807 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -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"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex 033111cb..41cb97b8 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -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"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex 610cbb80..b4cf2cf2 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -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"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex d7d3dc37..efc40517 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -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"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex d908c04e..da2a145c 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -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"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 19bce4b5..0f3d5ff2 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -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"
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex c1e2b0ee..ec610163 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -33,8 +33,8 @@
"service": "chat",
"name": "OpenVibe.Chat",
"origin": "https://openvibe.chat",
-- "operations": 8,
-- "capabilities": 5
++ "operations": 17,
++ "capabilities": 11
},
{
"service": "codes",
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex e3593050..2ecfd1b4 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -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"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex 3c2e5804..19d3c826 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -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"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex d9df3740..42bf128d 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -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"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 80ea3964..a5bfde02 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -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"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 1388d489..ff3bee93 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -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"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 725efbd6..2381f782 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -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"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex fa7021f4..d5d00eff 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -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"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex 7ae4661f..33bfbf45 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -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"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex a3259ef3..59d45cc5 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -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"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex 2a127c29..2e42ced9 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -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"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex 1552d933..1fe1ca00 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -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"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex 30380833..bd61b308 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -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"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex f7c6d0c2..1f5209fe 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -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"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex 42e0b044..b69e4daf 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -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"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex e4f8c9d1..ecfe018a 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -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).
ddiff --git a/manifests/capabilities/chat.analysis.read.json b/manifests/capabilities/chat.analysis.read.json
nnew file mode 100644
iindex 00000000..42c2454e
---- /dev/null
++++ b/manifests/capabilities/chat.analysis.read.json
@@@ -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"
++}
ddiff --git a/manifests/capabilities/chat.messages.read.json b/manifests/capabilities/chat.messages.read.json
nnew file mode 100644
iindex 00000000..641b1a9a
---- /dev/null
++++ b/manifests/capabilities/chat.messages.read.json
@@@ -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"
++}
ddiff --git a/manifests/capabilities/chat.moderation.queue.read.json b/manifests/capabilities/chat.moderation.queue.read.json
nnew file mode 100644
iindex 00000000..36225c7d
---- /dev/null
++++ b/manifests/capabilities/chat.moderation.queue.read.json
@@@ -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"
++}
ddiff --git a/manifests/capabilities/chat.sounds.read.json b/manifests/capabilities/chat.sounds.read.json
nnew file mode 100644
iindex 00000000..11b097f8
---- /dev/null
++++ b/manifests/capabilities/chat.sounds.read.json
@@@ -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"
++}
ddiff --git a/manifests/capabilities/chat.sounds.write.json b/manifests/capabilities/chat.sounds.write.json
nnew file mode 100644
iindex 00000000..e9939328
---- /dev/null
++++ b/manifests/capabilities/chat.sounds.write.json
@@@ -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"
++}
ddiff --git a/manifests/capabilities/chat.stats.read.json b/manifests/capabilities/chat.stats.read.json
nnew file mode 100644
iindex 00000000..d1bf5df2
---- /dev/null
++++ b/manifests/capabilities/chat.stats.read.json
@@@ -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"
++}
ddiff --git a/manifests/services/chat.json b/manifests/services/chat.json
iindex 56d37282..f3515a8e 100644
---- a/manifests/services/chat.json
++++ b/manifests/services/chat.json
@@@ -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",
ddiff --git a/package-lock.json b/package-lock.json
iindex c3ce14e8..6934ade2 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -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",
ddiff --git a/package.json b/package.json
iindex 6213ecc2..8772f350 100644
---- a/package.json
++++ b/package.json
@@@ -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",