OpenVibe.Live/Docs
Source on GitHub

Chat System#

The OpenVibe.Live chat system provides real-time messaging, moderation, and extensibility.

Architecture#

OpenVibe.Chat (roadmap Wave 6; live since 2026-09-23)#

Chat runs in OpenVibe.Chat (127.0.0.1:4400) with the same WebSocket protocol and REST paths. Live runs no chat server, and there is no switch (the CHAT_AUTHORITY flag was retired on 2026-10-11); the local ChatServer and its /ws/chat upgrade path are deleted. Without Live's service credentials (development, tests) chat is inert.

Features#

Real-time Chat#

Moderation Tools#

Chat Logs & Admin#

Settings Sync#

What people wear#

A chat line shows what its sender wears from OpenVibe.Inventory. Live is the issuer of the live.* kinds; the Workshop's community badges are network.badge.

OpenVibe.Chat asks Live for these through POST /internal/chat-context/decor. Live reads Inventory 100 people at a time and keeps each answer 30 seconds (server/monetization/inventory-client.js), and an Inventory outage just leaves names plain. People earn items through OpenVibe.Quest's quest rewards, or are given community badges by their makers in the Workshop. Nothing is bought or sold. The same items show on openvibe.chat, Community and Network profiles, drawn by openvibe-shared items.js.

Text-to-Speech (TTS)#

Admin Endpoints#

Chat Logs#

GET /api/chat/admin/logs?page=1&limit=50&username=bob&from=2024-01-01&to=2024-12-31

Export Logs#

GET /api/chat/admin/logs/export?format=csv
GET /api/chat/admin/logs/export?format=json

Purge Preview#

POST /api/chat/admin/purge/preview
Body: { "streamId": 123, "from": "2024-01-01T00:00:00Z", "to": "2024-01-02T00:00:00Z" }

Execute Purge#

DELETE /api/chat/admin/purge
Body: { "streamId": 123, "from": "2024-01-01T00:00:00Z", "to": "2024-01-02T00:00:00Z" }

from and to (also on the log filter and /api/chat/:streamId/replay) are UTC, as ISO instants or SQLite's YYYY-MM-DD HH:MM:SS; both bounds are inclusive. The preview, the purge, the log filter and VOD chat replay all read them the same way, so a purged range disappears from replay exactly.

WebSocket Protocol#

Connect#

wss://openvibe.live/ws/chat?stream=123
Authorization: Bearer JWT_OR_API_TOKEN        (non-browser clients)

Browsers send the token in the first join message instead ({ "type": "join", "streamId": 123, "token": "…" }): the socket is anonymous until then. Neither a cookie nor a ?token= query parameter signs a socket in (both removed 2026-10-10, OpenVibe.Chat#55).

Message Types (Client → Server)#

TypeFieldsDescription
chatmessageSend a chat message
joinstreamIdJoin a stream's chat room

Message Types (Server → Client)#

TypeFieldsDescription
chatusername, message, timestamp, ...Chat message
systemmessageSystem notification
deletemessageIdSingle message deleted
purgefromTime, toTimeBulk messages purged
ttstext, voiceServer-generated TTS event
tts-audiourlTTS audio file URL
user_countcountUser presence update

TTS playback hardening (2026-08-31)#

media-src now includes data: (the mod voice preview plays a data:audio/… URL; without it the element dies with "no supported source"). The admin Test Voice (POST /api/tts/admin/test) and mod preview (POST /api/mod/tts-voice/preview) additionally return a same-origin url — the clip is parked in the shared TTS cache (data/tts-cache) and streamed from GET /api/tts/audio/<hash>.<wav|mp3> (strict filename, no traversal) — so playback works even where a browser or shield refuses blob:/data: audio. Chat/broadcast TTS players retry once with a data: URL when the element rejects the blob URL. Note: tts-audio WS payloads carry base64 (audio + mimeType), not a url.

Auto-translation (non-English streamers)#

Every channel has a language it lives in: the streamer's explicit channels.chat_language (PUT /api/streams/channel with chat_language: 'ja' / 'auto' …), or, when auto, the language detected from any non-English line in their bio or name (server/i18n/translate.js). GET /api/streams/channel/:username returns it as language: { code, name, flag, explicit, translate }, and the chat auth frame carries channel_language so the composer can show the "Auto-translated ↔ Japanese" pill.

When AI is enabled (ai_enabled, and AI_SERVICE not off; the chat_translate_enabled site setting defaults to on), each chat line is translated a beat after it is broadcast and delivered to the same rooms as

{ "type": "chat_translation", "id": 123, "from": "ja", "to": "en", "text": "…" }

Direction rules: any non-English message → English; an English message in a non-English channel → the channel's language (so a Japanese streamer reads his chat without anyone typing Japanese). Emote-only / command / link-only lines are skipped. Translations are cached by content hash in the translations table and persisted by Chat into the message's metadata.translation, so history renders them too. Calls go through ai/llm.js (metered, budgeted, max 3 in flight; overflow lines simply stay untranslated).

Any line, your language. Every chat message also has a hover 🌐 button that asks POST /api/i18n/translate { text, to } for the line in the viewer's browser language (rate-limited per IP: 15/min anonymous, 40/min signed in; cached like the automatic translations). This is the third direction — a Korean viewer reading a Japanese streamer, a Japanese viewer on an English stream — so nobody is left out, English users included.

Related surfaces: GET /api/streams/channel/:username/bio-en (bio in English), GET /api/chat-ai/live-captions/:username (English rendering of live speech — needs the AI timeline on and a multilingual whisper model, see WHISPER_MODEL_MULTI), and the home-page "Star of OpenVibe" spotlight (GET /api/home/star, star_streamer setting / STAR_STREAMER env).

Friendly global chat (2026-09-17)#

A viewer-side setting (chat settings → Behavior). When on, messages in global chat that match FRIENDLY_FILTER_CATEGORIES in server/chat/moderation-utils.js (slurs, hate slogans, threats, sexual content, anything sexualising minors) fold into a "N messages hidden by Friendly chat · Show" line. Nothing is deleted and nobody is moderated for it. It is on for a viewer's first day (the chat auth message carries newcomer, from the account age or the anon's first sighting) unless the viewer chose otherwise; the rules come from GET /api/chat/filters/friendly.

History and rendering#

buildChatMessageEl() in public/js/chat.js renders every surface (page, popout, broadcast desk, floating widget). History renders off-screen and swaps in once; the last 200 rows of a room are cached in localStorage per user for an instant paint on reload. The view follows new messages unless the reader scrolled back (_watchChatPin).

This page is rendered from docs/chat-system.md in the OpenVibe.Live repository. Found a mistake? Edit it there.