"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const express_1 = __importDefault(require("express")); const cors_1 = __importDefault(require("cors")); const http_1 = require("http"); const ws_1 = require("ws"); const uuid_1 = require("uuid"); const app = (0, express_1.default)(); const PORT = process.env.PORT || 3001; app.use((0, cors_1.default)()); app.use(express_1.default.json()); const servers = new Map(); const joinRequests = new Map(); const connectedUsers = new Map(); // Persistence const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const DATA_DIR = path_1.default.join(process.cwd(), 'data'); const SERVERS_FILE = path_1.default.join(DATA_DIR, 'servers.json'); const USERS_FILE = path_1.default.join(DATA_DIR, 'users.json'); function ensureDataDir() { if (!fs_1.default.existsSync(DATA_DIR)) fs_1.default.mkdirSync(DATA_DIR, { recursive: true }); } function saveServers() { ensureDataDir(); fs_1.default.writeFileSync(SERVERS_FILE, JSON.stringify(Array.from(servers.values()), null, 2)); } function loadServers() { ensureDataDir(); if (fs_1.default.existsSync(SERVERS_FILE)) { const raw = fs_1.default.readFileSync(SERVERS_FILE, 'utf-8'); const list = JSON.parse(raw); list.forEach(s => servers.set(s.id, s)); } } // REST API Routes // Health check endpoint app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: Date.now(), serverCount: servers.size, connectedUsers: connectedUsers.size, }); }); let authUsers = []; function saveUsers() { ensureDataDir(); fs_1.default.writeFileSync(USERS_FILE, JSON.stringify(authUsers, null, 2)); } function loadUsers() { ensureDataDir(); if (fs_1.default.existsSync(USERS_FILE)) { authUsers = JSON.parse(fs_1.default.readFileSync(USERS_FILE, 'utf-8')); } } const crypto_1 = __importDefault(require("crypto")); function hashPassword(pw) { return crypto_1.default.createHash('sha256').update(pw).digest('hex'); } app.post('/api/users/register', (req, res) => { const { username, password, displayName } = req.body; if (!username || !password) return res.status(400).json({ error: 'Missing username/password' }); if (authUsers.find(u => u.username === username)) return res.status(409).json({ error: 'Username taken' }); const user = { id: (0, uuid_1.v4)(), username, passwordHash: hashPassword(password), displayName: displayName || username, createdAt: Date.now() }; authUsers.push(user); saveUsers(); res.status(201).json({ id: user.id, username: user.username, displayName: user.displayName }); }); app.post('/api/users/login', (req, res) => { const { username, password } = req.body; const user = authUsers.find(u => u.username === username && u.passwordHash === hashPassword(password)); if (!user) return res.status(401).json({ error: 'Invalid credentials' }); res.json({ id: user.id, username: user.username, displayName: user.displayName }); }); // Search servers app.get('/api/servers', (req, res) => { const { q, tags, limit = 20, offset = 0 } = req.query; let results = Array.from(servers.values()) .filter(s => !s.isPrivate) .filter(s => { if (q) { const query = String(q).toLowerCase(); return s.name.toLowerCase().includes(query) || s.description?.toLowerCase().includes(query); } return true; }) .filter(s => { if (tags) { const tagList = String(tags).split(','); return tagList.some(t => s.tags.includes(t)); } return true; }); // Keep servers visible permanently until deleted; do not filter by lastSeen const total = results.length; results = results.slice(Number(offset), Number(offset) + Number(limit)); res.json({ servers: results, total, limit: Number(limit), offset: Number(offset) }); }); // Register a server app.post('/api/servers', (req, res) => { const { id: clientId, name, description, ownerId, ownerPublicKey, isPrivate, maxUsers, tags } = req.body; if (!name || !ownerId || !ownerPublicKey) { return res.status(400).json({ error: 'Missing required fields' }); } // Use client-provided ID if available, otherwise generate one const id = clientId || (0, uuid_1.v4)(); const server = { id, name, description, ownerId, ownerPublicKey, isPrivate: isPrivate ?? false, maxUsers: maxUsers ?? 0, currentUsers: 0, tags: tags ?? [], createdAt: Date.now(), lastSeen: Date.now(), }; servers.set(id, server); saveServers(); res.status(201).json(server); }); // Update server app.put('/api/servers/:id', (req, res) => { const { id } = req.params; const { ownerId, ...updates } = req.body; const server = servers.get(id); if (!server) { return res.status(404).json({ error: 'Server not found' }); } if (server.ownerId !== ownerId) { return res.status(403).json({ error: 'Not authorized' }); } const updated = { ...server, ...updates, lastSeen: Date.now() }; servers.set(id, updated); saveServers(); res.json(updated); }); // Heartbeat - keep server alive app.post('/api/servers/:id/heartbeat', (req, res) => { const { id } = req.params; const { currentUsers } = req.body; const server = servers.get(id); if (!server) { return res.status(404).json({ error: 'Server not found' }); } server.lastSeen = Date.now(); if (typeof currentUsers === 'number') { server.currentUsers = currentUsers; } servers.set(id, server); saveServers(); res.json({ ok: true }); }); // Remove server app.delete('/api/servers/:id', (req, res) => { const { id } = req.params; const { ownerId } = req.body; const server = servers.get(id); if (!server) { return res.status(404).json({ error: 'Server not found' }); } if (server.ownerId !== ownerId) { return res.status(403).json({ error: 'Not authorized' }); } servers.delete(id); saveServers(); res.json({ ok: true }); }); // Request to join a server app.post('/api/servers/:id/join', (req, res) => { const { id: serverId } = req.params; const { userId, userPublicKey, displayName } = req.body; const server = servers.get(serverId); if (!server) { return res.status(404).json({ error: 'Server not found' }); } const requestId = (0, uuid_1.v4)(); const request = { id: requestId, serverId, userId, userPublicKey, displayName, status: server.isPrivate ? 'pending' : 'approved', createdAt: Date.now(), }; joinRequests.set(requestId, request); // Notify server owner via WebSocket if (server.isPrivate) { notifyServerOwner(server.ownerId, { type: 'join_request', request, }); } res.status(201).json(request); }); // Get join requests for a server app.get('/api/servers/:id/requests', (req, res) => { const { id: serverId } = req.params; const { ownerId } = req.query; const server = servers.get(serverId); if (!server) { return res.status(404).json({ error: 'Server not found' }); } if (server.ownerId !== ownerId) { return res.status(403).json({ error: 'Not authorized' }); } const requests = Array.from(joinRequests.values()) .filter(r => r.serverId === serverId && r.status === 'pending'); res.json({ requests }); }); // Approve/reject join request app.put('/api/requests/:id', (req, res) => { const { id } = req.params; const { ownerId, status } = req.body; const request = joinRequests.get(id); if (!request) { return res.status(404).json({ error: 'Request not found' }); } const server = servers.get(request.serverId); if (!server || server.ownerId !== ownerId) { return res.status(403).json({ error: 'Not authorized' }); } request.status = status; joinRequests.set(id, request); // Notify the requester notifyUser(request.userId, { type: 'request_update', request, }); res.json(request); }); // WebSocket Server for real-time signaling const server = (0, http_1.createServer)(app); const wss = new ws_1.WebSocketServer({ server }); wss.on('connection', (ws) => { const oderId = (0, uuid_1.v4)(); connectedUsers.set(oderId, { oderId, ws }); ws.on('message', (data) => { try { const message = JSON.parse(data.toString()); handleWebSocketMessage(oderId, message); } catch (err) { console.error('Invalid WebSocket message:', err); } }); ws.on('close', () => { const user = connectedUsers.get(oderId); if (user?.serverId) { // Notify others in the room broadcastToServer(user.serverId, { type: 'user_left', oderId, displayName: user.displayName, }, oderId); } connectedUsers.delete(oderId); }); // Send connection acknowledgment ws.send(JSON.stringify({ type: 'connected', oderId })); }); function handleWebSocketMessage(connectionId, message) { const user = connectedUsers.get(connectionId); if (!user) return; switch (message.type) { case 'identify': // User identifies themselves with their permanent ID // Store their actual oderId for peer-to-peer routing user.oderId = message.oderId || connectionId; user.displayName = message.displayName || 'Anonymous'; connectedUsers.set(connectionId, user); console.log(`User identified: ${user.displayName} (${user.oderId})`); break; case 'join_server': user.serverId = message.serverId; connectedUsers.set(connectionId, user); console.log(`User ${user.displayName} (${user.oderId}) joined server ${message.serverId}`); // Get list of current users in server (exclude this user by oderId) const usersInServer = Array.from(connectedUsers.values()) .filter(u => u.serverId === message.serverId && u.oderId !== user.oderId) .map(u => ({ oderId: u.oderId, displayName: u.displayName })); console.log(`Sending server_users to ${user.displayName}:`, usersInServer); user.ws.send(JSON.stringify({ type: 'server_users', users: usersInServer, })); // Notify others (exclude by oderId, not connectionId) broadcastToServer(message.serverId, { type: 'user_joined', oderId: user.oderId, displayName: user.displayName, }, user.oderId); break; case 'leave_server': const oldServerId = user.serverId; user.serverId = undefined; connectedUsers.set(connectionId, user); if (oldServerId) { broadcastToServer(oldServerId, { type: 'user_left', oderId: user.oderId, displayName: user.displayName, }, user.oderId); } break; case 'offer': case 'answer': case 'ice_candidate': // Forward signaling messages to specific peer console.log(`Forwarding ${message.type} from ${user.oderId} to ${message.targetUserId}`); const targetUser = findUserByUserId(message.targetUserId); if (targetUser) { targetUser.ws.send(JSON.stringify({ ...message, fromUserId: user.oderId, })); console.log(`Successfully forwarded ${message.type} to ${message.targetUserId}`); } else { console.log(`Target user ${message.targetUserId} not found. Connected users:`, Array.from(connectedUsers.values()).map(u => ({ oderId: u.oderId, displayName: u.displayName }))); } break; case 'chat_message': // Broadcast chat message to all users in the server if (user.serverId) { broadcastToServer(user.serverId, { type: 'chat_message', message: message.message, senderId: user.oderId, senderName: user.displayName, timestamp: Date.now(), }); } break; case 'typing': // Broadcast typing indicator if (user.serverId) { broadcastToServer(user.serverId, { type: 'user_typing', oderId: user.oderId, displayName: user.displayName, }, user.oderId); } break; default: console.log('Unknown message type:', message.type); } } function broadcastToServer(serverId, message, excludeOderId) { console.log(`Broadcasting to server ${serverId}, excluding ${excludeOderId}:`, message.type); connectedUsers.forEach((user) => { if (user.serverId === serverId && user.oderId !== excludeOderId) { console.log(` -> Sending to ${user.displayName} (${user.oderId})`); user.ws.send(JSON.stringify(message)); } }); } function notifyServerOwner(ownerId, message) { const owner = findUserByUserId(ownerId); if (owner) { owner.ws.send(JSON.stringify(message)); } } function notifyUser(oderId, message) { const user = findUserByUserId(oderId); if (user) { user.ws.send(JSON.stringify(message)); } } function findUserByUserId(oderId) { return Array.from(connectedUsers.values()).find(u => u.oderId === oderId); } // Cleanup old data periodically // Simple cleanup only for stale join requests (keep servers permanent) setInterval(() => { const now = Date.now(); joinRequests.forEach((request, id) => { if (now - request.createdAt > 24 * 60 * 60 * 1000) { joinRequests.delete(id); } }); }, 60 * 1000); server.listen(PORT, () => { console.log(`🚀 MetoYou signaling server running on port ${PORT}`); console.log(` REST API: http://localhost:${PORT}/api`); console.log(` WebSocket: ws://localhost:${PORT}`); // Load servers on startup loadServers(); }); //# sourceMappingURL=index.js.map