Spaces:
Sleeping
Sleeping
| // --- START: Additions/Modifications in app.js --- | |
| const express = require('express'); | |
| const http = require('http'); | |
| const socketIo = require('socket.io'); | |
| const path = require('path'); | |
| const fs = require('fs').promises; // Use promise-based fs | |
| // const bcrypt = require('bcrypt'); // Still commented out, needed for secure apps | |
| // Create express app and server | |
| const app = express(); | |
| const server = http.createServer(app); | |
| const io = socketIo(server); | |
| // --- Configuration --- | |
| const PUBLIC_DIR = path.join(__dirname, 'public'); | |
| const DATA_DIR = path.join(__dirname, 'data'); // Directory for data files | |
| const USERS_FILE_PATH = path.join(DATA_DIR, 'users.json'); // Path to users file | |
| // Serve static files from the 'public' directory | |
| app.use(express.static(PUBLIC_DIR)); | |
| // --- State --- | |
| let registeredUsers = {}; // Will be loaded from file | |
| // onlineUsers structure: { socketId: { username, isTyping, inCallWith: null | string (socketId) } } | |
| const onlineUsers = {}; | |
| const usernameToSocketId = {}; // Helper map: { username: socketId } | |
| // --- File Helper Functions (loadUsers, saveUsers - remain the same) --- | |
| // ... (Keep existing loadUsers and saveUsers functions) ... | |
| async function loadUsers() { | |
| try { | |
| await fs.mkdir(DATA_DIR, { recursive: true }); | |
| const data = await fs.readFile(USERS_FILE_PATH, 'utf8'); | |
| return JSON.parse(data); | |
| } catch (error) { | |
| if (error.code === 'ENOENT') { | |
| console.log('users.json not found, starting with empty user list.'); | |
| return {}; | |
| } else if (error instanceof SyntaxError) { | |
| console.error('Error parsing users.json:', error); | |
| return {}; | |
| } else { | |
| console.error('Error loading users.json:', error); | |
| return {}; | |
| } | |
| } | |
| } | |
| async function saveUsers(usersData) { | |
| try { | |
| await fs.mkdir(DATA_DIR, { recursive: true }); | |
| const data = JSON.stringify(usersData, null, 2); | |
| await fs.writeFile(USERS_FILE_PATH, data, 'utf8'); | |
| console.log('User data saved to users.json'); | |
| } catch (error) { | |
| console.error('Error saving users.json:', error); | |
| } | |
| } | |
| // --- Socket.IO Logic --- | |
| io.on('connection', (socket) => { | |
| console.log('A user connected:', socket.id); | |
| // Note: currentUsername is only valid *after* successful login within this closure. | |
| // Use onlineUsers[socket.id]?.username safely elsewhere. | |
| // --- Handle Login/Registration Attempt (Modified to track usernameToSocketId) --- | |
| socket.on('attempt_login', async (credentials) => { | |
| if (!credentials || !credentials.username || !credentials.password) { | |
| socket.emit('login_fail', 'Username and password are required.'); | |
| return; | |
| } | |
| const { username, password } = credentials; | |
| const isAlreadyOnline = usernameToSocketId[username]; // Check map directly | |
| if (isAlreadyOnline) { | |
| socket.emit('login_fail', `User "${username}" is already logged in.`); | |
| return; | |
| } | |
| if (registeredUsers.hasOwnProperty(username)) { | |
| if (registeredUsers[username] === password) { // INSECURE | |
| console.log(`Login success: ${username} (${socket.id})`); | |
| loginUser(socket, username); | |
| } else { | |
| console.log(`Login fail: Invalid password for ${username} (${socket.id})`); | |
| socket.emit('login_fail', 'Invalid username or password.'); | |
| } | |
| } else { | |
| console.log(`Registering new user: ${username} (${socket.id})`); | |
| registeredUsers[username] = password; // INSECURE | |
| await saveUsers(registeredUsers); | |
| loginUser(socket, username); | |
| } | |
| }); | |
| // Modified loginUser to update maps and state | |
| function loginUser(socketInstance, username) { | |
| // Store user info | |
| onlineUsers[socketInstance.id] = { | |
| username: username, | |
| isTyping: false, | |
| inCallWith: null // Initially not in a call | |
| }; | |
| usernameToSocketId[username] = socketInstance.id; // Add to lookup map | |
| socketInstance.emit('login_success', username); | |
| io.emit('user_join', username); | |
| io.emit('user_list', getUsersPublicInfo()); | |
| } | |
| // --- Message Handling (remains the same) --- | |
| socket.on('message', (data) => { | |
| const senderInfo = onlineUsers[socket.id]; | |
| if (!senderInfo || !data || !data.text) return; | |
| if (senderInfo.isTyping) { | |
| senderInfo.isTyping = false; | |
| // Don't need to exclude sender ID here as getUsersTyping doesn't include self by default logic | |
| socket.broadcast.emit('user_typing_status', getUsersTyping()); | |
| } | |
| io.emit('message', { | |
| username: senderInfo.username, | |
| text: data.text, | |
| timestamp: new Date().toISOString() // Use ISO string for consistency | |
| }); | |
| }); | |
| // --- Typing Indicator Handling (remains the same) --- | |
| socket.on('typing', () => { | |
| const userInfo = onlineUsers[socket.id]; | |
| if (userInfo && !userInfo.isTyping) { | |
| userInfo.isTyping = true; | |
| socket.broadcast.emit('user_typing_status', getUsersTyping(socket.id)); // Exclude self | |
| } | |
| }); | |
| socket.on('stop_typing', () => { | |
| const userInfo = onlineUsers[socket.id]; | |
| if (userInfo && userInfo.isTyping) { | |
| userInfo.isTyping = false; | |
| socket.broadcast.emit('user_typing_status', getUsersTyping(socket.id)); // Exclude self | |
| } | |
| }); | |
| // --- Disconnect Handling (Modified to handle calls and maps) --- | |
| socket.on('disconnect', () => { | |
| const userInfo = onlineUsers[socket.id]; | |
| if (userInfo) { | |
| const username = userInfo.username; | |
| const userInCallWithSocketId = userInfo.inCallWith; | |
| console.log(`${username} (${socket.id}) disconnected`); | |
| // --- Hang up any active call --- | |
| if (userInCallWithSocketId && onlineUsers[userInCallWithSocketId]) { | |
| console.log(`Disconnect: Hanging up call between ${username} and ${onlineUsers[userInCallWithSocketId].username}`); | |
| io.to(userInCallWithSocketId).emit('call-ended'); // Notify the other user | |
| onlineUsers[userInCallWithSocketId].inCallWith = null; // Clear call state for the other user | |
| } | |
| // --- End hang up --- | |
| delete onlineUsers[socket.id]; // Remove from online list | |
| delete usernameToSocketId[username]; // Remove from lookup map | |
| socket.broadcast.emit('user_leave', username); | |
| io.emit('user_list', getUsersPublicInfo()); // Send updated list | |
| // Update typing status if the disconnected user was typing | |
| if (userInfo.isTyping) { | |
| socket.broadcast.emit('user_typing_status', getUsersTyping()); | |
| } | |
| } else { | |
| console.log('User disconnected before login:', socket.id); | |
| } | |
| }); | |
| // --- START: WebRTC Signaling Handlers --- | |
| /** | |
| * Initiates a call request from caller to target. | |
| * data: { targetUsername: string } | |
| */ | |
| socket.on('request-call', (data) => { | |
| const callerInfo = onlineUsers[socket.id]; | |
| if (!callerInfo) return; // Should not happen if logged in | |
| const targetUsername = data.targetUsername; | |
| const targetSocketId = usernameToSocketId[targetUsername]; | |
| const targetInfo = targetSocketId ? onlineUsers[targetSocketId] : null; | |
| console.log(`Call request from ${callerInfo.username} (${socket.id}) to ${targetUsername}`); | |
| if (!targetInfo) { | |
| console.log(`Call target ${targetUsername} not found or offline.`); | |
| socket.emit('call-denied', { reason: 'User is offline.' }); | |
| return; | |
| } | |
| if (targetInfo.inCallWith) { | |
| console.log(`Call target ${targetUsername} is already in a call.`); | |
| socket.emit('call-denied', { reason: `${targetUsername} is busy.` }); | |
| return; | |
| } | |
| if (callerInfo.inCallWith) { | |
| console.log(`Caller ${callerInfo.username} is already in a call.`); | |
| socket.emit('call-denied', { reason: `You are already in a call.` }); | |
| return; | |
| } | |
| // Send call offer prompt to the target user | |
| console.log(`Sending incoming call notification to ${targetUsername} (${targetSocketId})`); | |
| io.to(targetSocketId).emit('incoming-call', { | |
| callerUsername: callerInfo.username | |
| }); | |
| }); | |
| /** | |
| * Target user accepts the call. | |
| * data: { callerUsername: string } | |
| */ | |
| socket.on('accept-call', (data) => { | |
| const acceptorInfo = onlineUsers[socket.id]; // The user accepting the call | |
| if (!acceptorInfo) return; | |
| const callerUsername = data.callerUsername; | |
| const callerSocketId = usernameToSocketId[callerUsername]; | |
| const callerInfo = callerSocketId ? onlineUsers[callerSocketId] : null; | |
| console.log(`${acceptorInfo.username} accepted call from ${callerUsername}`); | |
| if (!callerInfo) { | |
| console.log(`Original caller ${callerUsername} not found.`); | |
| socket.emit('call-denied', { reason: 'Caller went offline.' }); | |
| return; | |
| } | |
| if (callerInfo.inCallWith || acceptorInfo.inCallWith) { | |
| console.log(`Call conflict: ${callerUsername} or ${acceptorInfo.username} already in call.`); | |
| // Notify both potential parties that call cannot proceed | |
| socket.emit('call-denied', { reason: 'Call conflict or one party is busy.' }); | |
| io.to(callerSocketId).emit('call-denied', { reason: 'Call conflict or the other party became busy.'}); | |
| return; | |
| } | |
| // Mark both users as in call with each other | |
| acceptorInfo.inCallWith = callerSocketId; | |
| callerInfo.inCallWith = socket.id; // socket.id is the acceptor's socket id | |
| // Notify the original caller to start the WebRTC offer process | |
| io.to(callerSocketId).emit('call-accepted', { | |
| acceptorUsername: acceptorInfo.username | |
| }); | |
| // Notify the acceptor to wait for the offer | |
| socket.emit('prepare-for-offer', { | |
| callerUsername: callerInfo.username | |
| }); | |
| console.log(`Call established between ${callerUsername} (${callerSocketId}) and ${acceptorInfo.username} (${socket.id})`); | |
| }); | |
| /** | |
| * Target user rejects the call. | |
| * data: { callerUsername: string } | |
| */ | |
| socket.on('reject-call', (data) => { | |
| const rejectorInfo = onlineUsers[socket.id]; | |
| if (!rejectorInfo) return; | |
| const callerUsername = data.callerUsername; | |
| const callerSocketId = usernameToSocketId[callerUsername]; | |
| console.log(`${rejectorInfo.username} rejected call from ${callerUsername}`); | |
| if (callerSocketId) { | |
| io.to(callerSocketId).emit('call-rejected', { | |
| rejectorUsername: rejectorInfo.username | |
| }); | |
| } | |
| }); | |
| /** | |
| * Relays the WebRTC Offer from caller to acceptor. | |
| * data: { targetUsername: string, offer: RTCSessionDescriptionInit } | |
| */ | |
| socket.on('webrtc-offer', (data) => { | |
| const callerInfo = onlineUsers[socket.id]; | |
| if (!callerInfo) return; | |
| const targetUsername = data.targetUsername; | |
| const targetSocketId = usernameToSocketId[targetUsername]; | |
| if (targetSocketId && onlineUsers[targetSocketId]?.inCallWith === socket.id) { | |
| console.log(`Relaying WebRTC offer from ${callerInfo.username} to ${targetUsername}`); | |
| io.to(targetSocketId).emit('webrtc-offer', { | |
| offer: data.offer, | |
| callerUsername: callerInfo.username // Send caller username with the offer | |
| }); | |
| } else { | |
| console.warn(`WebRTC Offer: Target ${targetUsername} not found or not in call with ${callerInfo.username}`); | |
| } | |
| }); | |
| /** | |
| * Relays the WebRTC Answer from acceptor to caller. | |
| * data: { targetUsername: string (original caller), answer: RTCSessionDescriptionInit } | |
| */ | |
| socket.on('webrtc-answer', (data) => { | |
| const acceptorInfo = onlineUsers[socket.id]; | |
| if (!acceptorInfo) return; | |
| const targetUsername = data.targetUsername; // This is the original caller | |
| const targetSocketId = usernameToSocketId[targetUsername]; | |
| if (targetSocketId && onlineUsers[targetSocketId]?.inCallWith === socket.id) { | |
| console.log(`Relaying WebRTC answer from ${acceptorInfo.username} to ${targetUsername}`); | |
| io.to(targetSocketId).emit('webrtc-answer', { | |
| answer: data.answer, | |
| acceptorUsername: acceptorInfo.username // Send acceptor username with the answer | |
| }); | |
| } else { | |
| console.warn(`WebRTC Answer: Target ${targetUsername} not found or not in call with ${acceptorInfo.username}`); | |
| } | |
| }); | |
| /** | |
| * Relays ICE Candidates between peers. | |
| * data: { targetUsername: string, candidate: RTCIceCandidateInit } | |
| */ | |
| socket.on('webrtc-ice-candidate', (data) => { | |
| const senderInfo = onlineUsers[socket.id]; | |
| if (!senderInfo) return; | |
| const targetUsername = data.targetUsername; | |
| const targetSocketId = usernameToSocketId[targetUsername]; | |
| // Check if the target is actually the one sender is in call with | |
| if (targetSocketId && senderInfo.inCallWith === targetSocketId) { | |
| // console.log(`Relaying ICE candidate from ${senderInfo.username} to ${targetUsername}`); // Too noisy | |
| io.to(targetSocketId).emit('webrtc-ice-candidate', { | |
| candidate: data.candidate | |
| }); | |
| } else { | |
| // console.warn(`ICE Candidate: Target ${targetUsername} not found or sender ${senderInfo.username} not in call with them.`); | |
| } | |
| }); | |
| /** | |
| * Handles user explicitly hanging up the call. | |
| * data: {} (might include target if needed, but server knows from inCallWith) | |
| */ | |
| socket.on('hangup-call', () => { | |
| const userInfo = onlineUsers[socket.id]; | |
| if (!userInfo || !userInfo.inCallWith) { | |
| // Not in a call, nothing to hang up | |
| return; | |
| } | |
| const targetSocketId = userInfo.inCallWith; | |
| const targetInfo = onlineUsers[targetSocketId]; | |
| console.log(`${userInfo.username} is hanging up call with ${targetInfo?.username || 'unknown'}`); | |
| // Notify the other user | |
| if (targetInfo) { | |
| io.to(targetSocketId).emit('call-ended'); | |
| targetInfo.inCallWith = null; // Clear target's call state | |
| } | |
| // Clear caller's call state | |
| userInfo.inCallWith = null; | |
| // Optional: send confirmation back to the hanger-upper | |
| socket.emit('call-ended'); | |
| }); | |
| // --- END: WebRTC Signaling Handlers --- | |
| }); | |
| // --- Helper Functions (Modified getUsersTyping) --- | |
| function getUsersPublicInfo() { | |
| // Return username and potentially busy status? For now just usernames. | |
| return Object.values(onlineUsers).map(u => u.username); | |
| } | |
| // Modified to exclude the caller from the list sent back to them | |
| function getUsersTyping(excludeSocketId = null) { | |
| return Object.entries(onlineUsers) | |
| .filter(([id, user]) => id !== excludeSocketId && user.isTyping) | |
| .map(([id, user]) => user.username); | |
| } | |
| // --- Find Local IP (remains the same) --- | |
| function getLocalIp() { | |
| // ... (Keep existing getLocalIp function) ... | |
| const { networkInterfaces } = require('os'); | |
| const nets = networkInterfaces(); | |
| for (const name of Object.keys(nets)) { | |
| for (const net of nets[name]) { | |
| if (net.family === 'IPv4' && !net.internal) { | |
| return net.address; | |
| } | |
| } | |
| } | |
| return '127.0.0.1'; | |
| } | |
| const localIp = getLocalIp(); | |
| // --- Start Server (IIFE remains the same) --- | |
| (async () => { | |
| registeredUsers = await loadUsers(); | |
| console.log(`Loaded ${Object.keys(registeredUsers).length} registered users.`); | |
| // const PORT = process.env.PORT || 3000; // Find this line or similar | |
| const PORT = process.env.PORT || 7860; // Change 3000 to 7860 | |
| server.listen(PORT, () => { | |
| console.log(`Server running on port ${PORT}`); | |
| console.log(`Access it locally at: http://localhost:${PORT}`); | |
| if (localIp !== '127.0.0.1') { | |
| console.log(`Access on local network (e.g., mobile): http://${localIp}:${PORT}`); | |
| } | |
| console.log(`Serving files from: ${PUBLIC_DIR}`); | |
| console.log(`User data file: ${USERS_FILE_PATH}`); | |
| console.warn('--- SECURITY WARNING: Storing plain text passwords in users.json! ---'); | |
| console.warn('--- WebRTC Note: Using public STUN servers. TURN server might be needed for reliability. ---'); | |
| }); | |
| server.on('error', (error) => { | |
| console.error('Server failed to start:', error); | |
| process.exit(1); | |
| }); | |
| })(); | |
| // --- END: Additions/Modifications in app.js --- |