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', display_duration_seconds INTEGER NOT NULL DEFAULT 10, ai_name TEXT NOT NULL DEFAULT 'Альтушка Ирина', 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) ); CREATE TABLE IF NOT EXISTS viewer_visits ( streamer_id INTEGER NOT NULL, google_id TEXT NOT NULL, first_seen_at INTEGER NOT NULL, last_seen_at INTEGER NOT NULL, PRIMARY KEY (streamer_id, google_id) ); CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, streamer_id INTEGER NOT NULL, google_id TEXT NOT NULL, created_at INTEGER NOT NULL ); `); // Idempotent migration for databases created before display_duration_seconds existed. const existingColumns = db.prepare('PRAGMA table_info(streamers)').all().map((c) => c.name); if (!existingColumns.includes('display_duration_seconds')) { db.exec('ALTER TABLE streamers ADD COLUMN display_duration_seconds INTEGER NOT NULL DEFAULT 10'); } if (!existingColumns.includes('ai_name')) { db.exec("ALTER TABLE streamers ADD COLUMN ai_name TEXT NOT NULL DEFAULT 'Альтушка Ирина'"); } 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 }); } 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()); } function updateDisplayDuration(streamerId, seconds) { db.prepare('UPDATE streamers SET display_duration_seconds = ? WHERE id = ?').run(seconds, streamerId); } function updateAiName(streamerId, aiName) { db.prepare('UPDATE streamers SET ai_name = ? WHERE id = ?').run(aiName, streamerId); } function recordVisit(streamerId, googleId) { const now = Date.now(); db.prepare( `INSERT INTO viewer_visits (streamer_id, google_id, first_seen_at, last_seen_at) VALUES (?, ?, ?, ?) ON CONFLICT(streamer_id, google_id) DO UPDATE SET last_seen_at = excluded.last_seen_at` ).run(streamerId, googleId, now, now); } function recordMessage(streamerId, googleId) { db.prepare( 'INSERT INTO messages (streamer_id, google_id, created_at) VALUES (?, ?, ?)' ).run(streamerId, googleId, Date.now()); } function getStreamerStats(streamerId) { const visitors = db.prepare('SELECT COUNT(*) AS c FROM viewer_visits WHERE streamer_id = ?').get(streamerId).c; const messages = db.prepare('SELECT COUNT(*) AS c FROM messages WHERE streamer_id = ?').get(streamerId).c; const uniqueSenders = db .prepare('SELECT COUNT(DISTINCT google_id) AS c FROM messages WHERE streamer_id = ?') .get(streamerId).c; return { visitors, messages, uniqueSenders }; } module.exports = { findStreamerByOverlayToken, findStreamerBySenderToken, getOrCreateStreamerByGoogle, getViewerName, setViewerName, updateDisplayDuration, updateAiName, recordVisit, recordMessage, getStreamerStats, };