{"id":45764317,"url":"https://github.com/oddsock/distokoloshe","last_synced_at":"2026-04-19T20:02:55.750Z","repository":{"id":340661353,"uuid":"1160946675","full_name":"oddsock/distokoloshe","owner":"oddsock","description":"Self hosted E2EE video/voice/screen sharing. TLS by design, docker containers. Quick Deployment","archived":false,"fork":false,"pushed_at":"2026-03-07T21:36:29.000Z","size":409,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-07T21:51:50.166Z","etag":null,"topics":["containers","docker-compose","ephemeral-messaging","streaming-video","video-call","voice-call"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/oddsock.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-02-18T15:01:34.000Z","updated_at":"2026-02-25T22:34:38.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/oddsock/distokoloshe","commit_stats":null,"previous_names":["oddsock/distokoloshe"],"tags_count":7,"template":false,"template_full_name":null,"purl":"pkg:github/oddsock/distokoloshe","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddsock%2Fdistokoloshe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddsock%2Fdistokoloshe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddsock%2Fdistokoloshe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddsock%2Fdistokoloshe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oddsock","download_url":"https://codeload.github.com/oddsock/distokoloshe/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oddsock%2Fdistokoloshe/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30278153,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-08T20:45:49.896Z","status":"ssl_error","status_checked_at":"2026-03-08T20:45:49.525Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["containers","docker-compose","ephemeral-messaging","streaming-video","video-call","voice-call"],"created_at":"2026-02-26T00:02:57.894Z","updated_at":"2026-04-11T23:11:37.934Z","avatar_url":"https://github.com/oddsock.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# disTokoloshe\n\nSelf-hosted real-time voice, video, and screen-sharing web application. Built on LiveKit (WebRTC SFU), with end-to-end encryption, per-user volume controls, and high-framerate screen sharing (up to native resolution/refresh rate with AV1/VP9).\n\n## Architecture\n\n```\n┌─────────┐       ┌──────────────┐       ┌─────────────┐\n│ Browser  │──────▶│ nginx (web)  │──────▶│  API (Node) │\n│ (SPA)    │       │  :80 / :443  │       │   :3000     │\n└────┬─────┘       └──────────────┘       └──────┬──────┘\n     │                                           │\n     │  WebRTC (media)                           │ LiveKit token\n     │                                           │ generation\n     ▼                                           ▼\n┌──────────────────────────────────────────────────────┐\n│             LiveKit SFU (host network)               │\n│  WS :7881  ·  UDP :50201-50400  ·  TURN :3478/5349  │\n└──────────────────────────────────────────────────────┘\n                         ▲\n                         │ audio track\n              ┌──────────────────────┐\n              │  Music Bot (Python)  │\n              │  DJ Tokoloshe :3001  │\n              └──────────────────────┘\n```\n\n- **web** — nginx serving the Vite/React SPA. Reverse-proxies `/api/` to the API container and `/livekit/` to the LiveKit WebSocket.\n- **api** — Node.js/Express. Handles user auth (JWT + SQLite), room management, votes, punishments, whispers, soundboard, chat, music state, SSE events for real-time presence, and LiveKit token generation.\n- **music** — Python bot (\"DJ Tokoloshe\") that streams internet radio into a dedicated LiveKit room. Exposes a small HTTP API on `:3001` for playback control; notifies the API server of state changes for SSE broadcast.\n- **desktop** — Tauri v2 desktop client (Windows/Linux/macOS). Shares UI code with the web app via `@distokoloshe/ui` package. Connects to any disTokoloshe server via configurable URL.\n- **livekit** — LiveKit SFU running in host network mode for proper WebRTC ICE negotiation. Includes built-in TURN relay for restrictive NATs (production only).\n- **certbot** — (production only) Auto-renews Let's Encrypt TLS certificates every 12 hours.\n\n## Quick Start (Development)\n\n```bash\n# 1. Clone and generate secrets\ngit clone \u003crepo-url\u003e \u0026\u0026 cd distokoloshe\n./infra/scripts/init.sh\n\n# 2. Build and start\ndocker compose build\ndocker compose up -d\n\n# 3. Open in browser\nopen http://localhost:3080\n```\n\nRegister a user, create a room, and start talking. No TLS required for local development.\n\n## Production Deployment\n\n### Prerequisites\n\n- A server with a public IP\n- A domain name with DNS A record pointing to that IP\n- Ports open on your firewall:\n\n| Port | Protocol | Purpose |\n|------|----------|---------|\n| 80 | TCP | HTTP (ACME challenge + redirect to HTTPS) |\n| 443 | TCP + UDP | HTTPS + HTTP/3 (QUIC) |\n| 7881 | TCP | LiveKit WebSocket signaling |\n| 3478 | UDP | TURN relay |\n| 5349 | TCP | TURN/TLS relay |\n| 50201-50400 | UDP | WebRTC media (RTP) |\n\n### Step 1: Generate secrets\n\n```bash\n./infra/scripts/init.sh\n```\n\n### Step 2: Edit `.env`\n\nOpen `.env` and set your domain and email:\n\n```\nDOMAIN=chat.yourdomain.com\nACME_EMAIL=you@yourdomain.com\n```\n\nFor production, set ports to standard values:\n\n```\nWEB_PORT=80\nWEB_TLS_PORT=443\nLK_PORT=7881\n```\n\nThe rest (`LIVEKIT_API_KEY`, `LIVEKIT_API_SECRET`, `JWT_SECRET`) were auto-generated by `init.sh`. Don't change them after users have registered (JWT_SECRET invalidates all sessions, LIVEKIT keys break media relay).\n\n### Step 3: Get TLS certificates\n\n```bash\n# Test first (doesn't hit rate limits)\n./infra/scripts/init-certs.sh --dry-run\n\n# If dry run succeeds, get a real certificate\n./infra/scripts/init-certs.sh\n```\n\nThis will:\n1. Start nginx in HTTP-only mode on port 80\n2. Run certbot to get a Let's Encrypt certificate via webroot challenge\n3. Save the certificate to `./data/certs/`\n4. Restart all services with TLS enabled (including the certbot renewal container)\n\n### Step 4: Verify\n\nVisit `https://chat.yourdomain.com`. You should see the login page with a valid TLS certificate.\n\nLiveKit's entrypoint automatically detects TLS certs and switches to production mode:\n- **Dev mode** (no certs): `use_external_ip: false`, TURN disabled\n- **Prod mode** (certs present): `use_external_ip: true`, TURN enabled with TLS\n\n### Certificate Renewal\n\nCertbot runs as a persistent container (under the `production` profile) and checks for renewal every 12 hours. Certificates are stored on disk at `./data/certs/` (bind-mounted into containers), so they survive `docker volume prune`.\n\n\u003e **Note:** After automatic renewal, nginx and livekit must be restarted to pick up the new certificate. Add a host-level cron job to handle this:\n\u003e ```\n\u003e 0 3 * * * cd /path/to/distokoloshe \u0026\u0026 docker compose restart web livekit 2\u003e/dev/null\n\u003e ```\n\nTo manually force a renewal:\n\n```bash\ndocker compose run --rm --entrypoint \"certbot\" certbot renew --force-renewal\ndocker compose restart web livekit\n```\n\n### Backup \u0026 Restore\n\nAll persistent data lives in `./data/`. To back up the SQLite database and certificates:\n\n```bash\n# Backup\ntar czf distokoloshe-backup-$(date +%Y%m%d).tar.gz ./data/api/ ./data/certs/\n\n# Restore\ntar xzf distokoloshe-backup-YYYYMMDD.tar.gz\ndocker compose up -d\n```\n\nThe database is at `./data/api/distokoloshe.db`. Soundboard audio clips are stored as BLOBs in the database, so a single backup covers everything.\n\n### Troubleshooting\n\n**Can't connect / audio drops in restrictive networks (corporate, strict NAT)**\n\nLiveKit uses TURN relay in production mode (when TLS certs are present). If clients can connect via direct P2P but audio drops for some users, check:\n\n1. Ports `3478/UDP` and `5349/TCP` are open in your firewall\n2. The TURN server is working — test with: `curl -s https://chat.yourdomain.com/api/server-info`\n3. LiveKit logs: `docker compose logs livekit --tail=50`\n\nIf TURN isn't helping, ensure `DOMAIN` is set correctly in `.env` — LiveKit uses it to advertise its public IP to clients.\n\n**Linux Docker: `host.docker.internal` not resolving**\n\nThe API and web containers use `host.docker.internal` to reach the LiveKit container (which runs in host network mode). This is pre-configured via `extra_hosts: host.docker.internal:host-gateway` in `compose.yml` — no additional setup required on Linux as long as you're using Docker Engine ≥ 20.10.\n\n**HTTP/3 (QUIC) not working**\n\nPort `443/UDP` must be open in your firewall for HTTP/3 to function. Clients fall back to TCP automatically if UDP 443 is blocked, but HTTP/3 improves latency and performance when available.\n\n### Updating\n\n```bash\ngit pull\ndocker compose build\ndocker compose up -d\n```\n\nThe SQLite database in `data/api/` persists across rebuilds.\n\n## Environment Variables\n\n| Variable | Required | Default | Description |\n|----------|----------|---------|-------------|\n| `DOMAIN` | Yes (prod) | `localhost` | Domain for TLS certs and TURN server |\n| `ACME_EMAIL` | Yes (prod) | — | Email for Let's Encrypt registration |\n| `LIVEKIT_API_KEY` | Yes | — | LiveKit API key (generated by init.sh) |\n| `LIVEKIT_API_SECRET` | Yes | — | LiveKit API secret (generated by init.sh) |\n| `JWT_SECRET` | Yes | — | JWT signing secret (generated by init.sh) |\n| `E2EE_SECRET` | Yes | — | E2EE key derivation secret (generated by init.sh) |\n| `WEB_PORT` | No | `3080` | Host port for HTTP |\n| `WEB_TLS_PORT` | No | `3443` | Host port for HTTPS / HTTP/3 |\n| `LK_PORT` | No | `7881` | LiveKit WebSocket port |\n| `SERVER_CITY` | No | — | Server location label (shown in connection quality UI) |\n| `MUSIC_ROOM_NAME` | No | `Music` | LiveKit room name the music bot joins |\n| `GITHUB_REPO` | No | — | GitHub repo for auto-update sync (e.g., `user/distokoloshe`) |\n| `GITHUB_TOKEN` | No | — | GitHub token (only needed for private repos) |\n\n## Features\n\n- **Voice \u0026 video rooms** — Create voice or video rooms. Auto-joins your last room on login. Click any participant's camera to spotlight/maximize it (same as screen shares)\n- **Screen sharing** — Quality picker (720p/1080p/1080p60/native), browser-aware codec selection (AV1 on Firefox, VP9 on Chromium, VP8/H.264 on Safari), optional audio capture\n- **End-to-end encryption** — AES-GCM via LiveKit Encoded Transforms with per-room HMAC-SHA256 key derivation. Graceful fallback to unencrypted on unsupported browsers (Safari)\n- **Per-user volume control** — Individual volume sliders (0-200%) with persistent settings per participant. Audio routed through Web Audio API for proper stereo upmix across all platforms\n- **Soundboard** — Upload audio clips (max 10 seconds) to play to the room. Inline clip trimmer with waveform visualization, draggable start/end handles, preview playback with animated scrubber, and trim-to-WAV before upload. Per-clip preview (local-only) and room playback with volume control\n- **Democratic moderation** — Vote to jail disruptive users. 30-second voting window, quorum of 3, simple majority. Passed votes auto-create a timed jail room and kick the target from LiveKit. Room creator can lift punishments early\n- **Whispers mode** — Room mode that shuffles participants into a random chain (Fisher-Yates). Chain updates live as users join/leave\n- **Music bot** — DJ Tokoloshe streams internet radio into rooms via a Python LiveKit bot. HTTP API for playback control, now-playing display with marquee scroll, E2EE compatible\n- **Sound notifications** — 4 synthesized sound packs (Mystical Chimes, Mischievous Pops, Retro Arcade, Digital Whispers) with 8 events each. All generated via Web Audio API oscillators — no audio files\n- **Real-time presence** — SSE-based online/offline status, room membership, vote/punishment/whispers events (13 event types)\n- **Connection quality** — Live signal strength indicator with RTT (round-trip time) and jitter stats, server region display\n- **Desktop client** — Tauri v2 app (Windows/Linux/macOS) sharing UI code with web via `@distokoloshe/ui`. Configurable server URL, window state persistence, global shortcuts (OS-level mute/deafen with modifier keys), and self-hosted auto-updater with ed25519 signed updates\n- **Noise cancellation** — Real-time background noise removal via WASM AudioWorklet. Choose between RNNoise (ML neural network, best voice isolation) or Speex DSP (lighter CPU). Toggle and engine preference persist across sessions\n- **Soundbites** — Save the last 10 seconds of any participant's audio as a WAV file. Client-side ring buffer recording (~1.9 MB/participant), fully E2EE compatible. Participants can opt out via settings\n- **Light/dark theme** — Toggle with persistent preference\n- **Device selection** — Choose microphone, speaker, and camera from settings\n\n## Project Structure\n\n```\n├── compose.yml              # Docker orchestration\n├── .env                     # Secrets + config (generated by init.sh, gitignored)\n├── package.json             # npm workspaces root\n├── apps/\n│   ├── web/                 # Vite + React SPA (nginx + Docker)\n│   │   ├── nginx.conf       # Dev proxy config\n│   │   ├── nginx.tls.conf   # Production TLS config (HTTPS + HTTP/3)\n│   │   └── src/             # Web-specific entry (App.tsx, main.tsx)\n│   ├── desktop/             # Tauri v2 desktop client\n│   │   ├── src/             # Desktop entry (App.tsx, ServerConfig.tsx)\n│   │   └── src-tauri/       # Rust shell (global shortcuts, window state)\n│   ├── server/              # Node.js/Express API (JWT + SQLite)\n│   │   └── src/\n│   │       ├── routes/      # auth, rooms, users, votes, punishments, whispers,\n│   │       │                #   soundboard, chat, music, updates, events (SSE)\n│   │       ├── livekit.ts   # Token generation + E2EE key derivation\n│   │       └── events.ts    # SSE client tracking + room membership\n│   └── music/               # DJ Tokoloshe — Python music bot\n│       ├── main.py          # Entry point + HTTP API (:3001)\n│       ├── bot.py           # LiveKit participant (publishes audio track)\n│       ├── player.py        # Internet radio playback (FFmpeg)\n│       └── stations.py      # Station list\n├── packages/\n│   └── ui/                  # Shared React components, hooks, and utilities\n│       └── src/\n│           ├── pages/       # Login, Room\n│           ├── hooks/       # useLiveKitRoom, useScreenShare, useAudioMixer, etc.\n│           ├── components/  # ScreenShareView, VolumeSlider, UserList, etc.\n│           └── lib/         # api.ts, sounds.ts, utils.ts, codec.ts, imageResize.ts\n├── infra/\n│   ├── livekit/             # LiveKit SFU config + entrypoint\n│   ├── music/               # Music bot Node.js dependencies (build cache)\n│   └── scripts/             # init.sh, init-certs.sh\n└── data/                    # Persistent data (gitignored)\n    ├── api/                 # SQLite database\n    ├── certs/               # TLS certificates (Let's Encrypt)\n    └── certbot/www/         # ACME challenge webroot\n```\n\n## Desktop Auto-Updates\n\nThe desktop app supports self-hosted auto-updates via ed25519-signed binaries. Updates flow through the server the client is connected to.\n\n### Setup\n\n1. **Generate signing keys**: `npx tauri signer generate -w ~/.tauri/distokoloshe.key`\n2. **Configure**: Put the public key in `apps/desktop/src-tauri/tauri.conf.json` under `plugins.updater.pubkey`\n3. **CI**: Add `TAURI_SIGNING_PRIVATE_KEY` and `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` as GitHub Actions secrets\n4. **Server**: Set `GITHUB_REPO=youruser/distokoloshe` in `.env` — the server auto-downloads new releases from GitHub every 60 minutes\n\n### Pipeline\n\nPush a version tag (e.g., `git tag v1.1.0 \u0026\u0026 git push --tags`) → CI builds + signs → GitHub Release created → server syncs within 60 min → connected clients auto-update.\n\nWithout `GITHUB_REPO`, admins can manually place the `.exe` + `.sig` files in the server's `data/api/updates/` directory.\n\n### Security\n\nThe public key is baked into the app binary at build time. Only updates signed with the matching private key are accepted — even a compromised server cannot push unsigned updates.\n\n## macOS Desktop App\n\nThe macOS build is currently unsigned (no Apple Developer certificate). You may encounter:\n\n- **\"disTokoloshe is damaged and can't be opened\"** — Run `xattr -cr /Applications/disTokoloshe.app` to remove the quarantine flag, then open normally.\n- **Keychain password prompt** (\"distokoloshe wants to use your confidential information...\") — This appears because the E2EE encryption keys are stored in the macOS Keychain. Click **\"Always Allow\"** to permanently grant access and stop the prompt from recurring.\n\nThese issues will be resolved once the app is signed with an Apple Developer certificate.\n\n## Planned Features\n\n- **Desktop: system tray menu** — Context menu with mute/deafen/quit actions.\n- **Apple Developer signing** — Codesign + notarize the macOS build to eliminate Gatekeeper and Keychain prompts.\n\n## Security\n\n- **Auth** — bcrypt password hashing (12 rounds), JWT with 7-day expiry (HS256), rate-limited login (20/15min) and registration (5/hour per IP)\n- **E2EE** — AES-GCM end-to-end encryption via LiveKit Encoded Transforms. Per-room key derivation using HMAC-SHA256 with a dedicated `E2EE_SECRET` (separate from `JWT_SECRET`)\n- **TLS** — TLS 1.2/1.3 only, HSTS with 2-year max-age, OCSP stapling, HTTP/3 (QUIC)\n- **Headers** — CSP (frame-ancestors 'none', strict script/connect/media sources), X-Frame-Options DENY, X-Content-Type-Options nosniff, Referrer-Policy strict-origin-when-cross-origin, Permissions-Policy (camera/mic/display-capture self-only)\n- **API** — CORS disabled at Express level (`origin: false`), nginx whitelists only Tauri desktop origins. 16KB body limit, parameterized SQL queries (no injection), input validation with regex/whitelist on all user inputs\n- **Database** — SQLite with WAL mode, foreign key constraints enabled\n- **SSE** — Per-user connection cap (3) and global cap (500), 30s keepalive ping\n- **Desktop** — Tauri CSP restricts connect-src, media-src, script-src. WASM eval allowed only for livekit-client\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foddsock%2Fdistokoloshe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foddsock%2Fdistokoloshe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foddsock%2Fdistokoloshe/lists"}