From 9c91966730ea20ae47149c1be730d476b23a76fb Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Wed, 1 Jul 2026 15:53:07 +0000 Subject: [PATCH] Multi-tenant platform: per-streamer overlay/sender links + CI/CD Streamers log in with Google on a new landing page and get a dashboard with two auto-generated, permanent links: one for their OBS/StreamLabs Browser Source, one to share with viewers. Messages are now scoped to the right streamer via Socket.IO rooms instead of broadcasting globally. Streamer records and tokens are persisted in SQLite (node:sqlite) so links survive restarts/redeploys. Also adds a Forgejo Actions pipeline mirroring t2sTelegramBot's: build, smoke-test, push to the local registry, then deploy to the prod host over SSH. Co-Authored-By: Claude Sonnet 5 --- .dockerignore | 1 + .env.example | 16 +++-- .forgejo/workflows/deploy.yml | 49 +++++++++++++ .gitignore | 1 + Dockerfile | 2 +- db.js | 59 +++++++++++++++ docker-compose.prod.yml | 10 +++ docker-compose.yml | 9 +-- public/dashboard.html | 115 ++++++++++++++++++++++++++++++ public/index.html | 100 ++++++++++++++++++++++++++ public/overlay.js | 8 ++- public/{sender.html => send.html} | 18 ++++- public/{sender.js => send.js} | 5 +- server.js | 85 +++++++++++++++++----- 14 files changed, 444 insertions(+), 34 deletions(-) create mode 100644 .forgejo/workflows/deploy.yml create mode 100644 db.js create mode 100644 docker-compose.prod.yml create mode 100644 public/dashboard.html create mode 100644 public/index.html rename public/{sender.html => send.html} (71%) rename public/{sender.js => send.js} (93%) diff --git a/.dockerignore b/.dockerignore index ca83ee8..f360dc7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,4 @@ node_modules .env .git +data diff --git a/.env.example b/.env.example index fbf3d3f..c7774d1 100644 --- a/.env.example +++ b/.env.example @@ -1,15 +1,17 @@ PORT=3000 # OAuth 2.0 Client ID (Web application) from https://console.cloud.google.com/apis/credentials -# Authorized JavaScript origins must include the origin you open sender.html from, -# e.g. http://localhost:3000 for local dev. -GOOGLE_CLIENT_ID= +# Authorized JavaScript origins must include every origin the app is served from +# (e.g. http://localhost:3000 for local dev, plus your production domain). +# The value below is a placeholder so `docker-compose build` / CI smoke tests can +# boot the container; replace with the real Client ID for actual logins to work. +GOOGLE_CLIENT_ID=placeholder-client-id.apps.googleusercontent.com # Random long string, used to sign the session cookie. Generate with: # node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" -SESSION_SECRET= +SESSION_SECRET=placeholder-session-secret-change-me -# Microsoft Edge Read Aloud voice used for TTS (free, no API key needed). -# Same engine as t2sTelegramBot uses. List of voices: `npx edge-tts-node --list-voices` (if available) -# or see https://github.com/Migushthe2nd/MsEdgeTTS +# Default Microsoft Edge Read Aloud voice used for TTS (free, no API key needed, +# same engine as t2sTelegramBot). Used for newly created streamers; each +# streamer's own voice is stored in the database and can be changed later. TTS_VOICE=ru-RU-DmitryNeural diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..7941974 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,49 @@ +name: CI/CD Pipeline + +on: + push: + branches: + - main + +jobs: + build-and-deploy: + runs-on: shell + steps: + - name: Clone repository + run: git clone --depth 1 "http://192.168.8.174:3000/${GITHUB_REPOSITORY}.git" . + env: + GIT_TERMINAL_PROMPT: '0' + + - name: Ensure Docker CLI + run: | + apk add --no-cache docker-cli docker-cli-compose openssh-client || true + grep -q '^vrubel:' /etc/passwd || echo 'vrubel:x:1000:1000::/data:/bin/sh' >> /etc/passwd + + - name: Setup .env for CI/CD + run: cp .env.example .env + + - name: Build Docker image + run: docker-compose build + + - name: Start container for test + run: docker-compose up -d + + - name: Verify service running + run: | + sleep 5 + docker-compose logs --tail=20 + + - name: Stop container + run: docker-compose down + + - name: Login to Gitea Container Registry + run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login http://192.168.8.174:3000 -u "${{ secrets.REGISTRY_USER }}" --password-stdin + + - name: Push image to registry + run: docker push 192.168.8.174:3000/vrubel/send2streamer:latest + + - name: Copy docker-compose to prod host + run: cat docker-compose.prod.yml | ssh -i /data/.ssh/id_ed25519 -o StrictHostKeyChecking=no vrubel@192.168.8.171 "mkdir -p ~/services/sendMessageToStreamer && cat > ~/services/sendMessageToStreamer/docker-compose.yml" + + - name: Deploy on prod host + run: ssh -i /data/.ssh/id_ed25519 -o StrictHostKeyChecking=no vrubel@192.168.8.171 "cd ~/services/sendMessageToStreamer && docker pull 192.168.8.174:3000/vrubel/send2streamer:latest && docker compose up -d --remove-orphans" diff --git a/.gitignore b/.gitignore index 713d500..c05e655 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ .env +/data/ diff --git a/Dockerfile b/Dockerfile index 57564ac..583a3e1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,7 @@ WORKDIR /app COPY package.json package-lock.json* ./ RUN npm ci --omit=dev -COPY server.js ./ +COPY server.js db.js ./ COPY public ./public EXPOSE 3000 diff --git a/db.js b/db.js new file mode 100644 index 0000000..12581e8 --- /dev/null +++ b/db.js @@ -0,0 +1,59 @@ +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const { DatabaseSync } = require('node:sqlite'); + +const DB_PATH = process.env.DB_PATH || path.join(__dirname, 'data', 'app.db'); +fs.mkdirSync(path.dirname(DB_PATH), { recursive: true }); + +const db = new DatabaseSync(DB_PATH); + +db.exec(` + CREATE TABLE IF NOT EXISTS streamers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + google_id TEXT UNIQUE NOT NULL, + email TEXT NOT NULL, + name TEXT NOT NULL, + picture TEXT, + overlay_token TEXT UNIQUE NOT NULL, + sender_token TEXT UNIQUE NOT NULL, + tts_voice TEXT NOT NULL DEFAULT 'ru-RU-DmitryNeural', + created_at INTEGER NOT NULL + ); +`); + +function generateToken() { + return crypto.randomBytes(16).toString('hex'); +} + +function findStreamerByGoogleId(googleId) { + return db.prepare('SELECT * FROM streamers WHERE google_id = ?').get(googleId); +} + +function findStreamerByOverlayToken(token) { + return db.prepare('SELECT * FROM streamers WHERE overlay_token = ?').get(token); +} + +function findStreamerBySenderToken(token) { + return db.prepare('SELECT * FROM streamers WHERE sender_token = ?').get(token); +} + +function createStreamer({ googleId, email, name, picture }) { + const overlayToken = generateToken(); + const senderToken = generateToken(); + db.prepare( + `INSERT INTO streamers (google_id, email, name, picture, overlay_token, sender_token, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)` + ).run(googleId, email, name, picture || null, overlayToken, senderToken, Date.now()); + return findStreamerByGoogleId(googleId); +} + +function getOrCreateStreamerByGoogle({ googleId, email, name, picture }) { + return findStreamerByGoogleId(googleId) || createStreamer({ googleId, email, name, picture }); +} + +module.exports = { + findStreamerByOverlayToken, + findStreamerBySenderToken, + getOrCreateStreamerByGoogle, +}; diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..f6e46a5 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,10 @@ +services: + app: + image: 192.168.8.174:3000/vrubel/send2streamer:latest + container_name: send2streamer + ports: + - "3000:3000" + env_file: .env + volumes: + - ./data:/app/data + restart: unless-stopped diff --git a/docker-compose.yml b/docker-compose.yml index 5fd1093..972c87f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,11 @@ services: app: build: . + image: 192.168.8.174:3000/vrubel/send2streamer:latest + container_name: send2streamer ports: - "3000:3000" - environment: - - PORT=3000 - - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} - - SESSION_SECRET=${SESSION_SECRET} + env_file: .env + volumes: + - ./data:/app/data restart: unless-stopped diff --git a/public/dashboard.html b/public/dashboard.html new file mode 100644 index 0000000..e67510c --- /dev/null +++ b/public/dashboard.html @@ -0,0 +1,115 @@ + + + + +Ваши ссылки — Send2Streamer + + + + + +

Ваши ссылки

+ +
+

Ссылка для OBS / StreamLabs

+

Вставьте в свойства Browser Source (URL).

+
+ + +
+
+ +
+

Ссылка для зрителей

+

Опубликуйте её — зрители авторизуются через Google и смогут слать вам сообщения на стрим.

+
+ + +
+
+ +

выйти

+ + + + diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3ade89e --- /dev/null +++ b/public/index.html @@ -0,0 +1,100 @@ + + + + +Send2Streamer — сообщения от зрителей прямо на стрим + + + + + + +

Send2Streamer

+

Зрители пишут сообщения — они всплывают у вас на стриме голосом.

+ +
+

Как это работает:

+
    +
  1. Вы входите через Google — сервис выдаёт вам две ссылки: для OBS/StreamLabs и для зрителей.
  2. +
  3. Первую вставляете в Browser Source в OBS/StreamLabs.
  4. +
  5. Вторую публикуете для зрителей (например, в описании канала или в чате).
  6. +
  7. Зритель авторизуется через Google по этой ссылке и пишет вам сообщение — оно появляется на стриме с текстом и озвучкой.
  8. +
+
+ +

Войдите как стример, чтобы получить свои ссылки:

+
+
+ + + + diff --git a/public/overlay.js b/public/overlay.js index a063a87..b374fdf 100644 --- a/public/overlay.js +++ b/public/overlay.js @@ -1,7 +1,9 @@ const box = document.getElementById('box'); const fromEl = document.getElementById('from'); const textEl = document.getElementById('text'); -const socket = io(); + +const token = location.pathname.split('/').filter(Boolean).pop(); +const socket = io({ query: { role: 'overlay', token } }); let hideTimer = null; let audioCtx = null; @@ -70,3 +72,7 @@ socket.on('display_message', (payload) => { socket.on('display_audio', (payload) => { playSpeech(payload.audio, payload.mimeType); }); + +socket.on('invalid_link', () => { + console.error('Invalid overlay link — check the URL from your dashboard.'); +}); diff --git a/public/sender.html b/public/send.html similarity index 71% rename from public/sender.html rename to public/send.html index 21eb0dc..5c62464 100644 --- a/public/sender.html +++ b/public/send.html @@ -2,7 +2,7 @@ -Отправить сообщение стримеру +Отправить сообщение на стрим