project-together / server.js
arturitu's picture
Enhance room validation logic and update role assignment in WebSocket connection
8fe1e1f
Raw
History Blame Contribute Delete
6.24 kB
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]}`)
}
})
})