From fab55cde1d7756412d6c742812600b55e0784f40 Mon Sep 17 00:00:00 2001 From: vrubelroman Date: Wed, 1 Jul 2026 16:49:51 +0000 Subject: [PATCH] Let viewers set a per-streamer display name Viewers can type a custom name shown to the streamer instead of their Google account name. It's saved per (streamer, viewer) pair in SQLite and pre-filled on future visits to the same send link. Co-Authored-By: Claude Sonnet 5 --- db.js | 25 +++++++++++++++++++++++++ public/send.html | 16 ++++++++++++++++ public/send.js | 13 ++++++++++++- server.js | 27 +++++++++++++++++++++++---- 4 files changed, 76 insertions(+), 5 deletions(-) diff --git a/db.js b/db.js index 12581e8..1bf9716 100644 --- a/db.js +++ b/db.js @@ -20,6 +20,14 @@ db.exec(` tts_voice TEXT NOT NULL DEFAULT 'ru-RU-DmitryNeural', created_at INTEGER NOT NULL ); + + CREATE TABLE IF NOT EXISTS viewer_names ( + streamer_id INTEGER NOT NULL, + google_id TEXT NOT NULL, + display_name TEXT NOT NULL, + updated_at INTEGER NOT NULL, + PRIMARY KEY (streamer_id, google_id) + ); `); function generateToken() { @@ -52,8 +60,25 @@ function getOrCreateStreamerByGoogle({ googleId, email, name, picture }) { return findStreamerByGoogleId(googleId) || createStreamer({ googleId, email, name, picture }); } +function getViewerName(streamerId, googleId) { + const row = db + .prepare('SELECT display_name FROM viewer_names WHERE streamer_id = ? AND google_id = ?') + .get(streamerId, googleId); + return row ? row.display_name : null; +} + +function setViewerName(streamerId, googleId, displayName) { + db.prepare( + `INSERT INTO viewer_names (streamer_id, google_id, display_name, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(streamer_id, google_id) DO UPDATE SET display_name = excluded.display_name, updated_at = excluded.updated_at` + ).run(streamerId, googleId, displayName, Date.now()); +} + module.exports = { findStreamerByOverlayToken, findStreamerBySenderToken, getOrCreateStreamerByGoogle, + getViewerName, + setViewerName, }; diff --git a/public/send.html b/public/send.html index 5c62464..6160adf 100644 --- a/public/send.html +++ b/public/send.html @@ -38,6 +38,19 @@ color: #888; font-size: 14px; } + label { + display: block; + font-size: 13px; + color: #666; + margin-bottom: 4px; + } + #display-name { + width: 100%; + font-size: 16px; + padding: 8px 10px; + box-sizing: border-box; + margin-bottom: 16px; + } textarea { width: 100%; height: 100px; @@ -77,6 +90,9 @@ выйти + + +
diff --git a/public/send.js b/public/send.js index f8598c1..dfca437 100644 --- a/public/send.js +++ b/public/send.js @@ -3,6 +3,7 @@ const mainSection = document.getElementById('hidden-until-login'); const userPic = document.getElementById('user-pic'); const userName = document.getElementById('user-name'); const logoutBtn = document.getElementById('logout'); +const displayNameInput = document.getElementById('display-name'); const messageInput = document.getElementById('message'); const sendBtn = document.getElementById('send'); const statusEl = document.getElementById('status'); @@ -22,6 +23,13 @@ function showLoggedOut() { mainSection.style.display = 'none'; } +async function loadDisplayName() { + const res = await fetch(`/api/viewer-name?token=${encodeURIComponent(token)}`); + if (!res.ok) return; + const { name } = await res.json(); + displayNameInput.value = name || ''; +} + async function handleCredentialResponse(response) { const res = await fetch('/auth/google', { method: 'POST', @@ -32,6 +40,7 @@ async function handleCredentialResponse(response) { if (res.ok) { const { user } = await res.json(); showLoggedIn(user); + loadDisplayName(); // The socket connected before login with no session cookie; reconnect so // its handshake picks up the freshly-issued, now-authenticated cookie. socket.disconnect(); @@ -55,6 +64,7 @@ async function init() { const { user } = await meRes.json(); if (user) { showLoggedIn(user); + loadDisplayName(); } else { showLoggedOut(); } @@ -63,7 +73,8 @@ async function init() { sendBtn.addEventListener('click', () => { const text = messageInput.value.trim(); if (!text) return; - socket.emit('send_message', { token, text }); + const name = displayNameInput.value.trim(); + socket.emit('send_message', { token, text, name }); messageInput.value = ''; statusEl.textContent = 'Отправлено'; setTimeout(() => { statusEl.textContent = ''; }, 1500); diff --git a/server.js b/server.js index f571942..65b75f7 100644 --- a/server.js +++ b/server.js @@ -7,7 +7,13 @@ const { createServer } = require('http'); const { Server } = require('socket.io'); const { OAuth2Client } = require('google-auth-library'); const { execFile } = require('child_process'); -const { findStreamerByOverlayToken, findStreamerBySenderToken, getOrCreateStreamerByGoogle } = require('./db'); +const { + findStreamerByOverlayToken, + findStreamerBySenderToken, + getOrCreateStreamerByGoogle, + getViewerName, + setViewerName, +} = require('./db'); const PORT = process.env.PORT || 3000; const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; @@ -113,6 +119,16 @@ app.get('/api/dashboard', requireAuth, (req, res) => { }); }); +app.get('/api/viewer-name', requireAuth, (req, res) => { + const streamer = findStreamerBySenderToken(req.query.token); + if (!streamer) { + return res.status(404).json({ error: 'unknown link' }); + } + const user = req.session.user; + const name = getViewerName(streamer.id, user.id) || user.name; + res.json({ name }); +}); + app.get('/overlay/:token', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'overlay.html')); }); @@ -159,7 +175,7 @@ io.on('connection', (socket) => { console.log(`[socket] disconnected id=${socket.id} reason=${reason}`); }); - socket.on('send_message', ({ token: senderToken, text } = {}) => { + socket.on('send_message', ({ token: senderToken, text, name } = {}) => { const streamer = findStreamerBySenderToken(senderToken); if (!streamer) { socket.emit('send_error', 'Unknown link'); @@ -183,11 +199,14 @@ io.on('connection', (socket) => { } const message = text.trim().slice(0, 500); + const displayName = (typeof name === 'string' && name.trim().slice(0, 60)) || user.name; + setViewerName(streamer.id, user.id, displayName); + const room = `streamer-${streamer.id}`; - console.log(`[broadcast] streamer=${streamer.id} "${message}"`); + console.log(`[broadcast] streamer=${streamer.id} from="${displayName}" "${message}"`); io.to(room).emit('display_message', { text: message, - from: user.name, + from: displayName, at: Date.now(), });