This repository has been archived on 2026-10-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
collavas-src/server-96wMDN.js
2025-03-05 15:58:01 -08:00

411 lines
13 KiB
JavaScript

const fs = require('fs');
const winston = require('winston');
const path = require('path');
const logDir = 'logs';
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir);
}
const getLogFileName = () => {
const now = new Date();
return `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}.log`;
};
const createLogger = () => {
const logFileName = path.join(logDir, getLogFileName());
return winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
winston.format.printf(({ timestamp, level, message }) => `[${timestamp}] [${level.toUpperCase()}] ${message}`)
),
transports: [
new winston.transports.File({ filename: logFileName }),
new winston.transports.Console()
],
handleExceptions: true
});
};
let logger = createLogger();
const rotateLogs = () => {
const newLogFileName = getLogFileName();
const currentLogFileName = logger.transports.find(t => t instanceof winston.transports.File).filename;
if (newLogFileName !== path.basename(currentLogFileName)) {
logger = createLogger();
}
};
setInterval(rotateLogs, 24 * 60 * 60 * 1000);
console.log = (msg) => logger.info(msg);
console.error = (msg) => logger.error(msg);
const yaml = require('yaml');
let config = yaml.parse(fs.readFileSync('config.yaml', 'utf8'));
console.log(consoleInfo("info") + "Starting Collavas server v" + config.version);
console.log(consoleInfo("info") + "Loading Dependencies...");
const express = require('express');
const http = require('http');
const socketIO = require('socket.io');
const cors = require('cors');
const readline = require('readline');
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
let users = JSON.parse(fs.readFileSync("users.json", 'utf-8'));
const app = express();
app.use(cors());
const server = http.createServer(app);
const io = socketIO(server);
console.log(consoleInfo("info") + "Successfully started!");
const DRAWINGS_FILE = 'drawings.json';
app.use(express.static('public'));
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
let drawings = loadDrawings();
let messages = JSON.parse(fs.readFileSync("chat.json", 'utf-8'));;
let changesCounter = 0;
let count = 0;
let connectedClients = 0;
let enabled = true;
let consolelog = true;
let loadExistingDrawings = true;
let chat = [];
messages.forEach(message => {
const messageUserIndex = users.findIndex(user => user.uuid == message.uuid);
const messageUser = users[messageUserIndex];
if (messageUser) {
message.name = (messageUser.name.trim() != "" ? messageUser.name : "user_" + messageUserIndex);
chat.push(message);
}
});
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.on('line', (input) => {
const commandParts = input.trim().split(' ');
const command = commandParts.shift();
const colorIndex = commandParts.findIndex(part => ['blue', 'indigo', 'purple', 'pink', 'red', 'orange', 'yellow', 'green', 'teal', 'cyan', 'white', 'gray', 'gray-dark', 'primary', 'secondary', 'success', 'info', 'warning', 'danger', 'light', 'dark'].includes(part));
let message, color;
if (colorIndex !== -1) {
color = commandParts[colorIndex];
message = commandParts.slice(0, colorIndex).join(' ');
} else {
message = commandParts.join(' ');
}
switch (command) {
case 'announce':
announce(message, color);
break;
case 'reload':
reloadall();
break;
case 'play':
playaudio(message);
break;
case 'save':
saveDrawings();
drawings = loadDrawings();
break;
case 'users':
console.log(JSON.stringify(users));
break;
case 'remove':
users = users.filter(item => item !== message);
console.log(consoleInfo("info") + "Removed " + message);
break;
case 'drawTog':
enabled = (enabled) ? false : true;
console.log(consoleInfo("info") + ((enabled) ? "Enabled drawing" : "Disabled drawing"));
break;
case 'loadTog':
loadExistingDrawings = (loadExistingDrawings) ? false : true;
console.log(consoleInfo("info") + ((loadExistingDrawings) ? "Enabled loading existing drawings" : "Disabled loading existing drawings"));
break;
case 'logTog':
consolelog = (consolelog) ? false : true;
console.log(consoleInfo("info") + ((consolelog) ? "Enabled console logging" : "Disabled console logging"));
break;
case 'exit':
exitServer();
break;
default:
console.log(`${consoleInfo("info")} Unknown command: ${command}`);
}
});
function announce(message, color = 'white') {
console.log(`${consoleInfo("info")} Announcement: ${message} (Color: ${color})`);
io.emit('announcement', { message, color });
}
function reloadall() {
io.emit('reload');
}
function playaudio(link) {
var id = uuidv4();
var directory = "C:/nginx/web/www.collavas.com/public/static/audio/";
if(link != "stop") {
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, { recursive: true });
}
console.log(consoleInfo("info") + "Downloading...");
axios({
method: 'get',
url: link,
responseType: 'stream'
})
.then(response => {
response.data.pipe(fs.createWriteStream(directory + id + ".mp3"));
link = "/static/audio/" + id + ".mp3";
setTimeout(() => {
io.emit('play', { audio: link });
console.log(consoleInfo("info") + "Done, now playing to clients");
}, 5000);
})
.catch(error => {
console.error(consoleInfo("info") + 'Error downloading the MP3 file:', error);
});
} else {
io.emit('play', { audio: "stop" });
console.log(consoleInfo("info") + "Stopped");
}
}
function exitServer() {
console.log(consoleInfo("info") + "Server is shutting down...");
saveDrawings();
saveChat();
setTimeout(() => {
process.exit(0);
}, 500);
}
io.on('connection', (socket) => {
connectedClients++;
updateConsole();
io.emit('dataSize', { size: (fs.statSync(DRAWINGS_FILE).size / (1024 * 1024)).toFixed(2) });
io.emit('connectedClients', connectedClients);
io.emit('version', config.version);
let userFound = false;
users.forEach(entry => {
if(entry.ip == socket.handshake.headers['x-forwarded-for']) {
const userIndex = users.findIndex(user => user.uuid == entry.uuid);
const user = users[userIndex];
if(user) {
if(!entry.name) {
users[userIndex].name = "";
entry.name = "";
}
if(!entry.lastNameChange) {
users[userIndex].lastNameChange = new Date();
}
if(!entry.lastChat) {
users[userIndex].lastChat = new Date();
}
userFound = true;
if(loadExistingDrawings) {
socket.emit('draw', { type: 'existingDrawings', drawings: JSON.stringify(drawings), id: entry.uuid, name: entry.name, chat: JSON.stringify(chat) });
}
socket.emit('chat', { type: 'existingMessages', messages: JSON.stringify(chat) });
}
}
});
if(!userFound) {
const uuid = uuidv4();
users.push({ uuid: uuid, name: "", ip: socket.handshake.headers['x-forwarded-for'], lastdraw: new Date(), lastNameChange: new Date(), lastChat: new Date() });
if(loadExistingDrawings) {
socket.emit('draw', { type: 'existingDrawings', drawings: JSON.stringify(drawings), id: uuid, name: "", chat: JSON.stringify(chat) });
}
}
socket.on('changeName', (data) => {
const invalidChars = /[<>\s;:.\)\(*&^%$?/#\-+=\\|"@!~`]/;
if(invalidChars.test(data.name)) {
socket.emit('changeName', { status: { message: "Invalid characters" } });
} else if(users.find(user => user.name == data.name) && data.name.trim() != "") {
socket.emit('changeName', { status: { message: "A user already exists with this name" } });
} else if(new Date().getTime() - new Date(users.find(user => user.uuid == data.uuid).lastNameChange).getTime() < 10000) {
socket.emit('changeName', { status: { message: "Wait before you change your name again" } });
} else {
users[users.findIndex(user => user.uuid == data.uuid)].name = data.name;
users[users.findIndex(user => user.uuid == data.uuid)].lastNameChange = new Date();
socket.emit('changeName', { status: { message: "Successfully changed name" } });
}
});
socket.on('message', (data) => {
if (data && typeof data.content === 'string' && data.content.trim() != "" && data.content.length < 150) {
const user = users.find(user => user.uuid == data.uuid);
if (user && new Date().getTime() - new Date(user.lastChat).getTime() > 1000) {
data.date = new Date();
messages.push(data);
users[users.findIndex(entry => entry.uuid == data.uuid)].lastChat = new Date();
io.emit('chat', {
type: 'message',
uuid: data.uuid,
name: (user.name.trim() != "" ? user.name : "user_" + users.findIndex(entry => entry.uuid == data.uuid)),
content: data.content
});
saveChat();
}
}
});
socket.on('draw', (data) => {
const keysOrder = JSON.stringify(Object.keys(data));
if (keysOrder == JSON.stringify(["type", "drawing", "valid"]) && data.type === 'draw' && data.drawing && data.drawing.length > 1 && data.valid /*&& data.drawing[1] && !(data.drawing[0].x < 100) && !(data.drawing[data.drawing.length - 1].x > 9000) */&& users.find(user => user.uuid == data.valid) && enabled) {
const entry = users.find(user => user.uuid == data.valid);
console.log(new Date().getTime() - entry.lastdraw + " " + new Date().getTime());
if (new Date().getTime() - new Date(entry.lastdraw).getTime() < 2000) {
if(consolelog) {
console.error(consoleInfo("info") + 'Drawing data exceeds the maximum size. Seconds: ' + new Date().getTime() - new Date(entry.lastdraw).getTime() + ' Length: ' + data.drawing.length);
}
socket.emit('drawLimit');
/*
data.drawing = data.drawing.slice(0, 25);
drawings.push(data.drawing);
saveDrawings();
drawings = loadDrawings();
socket.emit('dataSize', { size: (fs.statSync(DRAWINGS_FILE).size / (1024 * 1024)).toFixed(2) });
*/
} else {
socket.broadcast.emit('draw', { type: 'draw', drawing: data.drawing });
drawings.push(data.drawing);
users[users.findIndex(user => user.uuid == data.valid)].lastdraw = new Date();
if(consolelog) {
console.log(consoleInfo("info") + data.valid + " " + JSON.stringify(data));
}
if(count > 10) {
saveDrawings();
drawings = loadDrawings();
socket.emit('dataSize', { size: (fs.statSync(DRAWINGS_FILE).size / (1024 * 1024)).toFixed(2) });
count = 0;
} else {
count++;
}
}
} else {
if(consolelog) {
console.log(consoleInfo("info") + "Drawing not authenticated" + " " + data.valid);
}
socket.emit('drawLimit');
}
});
socket.on('ping', () => {
socket.emit('pong');
});
socket.on('disconnect', () => {
connectedClients--;
updateConsole();
io.emit('connectedClients', connectedClients);
});
});
server.listen(config.port, () => {
console.log(consoleInfo("info") + 'Server is running on port ' + config.port);
updateConsole();
});
function removeDuplicates(arr) {
if (!Array.isArray(arr)) {
return [];
}
function isEqual(obj1, obj2) {
return obj1.x === obj2.x && obj1.y === obj2.y && obj1.color === obj2.color;
}
const uniqueArrays = arr.map(innerArray =>
innerArray.filter((elem, index) =>
index === innerArray.findIndex((t) => isEqual(t, elem))
)
);
return uniqueArrays;
}
function loadDrawings() {
try {
const data = fs.readFileSync(DRAWINGS_FILE, 'utf-8');
return JSON.parse(data) || [];
} catch (error) {
console.error(consoleInfo("error") + 'Error loading drawings:', error.message);
return [];
}
}
function saveDrawings() {
console.log(consoleInfo("info") + "Saving drawings");
try {
fs.writeFileSync(DRAWINGS_FILE, JSON.stringify(removeDuplicates(drawings)), 'utf-8');
console.log(consoleInfo("info") + "Drawings saved");
} catch (error) {
console.error(consoleInfo("error") + 'Error saving drawings:', error.message);
}
}
function saveChat() {
console.log(consoleInfo("info") + "Saving chat");
try {
fs.writeFileSync("chat.json", JSON.stringify(messages), 'utf-8');
console.log(consoleInfo("info") + "Chat saved");
} catch (error) {
console.error(consoleInfo("error") + 'Error saving chat:', error.message);
}
}
function updateConsole() {
if(consolelog) {
console.log(`${consoleInfo("info")}Connected Clients: ${io.sockets.sockets.size}`);
}
}
function consoleInfo(type) {
const currentDate = new Date();
return "";
//return "[" + currentDate.getFullYear() + "-" + String(currentDate.getMonth() + 1).padStart(2, '0') + "-" + String(currentDate.getDate()).padStart(2, '0') + " " + String(currentDate.getHours()).padStart(2, '0') + ":" + String(currentDate.getMinutes()).padStart(2, '0') + ":" + String(currentDate.getSeconds()).padStart(2, '0') + "] [" + type.toUpperCase() + "] ";
}