openvibers/openvibe.contracts Commits

Register Chat's service-ingress capabilities and request contracts

OP OpenVibers committed 4 Oct 2026 · 4f91a797a3 · 86 files, +7341 −106

Asked: T3 J2 contract half — OpenVibe.Contracts: register the capabilities OpenVibe.Chat's new service-token ingress uses, so Chat PR #16 (https://github.com/OpenVibers/OpenVibe.Chat/pull/16) passes its contracts check. ONE pull request from current origin/main (git fetch first; main is 836bfcf, tagged v0…
Harness job contracts-t3-j2-contract-half-102719 (plan T3).

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 255973c8..31cf7d6d 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,34 @@ 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.91.0 — 2026-10-04
++
++**Chat's service-token ingress** (plan T3 J2, OpenVibe.Chat PR #16). These are the capabilities and request contracts of
++`/internal/chat/*`, the typed Chat endpoints that replace Live's generic bridge (`POST /internal/live/calls`). All of it is
++additive.
++- New active capabilities, owner `chat`, `first-party`, quota class `internal-write`, each with an input and an output
++ schema. They are loopback only and need a service token. They are never granted to an app or delegated to an agent.
++ - `chat.event.publish` (`POST /internal/chat/events`, sensitive): transient frames to a stream, a channel, an owner's
++ streams, global chat, everyone or one person.
++ - `chat.moderation.write` (`POST /internal/chat/moderation`, sensitive): one typed action per request (deletes,
++ IP-approval reviews, relay identities, TTS voice overrides, disconnect, log). It emits `chat.message.deleted` and
++ `chat.moderation.action`.
++ - `chat.cache.invalidate` (`POST /internal/chat/invalidate`): cache hints for a person, a channel, IP approvals and bans.
++ Chat's manifest lists all three.
++- `chat.message.send` also names `POST /internal/chat/messages`, and `chat.presence.read` names
++ `GET /internal/chat/presence`. Both keep their existing routes.
++- New contracts under `contracts/chat/`, which follow Chat's validators:
++ - `chat.event-request@1`: target kinds, the 16 frame types, the per-frame fields and the target each frame needs.
++ - `chat.moderation-request@1`: one `oneOf` branch per action, each with its own fields only.
++ - `chat.invalidate-request@1`: at least one hint, and `user_data` needs `user`.
++ - `chat.moderation-result@1`: deleted `ids`, or `changes` and `id`.
++ - `chat.ingress-ack@1`: `{ ok: true }`, or the `{ ok: false, error }` refusal every `/internal/chat` route answers.
++ Every request needs an idempotency `key` of 1–160 characters `[A-Za-z0-9:._-]`. Chat applies it once per caller and
++ route; a reused key with another body gets 409.
++- `chat.send-request` 1.1.0 adds the `/internal/chat/messages` body (room line or DM, `mirror`, `tts`, `frame`,
++ `first_chat_key`). `chat.send-result` 1.1.0 adds its `{ ok, id, … }` answer and the ingress error.
++- Fixtures: valid and invalid fixtures for every new contract and for the new branches.
++
## 0.90.0 — 2026-10-04
**OpenVibe.Run service and job API** (plan T14 R1, ADR-034). The contract OpenVibe.Run is built against; everything is
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex a4f3b81f..847adca2 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -4550,7 +4550,7 @@
},
{
"id": "chat.send-request",
-- "version": "1.0.0",
++ "version": "1.1.0",
"owner": "chat",
"schema": "chat/send-request.v1.json",
"visibility": "first-party",
@@@ -4559,13 +4559,58 @@
},
{
"id": "chat.send-result",
-- "version": "1.0.0",
++ "version": "1.1.0",
"owner": "chat",
"schema": "chat/send-result.v1.json",
"visibility": "first-party",
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "chat.event-request",
++ "version": "1.0.0",
++ "owner": "chat",
++ "schema": "chat/event-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "chat.moderation-request",
++ "version": "1.0.0",
++ "owner": "chat",
++ "schema": "chat/moderation-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "chat.moderation-result",
++ "version": "1.0.0",
++ "owner": "chat",
++ "schema": "chat/moderation-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "chat.invalidate-request",
++ "version": "1.0.0",
++ "owner": "chat",
++ "schema": "chat/invalidate-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "chat.ingress-ack",
++ "version": "1.0.0",
++ "owner": "chat",
++ "schema": "chat/ingress-ack.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "chat.channel-moderation-result",
"version": "1.0.0",
ddiff --git a/contracts/chat/event-request.v1.json b/contracts/chat/event-request.v1.json
nnew file mode 100644
iindex 00000000..904f800a
---- /dev/null
++++ b/contracts/chat/event-request.v1.json
@@@ -0,0 +1,734 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/event-request.v1.json",
++ "title": "ChatEventRequest",
++ "description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",
++ "type": "object",
++ "required": [
++ "key",
++ "target",
++ "frame"
++ ],
++ "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."
++ },
++ "target": {
++ "type": "object",
++ "required": [
++ "kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "kind": {
++ "enum": [
++ "stream",
++ "channel",
++ "owner-streams",
++ "global",
++ "all",
++ "user"
++ ],
++ "description": "stream: one stream's room; channel: a channel owner's room; owner-streams: every stream of that owner; global: the homepage global chat; all: every socket; user: one person's sockets (call frames only)."
++ },
++ "id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1,
++ "description": "The stream, channel owner or user id; required for stream, channel, owner-streams and user, absent or null for global and all."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "enum": [
++ "stream",
++ "channel",
++ "owner-streams",
++ "user"
++ ]
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "properties": {
++ "id": {
++ "type": "integer"
++ }
++ }
++ },
++ "else": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ ]
++ },
++ "frame": {
++ "type": "object",
++ "required": [
++ "type"
++ ],
++ "properties": {
++ "type": {
++ "enum": [
++ "chat",
++ "system",
++ "donation",
++ "goal-update",
++ "goal-reached",
++ "update",
++ "voice-channels",
++ "alert",
++ "channel-sound",
++ "server_restart",
++ "media_queue_update",
++ "media_now_playing",
++ "redemption",
++ "vibe-coding",
++ "vc-call-invite",
++ "vc-call-response"
++ ],
++ "description": "The client frame type. chat is only the transient news card (never saved); a stored line goes through chat.message.send."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "enum": [
++ "system",
++ "server_restart"
++ ]
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "message"
++ ],
++ "properties": {
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 4000
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "chat"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "message_type",
++ "username",
++ "message",
++ "timestamp"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {},
++ "message_type": {
++ "const": "news"
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 4000
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "news_source": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "timestamp": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 40
++ },
++ "source_platform": {
++ "enum": [
++ "news",
++ null
++ ]
++ },
++ "system": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "alert"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "streamerId",
++ "kind"
++ ],
++ "properties": {
++ "streamerId": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The channel owner; must equal target.id with target.kind channel."
++ },
++ "streamId": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "kind": {
++ "enum": [
++ "goal",
++ "donation"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "voice-channels"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "channels"
++ ],
++ "properties": {
++ "channels": {
++ "type": "array",
++ "maxItems": 128
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "channel-sound"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "streamId",
++ "command"
++ ],
++ "properties": {
++ "streamId": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Must equal target.id with target.kind stream."
++ },
++ "command": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "userId": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "anonId": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "args": {
++ "type": [
++ "array",
++ "null"
++ ],
++ "maxItems": 20,
++ "items": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ }
++ },
++ "relay": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "username"
++ ],
++ "properties": {
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "media_queue_update"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "type": "object"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "media_now_playing"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "request"
++ ],
++ "properties": {
++ "request": {
++ "type": [
++ "object",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "redemption"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "username",
++ "reward_title",
++ "cost"
++ ],
++ "properties": {
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "reward_title": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ },
++ "cost": {
++ "type": "number"
++ },
++ "user_input": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "vibe-coding"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "managed_stream_id",
++ "event"
++ ],
++ "properties": {
++ "managed_stream_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "event": {
++ "type": "object"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "enum": [
++ "vc-call-invite",
++ "vc-call-response"
++ ]
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "channelId",
++ "fromUserId",
++ "fromUsername",
++ "createdAt"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {},
++ "channelId": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "channelName": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "fromUserId": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "fromUsername": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "fromDisplayName": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "fromAvatarUrl": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "createdAt": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Milliseconds since the epoch."
++ },
++ "status": {
++ "enum": [
++ "accepted",
++ "declined",
++ "busy",
++ "no-answer",
++ "canceled"
++ ],
++ "description": "vc-call-response only, and required there."
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "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",
++ "properties": {
++ "target": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "const": "channel"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "frame": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "voice-channels"
++ }
++ }
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "properties": {
++ "target": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "const": "all"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "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"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/chat/ingress-ack.v1.json b/contracts/chat/ingress-ack.v1.json
nnew file mode 100644
iindex 00000000..ab5bbe83
---- /dev/null
++++ b/contracts/chat/ingress-ack.v1.json
@@@ -0,0 +1,41 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/ingress-ack.v1.json",
++ "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": "#/$defs/error"
++ }
++ ]
++}
ddiff --git a/contracts/chat/invalidate-request.v1.json b/contracts/chat/invalidate-request.v1.json
nnew file mode 100644
iindex 00000000..d4e1a211
---- /dev/null
++++ b/contracts/chat/invalidate-request.v1.json
@@@ -0,0 +1,157 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/invalidate-request.v1.json",
++ "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
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "bans"
++ ],
++ "properties": {
++ "bans": {
++ "const": true
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/chat/moderation-request.v1.json b/contracts/chat/moderation-request.v1.json
nnew file mode 100644
iindex 00000000..a9ee86f1
---- /dev/null
++++ b/contracts/chat/moderation-request.v1.json
@@@ -0,0 +1,748 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/moderation-request.v1.json",
++ "title": "ChatModerationRequest",
++ "description": "chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.",
++ "$defs": {
++ "key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160,
++ "pattern": "^[A-Za-z0-9:._-]+$",
++ "description": "Idempotency key, scoped to the caller and to this route."
++ }
++ },
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-message"
++ },
++ "id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "user_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-user-messages"
++ },
++ "user_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ },
++ "description": "Every visible message of a signed-in person, in one stream or everywhere."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "anon_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-anon-messages"
++ },
++ "anon_id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "username"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-relay-messages"
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ },
++ "description": "Every visible message stored under a relay username."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "from",
++ "to"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-by-range"
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "from": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
++ },
++ "to": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ },
++ "description": "Every visible message between from and to, in one stream or (without stream_id) in global chat."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "id",
++ "status"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "review-pending-ip"
++ },
++ "id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "status": {
++ "enum": [
++ "approved",
++ "denied"
++ ]
++ },
++ "reviewed_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "channel_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "channel_id",
++ "ip"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "approve-ip-messages"
++ },
++ "channel_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "ip": {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ },
++ "reviewed_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "channel_id",
++ "ip"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "deny-ip-messages"
++ },
++ "channel_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "ip": {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ },
++ "reviewed_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "platform",
++ "external_username"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "relay-hide"
++ },
++ "channel_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "platform": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "external_username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "created_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "mode": {
++ "enum": [
++ "hide",
++ "ban",
++ null
++ ],
++ "description": "Default hide."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "relay-unhide"
++ },
++ "id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "channel_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "platform": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "external_username": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ }
++ },
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "properties": {
++ "id": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "platform",
++ "external_username"
++ ]
++ }
++ ],
++ "description": "By the hidden row's id, or by channel, platform and external username."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "platform",
++ "username"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "relay-record"
++ },
++ "platform": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "identity_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "tts-voice-override"
++ },
++ "identity_key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "set_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "params": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "voice"
++ ],
++ "additionalProperties": false,
++ "description": "Absent or null: the override is cleared.",
++ "properties": {
++ "voice": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "pitch": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 10
++ },
++ "speed": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 10
++ },
++ "gap": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 10
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "disconnect"
++ },
++ "user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "ip": {
++ "anyOf": [
++ {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ },
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "user_id"
++ ],
++ "properties": {
++ "user_id": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "ip"
++ ],
++ "properties": {
++ "ip": {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ }
++ }
++ }
++ ],
++ "description": "Chat refreshes its ban cache and closes the matching sockets (in one stream, or everywhere)."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "action_type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "log"
++ },
++ "scope_type": {
++ "enum": [
++ "site",
++ "channel",
++ "stream",
++ "room",
++ null
++ ]
++ },
++ "scope_id": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "A string for scope_type room, otherwise an id."
++ },
++ "actor_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "target_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "action_type": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "details": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "description": "At most 8192 characters as JSON."
++ }
++ },
++ "description": "Append a row to Chat's moderation log."
++ }
++ ]
++}
ddiff --git a/contracts/chat/moderation-result.v1.json b/contracts/chat/moderation-result.v1.json
nnew file mode 100644
iindex 00000000..8d21a79d
---- /dev/null
++++ b/contracts/chat/moderation-result.v1.json
@@@ -0,0 +1,56 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/moderation-result.v1.json",
++ "title": "ChatModerationResult",
++ "description": "chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).",
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "ok",
++ "ids"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ok": {
++ "const": true
++ },
++ "ids": {
++ "type": "array",
++ "items": {
++ "type": "integer"
++ },
++ "description": "The deleted message ids."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "ok",
++ "changes",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ok": {
++ "const": true
++ },
++ "changes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "description": "The inserted row (a log entry, a hidden relay identity), else null."
++ }
++ }
++ },
++ {
++ "$ref": "ingress-ack.v1.json#/$defs/error"
++ }
++ ]
++}
ddiff --git a/contracts/chat/send-request.v1.json b/contracts/chat/send-request.v1.json
iindex 23c2028f..d6781df4 100644
---- a/contracts/chat/send-request.v1.json
++++ b/contracts/chat/send-request.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/chat/send-request.v1.json",
"title": "ChatSendRequest",
-- "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",
++ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.",
"anyOf": [
{
"type": "object",
@@@ -84,6 +84,300 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "username",
++ "message"
++ ],
++ "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."
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "channel_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "anon_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 2000,
++ "pattern": "\\S",
++ "description": "The text; not blank."
++ },
++ "message_type": {
++ "enum": [
++ "chat",
++ "system",
++ "donation",
++ "command",
++ "tts",
++ "channel-sound",
++ "soundboard",
++ "clip",
++ null
++ ]
++ },
++ "is_global": {
++ "type": [
++ "boolean",
++ "null"
++ ],
++ "description": "Default: true when there is no stream and no channel."
++ },
++ "reply_to_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "source_platform": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "auto_delete_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "When Chat deletes the line (a date Chat parses)."
++ },
++ "metadata": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "description": "At most 8192 characters as JSON."
++ },
++ "mirror": {
++ "type": [
++ "boolean",
++ "null"
++ ],
++ "description": "Also send the line to global chat and the streamer rooms."
++ },
++ "dm": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "to_user_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "to_user_id": {
++ "type": "integer",
++ "minimum": 1
++ }
++ },
++ "description": "Send a direct message from user_id instead (no room, mirror, tts or frame; 403 if either blocks the other, 404 if either is unknown)."
++ },
++ "tts": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "description": "Read the line aloud in its room (needs stream_id or channel_user_id).",
++ "properties": {
++ "voice": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "identity_key": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "key": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 160,
++ "description": "Audio key; default m<id>."
++ }
++ }
++ },
++ "frame": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "description": "How the broadcast shows the sender.",
++ "properties": {
++ "role": {
++ "enum": [
++ "admin",
++ "global_mod",
++ "streamer",
++ "user",
++ "anon",
++ "external",
++ null
++ ]
++ },
++ "profile_color": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^#[0-9a-fA-F]{3,8}$"
++ },
++ "avatar_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "is_ai": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "is_bot": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "filtered": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "core_username": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "display_name": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ }
++ }
++ },
++ "first_chat_key": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 160,
++ "pattern": "^(?:user|anon|ext):.",
++ "description": "Whose first chat in the channel this is (user:, anon: or ext:); default from user_id or anon_id."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "required": [
++ "dm"
++ ],
++ "properties": {
++ "dm": {
++ "type": "object"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "user_id"
++ ],
++ "properties": {
++ "user_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "stream_id": {
++ "type": "null"
++ },
++ "channel_user_id": {
++ "type": "null"
++ },
++ "mirror": {
++ "enum": [
++ false,
++ null
++ ]
++ },
++ "tts": {
++ "type": "null"
++ },
++ "frame": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ ]
}
]
}
ddiff --git a/contracts/chat/send-result.v1.json b/contracts/chat/send-result.v1.json
iindex 9afdb222..3d33118b 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 }.",
++ "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 }).",
"anyOf": [
{
"type": "object",
@@@ -195,6 +195,46 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "ok",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ok": {
++ "const": true
++ },
++ "id": {
++ "type": "integer",
++ "description": "The stored message id."
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "channel_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "conversation_id": {
++ "type": "integer",
++ "description": "DM only."
++ },
++ "to_user_id": {
++ "type": "integer",
++ "description": "DM only."
++ }
++ }
++ },
++ {
++ "$ref": "ingress-ack.v1.json#/$defs/error"
}
]
}
ddiff --git a/fixtures/chat.event-request/invalid/alert-not-to-channel.json b/fixtures/chat.event-request/invalid/alert-not-to-channel.json
nnew file mode 100644
iindex 00000000..a6a7ff4f
---- /dev/null
++++ b/fixtures/chat.event-request/invalid/alert-not-to-channel.json
@@@ -0,0 +1,12 @@
++{
++ "key": "k2",
++ "target": {
++ "kind": "stream",
++ "id": 42
++ },
++ "frame": {
++ "type": "alert",
++ "streamerId": 77,
++ "kind": "donation"
++ }
++}
ddiff --git a/fixtures/chat.event-request/invalid/bad-key-characters.json b/fixtures/chat.event-request/invalid/bad-key-characters.json
nnew file mode 100644
iindex 00000000..c373a8a0
---- /dev/null
++++ b/fixtures/chat.event-request/invalid/bad-key-characters.json
@@@ -0,0 +1,10 @@
++{
++ "key": "has spaces",
++ "target": {
++ "kind": "global"
++ },
++ "frame": {
++ "type": "system",
++ "message": "hi"
++ }
++}
ddiff --git a/fixtures/chat.event-request/invalid/card-to-a-user.json b/fixtures/chat.event-request/invalid/card-to-a-user.json
nnew file mode 100644
iindex 00000000..39fe2c1f
---- /dev/null
++++ b/fixtures/chat.event-request/invalid/card-to-a-user.json
@@@ -0,0 +1,11 @@
++{
++ "key": "k4",
++ "target": {
++ "kind": "user",
++ "id": 5
++ },
++ "frame": {
++ "type": "system",
++ "message": "hi"
++ }
++}
ddiff --git a/fixtures/chat.event-request/invalid/global-with-id.json b/fixtures/chat.event-request/invalid/global-with-id.json
nnew file mode 100644
iindex 00000000..3d2f0b3c
---- /dev/null
++++ b/fixtures/chat.event-request/invalid/global-with-id.json
@@@ -0,0 +1,11 @@
++{
++ "key": "k3",
++ "target": {
++ "kind": "global",
++ "id": 5
++ },
++ "frame": {
++ "type": "system",
++ "message": "hi"
++ }
++}
ddiff --git a/fixtures/chat.event-request/invalid/missing-key.json b/fixtures/chat.event-request/invalid/missing-key.json
nnew file mode 100644
iindex 00000000..1f990491
---- /dev/null
++++ b/fixtures/chat.event-request/invalid/missing-key.json
@@@ -0,0 +1,9 @@
++{
++ "target": {
++ "kind": "global"
++ },
++ "frame": {
++ "type": "system",
++ "message": "hi"
++ }
++}
ddiff --git a/fixtures/chat.event-request/invalid/unknown-frame.json b/fixtures/chat.event-request/invalid/unknown-frame.json
nnew file mode 100644
iindex 00000000..abb792b1
---- /dev/null
++++ b/fixtures/chat.event-request/invalid/unknown-frame.json
@@@ -0,0 +1,10 @@
++{
++ "key": "k1",
++ "target": {
++ "kind": "global"
++ },
++ "frame": {
++ "type": "eval",
++ "code": "x"
++ }
++}
ddiff --git a/fixtures/chat.event-request/valid/call-invite.json b/fixtures/chat.event-request/valid/call-invite.json
nnew file mode 100644
iindex 00000000..34b931c8
---- /dev/null
++++ b/fixtures/chat.event-request/valid/call-invite.json
@@@ -0,0 +1,17 @@
++{
++ "key": "live:call:inv:9f2c",
++ "target": {
++ "kind": "user",
++ "id": 15
++ },
++ "frame": {
++ "type": "vc-call-invite",
++ "channelId": "vc-abc",
++ "channelName": "Lounge",
++ "fromUserId": 3,
++ "fromUsername": "host",
++ "fromDisplayName": null,
++ "fromAvatarUrl": null,
++ "createdAt": 1759600000000
++ }
++}
ddiff --git a/fixtures/chat.event-request/valid/donation-card.json b/fixtures/chat.event-request/valid/donation-card.json
nnew file mode 100644
iindex 00000000..7dad5bed
---- /dev/null
++++ b/fixtures/chat.event-request/valid/donation-card.json
@@@ -0,0 +1,13 @@
++{
++ "key": "live:donation:4812",
++ "target": {
++ "kind": "stream",
++ "id": 42
++ },
++ "frame": {
++ "type": "donation",
++ "username": "viewer1",
++ "amount": 5,
++ "message": "gg"
++ }
++}
ddiff --git a/fixtures/chat.event-request/valid/goal-alert.json b/fixtures/chat.event-request/valid/goal-alert.json
nnew file mode 100644
iindex 00000000..66417b03
---- /dev/null
++++ b/fixtures/chat.event-request/valid/goal-alert.json
@@@ -0,0 +1,13 @@
++{
++ "key": "live:alert:goal:77:3",
++ "target": {
++ "kind": "channel",
++ "id": 77
++ },
++ "frame": {
++ "type": "alert",
++ "streamerId": 77,
++ "streamId": null,
++ "kind": "goal"
++ }
++}
ddiff --git a/fixtures/chat.event-request/valid/restart-notice.json b/fixtures/chat.event-request/valid/restart-notice.json
nnew file mode 100644
iindex 00000000..bb925477
---- /dev/null
++++ b/fixtures/chat.event-request/valid/restart-notice.json
@@@ -0,0 +1,11 @@
++{
++ "key": "live:restart:1759600000",
++ "target": {
++ "kind": "all",
++ "id": null
++ },
++ "frame": {
++ "type": "server_restart",
++ "message": "Live restarts in a moment."
++ }
++}
ddiff --git a/fixtures/chat.ingress-ack/invalid/missing-ok.json b/fixtures/chat.ingress-ack/invalid/missing-ok.json
nnew file mode 100644
iindex 00000000..fdd23fb6
---- /dev/null
++++ b/fixtures/chat.ingress-ack/invalid/missing-ok.json
@@@ -0,0 +1,3 @@
++{
++ "error": "x"
++}
ddiff --git a/fixtures/chat.ingress-ack/invalid/ok-with-payload.json b/fixtures/chat.ingress-ack/invalid/ok-with-payload.json
nnew file mode 100644
iindex 00000000..0b564f47
---- /dev/null
++++ b/fixtures/chat.ingress-ack/invalid/ok-with-payload.json
@@@ -0,0 +1,4 @@
++{
++ "ok": true,
++ "id": 5
++}
ddiff --git a/fixtures/chat.ingress-ack/valid/ok.json b/fixtures/chat.ingress-ack/valid/ok.json
nnew file mode 100644
iindex 00000000..0287aedd
---- /dev/null
++++ b/fixtures/chat.ingress-ack/valid/ok.json
@@@ -0,0 +1,3 @@
++{
++ "ok": true
++}
ddiff --git a/fixtures/chat.ingress-ack/valid/retry.json b/fixtures/chat.ingress-ack/valid/retry.json
nnew file mode 100644
iindex 00000000..e1eca076
---- /dev/null
++++ b/fixtures/chat.ingress-ack/valid/retry.json
@@@ -0,0 +1,4 @@
++{
++ "ok": false,
++ "error": "Chat ingress unavailable; retry with the same key"
++}
ddiff --git a/fixtures/chat.invalidate-request/invalid/bans-false-only.json b/fixtures/chat.invalidate-request/invalid/bans-false-only.json
nnew file mode 100644
iindex 00000000..3dfab461
---- /dev/null
++++ b/fixtures/chat.invalidate-request/invalid/bans-false-only.json
@@@ -0,0 +1,4 @@
++{
++ "key": "k2",
++ "bans": false
++}
ddiff --git a/fixtures/chat.invalidate-request/invalid/no-hint.json b/fixtures/chat.invalidate-request/invalid/no-hint.json
nnew file mode 100644
iindex 00000000..61cefd6c
---- /dev/null
++++ b/fixtures/chat.invalidate-request/invalid/no-hint.json
@@@ -0,0 +1,3 @@
++{
++ "key": "k1"
++}
ddiff --git a/fixtures/chat.invalidate-request/invalid/user-data-without-user.json b/fixtures/chat.invalidate-request/invalid/user-data-without-user.json
nnew file mode 100644
iindex 00000000..42b3a078
---- /dev/null
++++ b/fixtures/chat.invalidate-request/invalid/user-data-without-user.json
@@@ -0,0 +1,7 @@
++{
++ "key": "k3",
++ "user_data": {
++ "id": 12,
++ "username": "viewer1"
++ }
++}
ddiff --git a/fixtures/chat.invalidate-request/valid/bans.json b/fixtures/chat.invalidate-request/valid/bans.json
nnew file mode 100644
iindex 00000000..9b2b154e
---- /dev/null
++++ b/fixtures/chat.invalidate-request/valid/bans.json
@@@ -0,0 +1,4 @@
++{
++ "key": "live:bans:1759600001",
++ "bans": true
++}
ddiff --git a/fixtures/chat.invalidate-request/valid/user-update.json b/fixtures/chat.invalidate-request/valid/user-update.json
nnew file mode 100644
iindex 00000000..ba5434d5
---- /dev/null
++++ b/fixtures/chat.invalidate-request/valid/user-update.json
@@@ -0,0 +1,12 @@
++{
++ "key": "live:user:12:1759600000",
++ "user": 12,
++ "user_data": {
++ "id": 12,
++ "username": "viewer1",
++ "display_name": "Viewer One",
++ "role": "user",
++ "avatar_url": null,
++ "profile_color": "#33aaff"
++ }
++}
ddiff --git a/fixtures/chat.moderation-request/invalid/bad-ip.json b/fixtures/chat.moderation-request/invalid/bad-ip.json
nnew file mode 100644
iindex 00000000..e744902c
---- /dev/null
++++ b/fixtures/chat.moderation-request/invalid/bad-ip.json
@@@ -0,0 +1,6 @@
++{
++ "key": "k4",
++ "action": "approve-ip-messages",
++ "channel_id": 77,
++ "ip": "not-an-ip"
++}
ddiff --git a/fixtures/chat.moderation-request/invalid/disconnect-without-target.json b/fixtures/chat.moderation-request/invalid/disconnect-without-target.json
nnew file mode 100644
iindex 00000000..0f875c32
---- /dev/null
++++ b/fixtures/chat.moderation-request/invalid/disconnect-without-target.json
@@@ -0,0 +1,5 @@
++{
++ "key": "k3",
++ "action": "disconnect",
++ "stream_id": 42
++}
ddiff --git a/fixtures/chat.moderation-request/invalid/field-of-another-action.json b/fixtures/chat.moderation-request/invalid/field-of-another-action.json
nnew file mode 100644
iindex 00000000..edc8aca8
---- /dev/null
++++ b/fixtures/chat.moderation-request/invalid/field-of-another-action.json
@@@ -0,0 +1,6 @@
++{
++ "key": "k2",
++ "action": "delete-message",
++ "id": 5,
++ "user_id": 12
++}
ddiff --git a/fixtures/chat.moderation-request/invalid/tts-pitch-out-of-range.json b/fixtures/chat.moderation-request/invalid/tts-pitch-out-of-range.json
nnew file mode 100644
iindex 00000000..333a934a
---- /dev/null
++++ b/fixtures/chat.moderation-request/invalid/tts-pitch-out-of-range.json
@@@ -0,0 +1,9 @@
++{
++ "key": "k5",
++ "action": "tts-voice-override",
++ "identity_key": "user:12",
++ "params": {
++ "voice": "en-1",
++ "pitch": 11
++ }
++}
ddiff --git a/fixtures/chat.moderation-request/invalid/unknown-action.json b/fixtures/chat.moderation-request/invalid/unknown-action.json
nnew file mode 100644
iindex 00000000..c7c10227
---- /dev/null
++++ b/fixtures/chat.moderation-request/invalid/unknown-action.json
@@@ -0,0 +1,4 @@
++{
++ "key": "k1",
++ "action": "drop-table"
++}
ddiff --git a/fixtures/chat.moderation-request/valid/clear-tts-override.json b/fixtures/chat.moderation-request/valid/clear-tts-override.json
nnew file mode 100644
iindex 00000000..85d52df4
---- /dev/null
++++ b/fixtures/chat.moderation-request/valid/clear-tts-override.json
@@@ -0,0 +1,7 @@
++{
++ "key": "live:mod:tts:clear:9",
++ "action": "tts-voice-override",
++ "identity_key": "user:12",
++ "params": null,
++ "set_by": 3
++}
ddiff --git a/fixtures/chat.moderation-request/valid/delete-user-messages.json b/fixtures/chat.moderation-request/valid/delete-user-messages.json
nnew file mode 100644
iindex 00000000..61121376
---- /dev/null
++++ b/fixtures/chat.moderation-request/valid/delete-user-messages.json
@@@ -0,0 +1,7 @@
++{
++ "key": "live:mod:del-user:12:991",
++ "action": "delete-user-messages",
++ "user_id": 12,
++ "stream_id": 42,
++ "deleted_by": 3
++}
ddiff --git a/fixtures/chat.moderation-request/valid/disconnect-by-ip.json b/fixtures/chat.moderation-request/valid/disconnect-by-ip.json
nnew file mode 100644
iindex 00000000..91d61dcf
---- /dev/null
++++ b/fixtures/chat.moderation-request/valid/disconnect-by-ip.json
@@@ -0,0 +1,7 @@
++{
++ "key": "live:mod:dc:203.0.113.9",
++ "action": "disconnect",
++ "user_id": null,
++ "ip": "203.0.113.9",
++ "stream_id": null
++}
ddiff --git a/fixtures/chat.moderation-request/valid/log.json b/fixtures/chat.moderation-request/valid/log.json
nnew file mode 100644
iindex 00000000..34deb91f
---- /dev/null
++++ b/fixtures/chat.moderation-request/valid/log.json
@@@ -0,0 +1,12 @@
++{
++ "key": "live:mod:log:5512",
++ "action": "log",
++ "scope_type": "channel",
++ "scope_id": 77,
++ "actor_user_id": 3,
++ "target_user_id": 12,
++ "action_type": "ban",
++ "details": {
++ "reason": "spam"
++ }
++}
ddiff --git a/fixtures/chat.moderation-request/valid/relay-unhide-by-identity.json b/fixtures/chat.moderation-request/valid/relay-unhide-by-identity.json
nnew file mode 100644
iindex 00000000..bd7a4ebf
---- /dev/null
++++ b/fixtures/chat.moderation-request/valid/relay-unhide-by-identity.json
@@@ -0,0 +1,7 @@
++{
++ "key": "live:mod:unhide:1",
++ "action": "relay-unhide",
++ "channel_id": 77,
++ "platform": "twitch",
++ "external_username": "someone"
++}
ddiff --git a/fixtures/chat.moderation-result/invalid/error-without-reason.json b/fixtures/chat.moderation-result/invalid/error-without-reason.json
nnew file mode 100644
iindex 00000000..c92c8f5c
---- /dev/null
++++ b/fixtures/chat.moderation-result/invalid/error-without-reason.json
@@@ -0,0 +1,3 @@
++{
++ "ok": false
++}
ddiff --git a/fixtures/chat.moderation-result/invalid/ids-and-changes.json b/fixtures/chat.moderation-result/invalid/ids-and-changes.json
nnew file mode 100644
iindex 00000000..f17ce977
---- /dev/null
++++ b/fixtures/chat.moderation-result/invalid/ids-and-changes.json
@@@ -0,0 +1,7 @@
++{
++ "ok": true,
++ "ids": [
++ 1
++ ],
++ "changes": 1
++}
ddiff --git a/fixtures/chat.moderation-result/valid/deleted.json b/fixtures/chat.moderation-result/valid/deleted.json
nnew file mode 100644
iindex 00000000..9f4ea997
---- /dev/null
++++ b/fixtures/chat.moderation-result/valid/deleted.json
@@@ -0,0 +1,7 @@
++{
++ "ok": true,
++ "ids": [
++ 101,
++ 102
++ ]
++}
ddiff --git a/fixtures/chat.moderation-result/valid/logged.json b/fixtures/chat.moderation-result/valid/logged.json
nnew file mode 100644
iindex 00000000..400bd8d5
---- /dev/null
++++ b/fixtures/chat.moderation-result/valid/logged.json
@@@ -0,0 +1,5 @@
++{
++ "ok": true,
++ "changes": 1,
++ "id": 5512
++}
ddiff --git a/fixtures/chat.moderation-result/valid/refused.json b/fixtures/chat.moderation-result/valid/refused.json
nnew file mode 100644
iindex 00000000..ae69e79e
---- /dev/null
++++ b/fixtures/chat.moderation-result/valid/refused.json
@@@ -0,0 +1,4 @@
++{
++ "ok": false,
++ "error": "Idempotency key reused with a different body"
++}
ddiff --git a/fixtures/chat.send-request/invalid/ingress-blank-message.json b/fixtures/chat.send-request/invalid/ingress-blank-message.json
nnew file mode 100644
iindex 00000000..5a7c28e6
---- /dev/null
++++ b/fixtures/chat.send-request/invalid/ingress-blank-message.json
@@@ -0,0 +1,5 @@
++{
++ "key": "k2",
++ "username": "host",
++ "message": " "
++}
ddiff --git a/fixtures/chat.send-request/invalid/ingress-dm-with-room.json b/fixtures/chat.send-request/invalid/ingress-dm-with-room.json
nnew file mode 100644
iindex 00000000..7a4cc6bd
---- /dev/null
++++ b/fixtures/chat.send-request/invalid/ingress-dm-with-room.json
@@@ -0,0 +1,10 @@
++{
++ "key": "k1",
++ "user_id": 3,
++ "username": "host",
++ "message": "hi",
++ "stream_id": 42,
++ "dm": {
++ "to_user_id": 15
++ }
++}
ddiff --git a/fixtures/chat.send-request/valid/ingress-dm.json b/fixtures/chat.send-request/valid/ingress-dm.json
nnew file mode 100644
iindex 00000000..e3dddae4
---- /dev/null
++++ b/fixtures/chat.send-request/valid/ingress-dm.json
@@@ -0,0 +1,9 @@
++{
++ "key": "live:call-dm:15:9f2c",
++ "user_id": 3,
++ "username": "host",
++ "message": "Join my call",
++ "dm": {
++ "to_user_id": 15
++ }
++}
ddiff --git a/fixtures/chat.send-request/valid/ingress-relay-line.json b/fixtures/chat.send-request/valid/ingress-relay-line.json
nnew file mode 100644
iindex 00000000..1c0e03b4
---- /dev/null
++++ b/fixtures/chat.send-request/valid/ingress-relay-line.json
@@@ -0,0 +1,18 @@
++{
++ "key": "live:relay:twitch:abc123",
++ "stream_id": 42,
++ "username": "tw:someone",
++ "message": "hello from twitch",
++ "message_type": "chat",
++ "source_platform": "twitch",
++ "mirror": true,
++ "tts": {
++ "voice": null,
++ "identity_key": "ext:twitch:someone"
++ },
++ "frame": {
++ "role": "external",
++ "is_bot": false
++ },
++ "first_chat_key": "ext:tw:someone"
++}
ddiff --git a/fixtures/chat.send-result/valid/ingress-refused.json b/fixtures/chat.send-result/valid/ingress-refused.json
nnew file mode 100644
iindex 00000000..a08aec2e
---- /dev/null
++++ b/fixtures/chat.send-result/valid/ingress-refused.json
@@@ -0,0 +1,4 @@
++{
++ "ok": false,
++ "error": "Stream not found"
++}
ddiff --git a/fixtures/chat.send-result/valid/ingress-saved.json b/fixtures/chat.send-result/valid/ingress-saved.json
nnew file mode 100644
iindex 00000000..71b4cd62
---- /dev/null
++++ b/fixtures/chat.send-result/valid/ingress-saved.json
@@@ -0,0 +1,6 @@
++{
++ "ok": true,
++ "id": 90211,
++ "stream_id": 42,
++ "channel_user_id": 77
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 5a5a438e..21ecf7c7 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -48902,14 +48902,14 @@
}
},
"chat.send-request": {
-- "version": "1.0.0",
++ "version": "1.1.0",
"owner": "chat",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/chat/send-request.v1.json",
"title": "ChatSendRequest",
-- "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",
++ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.",
"anyOf": [
{
"type": "object",
@@@ -48991,19 +48991,313 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "username",
++ "message"
++ ],
++ "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."
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "channel_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "anon_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 2000,
++ "pattern": "\\S",
++ "description": "The text; not blank."
++ },
++ "message_type": {
++ "enum": [
++ "chat",
++ "system",
++ "donation",
++ "command",
++ "tts",
++ "channel-sound",
++ "soundboard",
++ "clip",
++ null
++ ]
++ },
++ "is_global": {
++ "type": [
++ "boolean",
++ "null"
++ ],
++ "description": "Default: true when there is no stream and no channel."
++ },
++ "reply_to_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "source_platform": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "auto_delete_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "When Chat deletes the line (a date Chat parses)."
++ },
++ "metadata": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "description": "At most 8192 characters as JSON."
++ },
++ "mirror": {
++ "type": [
++ "boolean",
++ "null"
++ ],
++ "description": "Also send the line to global chat and the streamer rooms."
++ },
++ "dm": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "to_user_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "to_user_id": {
++ "type": "integer",
++ "minimum": 1
++ }
++ },
++ "description": "Send a direct message from user_id instead (no room, mirror, tts or frame; 403 if either blocks the other, 404 if either is unknown)."
++ },
++ "tts": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "description": "Read the line aloud in its room (needs stream_id or channel_user_id).",
++ "properties": {
++ "voice": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "identity_key": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "key": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 160,
++ "description": "Audio key; default m<id>."
++ }
++ }
++ },
++ "frame": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "description": "How the broadcast shows the sender.",
++ "properties": {
++ "role": {
++ "enum": [
++ "admin",
++ "global_mod",
++ "streamer",
++ "user",
++ "anon",
++ "external",
++ null
++ ]
++ },
++ "profile_color": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^#[0-9a-fA-F]{3,8}$"
++ },
++ "avatar_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "is_ai": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "is_bot": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "filtered": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "core_username": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "display_name": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ }
++ }
++ },
++ "first_chat_key": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 160,
++ "pattern": "^(?:user|anon|ext):.",
++ "description": "Whose first chat in the channel this is (user:, anon: or ext:); default from user_id or anon_id."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "required": [
++ "dm"
++ ],
++ "properties": {
++ "dm": {
++ "type": "object"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "user_id"
++ ],
++ "properties": {
++ "user_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "stream_id": {
++ "type": "null"
++ },
++ "channel_user_id": {
++ "type": "null"
++ },
++ "mirror": {
++ "enum": [
++ false,
++ null
++ ]
++ },
++ "tts": {
++ "type": "null"
++ },
++ "frame": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ ]
}
]
}
},
"chat.send-result": {
-- "version": "1.0.0",
++ "version": "1.1.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 }.",
++ "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 }).",
"anyOf": [
{
"type": "object",
@@@ -49196,6 +49490,1807 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "ok",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ok": {
++ "const": true
++ },
++ "id": {
++ "type": "integer",
++ "description": "The stored message id."
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "channel_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "conversation_id": {
++ "type": "integer",
++ "description": "DM only."
++ },
++ "to_user_id": {
++ "type": "integer",
++ "description": "DM only."
++ }
++ }
++ },
++ {
++ "$ref": "ingress-ack.v1.json#/$defs/error"
++ }
++ ]
++ }
++ },
++ "chat.event-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/event-request.v1.json",
++ "title": "ChatEventRequest",
++ "description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",
++ "type": "object",
++ "required": [
++ "key",
++ "target",
++ "frame"
++ ],
++ "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."
++ },
++ "target": {
++ "type": "object",
++ "required": [
++ "kind"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "kind": {
++ "enum": [
++ "stream",
++ "channel",
++ "owner-streams",
++ "global",
++ "all",
++ "user"
++ ],
++ "description": "stream: one stream's room; channel: a channel owner's room; owner-streams: every stream of that owner; global: the homepage global chat; all: every socket; user: one person's sockets (call frames only)."
++ },
++ "id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1,
++ "description": "The stream, channel owner or user id; required for stream, channel, owner-streams and user, absent or null for global and all."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "enum": [
++ "stream",
++ "channel",
++ "owner-streams",
++ "user"
++ ]
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "properties": {
++ "id": {
++ "type": "integer"
++ }
++ }
++ },
++ "else": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ ]
++ },
++ "frame": {
++ "type": "object",
++ "required": [
++ "type"
++ ],
++ "properties": {
++ "type": {
++ "enum": [
++ "chat",
++ "system",
++ "donation",
++ "goal-update",
++ "goal-reached",
++ "update",
++ "voice-channels",
++ "alert",
++ "channel-sound",
++ "server_restart",
++ "media_queue_update",
++ "media_now_playing",
++ "redemption",
++ "vibe-coding",
++ "vc-call-invite",
++ "vc-call-response"
++ ],
++ "description": "The client frame type. chat is only the transient news card (never saved); a stored line goes through chat.message.send."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "enum": [
++ "system",
++ "server_restart"
++ ]
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "message"
++ ],
++ "properties": {
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 4000
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "chat"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "message_type",
++ "username",
++ "message",
++ "timestamp"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {},
++ "message_type": {
++ "const": "news"
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 4000
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "news_source": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "timestamp": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 40
++ },
++ "source_platform": {
++ "enum": [
++ "news",
++ null
++ ]
++ },
++ "system": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "alert"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "streamerId",
++ "kind"
++ ],
++ "properties": {
++ "streamerId": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The channel owner; must equal target.id with target.kind channel."
++ },
++ "streamId": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "kind": {
++ "enum": [
++ "goal",
++ "donation"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "voice-channels"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "channels"
++ ],
++ "properties": {
++ "channels": {
++ "type": "array",
++ "maxItems": 128
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "channel-sound"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "streamId",
++ "command"
++ ],
++ "properties": {
++ "streamId": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Must equal target.id with target.kind stream."
++ },
++ "command": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "userId": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "anonId": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "args": {
++ "type": [
++ "array",
++ "null"
++ ],
++ "maxItems": 20,
++ "items": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ }
++ },
++ "relay": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "username"
++ ],
++ "properties": {
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "media_queue_update"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "type": "object"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "media_now_playing"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "request"
++ ],
++ "properties": {
++ "request": {
++ "type": [
++ "object",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "redemption"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "username",
++ "reward_title",
++ "cost"
++ ],
++ "properties": {
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "reward_title": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ },
++ "cost": {
++ "type": "number"
++ },
++ "user_input": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "vibe-coding"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "managed_stream_id",
++ "event"
++ ],
++ "properties": {
++ "managed_stream_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "event": {
++ "type": "object"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "enum": [
++ "vc-call-invite",
++ "vc-call-response"
++ ]
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "channelId",
++ "fromUserId",
++ "fromUsername",
++ "createdAt"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {},
++ "channelId": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "channelName": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "fromUserId": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "fromUsername": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "fromDisplayName": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "fromAvatarUrl": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "createdAt": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Milliseconds since the epoch."
++ },
++ "status": {
++ "enum": [
++ "accepted",
++ "declined",
++ "busy",
++ "no-answer",
++ "canceled"
++ ],
++ "description": "vc-call-response only, and required there."
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "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",
++ "properties": {
++ "target": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "const": "channel"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "frame": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "voice-channels"
++ }
++ }
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "properties": {
++ "target": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "const": "all"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "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"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
++ }
++ },
++ "chat.moderation-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/moderation-request.v1.json",
++ "title": "ChatModerationRequest",
++ "description": "chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.",
++ "$defs": {
++ "key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160,
++ "pattern": "^[A-Za-z0-9:._-]+$",
++ "description": "Idempotency key, scoped to the caller and to this route."
++ }
++ },
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-message"
++ },
++ "id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "user_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-user-messages"
++ },
++ "user_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ },
++ "description": "Every visible message of a signed-in person, in one stream or everywhere."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "anon_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-anon-messages"
++ },
++ "anon_id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "username"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-relay-messages"
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ },
++ "description": "Every visible message stored under a relay username."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "from",
++ "to"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "delete-by-range"
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "from": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
++ },
++ "to": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ },
++ "description": "Every visible message between from and to, in one stream or (without stream_id) in global chat."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "id",
++ "status"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "review-pending-ip"
++ },
++ "id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "status": {
++ "enum": [
++ "approved",
++ "denied"
++ ]
++ },
++ "reviewed_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "channel_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "channel_id",
++ "ip"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "approve-ip-messages"
++ },
++ "channel_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "ip": {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ },
++ "reviewed_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "channel_id",
++ "ip"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "deny-ip-messages"
++ },
++ "channel_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "ip": {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ },
++ "reviewed_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "platform",
++ "external_username"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "relay-hide"
++ },
++ "channel_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "platform": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "external_username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "created_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "mode": {
++ "enum": [
++ "hide",
++ "ban",
++ null
++ ],
++ "description": "Default hide."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "relay-unhide"
++ },
++ "id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "channel_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "platform": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "external_username": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ }
++ },
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "properties": {
++ "id": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "platform",
++ "external_username"
++ ]
++ }
++ ],
++ "description": "By the hidden row's id, or by channel, platform and external username."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "platform",
++ "username"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "relay-record"
++ },
++ "platform": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "identity_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "tts-voice-override"
++ },
++ "identity_key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "set_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "params": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "voice"
++ ],
++ "additionalProperties": false,
++ "description": "Absent or null: the override is cleared.",
++ "properties": {
++ "voice": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "pitch": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 10
++ },
++ "speed": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 10
++ },
++ "gap": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 10
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "disconnect"
++ },
++ "user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "ip": {
++ "anyOf": [
++ {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ },
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "user_id"
++ ],
++ "properties": {
++ "user_id": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "ip"
++ ],
++ "properties": {
++ "ip": {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ }
++ }
++ }
++ ],
++ "description": "Chat refreshes its ban cache and closes the matching sockets (in one stream, or everywhere)."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "action_type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/$defs/key"
++ },
++ "action": {
++ "const": "log"
++ },
++ "scope_type": {
++ "enum": [
++ "site",
++ "channel",
++ "stream",
++ "room",
++ null
++ ]
++ },
++ "scope_id": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "A string for scope_type room, otherwise an id."
++ },
++ "actor_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "target_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "action_type": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "details": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "description": "At most 8192 characters as JSON."
++ }
++ },
++ "description": "Append a row to Chat's moderation log."
++ }
++ ]
++ }
++ },
++ "chat.moderation-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-result.v1.json",
++ "title": "ChatModerationResult",
++ "description": "chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).",
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "ok",
++ "ids"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ok": {
++ "const": true
++ },
++ "ids": {
++ "type": "array",
++ "items": {
++ "type": "integer"
++ },
++ "description": "The deleted message ids."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "ok",
++ "changes",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ok": {
++ "const": true
++ },
++ "changes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "description": "The inserted row (a log entry, a hidden relay identity), else null."
++ }
++ }
++ },
++ {
++ "$ref": "ingress-ack.v1.json#/$defs/error"
++ }
++ ]
++ }
++ },
++ "chat.invalidate-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/invalidate-request.v1.json",
++ "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
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "bans"
++ ],
++ "properties": {
++ "bans": {
++ "const": true
++ }
++ }
++ }
++ ]
++ }
++ },
++ "chat.ingress-ack": {
++ "version": "1.0.0",
++ "owner": "chat",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/chat/ingress-ack.v1.json",
++ "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": "#/$defs/error"
}
]
}
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex 5f1f91c0..f254b188 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.90.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 67ccc9f6..71af3a2e 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.90.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 21fcf349..4ecdeea6 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.90.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 0db98cf0..0299978b 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.90.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 e49508c8..dc313f41 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.90.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.91.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"
@@@ -16,6 +16,230 @@
}
],
"paths": {
++ "/internal/chat/events": {
++ "post": {
++ "operationId": "post_internal_chat_events",
++ "summary": "Push a transient frame to chat sockets through OpenVibe.Chat: news cards, donation and goal cards, alerts and channel sounds, media queue state, redemptions, vibe-coding events, voice channel lists, s",
++ "description": "**chat.event.publish** (first-party): Push a transient frame to chat sockets through OpenVibe.Chat: news cards, donation and goal cards, alerts and channel sounds, media queue state, redemptions, vibe-coding events, voice channel lists, system and restart notices, and call invites to one person. Nothing is stored; Chat validates the frame and its target (a stream, a channel, an owner's streams, global chat, everyone or one person) and broadcasts it once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",
++ "tags": [
++ "chat.event"
++ ],
++ "x-openvibe-capabilities": [
++ "chat.event.publish"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "chat.event.publish"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/chat.event-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/chat.ingress-ack.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/internal/chat/invalidate": {
++ "post": {
++ "operationId": "post_internal_chat_invalidate",
++ "summary": "Tell OpenVibe.Chat that something it caches changed elsewhere: a person (optionally with their new public fields, pushed to their sockets), a channel's settings, a channel's IP approvals, or bans.",
++ "description": "**chat.cache.invalidate** (first-party): Tell OpenVibe.Chat that something it caches changed elsewhere: a person (optionally with their new public fields, pushed to their sockets), a channel's settings, a channel's IP approvals, or bans. A cache hint only; Chat reloads the entry. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",
++ "tags": [
++ "chat.cache"
++ ],
++ "x-openvibe-capabilities": [
++ "chat.cache.invalidate"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "chat.cache.invalidate"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/chat.invalidate-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/chat.ingress-ack.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/internal/chat/messages": {
++ "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).",
++ "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/chat/moderation": {
++ "post": {
++ "operationId": "post_internal_chat_moderation",
++ "summary": "Change the chat rows OpenVibe.Chat owns and act on its sockets for a moderator: delete one message, a person's, an anonymous id's or a relay name's messages, or a time range (Chat broadcasts the delet",
++ "description": "**chat.moderation.write** (first-party): Change the chat rows OpenVibe.Chat owns and act on its sockets for a moderator: delete one message, a person's, an anonymous id's or a relay name's messages, or a time range (Chat broadcasts the deletes), review messages held for IP approval, hide, unhide or record relay identities, set or clear TTS voice overrides, disconnect a person or an address, and log moderation actions. One typed action per request, applied once per idempotency key. Loopback only, first-party service token (Live's principal today); never granted to an app or delegated to an agent.",
++ "tags": [
++ "chat.moderation"
++ ],
++ "x-openvibe-capabilities": [
++ "chat.moderation.write"
++ ],
++ "x-openvibe-visibility": [
++ "first-party"
++ ],
++ "x-openvibe-quota-class": "internal-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "chat.moderation.write"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/chat.moderation-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",
@@@ -521,55 +745,1781 @@
}
}
},
-- "chat.send-request.v1": {
-- "title": "ChatSendRequest",
-- "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.",
-- "anyOf": [
-- {
++ "chat.event-request.v1": {
++ "title": "ChatEventRequest",
++ "description": "chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.",
++ "type": "object",
++ "required": [
++ "key",
++ "target",
++ "frame"
++ ],
++ "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."
++ },
++ "target": {
"type": "object",
"required": [
-- "type",
-- "message"
++ "kind"
],
++ "additionalProperties": false,
"properties": {
-- "type": {
-- "const": "chat"
-- },
-- "message": {
-- "type": "string",
-- "minLength": 1,
-- "maxLength": 6000,
-- "description": "The text (trimmed by Chat)."
-- },
-- "reply_to_id": {
-- "type": [
-- "integer",
-- "string"
++ "kind": {
++ "enum": [
++ "stream",
++ "channel",
++ "owner-streams",
++ "global",
++ "all",
++ "user"
],
-- "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."
++ "description": "stream: one stream's room; channel: a channel owner's room; owner-streams: every stream of that owner; global: the homepage global chat; all: every socket; user: one person's sockets (call frames only)."
},
-- "auto_delete_minutes": {
++ "id": {
"type": [
"integer",
-- "string"
++ "null"
],
-- "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."
-- },
-- "voiceChannelId": {
-- "type": "string",
-- "description": "The voice channel the sender is in, echoed on the broadcast."
++ "minimum": 1,
++ "description": "The stream, channel owner or user id; required for stream, channel, owner-streams and user, absent or null for global and all."
}
-- }
++ },
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "enum": [
++ "stream",
++ "channel",
++ "owner-streams",
++ "user"
++ ]
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "properties": {
++ "id": {
++ "type": "integer"
++ }
++ }
++ },
++ "else": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ ]
},
-- {
++ "frame": {
"type": "object",
"required": [
-- "ops"
++ "type"
],
"properties": {
-- "boot": {
-- "type": "string",
-- "maxLength": 64,
++ "type": {
++ "enum": [
++ "chat",
++ "system",
++ "donation",
++ "goal-update",
++ "goal-reached",
++ "update",
++ "voice-channels",
++ "alert",
++ "channel-sound",
++ "server_restart",
++ "media_queue_update",
++ "media_now_playing",
++ "redemption",
++ "vibe-coding",
++ "vc-call-invite",
++ "vc-call-response"
++ ],
++ "description": "The client frame type. chat is only the transient news card (never saved); a stored line goes through chat.message.send."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "enum": [
++ "system",
++ "server_restart"
++ ]
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "message"
++ ],
++ "properties": {
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 4000
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "chat"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "message_type",
++ "username",
++ "message",
++ "timestamp"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {},
++ "message_type": {
++ "const": "news"
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 4000
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "news_source": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "timestamp": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 40
++ },
++ "source_platform": {
++ "enum": [
++ "news",
++ null
++ ]
++ },
++ "system": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "alert"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "streamerId",
++ "kind"
++ ],
++ "properties": {
++ "streamerId": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The channel owner; must equal target.id with target.kind channel."
++ },
++ "streamId": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "kind": {
++ "enum": [
++ "goal",
++ "donation"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "voice-channels"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "channels"
++ ],
++ "properties": {
++ "channels": {
++ "type": "array",
++ "maxItems": 128
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "channel-sound"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "streamId",
++ "command"
++ ],
++ "properties": {
++ "streamId": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Must equal target.id with target.kind stream."
++ },
++ "command": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "userId": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "anonId": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "args": {
++ "type": [
++ "array",
++ "null"
++ ],
++ "maxItems": 20,
++ "items": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ }
++ },
++ "relay": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "username"
++ ],
++ "properties": {
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "media_queue_update"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "state"
++ ],
++ "properties": {
++ "state": {
++ "type": "object"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "media_now_playing"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "request"
++ ],
++ "properties": {
++ "request": {
++ "type": [
++ "object",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "redemption"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "username",
++ "reward_title",
++ "cost"
++ ],
++ "properties": {
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "reward_title": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ },
++ "cost": {
++ "type": "number"
++ },
++ "user_input": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "vibe-coding"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "managed_stream_id",
++ "event"
++ ],
++ "properties": {
++ "managed_stream_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "event": {
++ "type": "object"
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "enum": [
++ "vc-call-invite",
++ "vc-call-response"
++ ]
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "channelId",
++ "fromUserId",
++ "fromUsername",
++ "createdAt"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {},
++ "channelId": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "channelName": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "fromUserId": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "fromUsername": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "fromDisplayName": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "fromAvatarUrl": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "createdAt": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Milliseconds since the epoch."
++ },
++ "status": {
++ "enum": [
++ "accepted",
++ "declined",
++ "busy",
++ "no-answer",
++ "canceled"
++ ],
++ "description": "vc-call-response only, and required there."
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "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",
++ "properties": {
++ "target": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "const": "channel"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "frame": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "voice-channels"
++ }
++ }
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "properties": {
++ "target": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "const": "all"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "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"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
++ },
++ "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
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "bans"
++ ],
++ "properties": {
++ "bans": {
++ "const": true
++ }
++ }
++ }
++ ]
++ },
++ "chat.moderation-request.v1": {
++ "title": "ChatModerationRequest",
++ "description": "chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.",
++ "$defs": {
++ "key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 160,
++ "pattern": "^[A-Za-z0-9:._-]+$",
++ "description": "Idempotency key, scoped to the caller and to this route."
++ }
++ },
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "delete-message"
++ },
++ "id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "user_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "delete-user-messages"
++ },
++ "user_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ },
++ "description": "Every visible message of a signed-in person, in one stream or everywhere."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "anon_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "delete-anon-messages"
++ },
++ "anon_id": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "username"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "delete-relay-messages"
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ },
++ "description": "Every visible message stored under a relay username."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "from",
++ "to"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "delete-by-range"
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "from": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
++ },
++ "to": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "A date Chat parses (Date.parse); compared with the stored message timestamp."
++ },
++ "deleted_by": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "The moderator: a user id or a username (chat_messages.deleted_by is text)."
++ }
++ },
++ "description": "Every visible message between from and to, in one stream or (without stream_id) in global chat."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "id",
++ "status"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "review-pending-ip"
++ },
++ "id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "status": {
++ "enum": [
++ "approved",
++ "denied"
++ ]
++ },
++ "reviewed_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "channel_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "channel_id",
++ "ip"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "approve-ip-messages"
++ },
++ "channel_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "ip": {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ },
++ "reviewed_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "channel_id",
++ "ip"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "deny-ip-messages"
++ },
++ "channel_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "ip": {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ },
++ "reviewed_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "platform",
++ "external_username"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "relay-hide"
++ },
++ "channel_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "platform": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "external_username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "created_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "mode": {
++ "enum": [
++ "hide",
++ "ban",
++ null
++ ],
++ "description": "Default hide."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "relay-unhide"
++ },
++ "id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "channel_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "platform": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "external_username": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ }
++ },
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "properties": {
++ "id": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "platform",
++ "external_username"
++ ]
++ }
++ ],
++ "description": "By the hidden row's id, or by channel, platform and external username."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "platform",
++ "username"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "relay-record"
++ },
++ "platform": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "identity_key"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "tts-voice-override"
++ },
++ "identity_key": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "set_by": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "params": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "voice"
++ ],
++ "additionalProperties": false,
++ "description": "Absent or null: the override is cleared.",
++ "properties": {
++ "voice": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "pitch": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 10
++ },
++ "speed": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 10
++ },
++ "gap": {
++ "type": [
++ "number",
++ "null"
++ ],
++ "minimum": 0,
++ "maximum": 10
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "disconnect"
++ },
++ "user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "ip": {
++ "anyOf": [
++ {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ }
++ },
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "user_id"
++ ],
++ "properties": {
++ "user_id": {
++ "type": "integer",
++ "minimum": 1
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "ip"
++ ],
++ "properties": {
++ "ip": {
++ "type": "string",
++ "anyOf": [
++ {
++ "format": "ipv4"
++ },
++ {
++ "format": "ipv6"
++ }
++ ]
++ }
++ }
++ }
++ ],
++ "description": "Chat refreshes its ban cache and closes the matching sockets (in one stream, or everywhere)."
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "action",
++ "action_type"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "$ref": "#/components/schemas/chat.moderation-request.v1/$defs/key"
++ },
++ "action": {
++ "const": "log"
++ },
++ "scope_type": {
++ "enum": [
++ "site",
++ "channel",
++ "stream",
++ "room",
++ null
++ ]
++ },
++ "scope_id": {
++ "anyOf": [
++ {
++ "type": "integer",
++ "minimum": 1
++ },
++ {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ {
++ "type": "null"
++ }
++ ],
++ "description": "A string for scope_type room, otherwise an id."
++ },
++ "actor_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "target_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "action_type": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "details": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "description": "At most 8192 characters as JSON."
++ }
++ },
++ "description": "Append a row to Chat's moderation log."
++ }
++ ]
++ },
++ "chat.moderation-result.v1": {
++ "title": "ChatModerationResult",
++ "description": "chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).",
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "ok",
++ "ids"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ok": {
++ "const": true
++ },
++ "ids": {
++ "type": "array",
++ "items": {
++ "type": "integer"
++ },
++ "description": "The deleted message ids."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "ok",
++ "changes",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ok": {
++ "const": true
++ },
++ "changes": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "description": "The inserted row (a log entry, a hidden relay identity), else null."
++ }
++ }
++ },
++ {
++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
++ }
++ ]
++ },
++ "chat.send-request.v1": {
++ "title": "ChatSendRequest",
++ "description": "chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "message"
++ ],
++ "properties": {
++ "type": {
++ "const": "chat"
++ },
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 6000,
++ "description": "The text (trimmed by Chat)."
++ },
++ "reply_to_id": {
++ "type": [
++ "integer",
++ "string"
++ ],
++ "description": "Id of a visible message in the room this replies to (parseInt; an unknown or deleted parent is ignored)."
++ },
++ "auto_delete_minutes": {
++ "type": [
++ "integer",
++ "string"
++ ],
++ "description": "Delete the line after this many minutes (clamped to Chat's minimum and maximum; ignored where the channel turned viewer auto-delete off)."
++ },
++ "voiceChannelId": {
++ "type": "string",
++ "description": "The voice channel the sender is in, echoed on the broadcast."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "ops"
++ ],
++ "properties": {
++ "boot": {
++ "type": "string",
++ "maxLength": 64,
"description": "Live's boot id; placeholder message ids are mapped per boot."
},
"ops": {
@@@ -605,12 +2555,306 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "key",
++ "username",
++ "message"
++ ],
++ "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."
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "channel_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "user_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "anon_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "username": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "message": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 2000,
++ "pattern": "\\S",
++ "description": "The text; not blank."
++ },
++ "message_type": {
++ "enum": [
++ "chat",
++ "system",
++ "donation",
++ "command",
++ "tts",
++ "channel-sound",
++ "soundboard",
++ "clip",
++ null
++ ]
++ },
++ "is_global": {
++ "type": [
++ "boolean",
++ "null"
++ ],
++ "description": "Default: true when there is no stream and no channel."
++ },
++ "reply_to_id": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 1
++ },
++ "source_platform": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "auto_delete_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "When Chat deletes the line (a date Chat parses)."
++ },
++ "metadata": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "description": "At most 8192 characters as JSON."
++ },
++ "mirror": {
++ "type": [
++ "boolean",
++ "null"
++ ],
++ "description": "Also send the line to global chat and the streamer rooms."
++ },
++ "dm": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "required": [
++ "to_user_id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "to_user_id": {
++ "type": "integer",
++ "minimum": 1
++ }
++ },
++ "description": "Send a direct message from user_id instead (no room, mirror, tts or frame; 403 if either blocks the other, 404 if either is unknown)."
++ },
++ "tts": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "description": "Read the line aloud in its room (needs stream_id or channel_user_id).",
++ "properties": {
++ "voice": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 80
++ },
++ "identity_key": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "key": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 160,
++ "description": "Audio key; default m<id>."
++ }
++ }
++ },
++ "frame": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "additionalProperties": false,
++ "description": "How the broadcast shows the sender.",
++ "properties": {
++ "role": {
++ "enum": [
++ "admin",
++ "global_mod",
++ "streamer",
++ "user",
++ "anon",
++ "external",
++ null
++ ]
++ },
++ "profile_color": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^#[0-9a-fA-F]{3,8}$"
++ },
++ "avatar_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 500
++ },
++ "is_ai": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "is_bot": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "filtered": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "core_username": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ },
++ "display_name": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 120
++ }
++ }
++ },
++ "first_chat_key": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 160,
++ "pattern": "^(?:user|anon|ext):.",
++ "description": "Whose first chat in the channel this is (user:, anon: or ext:); default from user_id or anon_id."
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "required": [
++ "dm"
++ ],
++ "properties": {
++ "dm": {
++ "type": "object"
++ }
++ }
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "user_id"
++ ],
++ "properties": {
++ "user_id": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "stream_id": {
++ "type": "null"
++ },
++ "channel_user_id": {
++ "type": "null"
++ },
++ "mirror": {
++ "enum": [
++ false,
++ null
++ ]
++ },
++ "tts": {
++ "type": "null"
++ },
++ "frame": {
++ "type": "null"
++ }
++ }
++ }
++ }
++ ]
}
]
},
"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 }.",
++ "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 }).",
"anyOf": [
{
"type": "object",
@@@ -803,6 +3047,46 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "ok",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "ok": {
++ "const": true
++ },
++ "id": {
++ "type": "integer",
++ "description": "The stored message id."
++ },
++ "stream_id": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "channel_user_id": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "conversation_id": {
++ "type": "integer",
++ "description": "DM only."
++ },
++ "to_user_id": {
++ "type": "integer",
++ "description": "DM only."
++ }
++ }
++ },
++ {
++ "$ref": "#/components/schemas/chat.ingress-ack.v1/$defs/error"
}
]
},
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex ea9cdd5b..e08d2434 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.90.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 3ed481d7..a040a35f 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.90.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 c0d807e8..c9de9f9a 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.90.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 61bc236b..5a6a2fb1 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.90.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 5a1e57ee..1980215c 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.90.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 e2f5481d..3777d58f 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.90.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 09c29b46..460cb8fb 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.90.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 64c6731b..c1e2b0ee 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": 4,
-- "capabilities": 2
++ "operations": 8,
++ "capabilities": 5
},
{
"service": "codes",
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 5620e744..4a7e9bc5 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.90.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 490ce8bf..fbd2ecef 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.90.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 85a8d734..87f8d8a1 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.90.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 9236b022..0818000d 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.90.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 53ed09df..07642a99 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.90.0",
-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 bbed9c67..47645466 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.90.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 47141ae1..7a88f65a 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.90.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 87ef3511..5265f895 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.90.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 a6d7d3e0..2fc289d4 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.90.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 87e28a10..065cc958 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.90.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 f27860d6..b7f7d8e2 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.90.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 04f5110e..aa80a040 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.90.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 33a23ccb..8a8138a4 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.90.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 19f5939f..94c125ba 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.90.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.90.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.",
++ "version": "0.91.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.91.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 e3024909..84b4a8a6 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -47380,9 +47380,9 @@ export interface ChatModerationActionPayload {
details?: {};
}
--/** [email protected] (owner: chat) */
++/** [email protected] (owner: chat) */
/**
-- * chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused.
++ * chat.send-request@1: what a sender hands OpenVibe.Chat to post a chat line (chat.message.send). From a browser, a frame on WS /ws/chat of type chat in the room the socket joined (the text is trimmed; empty or longer than 6000 characters is dropped, the channel's own max_message_length is enforced after that; a line starting with ! or / is a command, not a message). From Live's bridge, POST /internal/live/calls: a chat.live_bridge.write batch in which every op that sends a message (a chat or dm frame, db saveChatMessage, deployNotice) also needs chat.message.send; without it that op alone is refused. From a first-party service, POST /internal/chat/messages: one line (or one DM) that Chat stores, broadcasts to the room (with mirror, also to global chat and the streamer rooms), reads aloud (tts) and answers with the real id; applied once per (caller, key), the same key with another body gets 409.
*/
export type ChatSendRequest =
| {
@@@ -47431,11 +47431,14 @@ export type ChatSendRequest =
*/
key?: string;
}[];
++ }
++ | {
++ [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 }.
++ * 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 }).
*/
export type ChatSendResult =
| {
@@@ -47530,6 +47533,334 @@ export type ChatSendResult =
code?: string;
error?: string;
}[];
++ }
++ | {
++ ok: true;
++ /**
++ * The stored message id.
++ */
++ id: number;
++ stream_id?: number | null;
++ channel_user_id?: number | null;
++ /**
++ * DM only.
++ */
++ conversation_id?: number;
++ /**
++ * DM only.
++ */
++ to_user_id?: number;
++ }
++ | {
++ ok: false;
++ /**
++ * A human-readable reason.
++ */
++ error: string;
++ };
++
++/** [email protected] (owner: chat) */
++/**
++ * chat.event-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/events (chat.event.publish) to push a transient frame to chat sockets: cards, alerts, sounds, media queue state, call invites. Nothing is stored; Chat broadcasts the frame to the target. Applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. Chat also refuses a frame whose JSON is longer than 16384 characters or that has a __proto__, constructor or prototype key; a news card's timestamp must parse as a date.
++ */
++export type ChatEventRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ target: {
++ [k: string]: unknown | undefined;
++ };
++ frame: {
++ [k: string]: unknown | undefined;
++ };
++};
++
++/** [email protected] (owner: chat) */
++/**
++ * chat.moderation-request@1: what a first-party service hands OpenVibe.Chat's POST /internal/chat/moderation (chat.moderation.write) to change chat rows Chat owns and act on its sockets: delete messages (Chat deletes the rows and broadcasts the delete notices to the room and its mirrors), review messages held for IP approval, hide, unhide or record relay identities, set or clear a TTS voice override, disconnect a person or an address, and log a moderation action. One action per request, applied once per (caller, key): a retry with the same key and body answers the first result, the same key with another body gets 409. The fields an action does not list are refused.
++ */
++export type ChatModerationRequest =
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "delete-message";
++ id: number;
++ /**
++ * The moderator: a user id or a username (chat_messages.deleted_by is text).
++ */
++ deleted_by?: number | string | null;
++ }
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "delete-user-messages";
++ user_id: number;
++ stream_id?: number | null;
++ /**
++ * The moderator: a user id or a username (chat_messages.deleted_by is text).
++ */
++ deleted_by?: number | string | null;
++ }
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "delete-anon-messages";
++ anon_id: string;
++ stream_id?: number | null;
++ /**
++ * The moderator: a user id or a username (chat_messages.deleted_by is text).
++ */
++ deleted_by?: number | string | null;
++ }
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "delete-relay-messages";
++ username: string;
++ stream_id?: number | null;
++ /**
++ * The moderator: a user id or a username (chat_messages.deleted_by is text).
++ */
++ deleted_by?: number | string | null;
++ }
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "delete-by-range";
++ stream_id?: number | null;
++ /**
++ * A date Chat parses (Date.parse); compared with the stored message timestamp.
++ */
++ from: string;
++ /**
++ * A date Chat parses (Date.parse); compared with the stored message timestamp.
++ */
++ to: string;
++ /**
++ * The moderator: a user id or a username (chat_messages.deleted_by is text).
++ */
++ deleted_by?: number | string | null;
++ }
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "review-pending-ip";
++ id: number;
++ status: "approved" | "denied";
++ reviewed_by?: number | null;
++ channel_id?: number | null;
++ }
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "approve-ip-messages";
++ channel_id: number;
++ ip: {
++ [k: string]: unknown | undefined;
++ } & string;
++ reviewed_by?: number | null;
++ }
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "deny-ip-messages";
++ channel_id: number;
++ ip: {
++ [k: string]: unknown | undefined;
++ } & string;
++ reviewed_by?: number | null;
++ }
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "relay-hide";
++ channel_id?: number | null;
++ platform: string;
++ external_username: string;
++ reason?: string | null;
++ created_by?: number | null;
++ /**
++ * Default hide.
++ */
++ mode?: "hide" | "ban" | null;
++ }
++ | (
++ | {
++ id: number;
++ }
++ | {}
++ )
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "relay-record";
++ platform: string;
++ username: string;
++ }
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "tts-voice-override";
++ identity_key: string;
++ set_by?: number | null;
++ /**
++ * Absent or null: the override is cleared.
++ */
++ params?: {
++ voice: string;
++ pitch?: number | null;
++ speed?: number | null;
++ gap?: number | null;
++ } | null;
++ }
++ | (
++ | {
++ user_id: number;
++ }
++ | {
++ ip: {
++ [k: string]: unknown | undefined;
++ } & string;
++ }
++ )
++ | {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ action: "log";
++ scope_type?: "site" | "channel" | "stream" | "room" | null;
++ /**
++ * A string for scope_type room, otherwise an id.
++ */
++ scope_id?: number | string | null;
++ actor_user_id?: number | null;
++ target_user_id?: number | null;
++ action_type: string;
++ /**
++ * At most 8192 characters as JSON.
++ */
++ details?: {} | null;
++ };
++
++/** [email protected] (owner: chat) */
++/**
++ * chat.moderation-result@1: what POST /internal/chat/moderation (chat.moderation.write) answers. 200 with the ids a delete action removed (empty when nothing matched), or with the rows another action changed and the id it inserted. A refusal is chat.ingress-ack@1's error: 400 a body Chat refuses, 409 a key reused with another body, 503 Chat or its delivery unavailable (retry with the same key).
++ */
++export type ChatModerationResult =
++ | {
++ ok: true;
++ /**
++ * The deleted message ids.
++ */
++ ids: number[];
++ }
++ | {
++ ok: true;
++ changes: number;
++ /**
++ * The inserted row (a log entry, a hidden relay identity), else null.
++ */
++ id: number | null;
++ }
++ | {
++ ok: false;
++ /**
++ * A human-readable reason.
++ */
++ error: string;
++ };
++
++/** [email protected] (owner: chat) */
++/**
++ * 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).
++ */
++export type ChatInvalidateRequest = (
++ | {
++ user: number;
++ }
++ | {
++ approvals: number;
++ }
++ | {
++ channel: number;
++ }
++ | {
++ bans: true;
++ }
++) & {
++ /**
++ * Idempotency key, scoped to the caller and to this route.
++ */
++ key: string;
++ /**
++ * A person whose cached row is stale.
++ */
++ user?: number | null;
++ /**
++ * The person's new public fields, pushed to their sockets; id must equal user.
++ */
++ user_data?: {
++ id: number;
++ username: string;
++ display_name?: string | null;
++ role?: string;
++ avatar_url?: string | null;
++ profile_color?: string | null;
++ };
++ /**
++ * A channel whose IP approvals changed.
++ */
++ approvals?: number | null;
++ /**
++ * true: bans changed.
++ */
++ bans?: boolean | null;
++ /**
++ * A channel whose cached settings changed.
++ */
++ channel?: number | null;
++};
++
++/** [email protected] (owner: chat) */
++/**
++ * 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).
++ */
++export type ChatIngressAck =
++ | {
++ ok: true;
++ }
++ | {
++ ok: false;
++ /**
++ * A human-readable reason.
++ */
++ error: string;
};
/** [email protected] (owner: chat) */
ddiff --git a/manifests/capabilities/chat.cache.invalidate.json b/manifests/capabilities/chat.cache.invalidate.json
nnew file mode 100644
iindex 00000000..9b9f11ff
---- /dev/null
++++ b/manifests/capabilities/chat.cache.invalidate.json
@@@ -0,0 +1,21 @@
++{
++ "id": "chat.cache.invalidate",
++ "version": "1.0",
++ "owner": "chat",
++ "status": "active",
++ "visibility": "first-party",
++ "description": "Tell OpenVibe.Chat that something it caches changed elsewhere: a person (optionally with their new public fields, pushed to their sockets), a channel's settings, a channel's IP approvals, or bans. A cache hint only; Chat reloads the entry. Loopback only, first-party 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/invalidate"
++ ],
++ "inputSchema": "chat.invalidate-request@1",
++ "outputSchema": "chat.ingress-ack@1"
++}
ddiff --git a/manifests/capabilities/chat.event.publish.json b/manifests/capabilities/chat.event.publish.json
nnew file mode 100644
iindex 00000000..78710575
---- /dev/null
++++ b/manifests/capabilities/chat.event.publish.json
@@@ -0,0 +1,22 @@
++{
++ "id": "chat.event.publish",
++ "version": "1.0",
++ "owner": "chat",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Push a transient frame to chat sockets through OpenVibe.Chat: news cards, donation and goal cards, alerts and channel sounds, media queue state, redemptions, vibe-coding events, voice channel lists, system and restart notices, and call invites to one person. Nothing is stored; Chat validates the frame and its target (a stream, a channel, an owner's streams, global chat, everyone or one person) and broadcasts it once per idempotency key. Loopback only, first-party 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/events"
++ ],
++ "inputSchema": "chat.event-request@1",
++ "outputSchema": "chat.ingress-ack@1"
++}
ddiff --git a/manifests/capabilities/chat.message.send.json b/manifests/capabilities/chat.message.send.json
iindex 208828dc..01f0e31b 100644
---- a/manifests/capabilities/chat.message.send.json
++++ b/manifests/capabilities/chat.message.send.json
@@@ -18,7 +18,8 @@
],
"implementedBy": [
"WS /ws/chat {type:\"chat\"}",
-- "POST /internal/live/calls"
++ "POST /internal/live/calls",
++ "POST /internal/chat/messages"
],
"inputSchema": "chat.send-request@1",
"outputSchema": "chat.send-result@1"
ddiff --git a/manifests/capabilities/chat.moderation.write.json b/manifests/capabilities/chat.moderation.write.json
nnew file mode 100644
iindex 00000000..207ef107
---- /dev/null
++++ b/manifests/capabilities/chat.moderation.write.json
@@@ -0,0 +1,25 @@
++{
++ "id": "chat.moderation.write",
++ "version": "1.0",
++ "owner": "chat",
++ "status": "active",
++ "visibility": "first-party",
++ "sensitive": true,
++ "description": "Change the chat rows OpenVibe.Chat owns and act on its sockets for a moderator: delete one message, a person's, an anonymous id's or a relay name's messages, or a time range (Chat broadcasts the deletes), review messages held for IP approval, hide, unhide or record relay identities, set or clear TTS voice overrides, disconnect a person or an address, and log moderation actions. One typed action per request, applied once per idempotency key. Loopback only, first-party 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": [
++ "chat.message.deleted",
++ "chat.moderation.action"
++ ],
++ "implementedBy": [
++ "POST /internal/chat/moderation"
++ ],
++ "inputSchema": "chat.moderation-request@1",
++ "outputSchema": "chat.moderation-result@1"
++}
ddiff --git a/manifests/capabilities/chat.presence.read.json b/manifests/capabilities/chat.presence.read.json
iindex 7afab024..c1f632de 100644
---- a/manifests/capabilities/chat.presence.read.json
++++ b/manifests/capabilities/chat.presence.read.json
@@@ -14,6 +14,7 @@
"quotaClass": "internal-read",
"events": [],
"implementedBy": [
-- "GET /internal/live/presence"
++ "GET /internal/live/presence",
++ "GET /internal/chat/presence"
]
}
ddiff --git a/manifests/services/chat.json b/manifests/services/chat.json
iindex 1d3fe778..56d37282 100644
---- a/manifests/services/chat.json
++++ b/manifests/services/chat.json
@@@ -12,9 +12,12 @@
],
"publicOrigin": "https://openvibe.chat",
"capabilities": [
++ "chat.cache.invalidate",
++ "chat.event.publish",
"chat.live_bridge.write",
"chat.message.send",
"chat.moderation.read",
++ "chat.moderation.write",
"chat.presence.read"
],
"eventsProduced": [
ddiff --git a/package-lock.json b/package-lock.json
iindex be4bb30b..df917864 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.90.0",
++ "version": "0.91.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.90.0",
++ "version": "0.91.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 1793a8ba..8e792868 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.90.0",
++ "version": "0.91.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",