Let viewers set a per-streamer display name
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 29s
All checks were successful
CI/CD Pipeline / build-and-deploy (push) Successful in 29s
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 <noreply@anthropic.com>
This commit is contained in:
parent
ed2b051c91
commit
fab55cde1d
4 changed files with 76 additions and 5 deletions
25
db.js
25
db.js
|
|
@ -20,6 +20,14 @@ db.exec(`
|
||||||
tts_voice TEXT NOT NULL DEFAULT 'ru-RU-DmitryNeural',
|
tts_voice TEXT NOT NULL DEFAULT 'ru-RU-DmitryNeural',
|
||||||
created_at INTEGER NOT NULL
|
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() {
|
function generateToken() {
|
||||||
|
|
@ -52,8 +60,25 @@ function getOrCreateStreamerByGoogle({ googleId, email, name, picture }) {
|
||||||
return findStreamerByGoogleId(googleId) || createStreamer({ 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 = {
|
module.exports = {
|
||||||
findStreamerByOverlayToken,
|
findStreamerByOverlayToken,
|
||||||
findStreamerBySenderToken,
|
findStreamerBySenderToken,
|
||||||
getOrCreateStreamerByGoogle,
|
getOrCreateStreamerByGoogle,
|
||||||
|
getViewerName,
|
||||||
|
setViewerName,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,19 @@
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 14px;
|
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 {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
|
@ -77,6 +90,9 @@
|
||||||
<span id="logout">выйти</span>
|
<span id="logout">выйти</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<label for="display-name">Ваше имя (покажется стримеру вместо имени из Google)</label>
|
||||||
|
<input id="display-name" type="text" maxlength="60" placeholder="Имя..." />
|
||||||
|
|
||||||
<textarea id="message" placeholder="Текст для стрима..." maxlength="500"></textarea>
|
<textarea id="message" placeholder="Текст для стрима..." maxlength="500"></textarea>
|
||||||
<br />
|
<br />
|
||||||
<button id="send">Отправить</button>
|
<button id="send">Отправить</button>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ const mainSection = document.getElementById('hidden-until-login');
|
||||||
const userPic = document.getElementById('user-pic');
|
const userPic = document.getElementById('user-pic');
|
||||||
const userName = document.getElementById('user-name');
|
const userName = document.getElementById('user-name');
|
||||||
const logoutBtn = document.getElementById('logout');
|
const logoutBtn = document.getElementById('logout');
|
||||||
|
const displayNameInput = document.getElementById('display-name');
|
||||||
const messageInput = document.getElementById('message');
|
const messageInput = document.getElementById('message');
|
||||||
const sendBtn = document.getElementById('send');
|
const sendBtn = document.getElementById('send');
|
||||||
const statusEl = document.getElementById('status');
|
const statusEl = document.getElementById('status');
|
||||||
|
|
@ -22,6 +23,13 @@ function showLoggedOut() {
|
||||||
mainSection.style.display = 'none';
|
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) {
|
async function handleCredentialResponse(response) {
|
||||||
const res = await fetch('/auth/google', {
|
const res = await fetch('/auth/google', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -32,6 +40,7 @@ async function handleCredentialResponse(response) {
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const { user } = await res.json();
|
const { user } = await res.json();
|
||||||
showLoggedIn(user);
|
showLoggedIn(user);
|
||||||
|
loadDisplayName();
|
||||||
// The socket connected before login with no session cookie; reconnect so
|
// The socket connected before login with no session cookie; reconnect so
|
||||||
// its handshake picks up the freshly-issued, now-authenticated cookie.
|
// its handshake picks up the freshly-issued, now-authenticated cookie.
|
||||||
socket.disconnect();
|
socket.disconnect();
|
||||||
|
|
@ -55,6 +64,7 @@ async function init() {
|
||||||
const { user } = await meRes.json();
|
const { user } = await meRes.json();
|
||||||
if (user) {
|
if (user) {
|
||||||
showLoggedIn(user);
|
showLoggedIn(user);
|
||||||
|
loadDisplayName();
|
||||||
} else {
|
} else {
|
||||||
showLoggedOut();
|
showLoggedOut();
|
||||||
}
|
}
|
||||||
|
|
@ -63,7 +73,8 @@ async function init() {
|
||||||
sendBtn.addEventListener('click', () => {
|
sendBtn.addEventListener('click', () => {
|
||||||
const text = messageInput.value.trim();
|
const text = messageInput.value.trim();
|
||||||
if (!text) return;
|
if (!text) return;
|
||||||
socket.emit('send_message', { token, text });
|
const name = displayNameInput.value.trim();
|
||||||
|
socket.emit('send_message', { token, text, name });
|
||||||
messageInput.value = '';
|
messageInput.value = '';
|
||||||
statusEl.textContent = 'Отправлено';
|
statusEl.textContent = 'Отправлено';
|
||||||
setTimeout(() => { statusEl.textContent = ''; }, 1500);
|
setTimeout(() => { statusEl.textContent = ''; }, 1500);
|
||||||
|
|
|
||||||
27
server.js
27
server.js
|
|
@ -7,7 +7,13 @@ const { createServer } = require('http');
|
||||||
const { Server } = require('socket.io');
|
const { Server } = require('socket.io');
|
||||||
const { OAuth2Client } = require('google-auth-library');
|
const { OAuth2Client } = require('google-auth-library');
|
||||||
const { execFile } = require('child_process');
|
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 PORT = process.env.PORT || 3000;
|
||||||
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
|
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) => {
|
app.get('/overlay/:token', (req, res) => {
|
||||||
res.sendFile(path.join(__dirname, 'public', 'overlay.html'));
|
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}`);
|
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);
|
const streamer = findStreamerBySenderToken(senderToken);
|
||||||
if (!streamer) {
|
if (!streamer) {
|
||||||
socket.emit('send_error', 'Unknown link');
|
socket.emit('send_error', 'Unknown link');
|
||||||
|
|
@ -183,11 +199,14 @@ io.on('connection', (socket) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const message = text.trim().slice(0, 500);
|
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}`;
|
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', {
|
io.to(room).emit('display_message', {
|
||||||
text: message,
|
text: message,
|
||||||
from: user.name,
|
from: displayName,
|
||||||
at: Date.now(),
|
at: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue