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(),
});