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 @@ -Отправить сообщение стримеру +Отправить сообщение на стрим