const path = require('path'); const express = require('express'); const WebSocket = require('ws'); const Database = require('./Database.js'); const SessionManager = require("./SessionManager.js"); const crypto = require('crypto'); const GPT4All = require('./GPT4All.js'); const aiModel = new GPT4All(); const MAX_INAP_MESSAGE = 3 const host = 'localhost'; const port = 3000; const clientApp = path.join(__dirname, 'client'); const cpen322 = require('./cpen322-tester.js'); const broker = new WebSocket.Server({ port: 8000 }); const db = new Database('mongodb://127.0.0.1:27017', 'cpen322-messenger'); function logRequest(req, res, next){ console.log(`${new Date()} ${req.ip} : ${req.method} ${req.path}`); next(); } function isCorrectPassword(password, saltedHash){ const salt = saltedHash.substring(0, 20); const originalHash = saltedHash.substring(20); const saltedPassword = password + salt; const hash = crypto.createHash('sha256').update(saltedPassword).digest('base64'); return hash === originalHash; } function errorHandler (err, req, res, next) { if(err instanceof SessionManager.Error) { console.error(req); if(req.headers.accept === 'application/json') { res.status(401).json({ error: err.message }); }else{ res.redirect('/login'); } }else{ res.status(500).send('Server Error'); } } function middlewareForIndex(req, res, next) { const requestedPath = req.path; if (requestedPath === '/' || requestedPath.includes('index')) { sessionManager.middleware(req, res, next); } else { next(); } } function sanitizeText(text){ return text.replace(//g, ''); } const messages = {}; const messageBlockSize = 10; // Generate a notification for the user that their account has been restricted const generateRestrictedNotification = (message, request, shouldRemoveLastMessage = true) => { const notification = { type: 'restricted-alert', alert: `Your account has been restricted, you will no longer be able to send messages due to repeated inappropriate content violations.`, originalMessage: { username: request.username, text: message.text }, roomId: message.roomId, shouldRemoveLastMessage: shouldRemoveLastMessage } return notification; } // Generate a notification for the user that their message has been filtered due to inappropriate content const generateInappropriateNotification = (message, request, justification, remainingAttempts) => { const notification = { type: 'inappropriate-alert', originalMessage: { username: request.username, text: message.text }, moderatedMessage: { username: request.username, text: `Message removed because: ${justification}` }, roomId: message.roomId, remainingAttempts: remainingAttempts }; if(remainingAttempts >= 1){ notification.alert = `Your message was removed because: ${justification}. You have ${remainingAttempts} warning(s) left before your account will be restricted.`; } else if(remainingAttempts == 0){ notification.alert = `Your message was removed because: ${justification}. This is your last warning, any further inappropriate messages will result in your account being restricted.`; } else if(remainingAttempts < 0){ notification.alert = `This message was removed because: ${justification}. Your account has been restricted due to repeated inappropriate content violations.`; } return notification; }; broker.on('connection', function(socket, request) { const next = (error) => { // Close socket if the cookie cannot be validated if (error) { socket.close(); } else { socket.on('message', function(data) { // Make sure that the data is a string so that it can be parsed by JSON if (data instanceof Buffer || data instanceof ArrayBuffer) { data = Buffer.from(data).toString(); } const message = JSON.parse(data); message.type = 'chat'; db.getInappropriateAttempts(message.username).then(attempts => { const remainingAttempts = MAX_INAP_MESSAGE - attempts; // If user has already sent enough messages to be restricted if(remainingAttempts < 0){ const restrictedNotification = generateRestrictedNotification(message, request); socket.send(JSON.stringify(restrictedNotification)); return; } // Call API to check if message is inappropriate via AI Model const url = `http://localhost:${port}/api/check-message`; fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: message.text }), }) .then(response => response.json()) .then(result => { if (result.isInappropriate) { db.incrementInappropriateAttempts(message.username).then(() => { // Get updated number of inappropriate attempts db.getInappropriateAttempts(message.username).then(attempts => { const remainingAttempts = MAX_INAP_MESSAGE - attempts; // Generate notification for the user that their message has been filtered const notification = generateInappropriateNotification(message, request, result.justification, remainingAttempts); socket.send(JSON.stringify(notification)); // If the user has no more attempts left, restrict their account if(remainingAttempts < 0){ const restrictedNotification = generateRestrictedNotification(message, request, false); socket.send(JSON.stringify(restrictedNotification)); } // Send filtered message to other users message.text = `Message removed because: ${result.justification}`; sendMessageToClient(socket, message, request); }).catch(error => { console.error('Error fetching inappropriate attempts', error); }); }).catch(error => { console.error('Error incrementing inappropriate attempts', error); }); } // If the message is not inappropriate just send messages to other users else{ sendMessageToClient(socket, message, request); } }) .catch(error => { console.error('Error while checking whether message is inappropriate.', error); }); }); }); } }; const response = {}; sessionManager.middleware(request, response, next); }); const sendMessageToClient = (socket, message, request) => { // Sanitize message before forwarding to clients or adding to database if (message.text) { message.text = sanitizeText(message.text); } message.username = request.username; if(messages[message.roomId]) { messages[message.roomId].push(message); if(messages[message.roomId].length === messageBlockSize) { const conversation = { room_id: message.roomId, timestamp: Date.now(), messages: messages[message.roomId] }; db.addConversation(conversation).then(() => { messages[message.roomId] = []; }).catch(error => { console.error('Failed to add conversation to database', error); }); } } // Iterate through clients broker.clients.forEach(client => { if(client !== socket && client.readyState === WebSocket.OPEN){ const updatedData = JSON.stringify(message); client.send(updatedData); } }); }; const sessionManager = new SessionManager(); // express app let app = express(); app.use(express.json()) // to parse application/json app.use(express.urlencoded({ extended: true })) // to parse application/x-www-form-urlencoded app.use(logRequest); // logging for debug app.use(errorHandler); app.use(middlewareForIndex); // serve static files (client-side) app.use('/app.js', sessionManager.middleware, express.static("public/app.js"), errorHandler); app.use('/', middlewareForIndex, express.static(clientApp, { extensions: ['html'] }), errorHandler); app.listen(port, () => { console.log(`${new Date()} App Started. Listening on ${host}:${port}, serving ${clientApp}`); }); db.getRooms().then(rooms => { rooms.forEach(room => { messages[room._id] = []; }); }); app.get('/chat', sessionManager.middleware, (req, res) => { db.getRooms().then(rooms => { const chatData = rooms.map(room => { return { ...room, messages: messages[room._id] }; }); res.json(chatData); }) }, errorHandler); app.get('/chat/:room_id', sessionManager.middleware, (req, res) => { const room_id = req.params.room_id; db.getRoom(room_id).then(room => { if(room) { res.json(room); }else{ res.status(404).send(`Chatroom ${room_id} not found`); } }) }, errorHandler); app.get('/chat/:room_id/messages', sessionManager.middleware, (req, res) => { const room_id = req.params.room_id; // Parse before query to integer if it is not null const before = req.query.before ? parseInt(req.query.before) : null; if(before) { db.getLastConversation(room_id, before).then(messages => { res.json(messages); }).catch(error => { res.status(500).send('Error while fetching messages'); }) }else{ // If no before query, just fetch the latest conversation db.getLastConversation(room_id).then(messages => { res.json(messages); }).catch(error => { res.status(500).send('Error while fetching messages'); }) } }, errorHandler); app.get('/profile', sessionManager.middleware, (req, res) => { if (req.username) { res.json({ username: req.username }); } else { res.status(401).json({ error: 'User could not be authenticated' }); } }, errorHandler) app.get('/logout', sessionManager.middleware, (req, res) => { sessionManager.deleteSession(req); res.redirect('/login'); }, errorHandler) app.post('/chat', sessionManager.middleware, (req, res) => { if(!req.body || !req.body.name) { res.status(400).send('Chatroom name is required'); return; }; const chatroom = { name: req.body.name, image: req.body.image ?? "assets/chat-icon.png" }; db.addRoom(chatroom).then(addedChatRoom => { messages[addedChatRoom._id] = []; res.status(200).json(addedChatRoom); }) .catch(error => { console.error('Failed to add chatroom to database', error); res.status(500).send('Error while creating chatroom.'); }) }, errorHandler) app.post('/login', (req, res) => { const { username, password } = req.body; db.getUser(username).then(user => { if(isCorrectPassword(password, user.password)) { sessionManager.createSession(res, username); res.redirect('/'); }else{ res.redirect('/login'); } }).catch(error => { res.redirect('/login'); }); }); // AI Feature API: Check if a message is inappropriate app.post('/api/check-message', async (req, res) => { const {message} = req.body; try { const result = await aiModel.checkIfMessageInappropriate(message); res.json(result); } catch (error) { console.error("Error processing AI check:", error); res.status(500).send("Internal Server Error"); } }); cpen322.connect('http://3.98.223.41/cpen322/test-a5-server.js'); cpen322.export(__filename, { app, messages, broker, db, messageBlockSize, sessionManager, isCorrectPassword });