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, '').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 });