395 lines
14 KiB
JavaScript
395 lines
14 KiB
JavaScript
"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
|