# Unified Chat One dark web dashboard that merges live chat from **YouTube, Twitch, Kick, Discord, Rumble, Trovo, DLive and Facebook** into a single scrolling feed. Messages are pushed to the browser over Server-Sent Events, so there's no front-end polling and multiple tabs stay in sync. It also includes a **YouTube live title editor** (`/yt`) so you can rename a running stream without opening YouTube Studio, with an optional theme-park ride/show picker. ## Quick start ```bash cp .env.example .env # then edit .env — fill in at least one platform docker compose up -d ``` Open `http://SERVER_IP:8088/` and press **Start**. That's the whole install. `docker compose` builds the image from this folder, so there's no Docker Hub account or prebuilt image required. ## Project layout ``` unified-chat/ ├── unified-chat.js # the whole app: adapters, SSE hub, and the web UI ├── package.json ├── package-lock.json # pinned, audited clean ├── Dockerfile ├── docker-entrypoint.sh # fixes volume ownership, drops privileges ├── docker-compose.yml ├── .env.example # copy to .env and edit ├── .dockerignore ├── .gitignore ├── unified-chat.xml # Unraid Community Applications template └── tokens/ # created on first run; holds the YouTube OAuth token ``` The UI is served inline from `unified-chat.js` — there is no static folder to deploy. ## How configuration works Everything comes from **environment variables**, loaded from `.env`. There is no settings page, and nothing is written to disk except the YouTube OAuth token. **Every platform is optional.** An adapter is constructed only when its required variables are present: | Platform | Enabled when | |---|---| | YouTube | `YT_API_KEYS` **and** `YT_CHANNEL_ID` | | Twitch (IRC) | `TWITCH_NICK` **and** `TWITCH_CHANNEL` **and** (`TWITCH_TOKEN_URL` or `TWITCH_IRC_OAUTH`) | | Twitch (Helix fallback) | `TWITCH_CLIENT_ID` **and** `TWITCH_CLIENT_SECRET` **and** `TWITCH_CHANNEL` | | Kick | `KICK_CLIENT_ID` **and** `KICK_CLIENT_SECRET` **and** `KICK_CHANNEL_SLUG` | | Discord | `DISCORD_BOT_TOKEN` **and** `DISCORD_CHANNEL_IDS` | | Rumble | `RUMBLE_API_URL` **or** `RUMBLE_CHANNEL_URL` | | Trovo | `TROVO_CLIENT_ID` **and** (`TROVO_CHANNEL_ID` or `TROVO_ACCESS_TOKEN`) | | DLive | `DLIVE_USERNAME` **and** `DLIVE_ACCESS_TOKEN` | | Facebook | `FB_PAGE_ID` **and** `FB_PAGE_TOKEN` | Anything left blank is skipped and logged as `[info] disabled (missing ...)`. The startup log ends with a `Platforms enabled:` line, so `docker compose logs` tells you exactly what loaded. A platform that's configured but broken (bad credentials, unreachable API) logs an error and is dropped — it can't stop the other platforms or the server from starting. ## Security **Set `AUTH_USER` and `AUTH_PASS` in `.env`.** Without them there is no login, and anyone who can reach port 8088 can read your chat, see your stream status, and rename your live YouTube broadcast via `/api/yt/title`. The app prints a warning at boot when auth is off. Two routes stay open by design: - `/healthz` — so container health checks don't need credentials. Returns only `{ok, running, uptime}`. - `/kick/webhook` — Kick's servers can't send an `Authorization` header. That route verifies Kick's RSA signature instead, and it's the only endpoint safe to expose publicly. Basic auth over plain HTTP sends the password base64-encoded on every request. On an untrusted network, put the container behind a reverse proxy with TLS. Your `.env` holds API keys, bot tokens and OAuth secrets in plain text, and `tokens/youtube.json` holds a YouTube refresh token. Both are excluded by `.dockerignore` and `.gitignore`, so they're never baked into an image or committed. ## Install option A — docker-compose (recommended) ```bash cp .env.example .env # then edit docker compose up -d docker compose logs -f # confirm your platforms loaded ``` ## Install option B — plain docker ```bash docker build -t unified-chat . docker run -d \ --name unified-chat \ -p 8088:8088 \ --restart unless-stopped \ --env-file .env \ -v "$PWD/tokens:/usr/src/app/tokens" \ unified-chat ``` ## Install option C — Unraid `unified-chat.xml` is a Community Applications template. It does **not** need a Docker Hub account or a published image: it runs the official `node:20` image and downloads the app files from streamhelpers.com into your appdata folder on start. 1. Put the XML in `/boot/config/plugins/dockerMan/templates-user/`, or host it and add it via **Docker → Add Container → Template**. 2. Fill in the variables for the platforms you want, and set `AUTH_USER`/`AUTH_PASS`. 3. Leave the app data path at `/mnt/user/appdata/unified-chat` — the app files and `tokens/youtube.json` both live there. First boot takes a minute or two while `npm install` runs; watch the container log. Restarting the container re-downloads the latest version. This path trades the hardened image for convenience: it runs as root, without the healthcheck or the privilege-dropping entrypoint. If you want those on Unraid, use the Docker Compose Manager plugin with option A instead. ## File permissions (PUID / PGID) Applies to the image built by the `Dockerfile` (options A and B). The container starts as root, chowns the mounted `tokens/` directory, then drops to `PUID:PGID` (default `1000:1000`) before running Node. This exists because Docker creates a missing bind-mount directory as root, which a non-root process then can't write to. Override both if you need a different owner — Unraid users typically want `99:100`. ## Using it | Route | What it does | |---|---| | `/` | The merged chat dashboard | | `/messages` | SSE stream (what the dashboard subscribes to) | | `/status` | JSON: which adapters are connected, live, and their titles | | `/healthz` | Liveness probe — no auth required | | `/start`, `/stop` | POST — connect/disconnect all adapters | | `/yt` | YouTube live title editor | | `/auth/youtube/start` | Begin the YouTube OAuth flow (visit once) | | `/kick/webhook` | Kick's inbound webhook endpoint — no auth, RSA-verified | **Adapters are idle on boot.** Nothing connects until you press **Start** in the dashboard. This is deliberate: it stops the container from burning YouTube API quota while you're not streaming. ## Getting the settings right - **YouTube quota is the thing that will bite you.** Live chat polling costs quota on every request and each Google Cloud project only gets 10,000 units/day. That's why `YT_API_KEYS` takes a comma-separated list — create the keys in *separate projects* and the app rotates to the next when a key returns a quota error. Keep `MIN_YT_POLL_MS` at 10000 or higher. - **Discord needs the Message Content intent.** In the Developer Portal under Bot → Privileged Gateway Intents, turn on *Message Content Intent*. Without it the bot connects fine but every message arrives with an empty body. - **`YT_OAUTH_REDIRECT` must match exactly** what you registered in Google Cloud Credentials — same scheme, host, and port — and your browser has to reach it. Authorizing from outside the LAN means a public hostname, not `192.168.x.x`. - **Kick webhooks need a public URL.** `KICK_WEBHOOK_EXTERNAL_URL` is called by Kick's servers, so it must be internet-reachable and end in `/kick/webhook`. Set `KICK_ENABLE_WEBHOOKS=false` if you're LAN-only. - **Twitch IRC tokens expire.** Prefer `TWITCH_TOKEN_URL` (an endpoint you host that returns a fresh token) over a static `TWITCH_IRC_OAUTH`, so reconnects don't need a manual `.env` edit. - **Editing `.env` on Windows?** Run `sed -i 's/\r$//' .env` first, or every value gets a trailing carriage return. ## Known gaps - **X / Twitter is not supported.** The adapter classes were never written. Any `X_*` variables in your `.env` are ignored, and the app logs a note saying so. - The theme-park picker on `/yt` is a niche feature built around [themeparks.wiki](https://api.themeparks.wiki/v1/destinations). Leave `TP_ALLOWED_DESTINATIONS` and `TP_DEFAULT_DESTINATION` blank if you don't stream from parks. ## Updating ```bash docker compose up -d --build # rebuild from updated source # or, if you're pulling a published image: docker compose pull && docker compose up -d ``` Your `.env` and the mounted `tokens/` folder persist, so you won't need to re-authorize YouTube. --- ## License, support and donations Provided free and **as-is, with no warranty and no support of any kind**. There is no help desk, no ticket queue, and no obligation on anyone to answer a question, fix a bug, or keep this working as third-party APIs change. This tool connects to your live broadcast and your platform accounts, and it can fail. You are responsible for securing your installation, for the credentials you supply, and for testing before you rely on it. If it's useful and you want to chip in, donations are welcome: A donation is a **voluntary gift, not a purchase**. It buys no support, no warranty, no bug fixes, no features and no priority, and it is non-refundable. **Donating does not change any of the above.** Only give what you're happy to give away with nothing expected back. Full terms: Another fine product of [Blue Soup Enterprises](https://bluesoup.biz/) · [StreamHelpers.com](https://streamhelpers.com/)