require('dotenv').config(); const path = require('path'); const express = require('express'); const session = require('express-session'); const { createServer } = require('http'); const { Server } = require('socket.io'); const { OAuth2Client } = require('google-auth-library'); const { execFile } = require('child_process'); const PORT = process.env.PORT || 3000; const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; const SESSION_SECRET = process.env.SESSION_SECRET; const TTS_VOICE = process.env.TTS_VOICE || 'ru-RU-DmitryNeural'; if (!GOOGLE_CLIENT_ID) { console.error('Missing GOOGLE_CLIENT_ID in .env (see .env.example)'); process.exit(1); } if (!SESSION_SECRET) { console.error('Missing SESSION_SECRET in .env (see .env.example)'); process.exit(1); } const oauthClient = new OAuth2Client(GOOGLE_CLIENT_ID); const app = express(); const httpServer = createServer(app); const io = new Server(httpServer); const sessionMiddleware = session({ secret: SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { maxAge: 1000 * 60 * 60 * 24 * 7, // 7 days // set to true once served over HTTPS in production secure: false, sameSite: 'lax', }, }); app.use(sessionMiddleware); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // Share the express session with socket.io connections. const wrap = (middleware) => (socket, next) => middleware(socket.request, {}, next); io.use(wrap(sessionMiddleware)); app.get('/config', (req, res) => { res.json({ googleClientId: GOOGLE_CLIENT_ID }); }); app.get('/me', (req, res) => { res.json({ user: req.session.user || null }); }); app.post('/auth/google', async (req, res) => { const { credential } = req.body; if (!credential) { return res.status(400).json({ error: 'missing credential' }); } try { const ticket = await oauthClient.verifyIdToken({ idToken: credential, audience: GOOGLE_CLIENT_ID, }); const payload = ticket.getPayload(); req.session.user = { email: payload.email, name: payload.name, picture: payload.picture, }; console.log('[auth] logged in:', payload.email); res.json({ user: req.session.user }); } catch (err) { console.error('Google token verification failed:', err.message); res.status(401).json({ error: 'invalid token' }); } }); // Shells out to the `edge-tts` Python CLI (same engine as t2sTelegramBot uses) // instead of the edge-tts-node npm package, whose request-signing algorithm // currently gets rejected by Microsoft's servers with a 403. function synthesizeSpeech(text) { return new Promise((resolve, reject) => { execFile( 'edge-tts', ['-t', text, '-v', TTS_VOICE], { encoding: 'buffer', maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => { if (err) return reject(err); resolve(stdout); } ); }); } app.post('/auth/logout', (req, res) => { req.session.destroy(() => res.json({ ok: true })); }); io.on('connection', (socket) => { const connectedUser = socket.request.session.user; console.log(`[socket] connected id=${socket.id} user=${connectedUser ? connectedUser.email : '(none)'}`); socket.on('disconnect', (reason) => { console.log(`[socket] disconnected id=${socket.id} reason=${reason}`); }); socket.on('send_message', (text) => { // The session snapshot on socket.request is captured once, at connect time. // If the socket connected before login (page load opens it immediately), // it never sees a session updated later by the separate /auth/google request // unless we explicitly reload it from the store here. socket.request.session.reload((err) => { const user = !err && socket.request.session.user; console.log(`[send_message] id=${socket.id} user=${user ? user.email : '(none)'} text=${JSON.stringify(text)}`); if (!user) { socket.emit('send_error', 'Not authenticated'); return; } if (typeof text !== 'string' || !text.trim()) { return; } const message = text.trim().slice(0, 500); const clientCount = io.engine.clientsCount; console.log(`[broadcast] "${message}" -> ${clientCount} connected client(s)`); io.emit('display_message', { text: message, from: user.name, at: Date.now(), }); // Voice is generated after the fact so the text shows up immediately // instead of waiting on the round trip to the TTS service. synthesizeSpeech(message) .then((audioBuffer) => { io.emit('display_audio', { audio: audioBuffer.toString('base64'), mimeType: 'audio/mpeg', }); }) .catch((err) => { console.error('[tts] synthesis failed:', err); }); }); }); }); httpServer.listen(PORT, () => { console.log(`Listening on http://localhost:${PORT}`); });