Remove CMake configuration and application source files. This includes the deletion of CMakeLists.txt, application logic in app.cpp and app.h, and broadcast functionality in broadcast.cpp. This cleanup streamlines the project structure by removing unused files and configurations.
This commit is contained in:
@@ -18,6 +18,8 @@ class Client {
|
||||
this.loginTimeStamp = new Date();
|
||||
this.blockedUsers = new Set();
|
||||
this.socket = null; // Socket.IO Socket-Objekt
|
||||
this.chatAuth = null; // { username, rights: Set<string> }
|
||||
this.pendingChatLogin = null; // { step: 'username'|'password', username: string }
|
||||
}
|
||||
|
||||
setActivity() {
|
||||
@@ -50,6 +52,104 @@ class Client {
|
||||
}
|
||||
}
|
||||
|
||||
const CHAT_USERS_FILE_NAME = 'chat-users.json';
|
||||
const CHAT_RIGHTS = {
|
||||
STAT: 'stat',
|
||||
KICK: 'kick'
|
||||
};
|
||||
|
||||
function getLogsDir(__dirname) {
|
||||
return join(__dirname, '../logs');
|
||||
}
|
||||
|
||||
function ensureLogsDir(__dirname) {
|
||||
const logsDir = getLogsDir(__dirname);
|
||||
if (!existsSync(logsDir)) {
|
||||
mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
return logsDir;
|
||||
}
|
||||
|
||||
function getChatUsersPath(__dirname) {
|
||||
return join(ensureLogsDir(__dirname), CHAT_USERS_FILE_NAME);
|
||||
}
|
||||
|
||||
function sha256(value) {
|
||||
return crypto.createHash('sha256').update(value).digest('hex');
|
||||
}
|
||||
|
||||
function ensureChatUsersFile(__dirname) {
|
||||
const usersPath = getChatUsersPath(__dirname);
|
||||
if (existsSync(usersPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultUsers = [
|
||||
{
|
||||
username: 'admin',
|
||||
passwordHash: `sha256:${sha256('changeme123')}`,
|
||||
rights: [CHAT_RIGHTS.STAT, CHAT_RIGHTS.KICK]
|
||||
}
|
||||
];
|
||||
writeFileSync(usersPath, JSON.stringify(defaultUsers, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
function loadChatUsers(__dirname) {
|
||||
ensureChatUsersFile(__dirname);
|
||||
const usersPath = getChatUsersPath(__dirname);
|
||||
const raw = readFileSync(usersPath, 'utf-8').trim();
|
||||
if (!raw) return [];
|
||||
|
||||
let users = [];
|
||||
try {
|
||||
users = JSON.parse(raw);
|
||||
} catch (error) {
|
||||
throw new Error(`Ungültige ${CHAT_USERS_FILE_NAME}: ${error.message}`);
|
||||
}
|
||||
|
||||
if (!Array.isArray(users)) {
|
||||
throw new Error(`${CHAT_USERS_FILE_NAME} muss ein Array sein`);
|
||||
}
|
||||
|
||||
return users
|
||||
.filter((entry) => entry && typeof entry.username === 'string')
|
||||
.map((entry) => ({
|
||||
username: entry.username.trim(),
|
||||
passwordHash: typeof entry.passwordHash === 'string' ? entry.passwordHash.trim() : '',
|
||||
rights: Array.isArray(entry.rights) ? entry.rights.map((r) => String(r).toLowerCase()) : []
|
||||
}))
|
||||
.filter((entry) => entry.username && entry.passwordHash);
|
||||
}
|
||||
|
||||
function parseLoginRecord(line) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) return null;
|
||||
const parts = trimmed.split(',');
|
||||
if (parts.length < 5) return null;
|
||||
|
||||
const timestamp = parts[0].trim();
|
||||
const userName = parts[1].trim();
|
||||
const gender = parts[parts.length - 1].trim();
|
||||
const ageRaw = parts[parts.length - 2].trim();
|
||||
const country = parts.slice(2, parts.length - 2).join(',').trim();
|
||||
const age = Number.parseInt(ageRaw, 10);
|
||||
|
||||
const date = new Date(timestamp);
|
||||
if (!timestamp || !userName || !country || Number.isNaN(age) || Number.isNaN(date.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp,
|
||||
date,
|
||||
day: timestamp.slice(0, 10),
|
||||
userName,
|
||||
country,
|
||||
age,
|
||||
gender
|
||||
};
|
||||
}
|
||||
|
||||
let clients = new Map();
|
||||
let conversations = new Map(); // Key: "user1:user2" (alphabetisch sortiert)
|
||||
|
||||
@@ -223,6 +323,313 @@ export function setupBroadcast(io, __dirname) {
|
||||
downloadCountries();
|
||||
setInterval(downloadCountries, 24 * 60 * 60 * 1000); // Täglich aktualisieren
|
||||
|
||||
function sendCommandResult(socket, lines) {
|
||||
const payload = Array.isArray(lines) ? lines : [String(lines)];
|
||||
socket.emit('commandResult', { lines: payload });
|
||||
}
|
||||
|
||||
function hasRight(client, right) {
|
||||
return !!client.chatAuth && client.chatAuth.rights instanceof Set && client.chatAuth.rights.has(right);
|
||||
}
|
||||
|
||||
function getLoginsPath() {
|
||||
return join(ensureLogsDir(__dirname), 'logins.log');
|
||||
}
|
||||
|
||||
function readLoginRecords() {
|
||||
const logPath = getLoginsPath();
|
||||
if (!existsSync(logPath)) return [];
|
||||
const raw = readFileSync(logPath, 'utf-8');
|
||||
return raw
|
||||
.split('\n')
|
||||
.map(parseLoginRecord)
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function aggregateTop(items, keySelector, limit = 10) {
|
||||
const counts = new Map();
|
||||
for (const item of items) {
|
||||
const key = keySelector(item);
|
||||
counts.set(key, (counts.get(key) || 0) + 1);
|
||||
}
|
||||
return Array.from(counts.entries())
|
||||
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function buildAllStats(records) {
|
||||
if (records.length === 0) {
|
||||
return ['Keine Login-Daten vorhanden.'];
|
||||
}
|
||||
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const todayCount = records.filter((r) => r.day === today).length;
|
||||
const uniqueNames = new Set(records.map((r) => r.userName)).size;
|
||||
const ages = records.map((r) => r.age);
|
||||
const minAge = Math.min(...ages);
|
||||
const maxAge = Math.max(...ages);
|
||||
const youngest = records.find((r) => r.age === minAge);
|
||||
const oldest = records.find((r) => r.age === maxAge);
|
||||
const topCountries = aggregateTop(records, (r) => r.country, 5)
|
||||
.map(([name, count]) => `${name}(${count})`)
|
||||
.join(', ');
|
||||
|
||||
return [
|
||||
`Logins gesamt: ${records.length}`,
|
||||
`Logins heute (${today}): ${todayCount}`,
|
||||
`Unterschiedliche Namen: ${uniqueNames}`,
|
||||
`Jüngster Nutzer: ${youngest.userName} (${youngest.age})`,
|
||||
`Ältester Nutzer: ${oldest.userName} (${oldest.age})`,
|
||||
`Top Länder: ${topCountries || 'keine'}`
|
||||
];
|
||||
}
|
||||
|
||||
function executeStatsCommand(socket, client, parts) {
|
||||
if (!hasRight(client, CHAT_RIGHTS.STAT)) {
|
||||
sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.');
|
||||
return;
|
||||
}
|
||||
|
||||
const records = readLoginRecords();
|
||||
if (records.length === 0) {
|
||||
sendCommandResult(socket, 'Keine Login-Daten vorhanden.');
|
||||
return;
|
||||
}
|
||||
|
||||
const sub = (parts[1] || '').toLowerCase();
|
||||
|
||||
if (!sub || sub === 'help') {
|
||||
sendCommandResult(socket, [
|
||||
'Stat-Befehle:',
|
||||
'/stat today',
|
||||
'/stat date YYYY-MM-DD',
|
||||
'/stat range YYYY-MM-DD YYYY-MM-DD',
|
||||
'/stat ages',
|
||||
'/stat names',
|
||||
'/stat countries',
|
||||
'/all-stats'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'today') {
|
||||
const day = new Date().toISOString().slice(0, 10);
|
||||
const dayRecords = records.filter((r) => r.day === day);
|
||||
sendCommandResult(socket, `Logins heute (${day}): ${dayRecords.length}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'date') {
|
||||
const day = parts[2];
|
||||
if (!day || !/^\d{4}-\d{2}-\d{2}$/.test(day)) {
|
||||
sendCommandResult(socket, 'Nutzung: /stat date YYYY-MM-DD');
|
||||
return;
|
||||
}
|
||||
const dayRecords = records.filter((r) => r.day === day);
|
||||
sendCommandResult(socket, `Logins am ${day}: ${dayRecords.length}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'range') {
|
||||
const from = parts[2];
|
||||
const to = parts[3];
|
||||
if (!from || !to || !/^\d{4}-\d{2}-\d{2}$/.test(from) || !/^\d{4}-\d{2}-\d{2}$/.test(to)) {
|
||||
sendCommandResult(socket, 'Nutzung: /stat range YYYY-MM-DD YYYY-MM-DD');
|
||||
return;
|
||||
}
|
||||
const filtered = records.filter((r) => r.day >= from && r.day <= to);
|
||||
const perDay = aggregateTop(filtered, (r) => r.day, 1000)
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([day, count]) => `${day}: ${count}`);
|
||||
sendCommandResult(socket, [`Logins ${from} bis ${to}: ${filtered.length}`, ...perDay]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'ages') {
|
||||
const ages = records.map((r) => r.age);
|
||||
const minAge = Math.min(...ages);
|
||||
const maxAge = Math.max(...ages);
|
||||
const youngest = records.find((r) => r.age === minAge);
|
||||
const oldest = records.find((r) => r.age === maxAge);
|
||||
sendCommandResult(socket, [
|
||||
`Jüngster Nutzer: ${youngest.userName} (${youngest.age})`,
|
||||
`Ältester Nutzer: ${oldest.userName} (${oldest.age})`
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'names') {
|
||||
const topNames = aggregateTop(records, (r) => r.userName, 20);
|
||||
sendCommandResult(socket, [
|
||||
`Namen gesamt (verschieden): ${new Set(records.map((r) => r.userName)).size}`,
|
||||
...topNames.map(([name, count]) => `${name}: ${count}`)
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'countries') {
|
||||
const topCountries = aggregateTop(records, (r) => r.country, 20);
|
||||
sendCommandResult(socket, topCountries.map(([country, count]) => `${country}: ${count}`));
|
||||
return;
|
||||
}
|
||||
|
||||
sendCommandResult(socket, 'Unbekannter /stat-Befehl. Nutze /stat help');
|
||||
}
|
||||
|
||||
function executeAllStatsCommand(socket, client) {
|
||||
if (!hasRight(client, CHAT_RIGHTS.STAT)) {
|
||||
sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.');
|
||||
return;
|
||||
}
|
||||
sendCommandResult(socket, buildAllStats(readLoginRecords()));
|
||||
}
|
||||
|
||||
function executeKickCommand(socket, client, parts) {
|
||||
if (!hasRight(client, CHAT_RIGHTS.KICK)) {
|
||||
sendCommandResult(socket, 'Keine Berechtigung: Recht "kick" fehlt.');
|
||||
return;
|
||||
}
|
||||
|
||||
const targetName = (parts[1] || '').trim();
|
||||
if (!targetName) {
|
||||
sendCommandResult(socket, 'Nutzung: /kick <username>');
|
||||
return;
|
||||
}
|
||||
|
||||
let targetSessionId = null;
|
||||
let targetClient = null;
|
||||
for (const [sid, c] of clients.entries()) {
|
||||
if (c.userName === targetName && c.socket && c.socket.connected) {
|
||||
targetSessionId = sid;
|
||||
targetClient = c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetClient) {
|
||||
sendCommandResult(socket, `User "${targetName}" ist nicht online.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetClient.socket) {
|
||||
targetClient.socket.emit('error', { message: 'Du wurdest vom Chat getrennt (kick).' });
|
||||
targetClient.socket.disconnect(true);
|
||||
}
|
||||
clients.delete(targetSessionId);
|
||||
broadcastUserList();
|
||||
sendCommandResult(socket, `User "${targetName}" wurde gekickt.`);
|
||||
}
|
||||
|
||||
function verifyChatUser(username, password) {
|
||||
const users = loadChatUsers(__dirname);
|
||||
const user = users.find((u) => u.username.toLowerCase() === username.toLowerCase());
|
||||
if (!user) return null;
|
||||
|
||||
const [algo, hash] = user.passwordHash.split(':');
|
||||
if (algo !== 'sha256' || !hash) return null;
|
||||
const inputHash = sha256(password);
|
||||
if (inputHash !== hash) return null;
|
||||
|
||||
return {
|
||||
username: user.username,
|
||||
rights: new Set(user.rights)
|
||||
};
|
||||
}
|
||||
|
||||
function executeCommand(socket, client, rawInput) {
|
||||
const input = rawInput.trim();
|
||||
|
||||
if (client.pendingChatLogin) {
|
||||
if (client.pendingChatLogin.step === 'username') {
|
||||
const enteredUser = input;
|
||||
if (!enteredUser) {
|
||||
sendCommandResult(socket, 'Username darf nicht leer sein. Bitte Username eingeben:');
|
||||
return true;
|
||||
}
|
||||
client.pendingChatLogin = { step: 'password', username: enteredUser };
|
||||
sendCommandResult(socket, 'Passwort eingeben:');
|
||||
return true;
|
||||
}
|
||||
|
||||
if (client.pendingChatLogin.step === 'password') {
|
||||
const username = client.pendingChatLogin.username;
|
||||
const auth = verifyChatUser(username, input);
|
||||
client.pendingChatLogin = null;
|
||||
if (!auth) {
|
||||
sendCommandResult(socket, 'Login fehlgeschlagen. Benutzername oder Passwort falsch.');
|
||||
return true;
|
||||
}
|
||||
|
||||
client.chatAuth = auth;
|
||||
sendCommandResult(
|
||||
socket,
|
||||
`Login erfolgreich als ${auth.username}. Rechte: ${Array.from(auth.rights).join(', ') || 'keine'}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!input.startsWith('/')) return false;
|
||||
|
||||
const parts = input.split(/\s+/);
|
||||
const command = parts[0].toLowerCase();
|
||||
|
||||
if (command === '/login') {
|
||||
const username = (parts[1] || '').trim();
|
||||
if (username) {
|
||||
client.pendingChatLogin = { step: 'password', username };
|
||||
sendCommandResult(socket, 'Passwort eingeben:');
|
||||
} else {
|
||||
client.pendingChatLogin = { step: 'username', username: '' };
|
||||
sendCommandResult(socket, 'Username eingeben:');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (command === '/logout-admin') {
|
||||
const wasLoggedIn = !!client.chatAuth;
|
||||
client.chatAuth = null;
|
||||
client.pendingChatLogin = null;
|
||||
sendCommandResult(
|
||||
socket,
|
||||
wasLoggedIn
|
||||
? 'Admin/Command-Login wurde abgemeldet.'
|
||||
: 'Es war kein Admin/Command-Login aktiv.'
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (command === '/whoami-rights') {
|
||||
if (!client.chatAuth) {
|
||||
sendCommandResult(socket, 'Nicht per Command-Login angemeldet.');
|
||||
return true;
|
||||
}
|
||||
sendCommandResult(socket, [
|
||||
`Angemeldet als: ${client.chatAuth.username}`,
|
||||
`Rechte: ${Array.from(client.chatAuth.rights).join(', ') || 'keine'}`
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (command === '/stat') {
|
||||
executeStatsCommand(socket, client, parts);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (command === '/all-stats') {
|
||||
executeAllStatsCommand(socket, client);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (command === '/kick') {
|
||||
executeKickCommand(socket, client, parts);
|
||||
return true;
|
||||
}
|
||||
|
||||
sendCommandResult(socket, `Unbekannter Befehl: ${command}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Socket.IO-Verbindungshandler
|
||||
io.on('connection', (socket) => {
|
||||
const request = socket.handshake;
|
||||
@@ -598,6 +1005,11 @@ export function setupBroadcast(io, __dirname) {
|
||||
}
|
||||
|
||||
const { toUserName, message, messageId, isImage, imageType, imageUrl } = data;
|
||||
|
||||
// Chat-Befehle werden direkt serverseitig verarbeitet
|
||||
if (!isImage && typeof message === 'string' && executeCommand(socket, client, message)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!toUserName) {
|
||||
socket.emit('error', { message: 'Empfänger fehlt' });
|
||||
|
||||
Reference in New Issue
Block a user