541
server/server.js
Archivo normal
541
server/server.js
Archivo normal
@@ -0,0 +1,541 @@
|
||||
const express = require('express');
|
||||
const http = require('http');
|
||||
const socketIo = require('socket.io');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = socketIo(server, {
|
||||
cors: {
|
||||
origin: "*",
|
||||
methods: ["GET", "POST"],
|
||||
allowedHeaders: ["Content-Type"],
|
||||
credentials: true
|
||||
},
|
||||
transports: ['websocket', 'polling']
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Store active users and rooms
|
||||
const users = new Map(); // socketId -> user info
|
||||
const rooms = new Map(); // roomId -> Set of socketIds
|
||||
const userRooms = new Map(); // socketId -> roomId
|
||||
|
||||
// Default room for all users
|
||||
const DEFAULT_ROOM = 'main-chat';
|
||||
|
||||
// Utility functions
|
||||
function getUserInfo(socketId) {
|
||||
return users.get(socketId);
|
||||
}
|
||||
|
||||
function addUserToRoom(socketId, roomId = DEFAULT_ROOM) {
|
||||
if (!rooms.has(roomId)) {
|
||||
rooms.set(roomId, new Set());
|
||||
}
|
||||
rooms.get(roomId).add(socketId);
|
||||
userRooms.set(socketId, roomId);
|
||||
}
|
||||
|
||||
function removeUserFromRoom(socketId) {
|
||||
const roomId = userRooms.get(socketId);
|
||||
if (roomId && rooms.has(roomId)) {
|
||||
rooms.get(roomId).delete(socketId);
|
||||
if (rooms.get(roomId).size === 0) {
|
||||
rooms.delete(roomId);
|
||||
}
|
||||
}
|
||||
userRooms.delete(socketId);
|
||||
}
|
||||
|
||||
function getRoomUsers(roomId) {
|
||||
const roomUsers = [];
|
||||
if (rooms.has(roomId)) {
|
||||
for (const socketId of rooms.get(roomId)) {
|
||||
const user = users.get(socketId);
|
||||
if (user) {
|
||||
roomUsers.push({
|
||||
socketId,
|
||||
nickname: user.nickname,
|
||||
joinedAt: user.joinedAt
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return roomUsers;
|
||||
}
|
||||
|
||||
function broadcastToRoom(roomId, event, data, exceptSocketId = null) {
|
||||
if (rooms.has(roomId)) {
|
||||
for (const socketId of rooms.get(roomId)) {
|
||||
if (socketId !== exceptSocketId) {
|
||||
io.to(socketId).emit(event, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Socket.IO connection handling
|
||||
io.on('connection', (socket) => {
|
||||
console.log(`[${new Date().toISOString()}] User connected: ${socket.id}`);
|
||||
|
||||
// Handle user joining
|
||||
socket.on('join-room', (data) => {
|
||||
try {
|
||||
const { nickname, roomId = DEFAULT_ROOM } = typeof data === 'string'
|
||||
? { nickname: data, roomId: DEFAULT_ROOM }
|
||||
: data;
|
||||
|
||||
if (!nickname || nickname.trim().length === 0) {
|
||||
socket.emit('error', { message: 'Nickname is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if nickname is already taken in the room
|
||||
const roomUsers = getRoomUsers(roomId);
|
||||
const nicknameExists = roomUsers.some(user =>
|
||||
user.nickname.toLowerCase() === nickname.toLowerCase()
|
||||
);
|
||||
|
||||
if (nicknameExists) {
|
||||
socket.emit('error', {
|
||||
message: 'Nickname already taken in this room',
|
||||
code: 'NICKNAME_TAKEN'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Store user information
|
||||
const userInfo = {
|
||||
nickname: nickname.trim(),
|
||||
roomId,
|
||||
joinedAt: new Date().toISOString(),
|
||||
isVideoEnabled: true,
|
||||
isAudioEnabled: true
|
||||
};
|
||||
|
||||
users.set(socket.id, userInfo);
|
||||
addUserToRoom(socket.id, roomId);
|
||||
|
||||
// Join socket room
|
||||
socket.join(roomId);
|
||||
|
||||
// Notify user they successfully joined
|
||||
socket.emit('joined-room', {
|
||||
roomId,
|
||||
nickname: userInfo.nickname,
|
||||
users: getRoomUsers(roomId)
|
||||
});
|
||||
|
||||
// Notify others in the room
|
||||
broadcastToRoom(roomId, 'user-joined', {
|
||||
socketId: socket.id,
|
||||
nickname: userInfo.nickname,
|
||||
joinedAt: userInfo.joinedAt
|
||||
}, socket.id);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] ${userInfo.nickname} joined room: ${roomId}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in join-room:', error);
|
||||
socket.emit('error', { message: 'Failed to join room' });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle WebRTC signaling - Offer
|
||||
socket.on('offer', (data) => {
|
||||
try {
|
||||
const user = getUserInfo(socket.id);
|
||||
if (!user) {
|
||||
socket.emit('error', { message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetSocketId, offer } = data;
|
||||
|
||||
if (targetSocketId) {
|
||||
// Send to specific user
|
||||
io.to(targetSocketId).emit('offer', {
|
||||
fromSocketId: socket.id,
|
||||
fromNickname: user.nickname,
|
||||
offer
|
||||
});
|
||||
} else {
|
||||
// Broadcast to room (for group calls)
|
||||
broadcastToRoom(user.roomId, 'offer', {
|
||||
fromSocketId: socket.id,
|
||||
fromNickname: user.nickname,
|
||||
offer
|
||||
}, socket.id);
|
||||
}
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Offer from ${user.nickname} to ${targetSocketId || 'room'}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in offer:', error);
|
||||
socket.emit('error', { message: 'Failed to send offer' });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle WebRTC signaling - Answer
|
||||
socket.on('answer', (data) => {
|
||||
try {
|
||||
const user = getUserInfo(socket.id);
|
||||
if (!user) {
|
||||
socket.emit('error', { message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetSocketId, answer } = data;
|
||||
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit('answer', {
|
||||
fromSocketId: socket.id,
|
||||
fromNickname: user.nickname,
|
||||
answer
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Answer from ${user.nickname} to ${targetSocketId}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in answer:', error);
|
||||
socket.emit('error', { message: 'Failed to send answer' });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle WebRTC signaling - ICE Candidate
|
||||
socket.on('ice-candidate', (data) => {
|
||||
try {
|
||||
const user = getUserInfo(socket.id);
|
||||
if (!user) {
|
||||
socket.emit('error', { message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { targetSocketId, candidate } = data;
|
||||
|
||||
if (targetSocketId) {
|
||||
io.to(targetSocketId).emit('ice-candidate', {
|
||||
fromSocketId: socket.id,
|
||||
fromNickname: user.nickname,
|
||||
candidate
|
||||
});
|
||||
} else {
|
||||
// Broadcast to room
|
||||
broadcastToRoom(user.roomId, 'ice-candidate', {
|
||||
fromSocketId: socket.id,
|
||||
fromNickname: user.nickname,
|
||||
candidate
|
||||
}, socket.id);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in ice-candidate:', error);
|
||||
socket.emit('error', { message: 'Failed to send ICE candidate' });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle media state changes
|
||||
socket.on('media-state', (data) => {
|
||||
try {
|
||||
const user = getUserInfo(socket.id);
|
||||
if (!user) {
|
||||
socket.emit('error', { message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { isVideoEnabled, isAudioEnabled } = data;
|
||||
|
||||
// Update user state
|
||||
user.isVideoEnabled = isVideoEnabled;
|
||||
user.isAudioEnabled = isAudioEnabled;
|
||||
|
||||
// Broadcast to room
|
||||
broadcastToRoom(user.roomId, 'user-media-state', {
|
||||
socketId: socket.id,
|
||||
nickname: user.nickname,
|
||||
isVideoEnabled,
|
||||
isAudioEnabled
|
||||
}, socket.id);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] ${user.nickname} media state - Video: ${isVideoEnabled}, Audio: ${isAudioEnabled}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in media-state:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle chat messages (backup for data channel failures)
|
||||
socket.on('chat-message', (data) => {
|
||||
try {
|
||||
const user = getUserInfo(socket.id);
|
||||
if (!user) {
|
||||
socket.emit('error', { message: 'Not authenticated' });
|
||||
return;
|
||||
}
|
||||
|
||||
const { message } = data;
|
||||
if (!message || message.trim().length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const messageData = {
|
||||
fromSocketId: socket.id,
|
||||
fromNickname: user.nickname,
|
||||
message: message.trim(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Broadcast to room
|
||||
broadcastToRoom(user.roomId, 'chat-message', messageData, socket.id);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] Message from ${user.nickname}: ${message}`);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error in chat-message:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle room list request
|
||||
socket.on('get-rooms', () => {
|
||||
try {
|
||||
const roomList = [];
|
||||
for (const [roomId, userSet] of rooms.entries()) {
|
||||
roomList.push({
|
||||
roomId,
|
||||
userCount: userSet.size,
|
||||
users: getRoomUsers(roomId).map(u => ({
|
||||
nickname: u.nickname,
|
||||
joinedAt: u.joinedAt
|
||||
}))
|
||||
});
|
||||
}
|
||||
socket.emit('rooms-list', roomList);
|
||||
} catch (error) {
|
||||
console.error('Error getting rooms:', error);
|
||||
socket.emit('error', { message: 'Failed to get rooms' });
|
||||
}
|
||||
});
|
||||
|
||||
// Handle user list request for current room
|
||||
socket.on('get-room-users', () => {
|
||||
try {
|
||||
const user = getUserInfo(socket.id);
|
||||
if (user) {
|
||||
const roomUsers = getRoomUsers(user.roomId);
|
||||
socket.emit('room-users', {
|
||||
roomId: user.roomId,
|
||||
users: roomUsers
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting room users:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle ping for connection monitoring
|
||||
socket.on('ping', () => {
|
||||
socket.emit('pong', { timestamp: Date.now() });
|
||||
});
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', (reason) => {
|
||||
try {
|
||||
const user = getUserInfo(socket.id);
|
||||
if (user) {
|
||||
// Notify others in the room
|
||||
broadcastToRoom(user.roomId, 'user-left', {
|
||||
socketId: socket.id,
|
||||
nickname: user.nickname,
|
||||
reason
|
||||
});
|
||||
|
||||
console.log(`[${new Date().toISOString()}] ${user.nickname} left room: ${user.roomId} (${reason})`);
|
||||
|
||||
// Clean up
|
||||
removeUserFromRoom(socket.id);
|
||||
users.delete(socket.id);
|
||||
} else {
|
||||
console.log(`[${new Date().toISOString()}] Unknown user disconnected: ${socket.id} (${reason})`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling disconnect:', error);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle errors
|
||||
socket.on('error', (error) => {
|
||||
console.error(`[${new Date().toISOString()}] Socket error for ${socket.id}:`, error);
|
||||
});
|
||||
});
|
||||
|
||||
// REST API endpoints
|
||||
app.get('/api/status', (req, res) => {
|
||||
res.json({
|
||||
status: 'running',
|
||||
timestamp: new Date().toISOString(),
|
||||
connectedUsers: users.size,
|
||||
activeRooms: rooms.size,
|
||||
uptime: process.uptime()
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/rooms', (req, res) => {
|
||||
const roomList = [];
|
||||
for (const [roomId, userSet] of rooms.entries()) {
|
||||
roomList.push({
|
||||
roomId,
|
||||
userCount: userSet.size,
|
||||
users: getRoomUsers(roomId).map(u => ({
|
||||
nickname: u.nickname,
|
||||
joinedAt: u.joinedAt
|
||||
}))
|
||||
});
|
||||
}
|
||||
res.json(roomList);
|
||||
});
|
||||
|
||||
app.get('/api/rooms/:roomId', (req, res) => {
|
||||
const { roomId } = req.params;
|
||||
if (rooms.has(roomId)) {
|
||||
res.json({
|
||||
roomId,
|
||||
userCount: rooms.get(roomId).size,
|
||||
users: getRoomUsers(roomId)
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({ error: 'Room not found' });
|
||||
}
|
||||
});
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.status(200).json({
|
||||
status: 'healthy',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
// Serve a simple test page
|
||||
app.get('/', (req, res) => {
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>ChatRTC Signaling Server</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 40px; background: #f5f5f5; }
|
||||
.container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
||||
h1 { color: #2196F3; }
|
||||
.status { background: #e8f5e8; padding: 15px; border-radius: 5px; margin: 20px 0; }
|
||||
.endpoint { background: #f8f9fa; padding: 10px; margin: 10px 0; border-left: 4px solid #2196F3; }
|
||||
code { background: #f0f0f0; padding: 2px 5px; border-radius: 3px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 ChatRTC Signaling Server</h1>
|
||||
<div class="status">
|
||||
<strong>✅ Server is running successfully!</strong><br>
|
||||
Connected Users: <span id="users">${users.size}</span><br>
|
||||
Active Rooms: <span id="rooms">${rooms.size}</span><br>
|
||||
Uptime: <span id="uptime">${Math.floor(process.uptime())}s</span>
|
||||
</div>
|
||||
|
||||
<h2>📡 API Endpoints</h2>
|
||||
<div class="endpoint">
|
||||
<strong>GET /api/status</strong> - Server status and statistics
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<strong>GET /api/rooms</strong> - List all active rooms
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<strong>GET /api/rooms/:roomId</strong> - Get specific room info
|
||||
</div>
|
||||
<div class="endpoint">
|
||||
<strong>GET /health</strong> - Health check endpoint
|
||||
</div>
|
||||
|
||||
<h2>🔧 Socket.IO Events</h2>
|
||||
<p>The server handles the following Socket.IO events for WebRTC signaling:</p>
|
||||
<ul>
|
||||
<li><code>join-room</code> - Join a chat room</li>
|
||||
<li><code>offer</code> - WebRTC offer signaling</li>
|
||||
<li><code>answer</code> - WebRTC answer signaling</li>
|
||||
<li><code>ice-candidate</code> - ICE candidate exchange</li>
|
||||
<li><code>media-state</code> - Video/audio state updates</li>
|
||||
<li><code>chat-message</code> - Text message fallback</li>
|
||||
</ul>
|
||||
|
||||
<h2>📱 Android App Connection</h2>
|
||||
<p>Update your Android app's WebRTCManager.java with this server URL:</p>
|
||||
<div class="endpoint">
|
||||
<code>private static final String SIGNALING_SERVER_URL = "http://YOUR_SERVER_IP:${process.env.PORT || 3000}";</code>
|
||||
</div>
|
||||
|
||||
<p><em>Server started at: ${new Date().toISOString()}</em></p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Auto-refresh stats every 5 seconds
|
||||
setInterval(async () => {
|
||||
try {
|
||||
const response = await fetch('/api/status');
|
||||
const data = await response.json();
|
||||
document.getElementById('users').textContent = data.connectedUsers;
|
||||
document.getElementById('rooms').textContent = data.activeRooms;
|
||||
document.getElementById('uptime').textContent = Math.floor(data.uptime) + 's';
|
||||
} catch (error) {
|
||||
console.error('Failed to update stats:', error);
|
||||
}
|
||||
}, 5000);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// Error handling
|
||||
app.use((err, req, res, next) => {
|
||||
console.error('Express error:', err);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
});
|
||||
|
||||
// Graceful shutdown
|
||||
process.on('SIGTERM', () => {
|
||||
console.log('SIGTERM received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
console.log('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
process.on('SIGINT', () => {
|
||||
console.log('SIGINT received, shutting down gracefully');
|
||||
server.close(() => {
|
||||
console.log('Server closed');
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
||||
|
||||
// Start server
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log('🚀 ChatRTC Signaling Server Started');
|
||||
console.log(`📡 Server running on http://${HOST}:${PORT}`);
|
||||
console.log(`🌐 Socket.IO enabled with CORS`);
|
||||
console.log(`📱 Ready for Android app connections`);
|
||||
console.log(`⏰ Started at: ${new Date().toISOString()}`);
|
||||
});
|
||||
|
||||
module.exports = { app, server, io };
|
||||
Referencia en una nueva incidencia
Block a user