// Sri AI Live Class - signaling + auth server // Multi-room conference: every participant can publish cam/mic/screen; teacher = host const express = require('express'); const http = require('http'); const path = require('path'); const { WebSocketServer } = require('ws'); const PORT = process.env.PORT || 3010; // ---- Dummy accounts (replace with srigen auth on migration) ---- const USERS = { 'teacher@sriai.lk': { password: 'teach123', role: 'teacher', name: 'Ms. Nova' }, 'teacher2@sriai.lk': { password: 'teach123', role: 'teacher', name: 'Mr. Silva' }, 'student1@sriai.lk': { password: 'stud123', role: 'student', name: 'Aisha' }, 'student2@sriai.lk': { password: 'stud123', role: 'student', name: 'Kasun' }, 'student3@sriai.lk': { password: 'stud123', role: 'student', name: 'Tharushi' }, }; const app = express(); app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); app.post('/login', (req, res) => { const { email, password } = req.body || {}; const u = USERS[(email || '').toLowerCase().trim()]; if (!u || u.password !== password) { return res.status(401).json({ ok: false, error: 'Invalid email or password' }); } res.json({ ok: true, role: u.role, name: u.name, email }); }); const server = http.createServer(app); const wss = new WebSocketServer({ server }); let nextId = 1; const clients = new Map(); // id -> { ws, role, name, roomId } const rooms = new Map(); // roomId -> { id, name, teacherId, teacherName, chat: [] } function send(id, msg) { const c = clients.get(id); if (c && c.ws.readyState === 1) c.ws.send(JSON.stringify(msg)); } function roomMembers(roomId) { return [...clients.entries()].filter(([, c]) => c.roomId === roomId); } function roomBroadcast(roomId, msg, exceptId = null) { for (const [id] of roomMembers(roomId)) if (id !== exceptId) send(id, msg); } function classList() { return [...rooms.values()].map(r => ({ id: r.id, name: r.name, teacher: r.teacherName, students: roomMembers(r.id).filter(([, c]) => c.role === 'student').length, })); } function broadcastClassList() { const msg = { type: 'class-list', classes: classList() }; for (const [id, c] of clients) { if (c.role === 'student' && !c.roomId) send(id, msg); } } function endRoom(roomId, reason) { const room = rooms.get(roomId); if (!room) return; for (const [mid, mc] of roomMembers(roomId)) { if (mid === room.teacherId) continue; mc.roomId = null; send(mid, { type: 'class-ended', reason }); } rooms.delete(roomId); // chat history dies with the room broadcastClassList(); } function teacherRoom(teacherId) { return [...rooms.values()].find(r => r.teacherId === teacherId); } wss.on('connection', (ws) => { const id = nextId++; let joined = false; ws.on('message', (raw) => { let msg; try { msg = JSON.parse(raw); } catch { return; } if (msg.type === 'hello') { clients.set(id, { ws, role: msg.role, name: msg.name, roomId: null }); joined = true; send(id, { type: 'welcome', id }); if (msg.role === 'student') send(id, { type: 'class-list', classes: classList() }); return; } if (!joined) return; const c = clients.get(id); switch (msg.type) { // WebRTC signaling relay (perfect-negotiation payloads) case 'signal': { const target = clients.get(msg.to); if (target && target.roomId && target.roomId === c.roomId) { send(msg.to, { type: 'signal', from: id, data: msg.data }); } break; } // announce which of my stream ids is camera vs screen case 'meta': if (c.roomId) roomBroadcast(c.roomId, { type: 'meta', from: id, cam: msg.cam || null, screen: msg.screen || null }, id); break; // mic/cam/sharing status for UI icons case 'state': if (c.roomId) roomBroadcast(c.roomId, { type: 'state', from: id, mic: !!msg.mic, cam: !!msg.cam, sharing: !!msg.sharing }, id); break; case 'chat': { if (!c.roomId) break; const room = rooms.get(c.roomId); const text = String(msg.text || '').slice(0, 500).trim(); if (!room || !text) break; const entry = { from: id, name: c.name, role: c.role, text, ts: Date.now() }; room.chat.push(entry); if (room.chat.length > 300) room.chat.shift(); roomBroadcast(c.roomId, { type: 'chat', ...entry }); break; } // host (teacher) mutes a student's mic or turns off their camera case 'host-control': { const room = teacherRoom(id); const target = clients.get(msg.to); if (room && target && target.roomId === room.id && ['mute-mic', 'stop-cam'].includes(msg.action)) { send(msg.to, { type: 'host-control', action: msg.action }); } break; } // ---- teacher ---- case 'create-class': { if (c.role !== 'teacher') break; const existing = teacherRoom(id); if (existing) endRoom(existing.id, 'Teacher started a new class'); const roomId = 'r' + id + '-' + nextId++; const name = String(msg.name || '').slice(0, 60).trim() || `${c.name}'s Class`; rooms.set(roomId, { id: roomId, name, teacherId: id, teacherName: c.name, chat: [] }); c.roomId = roomId; send(id, { type: 'class-created', roomId, name }); broadcastClassList(); break; } case 'end-class': { const room = teacherRoom(id); if (room) { endRoom(room.id, 'Class ended by teacher'); c.roomId = null; } break; } // ---- student ---- case 'join-class': { if (c.role !== 'student') break; const room = rooms.get(msg.roomId); if (!room) { send(id, { type: 'class-list', classes: classList() }); break; } const participants = roomMembers(room.id).map(([mid, mc]) => ({ id: mid, name: mc.name, role: mc.role })); c.roomId = room.id; send(id, { type: 'joined', roomId: room.id, className: room.name, teacherName: room.teacherName, teacherId: room.teacherId, participants, chat: room.chat, }); roomBroadcast(room.id, { type: 'peer-joined', id, name: c.name, role: c.role }, id); broadcastClassList(); break; } case 'leave-class': { if (!c.roomId) break; const roomId = c.roomId; c.roomId = null; roomBroadcast(roomId, { type: 'peer-left', id }); send(id, { type: 'class-list', classes: classList() }); broadcastClassList(); break; } case 'list-classes': send(id, { type: 'class-list', classes: classList() }); break; } }); ws.on('close', () => { if (!joined) return; const c = clients.get(id); clients.delete(id); if (!c) return; if (c.role === 'teacher') { const room = teacherRoom(id); if (room) endRoom(room.id, 'Teacher disconnected'); } else if (c.roomId) { roomBroadcast(c.roomId, { type: 'peer-left', id }); broadcastClassList(); } }); }); server.listen(PORT, () => { console.log(`Sri AI Live Class running at http://localhost:${PORT}`); });