const express = require('express') const http = require('http') const axios = require('axios') const WebSocket = require('ws') const app = express() const METERED_API_KEY = process.env.METERED_API_KEY const METERED_DOMAIN = process.env.METERED_DOMAIN let actualToken = [] // 🚀 GET para que Hugging Face detecte el server app.get('/', (req, res) => { console.log('GET a / recibido') res.status(200).json({ status: 'ok', message: '🚀 Project Together Server activo ✅', timestamp: new Date().toISOString(), }) }) // Endpoint de health check para Hugging Face app.get('/health', (req, res) => { console.log('Health check recibido') res.status(200).send('OK') }) // Endpoint específico para Hugging Face app.get('/healthz', (req, res) => { console.log('Healthz check recibido') res.status(200).send('OK') }) // 🔒 Bloque de CORS solo para peticiones POST app.use(function (req, res, next) { const allowedOrigins = [ 'https://unboring.net', 'https://0.0.0.0:8080', 'https://localhost:8080', 'https://192.168.0.42:8080', 'https://192.168.0.10:8080', 'https://arturitu-mixing-realities.hf.space', 'https://*.hf.space', ] const origin = req.headers.origin if ( origin && allowedOrigins.some( (o) => origin === o || (o.includes('*') && origin.endsWith(o.replace('*', ''))) ) ) { res.setHeader('Access-Control-Allow-Origin', origin) } res.header( 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept' ) next() }) // POST para devolver ICE servers app.post('/', function (req, res) { res.json(actualToken) }) // Crear servidor HTTP const server = http.createServer(app) // Crear WebSocket sobre el mismo server const wss = new WebSocket.Server({ server }) const maxRoomUsers = 2 function callback(ws, message) { ws.send(message) } function broadcastToRoommates(ws, message, room) { wss.clients.forEach((client) => { if (client.room && client.room.includes(room)) { if (ws !== client && client.readyState === WebSocket.OPEN) { client.send(message) } } }) } function broadcastToAllRoomMembers(ws, message, room) { wss.clients.forEach((client) => { if (client.room && client.room.includes(room)) { if (client.readyState === WebSocket.OPEN) { client.send(message) } } }) } function getRoomUsers(room) { let totalRoomUsers = 0 wss.clients.forEach((client) => { if (client.room && client.room.includes(room)) { totalRoomUsers += 1 } }) return totalRoomUsers } function setRole(ws, room, actualRoomUsers) { let setAsHost = true if (actualRoomUsers > 0) { setAsHost = false } wss.clients.forEach((client) => { if (client.room && client.room.includes(room) && ws !== client) { if (client.host) { setAsHost = false } else { setAsHost = true } } }) return setAsHost } function getIsValidRoom(room) { var roomNumber = parseInt(room, 10) var regex = /[0-9]/ var isValid = false if (regex.test(room) && roomNumber >= 100000000 && roomNumber <= 999999999) { isValid = true } return isValid } // Lógica signaling wss.on('connection', (ws) => { ws.room = [] ws.host = false ws.on('message', (message) => { let msgObj try { msgObj = JSON.parse(message) } catch (e) { console.error('Mensaje inválido:', e) return } if (msgObj.join) { if (!getIsValidRoom(msgObj.join)) { let fatalErrorMsg = { type: 'fatalError' } callback(ws, JSON.stringify(fatalErrorMsg)) return } let actualRoomUsers = getRoomUsers(msgObj.join) if (actualRoomUsers < maxRoomUsers) { ws.room.push(msgObj.join) ws.host = setRole(ws, msgObj.join, actualRoomUsers) let joinedMsg = { type: 'joined', host: ws.host, } callback(ws, JSON.stringify(joinedMsg)) } else { let fullMsg = { type: 'full' } callback(ws, JSON.stringify(fullMsg)) } } else if (msgObj.room) { broadcastToRoommates(ws, message, msgObj.room) } else if (msgObj.ping) { broadcastToRoommates(ws, message, msgObj.room) } else if (msgObj.start) { let startedMsg = { type: 'started', room: msgObj.start, } broadcastToAllRoomMembers(ws, JSON.stringify(startedMsg), msgObj.start) } else if (msgObj.close) { let closedMsg = { type: 'closed', room: msgObj.close, } broadcastToRoommates(ws, JSON.stringify(closedMsg), msgObj.close) } }) ws.on('close', () => { console.log('Cliente desconectado') }) }) // Obtener token ICE inicial y renovar cada hora async function renewToken() { try { if (!METERED_API_KEY || !METERED_DOMAIN) { console.warn( 'Missing METERED_API_KEY or METERED_DOMAIN environment variables' ) return } const response = await axios.get( `https://${METERED_DOMAIN}.metered.live/api/v1/turn/credentials?apiKey=${METERED_API_KEY}` ) actualToken = response.data console.log('Token ICE actualizado correctamente.') } catch (err) { console.error('Error al renovar ICE token:', err.message) // Continue with empty token if renewal fails actualToken = [] } } renewToken() setInterval(renewToken, 60 * 60 * 1000) // Iniciar server // En Hugging Face, usa puerto 7860 explícitamente const PORT = process.env.SPACE_ID ? 7860 : process.env.PORT || 3000 const HOST = '0.0.0.0' // Escuchar en todas las interfaces de red (importante para Docker) server.listen(PORT, HOST, () => { console.log(`Servidor HTTP y WebSocket escuchando en ${HOST}:${PORT}`) console.log(`Variable de entorno PORT: ${process.env.PORT || 'No definida'}`) console.log(`En entorno Hugging Face: ${process.env.SPACE_ID ? 'Sí' : 'No'}`) // Log todas las variables de entorno para diagnóstico console.log('Variables de entorno disponibles:') Object.keys(process.env).forEach((key) => { if ( !key.includes('KEY') && !key.includes('SECRET') && !key.includes('TOKEN') ) { console.log(`${key}: ${process.env[key]}`) } }) })