chat / app.js
sameernotes's picture
Upload 6 files
47a00d3 verified
// --- 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 ---