// Global logging toggles // const fileLogging = false; const consoleLogging = true; // Imports // const fs = require('fs'); const http = require('http'); const express = require('express'); // Global server setup const app = express(); const clientPath = __dirname+'/../html'; console.log('Serving static from ' + clientPath); app.use(express.json()); app.use(express.static(clientPath)); const server = http.createServer(app); // Check if string is already in array function isStringUnique(arr, str) { let match = true; for (const s of arr) if (s === str) { match = false; break; } return match; } // Generate the partial ID after username function generatePartialID() { return Math.floor(Math.random()*10000).toString().padStart(4, '0'); } // Check if input is a user function isUser(user, other) { // Check by full name if ('full' in other) return `${user.name}#${user.id}` === other.full; // Check by unique id else if ('id' in other && 'name' in other) return other.id === user.id && other.name === user.name; // Not applicable else return false; } // Check for user inactivity and delete accordingly function userInactivity(name, id) { for (let i = 0; i < users.length; i++) if (isUser(users[i], {name, id}) && new Date() - users[i].seen > 86400000) { // User with ID 'name#id' was inactive for longer than a day. if (consoleLogging) console.log(`Deleting user with ID '${name}#${id}' because of inactivity.`); users.splice(i, 1); break; } } // User and chat data const users = [], chats = []; // Incoming user connection app.post('/connect', (req, res) => { // Retrieve username const { name } = req.body; // Generate id in unique pattern 'name#id' let id = generatePartialID(); const name_id = users.map(u => `${u.name}#${u.id}`); while (!isStringUnique(name_id, `${name}#${id}`)) id = generatePartialID(); // Save user data users.push({ deletion: setInterval( ()=>userInactivity(name, id), 3600000), seen: new Date(), event: null, chats: {}, name, id }); // Log connection if (consoleLogging) console.log(`User with ID '${name}#${id}' connected.`); // Respond with id res.status(200).json({ id }); }); // User has disconnected app.post('/disconnect', (req, res) => { // Retrieve user data const { name, id } = req.body; // Search for user in data for (let i = 0; i < users.length; i++) // If user was found, delete all of their data if (isUser(users[i], {name, id})) { // Log disconnect if (consoleLogging) console.log(`User with ID '${name}#${id}' disconnected.`); // Delete user users.splice(i, 1); break; } // Respond with id as confirmation res.status(200).send(); }); // User wants to measure ping app.get('/ping', (req, res) => { res.status(200).send(); }); // User wants to change name app.post('/nickname', (req, res) => { // Retrieve user data and old name const { oldName, name, id } = req.body; // Check if unique pattern 'name#id' is valid, generate new one const name_id = users.map(u => `${u.name}#${u.id}`); let currentID = id; while (!isStringUnique(name_id, `${name}#${currentID}`)) currentID = generatePartialID(); // Find specified user for (const u of users) if (isUser(u, {name: oldName, id})) { // Log renaming if (consoleLogging) console.log(`(Re-)named user with ID '${oldName}#${id}'.`); // Save new name u.name = name; u.id = currentID; u.seen = new Date(); break; } // Respond with confirmation res.status(200).json({ id: currentID }); }); // User is pulling messages app.get('/getChat', (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); // Save user activity let full = req.query.full, found = false; for (const u of users) if (isUser(u, {full})) { found = true; u.event = res; u.seen = new Date(); break; } if (!found) { res.write('data: {"message":"Invalid user provided."}'); res.end(); return; } // Log if messages were delivered if (consoleLogging) console.log(`New message event listener opened by user with ID '${full}'.`); // Handle close-event for when client is no longer listening res.on('close', () => { for (const u of users) if (isUser(u, {full})) u.event = null; }); }); // User sent new message app.post('/message', (req, res) => { // Retrieve user data, their message and where to deliver it to const { name, id, to, message } = req.body; // Search for sender for (const u of users) if (isUser(u, {name, id})) { // Go through every recipient for (const recipient of to) { // Save message for recipient for (const o of users) if (isUser(o, {full: recipient}) && o.event !== null) o.event.write(`data: ${JSON.stringify({from: `${name}#${id}`, message})}\n\n`); } // Save user activity u.seen = new Date(); break; } // Log users' message if (consoleLogging) console.log(`User with ID '${name}#${id}' sent message '${message}' to '${to}'.`); // Confirm process res.status(200).send(); }); // User requesting all names app.get('/getNames', (req, res) => { // Log request if (consoleLogging) console.log('Some user requested all usernames + IDs.'); // Respond with unique patterns 'name#id' res.status(200).json(users.map(u => { return { name: u.name, id: u.id }; })); }); // User sends activity notification app.post('/activity', (req, res) => { // Retrieve user data const { name, id } = req.body; // Save activity for (const u of users) if (isUser(u, {name, id})) { // Log activity if (consoleLogging) console.log(`User with ID '${name}#${id}' is online.`); u.seen = new Date(); break; } // Confirm process res.status(200).send(); }); // User asks if their data exists app.post('/login', (req, res) => { // Retrieve user data const { name, id } = req.body; // Log login if (consoleLogging) console.log(`Some user requested existence of '${name}#${id}'.`); // Go through users, success if user exists let success = false; for (const u of users) if (isUser(u, {name, id})) { success = true; // Save activity u.seen = new Date(); break; } // Respond with status res.status(200).json({success}); }); // User wants to join chat app.post('/chatInit', (req, res) => { // Retrieve user data and chat name const { name, id, chat } = req.body; // Save user activity for (const u of users) if (isUser(u, {name, id})) { u.seen = new Date(); break; } // Check if chats already exist const fullName = `${name}#${id}`; let existance = chat.map(e => [e, false]); // Go through chats for (const c of chats) // Go through provided names for (const n of existance) { // If name is this chat if (n[0] === c.name) { // Chat exists, add user to it n[1] = true; if (!c.users.includes(fullName)) c.users.push(fullName); break; } } // Go through provided names for (const n of existance) { // Chat didn't exist if (!n[1]) // Create it and save user in it chats.push({ users: [fullName], name: n[0] }); } // Log chat initialization if (consoleLogging) console.log(`User with ID '${name}#${id}' joined chat '${chat}'.`); // Confirm process res.status(200).send(); }); // User sends a message in chat app.post('/sendGroup', (req, res) => { // Retrieve user data and chat name and message const { name, id, message, chat } = req.body; // Save user activity for (const u of users) if (isUser(u, {name, id})) { u.seen = new Date(); break; } // Find provided chat const fullName = `${name}#${id}`; for (const c of chats) if (chat.includes(c.name)) { // Go through users in chat for (const u of c.users) if (u !== fullName) { // Send message to recipient for (const o of users) if (isUser(o, {full: u}) && o.event !== null) o.event.write(`data: ${JSON.stringify({from: fullName, message})}\n\n`); } } // Log users' message if (consoleLogging) console.log(`User with ID '${name}#${id}' sent message '${message}' in '${chat}'.`); // Confirm process res.status(200).send(); }); // Internal server error server.on('error', err => { console.error('Internal server error', err); }); // Server has closed server.on('close', () => { console.log('Shutting down server ...'); }); // Listen on port 3000 for webserver connection server.listen(3000, () => { console.log('Server running on port 3000'); });