- Updated the statistics for 'today' to include a list of unique users who logged in, improving the detail and usefulness of the data presented. - Modified the command table structure to display metrics more clearly, including the date, total logins, and unique users logged in today. These changes enhance the clarity and depth of user engagement statistics.
1385 lines
46 KiB
JavaScript
1385 lines
46 KiB
JavaScript
import crypto from 'crypto';
|
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
import { join } from 'path';
|
|
import axios from 'axios';
|
|
|
|
const TIMEOUT_SECONDS = 1800; // 30 Minuten
|
|
|
|
class Client {
|
|
constructor(sessionId) {
|
|
this.sessionId = sessionId;
|
|
this.gender = '';
|
|
this.country = '';
|
|
this.isoCountryCode = '';
|
|
this.userName = '';
|
|
this.age = 0;
|
|
this.conversations = {};
|
|
this.lastActivity = new Date();
|
|
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() {
|
|
this.lastActivity = new Date();
|
|
}
|
|
|
|
activitiesTimedOut() {
|
|
const secondsSinceActivity = (new Date() - this.lastActivity) / 1000;
|
|
return secondsSinceActivity > TIMEOUT_SECONDS;
|
|
}
|
|
|
|
remainingSecondsToTimeout() {
|
|
const secondsSinceActivity = (new Date() - this.lastActivity) / 1000;
|
|
return Math.max(0, TIMEOUT_SECONDS - secondsSinceActivity);
|
|
}
|
|
|
|
currentlyLoggedInSeconds() {
|
|
return Math.floor((new Date() - this.loginTimeStamp) / 1000);
|
|
}
|
|
|
|
toJSON() {
|
|
return {
|
|
sessionId: this.sessionId,
|
|
userName: this.userName,
|
|
gender: this.gender,
|
|
country: this.country,
|
|
isoCountryCode: this.isoCountryCode,
|
|
age: this.age
|
|
};
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
// Security: never create predictable default credentials.
|
|
// Admin users must be configured explicitly in logs/chat-users.json.
|
|
writeFileSync(usersPath, '[]\n', 'utf-8');
|
|
console.warn(
|
|
`[Auth] ${CHAT_USERS_FILE_NAME} wurde neu erstellt. Bitte mindestens einen Admin-User mit Passwort-Hash konfigurieren.`
|
|
);
|
|
}
|
|
|
|
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)
|
|
|
|
// Map: Socket-ID -> Express-Session-ID (für Session-Wiederherstellung)
|
|
let socketToSessionMap = new Map();
|
|
|
|
// Exportiere clients Map für Zugriff von außen
|
|
export function getClientsMap() {
|
|
return clients;
|
|
}
|
|
|
|
// Exportiere Funktion zum Abrufen der Session-ID für einen Socket
|
|
export function getSessionIdForSocket(socketId) {
|
|
return socketToSessionMap.get(socketId);
|
|
}
|
|
|
|
export function extractSessionId(handshakeOrRequest) {
|
|
// Unterstützt sowohl Socket.IO handshake als auch Express request
|
|
const cookies = handshakeOrRequest.headers?.cookie || handshakeOrRequest.cookies || '';
|
|
|
|
console.log('extractSessionId - Cookies:', cookies);
|
|
|
|
const sessionMatch = cookies.match(/connect\.sid=([^;]+)/);
|
|
if (sessionMatch) {
|
|
let sessionId = sessionMatch[1];
|
|
|
|
console.log('extractSessionId - Gefundenes Cookie:', sessionId);
|
|
|
|
// Express-Session speichert die Session-ID als signierten Wert: s:xxxxx.signature
|
|
// Die tatsächliche Session-ID ist nur xxxxx
|
|
if (sessionId.startsWith('s:')) {
|
|
const parts = sessionId.split('.');
|
|
if (parts.length > 0) {
|
|
sessionId = parts[0].substring(2); // Entferne 's:' Präfix
|
|
console.log('extractSessionId - Bereinigte Session-ID:', sessionId);
|
|
}
|
|
}
|
|
|
|
return sessionId;
|
|
}
|
|
|
|
console.log('extractSessionId - Kein connect.sid Cookie gefunden');
|
|
// Fallback: Generiere temporäre UUID
|
|
return crypto.randomUUID();
|
|
}
|
|
|
|
// Exportiere Funktion zum Prüfen des Session-Status
|
|
export function getSessionStatus(req) {
|
|
// Verwende req.sessionID von Express-Session
|
|
const sessionId = req.sessionID || extractSessionId(req);
|
|
|
|
if (!sessionId) {
|
|
return null;
|
|
}
|
|
|
|
const client = clients.get(sessionId);
|
|
|
|
if (!client || !client.userName) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
sessionId: client.sessionId,
|
|
userName: client.userName,
|
|
gender: client.gender,
|
|
age: client.age,
|
|
country: client.country,
|
|
isoCountryCode: client.isoCountryCode
|
|
};
|
|
}
|
|
|
|
function getConversationKey(user1, user2) {
|
|
return [user1, user2].sort().join(':');
|
|
}
|
|
|
|
function logClientLogin(client, __dirname) {
|
|
try {
|
|
const logsDir = join(__dirname, '../logs');
|
|
// Erstelle logs-Verzeichnis falls es nicht existiert
|
|
if (!existsSync(logsDir)) {
|
|
try {
|
|
mkdirSync(logsDir, { recursive: true });
|
|
console.log(`[Log] Logs-Verzeichnis erstellt: ${logsDir}`);
|
|
} catch (mkdirError) {
|
|
console.error(`[Log] Fehler beim Erstellen des Logs-Verzeichnisses: ${mkdirError.message}`);
|
|
return; // Beende Funktion, wenn Verzeichnis nicht erstellt werden kann
|
|
}
|
|
}
|
|
const logPath = join(logsDir, 'logins.log');
|
|
const logEntry = `${new Date().toISOString()},${client.userName},${client.country},${client.age},${client.gender}\n`;
|
|
appendFileSync(logPath, logEntry, 'utf-8');
|
|
} catch (error) {
|
|
console.error('Fehler beim Loggen des Logins:', error.message);
|
|
}
|
|
}
|
|
|
|
function checkAndLogStart(__dirname) {
|
|
try {
|
|
const logsDir = join(__dirname, '../logs');
|
|
// Erstelle logs-Verzeichnis falls es nicht existiert
|
|
if (!existsSync(logsDir)) {
|
|
try {
|
|
mkdirSync(logsDir, { recursive: true });
|
|
console.log(`[Log] Logs-Verzeichnis erstellt: ${logsDir}`);
|
|
} catch (mkdirError) {
|
|
console.error(`[Log] Fehler beim Erstellen des Logs-Verzeichnisses: ${mkdirError.message}`);
|
|
return; // Beende Funktion, wenn Verzeichnis nicht erstellt werden kann
|
|
}
|
|
}
|
|
const logPath = join(logsDir, 'starts.log');
|
|
const logEntry = `${new Date().toISOString()}\n`;
|
|
appendFileSync(logPath, logEntry, 'utf-8');
|
|
} catch (error) {
|
|
console.error('Fehler beim Loggen des Starts:', error.message);
|
|
}
|
|
}
|
|
|
|
export function setupBroadcast(io, __dirname) {
|
|
// Länderliste beim Start laden
|
|
let countriesMap = {};
|
|
|
|
async function downloadCountries() {
|
|
try {
|
|
const response = await axios.get('https://pkgstore.datahub.io/core/country-list/data_csv/data/d7c9d7cfb42cb69f4422dec222dbbaa8/data_csv.csv');
|
|
const lines = response.data.split('\n');
|
|
countriesMap = {};
|
|
|
|
// Parse CSV-Zeile mit Berücksichtigung von Anführungszeichen
|
|
const parseCSVLine = (line) => {
|
|
const result = [];
|
|
let current = '';
|
|
let inQuotes = false;
|
|
|
|
for (let i = 0; i < line.length; i++) {
|
|
const char = line[i];
|
|
if (char === '"') {
|
|
// Ignoriere Anführungszeichen, sie werden nicht zum Wert hinzugefügt
|
|
inQuotes = !inQuotes;
|
|
} else if (char === ',' && !inQuotes) {
|
|
result.push(current.trim());
|
|
current = '';
|
|
} else {
|
|
current += char;
|
|
}
|
|
}
|
|
result.push(current.trim());
|
|
return result;
|
|
};
|
|
|
|
for (let i = 1; i < lines.length; i++) {
|
|
const line = lines[i].trim();
|
|
if (!line) continue;
|
|
|
|
const [name, code] = parseCSVLine(line);
|
|
if (name && code) {
|
|
// Entferne alle Anführungszeichen (auch am Anfang/Ende)
|
|
const cleanName = name.replace(/^["']+|["']+$/g, '').replace(/["']/g, '').trim();
|
|
const cleanCode = code.replace(/^["']+|["']+$/g, '').replace(/["']/g, '').trim();
|
|
if (cleanName && cleanCode) {
|
|
countriesMap[cleanName] = cleanCode.toLowerCase();
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log(`Länderliste geladen: ${Object.keys(countriesMap).length} Länder`);
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Länderliste:', error);
|
|
}
|
|
}
|
|
|
|
downloadCountries();
|
|
setInterval(downloadCountries, 24 * 60 * 60 * 1000); // Täglich aktualisieren
|
|
|
|
function sendCommandResult(socket, lines, kind = 'info') {
|
|
const payload = Array.isArray(lines) ? lines : [String(lines)];
|
|
socket.emit('commandResult', { lines: payload, kind });
|
|
}
|
|
|
|
function sendCommandTable(socket, title, columns, rows) {
|
|
socket.emit('commandTable', {
|
|
title: String(title || 'Ausgabe'),
|
|
columns: Array.isArray(columns) ? columns.map((c) => String(c)) : [],
|
|
rows: Array.isArray(rows) ? rows : []
|
|
});
|
|
}
|
|
|
|
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') {
|
|
sendCommandTable(socket, 'Hilfe: Statistik-Befehle', ['Befehl', 'Beschreibung'], [
|
|
['/stat today', 'Logins des heutigen Tages'],
|
|
['/stat date YYYY-MM-DD', 'Logins an einem bestimmten Datum'],
|
|
['/stat range YYYY-MM-DD YYYY-MM-DD', 'Logins pro Tag im Zeitraum'],
|
|
['/stat ages', 'Jüngster und ältester Nutzer'],
|
|
['/stat names', 'Häufigkeit der verwendeten Namen'],
|
|
['/stat countries', 'Häufigkeit der Länder'],
|
|
['/all-stats', 'Zusammenfassung wichtiger Kennzahlen']
|
|
]);
|
|
return;
|
|
}
|
|
|
|
if (sub === 'today') {
|
|
const day = new Date().toISOString().slice(0, 10);
|
|
const dayRecords = records.filter((r) => r.day === day);
|
|
const uniqueUsersToday = Array.from(new Set(dayRecords.map((r) => r.userName)))
|
|
.sort((a, b) => a.localeCompare(b, 'de'));
|
|
sendCommandTable(socket, 'Statistik: Heute', ['Metrik', 'Wert'], [
|
|
['Tag', day],
|
|
['Logins', dayRecords.length],
|
|
['Heute eingeloggt', uniqueUsersToday.length > 0 ? uniqueUsersToday.join(', ') : '-']
|
|
]);
|
|
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);
|
|
sendCommandTable(socket, 'Statistik: Datum', ['Tag', 'Logins'], [[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]);
|
|
sendCommandTable(socket, `Statistik: Zeitraum ${from} bis ${to}`, ['Tag', 'Logins'], 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);
|
|
sendCommandTable(socket, 'Statistik: Alter', ['Kategorie', 'Name', 'Alter'], [
|
|
['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);
|
|
sendCommandTable(
|
|
socket,
|
|
`Statistik: Namen (gesamt verschieden: ${new Set(records.map((r) => r.userName)).size})`,
|
|
['Name', 'Anzahl'],
|
|
topNames.map(([name, count]) => [name, count])
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (sub === 'countries') {
|
|
const topCountries = aggregateTop(records, (r) => r.country, 20);
|
|
sendCommandTable(
|
|
socket,
|
|
'Statistik: Länder',
|
|
['Land', 'Anzahl'],
|
|
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;
|
|
}
|
|
const lines = buildAllStats(readLoginRecords());
|
|
const rows = lines.map((line) => {
|
|
const separatorIndex = line.indexOf(':');
|
|
if (separatorIndex === -1) {
|
|
return [line, ''];
|
|
}
|
|
return [line.slice(0, separatorIndex).trim(), line.slice(separatorIndex + 1).trim()];
|
|
});
|
|
sendCommandTable(socket, 'Statistik: Übersicht', ['Metrik', 'Wert'], rows);
|
|
}
|
|
|
|
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();
|
|
|
|
// Laufender Login-Dialog: Nur Eingaben ohne Slash als Username/Passwort behandeln.
|
|
if (client.pendingChatLogin && !input.startsWith('/')) {
|
|
if (client.pendingChatLogin.step === 'username') {
|
|
const enteredUser = input;
|
|
if (!enteredUser) {
|
|
sendCommandResult(socket, 'Username darf nicht leer sein. Bitte Username eingeben:', 'loginPromptUsername');
|
|
return true;
|
|
}
|
|
client.pendingChatLogin = { step: 'password', username: enteredUser };
|
|
sendCommandResult(socket, 'Passwort eingeben:', 'loginPromptPassword');
|
|
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.', 'loginError');
|
|
return true;
|
|
}
|
|
|
|
client.chatAuth = auth;
|
|
sendCommandResult(
|
|
socket,
|
|
`Login erfolgreich als ${auth.username}. Rechte: ${Array.from(auth.rights).join(', ') || 'keine'}`,
|
|
'loginSuccess'
|
|
);
|
|
return true;
|
|
}
|
|
} else if (client.pendingChatLogin && input.startsWith('/')) {
|
|
// Ein neuer /Befehl bricht den Login-Vorgang ab.
|
|
client.pendingChatLogin = null;
|
|
sendCommandResult(socket, 'Login-Vorgang abgebrochen.', 'loginAbort');
|
|
// und läuft unten als normaler Befehl weiter
|
|
}
|
|
|
|
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:', 'loginPromptPassword');
|
|
} else {
|
|
client.pendingChatLogin = { step: 'username', username: '' };
|
|
sendCommandResult(socket, 'Username eingeben:', 'loginPromptUsername');
|
|
}
|
|
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.',
|
|
'loginLogout'
|
|
);
|
|
return true;
|
|
}
|
|
|
|
if (command === '/whoami-rights') {
|
|
if (!client.chatAuth) {
|
|
sendCommandResult(socket, 'Nicht per Command-Login angemeldet.', 'whoami');
|
|
return true;
|
|
}
|
|
sendCommandResult(socket, [
|
|
`Angemeldet als: ${client.chatAuth.username}`,
|
|
`Rechte: ${Array.from(client.chatAuth.rights).join(', ') || 'keine'}`
|
|
], 'whoami');
|
|
return true;
|
|
}
|
|
|
|
if (command === '/help' || command === '/?') {
|
|
sendCommandTable(socket, 'Hilfe: Verfügbare Befehle', ['Befehl', 'Beschreibung'], [
|
|
['/login [username]', 'Admin-/Command-Login starten'],
|
|
['/logout-admin', 'Admin-/Command-Login beenden'],
|
|
['/whoami-rights', 'Aktuelle Admin-Rechte anzeigen'],
|
|
['/stat help', 'Hilfe zu Statistikbefehlen anzeigen'],
|
|
['/all-stats', 'Zusammenfassung wichtiger Statistiken'],
|
|
['/kick <username>', 'Benutzer aus dem Chat werfen'],
|
|
['/help oder /?', 'Diese Hilfe anzeigen']
|
|
]);
|
|
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}`, 'unknown');
|
|
return true;
|
|
}
|
|
|
|
// Socket.IO-Verbindungshandler
|
|
io.on('connection', (socket) => {
|
|
const request = socket.handshake;
|
|
|
|
// Versuche Session-ID aus Cookie zu extrahieren
|
|
let sessionId = extractSessionId(request);
|
|
|
|
console.log('Socket.IO Connect - Session-ID (aus Cookie):', sessionId);
|
|
console.log('Socket.IO Connect - Cookie:', request.headers?.cookie);
|
|
|
|
// Wenn keine gültige Session-ID gefunden wurde, generiere eine temporäre UUID
|
|
// Diese wird beim Login durch die Express-Session-ID ersetzt (siehe handleLogin)
|
|
if (!sessionId || sessionId.length < 10 || !sessionId.match(/^[a-zA-Z0-9_-]+$/)) {
|
|
sessionId = crypto.randomUUID();
|
|
console.log('Socket.IO Connect - Keine gültige Session-ID gefunden, verwende temporäre UUID:', sessionId);
|
|
socket.data.temporarySessionId = true; // Markiere als temporär
|
|
}
|
|
|
|
// Speichere Socket-ID mit Session-ID
|
|
socket.data.sessionId = sessionId;
|
|
|
|
let client = clients.get(sessionId);
|
|
if (!client) {
|
|
console.log('Socket.IO Connect - Neuer Client erstellt für Session-ID:', sessionId);
|
|
client = new Client(sessionId);
|
|
clients.set(sessionId, client);
|
|
} else {
|
|
console.log('Socket.IO Connect - Bestehender Client gefunden für Session-ID:', sessionId, 'User:', client.userName);
|
|
}
|
|
|
|
// Speichere Socket mit Session-ID
|
|
client.socket = socket;
|
|
|
|
// Bestätigung an Client senden, inklusive Benutzerdaten falls bereits eingeloggt
|
|
const connectedData = { sessionId };
|
|
if (client.userName) {
|
|
connectedData.user = {
|
|
userName: client.userName,
|
|
gender: client.gender,
|
|
age: client.age,
|
|
country: client.country,
|
|
isoCountryCode: client.isoCountryCode
|
|
};
|
|
connectedData.loggedIn = true;
|
|
}
|
|
socket.emit('connected', connectedData);
|
|
|
|
socket.on('disconnect', (reason) => {
|
|
console.log(`[Disconnect] Socket getrennt für Session-ID: ${sessionId}, Grund: ${reason}`);
|
|
const client = clients.get(sessionId);
|
|
if (client) {
|
|
console.log(`[Disconnect] Client gefunden: ${client.userName || 'unbekannt'}, Socket war verbunden: ${client.socket ? client.socket.connected : 'null'}`);
|
|
|
|
// Setze Socket auf null, damit keine Nachrichten mehr an diesen Client gesendet werden
|
|
// ABER: Lösche den Client NICHT, damit die Session beim Reload wiederhergestellt werden kann
|
|
client.socket = null;
|
|
|
|
// Aktualisiere Benutzerliste, damit andere Clients sehen, dass dieser Benutzer offline ist
|
|
if (client.userName) {
|
|
console.log(`[Disconnect] Aktualisiere Benutzerliste nach Disconnect von ${client.userName}`);
|
|
broadcastUserList();
|
|
}
|
|
// Client bleibt in der Map, damit Session-Wiederherstellung funktioniert
|
|
} else {
|
|
console.log(`[Disconnect] Kein Client gefunden für Session-ID: ${sessionId}`);
|
|
}
|
|
});
|
|
|
|
socket.on('error', (error) => {
|
|
console.error('Socket.IO-Fehler:', error);
|
|
});
|
|
|
|
// Socket.IO Event-Handler: Setze Express-Session-ID
|
|
socket.on('setSessionId', (data) => {
|
|
const { expressSessionId } = data;
|
|
if (expressSessionId) {
|
|
console.log('setSessionId - Express-Session-ID erhalten:', expressSessionId);
|
|
const currentSessionId = socket.data.sessionId;
|
|
|
|
if (currentSessionId !== expressSessionId) {
|
|
console.log('setSessionId - Aktualisiere Session-ID von', currentSessionId, 'zu', expressSessionId);
|
|
|
|
// Prüfe, ob bereits ein Client mit dieser Session-ID existiert
|
|
let existingClient = clients.get(expressSessionId);
|
|
|
|
// Wenn kein Client mit dieser Session-ID gefunden wurde, prüfe ob ein Client mit dem aktuellen Socket existiert
|
|
// und bereits eingeloggt ist (für Session-Wiederherstellung nach Reload)
|
|
if (!existingClient) {
|
|
const currentClient = clients.get(currentSessionId);
|
|
if (currentClient && currentClient.userName) {
|
|
console.log('setSessionId - Client mit Session-ID', currentSessionId, 'ist bereits eingeloggt:', currentClient.userName);
|
|
// Verwende bestehenden Client und aktualisiere nur die Session-ID
|
|
existingClient = currentClient;
|
|
clients.delete(currentSessionId);
|
|
clients.set(expressSessionId, existingClient);
|
|
existingClient.sessionId = expressSessionId;
|
|
} else {
|
|
// Wenn kein Client mit der aktuellen Session-ID gefunden wurde, suche nach einem Client,
|
|
// der bereits eingeloggt ist und keinen aktiven Socket hat (für Session-Wiederherstellung)
|
|
// Dies ist ein Fallback für den Fall, dass die Session-ID beim Reload geändert wurde
|
|
for (const [sid, c] of clients.entries()) {
|
|
if (c.userName && (!c.socket || !c.socket.connected)) {
|
|
console.log('setSessionId - Gefundener Client ohne aktiven Socket:', sid, c.userName);
|
|
// Verwende diesen Client und aktualisiere die Session-ID
|
|
existingClient = c;
|
|
clients.delete(sid);
|
|
clients.set(expressSessionId, existingClient);
|
|
existingClient.sessionId = expressSessionId;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (existingClient) {
|
|
// Verwende bestehenden Client
|
|
console.log('setSessionId - Verwende bestehenden Client, User:', existingClient.userName || 'nicht eingeloggt');
|
|
existingClient.socket = socket;
|
|
socket.data.sessionId = expressSessionId;
|
|
|
|
// Entferne alten Client, falls unterschiedlich
|
|
if (currentSessionId !== expressSessionId && clients.has(currentSessionId)) {
|
|
clients.delete(currentSessionId);
|
|
}
|
|
|
|
// Sende Login-Status zurück, falls bereits eingeloggt
|
|
if (existingClient.userName) {
|
|
console.log('setSessionId - Sende Login-Status zurück für User:', existingClient.userName);
|
|
socket.emit('connected', {
|
|
sessionId: expressSessionId,
|
|
loggedIn: true,
|
|
user: existingClient.toJSON()
|
|
});
|
|
|
|
// Aktualisiere Userliste für alle Clients, damit der wiederhergestellte Client die Liste erhält
|
|
broadcastUserList();
|
|
}
|
|
} else {
|
|
// Erstelle neuen Client mit Express-Session-ID
|
|
console.log('setSessionId - Erstelle neuen Client');
|
|
const newClient = new Client(expressSessionId);
|
|
newClient.socket = socket;
|
|
clients.set(expressSessionId, newClient);
|
|
socket.data.sessionId = expressSessionId;
|
|
|
|
// Entferne alten Client
|
|
if (clients.has(currentSessionId)) {
|
|
clients.delete(currentSessionId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Socket.IO Event-Handler
|
|
// WICHTIG: Hole den Client immer dynamisch basierend auf socket.data.sessionId,
|
|
// da sich die Session-ID nach setSessionId ändern kann
|
|
socket.on('login', async (data) => {
|
|
try {
|
|
const currentClient = clients.get(socket.data.sessionId);
|
|
if (!currentClient) {
|
|
socket.emit('error', { message: 'Client nicht gefunden' });
|
|
return;
|
|
}
|
|
currentClient.setActivity();
|
|
await handleLogin(socket, currentClient, data);
|
|
} catch (error) {
|
|
console.error('Fehler beim Verarbeiten der Login-Nachricht:', error);
|
|
socket.emit('error', { message: error.message });
|
|
}
|
|
});
|
|
|
|
socket.on('message', async (data) => {
|
|
try {
|
|
const currentClient = clients.get(socket.data.sessionId);
|
|
if (!currentClient) {
|
|
socket.emit('error', { message: 'Client nicht gefunden' });
|
|
return;
|
|
}
|
|
currentClient.setActivity();
|
|
handleMessage(socket, currentClient, data);
|
|
} catch (error) {
|
|
console.error('Fehler beim Verarbeiten der Message-Nachricht:', error);
|
|
socket.emit('error', { message: error.message });
|
|
}
|
|
});
|
|
|
|
socket.on('requestConversation', async (data) => {
|
|
try {
|
|
const currentClient = clients.get(socket.data.sessionId);
|
|
if (!currentClient) {
|
|
socket.emit('error', { message: 'Client nicht gefunden' });
|
|
return;
|
|
}
|
|
currentClient.setActivity();
|
|
handleRequestConversation(socket, currentClient, data);
|
|
} catch (error) {
|
|
console.error('Fehler beim Verarbeiten der RequestConversation-Nachricht:', error);
|
|
socket.emit('error', { message: error.message });
|
|
}
|
|
});
|
|
|
|
socket.on('userSearch', async (data) => {
|
|
try {
|
|
const currentClient = clients.get(socket.data.sessionId);
|
|
if (!currentClient) {
|
|
socket.emit('error', { message: 'Client nicht gefunden' });
|
|
return;
|
|
}
|
|
currentClient.setActivity();
|
|
handleUserSearch(socket, currentClient, data);
|
|
} catch (error) {
|
|
console.error('Fehler beim Verarbeiten der UserSearch-Nachricht:', error);
|
|
socket.emit('error', { message: error.message });
|
|
}
|
|
});
|
|
|
|
socket.on('requestHistory', async () => {
|
|
try {
|
|
const currentClient = clients.get(socket.data.sessionId);
|
|
if (!currentClient) {
|
|
socket.emit('error', { message: 'Client nicht gefunden' });
|
|
return;
|
|
}
|
|
currentClient.setActivity();
|
|
handleRequestHistory(socket, currentClient);
|
|
} catch (error) {
|
|
console.error('Fehler beim Verarbeiten der RequestHistory-Nachricht:', error);
|
|
socket.emit('error', { message: error.message });
|
|
}
|
|
});
|
|
|
|
socket.on('requestOpenConversations', async () => {
|
|
try {
|
|
const currentClient = clients.get(socket.data.sessionId);
|
|
if (!currentClient) {
|
|
socket.emit('error', { message: 'Client nicht gefunden' });
|
|
return;
|
|
}
|
|
currentClient.setActivity();
|
|
handleRequestOpenConversations(socket, currentClient);
|
|
} catch (error) {
|
|
console.error('Fehler beim Verarbeiten der RequestOpenConversations-Nachricht:', error);
|
|
socket.emit('error', { message: error.message });
|
|
}
|
|
});
|
|
|
|
socket.on('blockUser', async (data) => {
|
|
try {
|
|
const currentClient = clients.get(socket.data.sessionId);
|
|
if (!currentClient) {
|
|
socket.emit('error', { message: 'Client nicht gefunden' });
|
|
return;
|
|
}
|
|
currentClient.setActivity();
|
|
handleBlockUser(socket, currentClient, data);
|
|
} catch (error) {
|
|
console.error('Fehler beim Verarbeiten der BlockUser-Nachricht:', error);
|
|
socket.emit('error', { message: error.message });
|
|
}
|
|
});
|
|
|
|
socket.on('unblockUser', async (data) => {
|
|
try {
|
|
const currentClient = clients.get(socket.data.sessionId);
|
|
if (!currentClient) {
|
|
socket.emit('error', { message: 'Client nicht gefunden' });
|
|
return;
|
|
}
|
|
currentClient.setActivity();
|
|
handleUnblockUser(socket, currentClient, data);
|
|
} catch (error) {
|
|
console.error('Fehler beim Verarbeiten der UnblockUser-Nachricht:', error);
|
|
socket.emit('error', { message: error.message });
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
async function handleLogin(socket, client, data) {
|
|
const { userName, gender, age, country, expressSessionId } = data;
|
|
|
|
// Validierung
|
|
if (!userName || userName.trim().length < 3) {
|
|
socket.emit('error', { message: 'Benutzername muss mindestens 3 Zeichen lang sein' });
|
|
return;
|
|
}
|
|
|
|
// Wenn eine Express-Session-ID übergeben wurde, verwende diese statt der Socket-ID
|
|
if (expressSessionId) {
|
|
console.log('handleLogin - Express-Session-ID erhalten:', expressSessionId);
|
|
|
|
// Entferne alten Client mit temporärer Socket-ID, falls vorhanden
|
|
const oldSessionId = socket.data.sessionId;
|
|
if (oldSessionId && oldSessionId !== expressSessionId && clients.has(oldSessionId)) {
|
|
console.log('handleLogin - Entferne alten Client mit Session-ID:', oldSessionId);
|
|
clients.delete(oldSessionId);
|
|
}
|
|
|
|
// Prüfe, ob bereits ein Client mit dieser Session-ID existiert
|
|
let existingClient = clients.get(expressSessionId);
|
|
if (existingClient) {
|
|
// Verwende bestehenden Client
|
|
console.log('handleLogin - Verwende bestehenden Client mit Session-ID:', expressSessionId);
|
|
client = existingClient;
|
|
// Aktualisiere Socket-Verbindung
|
|
client.socket = socket;
|
|
socket.data.sessionId = expressSessionId;
|
|
} else {
|
|
// Erstelle neuen Client mit Express-Session-ID
|
|
console.log('handleLogin - Erstelle neuen Client mit Session-ID:', expressSessionId);
|
|
client = new Client(expressSessionId);
|
|
clients.set(expressSessionId, client);
|
|
client.socket = socket;
|
|
socket.data.sessionId = expressSessionId;
|
|
}
|
|
} else {
|
|
console.log('handleLogin - Keine Express-Session-ID erhalten, verwende Socket-ID');
|
|
}
|
|
|
|
// Prüfe, ob Name bereits verwendet wird
|
|
for (const [sid, c] of clients.entries()) {
|
|
if (c.userName === userName && sid !== client.sessionId) {
|
|
socket.emit('error', { message: 'Dieser Benutzername ist bereits vergeben' });
|
|
return;
|
|
}
|
|
}
|
|
|
|
client.userName = userName.trim();
|
|
client.gender = gender;
|
|
client.age = age;
|
|
client.country = country;
|
|
|
|
// ISO-Code aus Länderliste ermitteln
|
|
client.isoCountryCode = countriesMap[country] || 'unknown';
|
|
|
|
client.loginTimeStamp = new Date();
|
|
client.setActivity();
|
|
|
|
// Stelle sicher, dass der Socket gesetzt ist
|
|
if (!client.socket) {
|
|
console.log('handleLogin - WARNUNG: Socket nicht gesetzt, setze ihn jetzt');
|
|
client.socket = socket;
|
|
}
|
|
|
|
console.log('handleLogin - Client nach Login:', {
|
|
sessionId: client.sessionId,
|
|
userName: client.userName,
|
|
hasSocket: !!client.socket,
|
|
socketConnected: client.socket ? client.socket.connected : false
|
|
});
|
|
|
|
logClientLogin(client, __dirname);
|
|
checkAndLogStart(__dirname);
|
|
|
|
// Benutzerliste an alle senden
|
|
broadcastUserList();
|
|
|
|
// Aktualisiere ungelesene Nachrichten für den neuen Benutzer
|
|
updateUnreadCount(client);
|
|
|
|
console.log('handleLogin - Sende loginSuccess an Socket');
|
|
socket.emit('loginSuccess', {
|
|
sessionId: client.sessionId,
|
|
user: client.toJSON()
|
|
});
|
|
}
|
|
|
|
function handleMessage(socket, client, data) {
|
|
if (!client.userName) {
|
|
socket.emit('error', { message: 'Nicht eingeloggt' });
|
|
return;
|
|
}
|
|
|
|
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' });
|
|
return;
|
|
}
|
|
|
|
// Finde Empfänger
|
|
let receiver = null;
|
|
for (const [sid, c] of clients.entries()) {
|
|
if (c.userName === toUserName) {
|
|
receiver = c;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!receiver) {
|
|
socket.emit('error', { message: 'Benutzer nicht gefunden' });
|
|
return;
|
|
}
|
|
|
|
// Prüfe, ob Empfänger noch verbunden ist
|
|
if (!receiver.socket || !receiver.socket.connected) {
|
|
// Empfänger ist nicht mehr verbunden, entferne ihn aus der Liste
|
|
clients.delete(receiver.sessionId);
|
|
broadcastUserList();
|
|
socket.emit('error', { message: 'Benutzer nicht gefunden' });
|
|
return;
|
|
}
|
|
|
|
// Prüfe Blockierung
|
|
if (receiver.blockedUsers.has(client.userName)) {
|
|
socket.emit('error', { message: 'Du wurdest von diesem Benutzer blockiert' });
|
|
return;
|
|
}
|
|
|
|
// Speichere Nachricht in Konversation
|
|
const convKey = getConversationKey(client.userName, toUserName);
|
|
if (!conversations.has(convKey)) {
|
|
conversations.set(convKey, []);
|
|
}
|
|
|
|
const conversation = conversations.get(convKey);
|
|
conversation.push({
|
|
from: client.userName,
|
|
to: toUserName,
|
|
message: isImage && imageUrl ? imageUrl : message, // Verwende URL für Bilder
|
|
messageId,
|
|
timestamp: new Date().toISOString(),
|
|
read: false,
|
|
isImage: isImage || false,
|
|
imageType: imageType || null,
|
|
imageUrl: imageUrl || null,
|
|
imageCode: isImage ? message : null
|
|
});
|
|
|
|
// Sende an Empfänger (wenn online)
|
|
const messagePayload = {
|
|
from: client.userName,
|
|
message: isImage && imageUrl ? imageUrl : message, // Verwende URL für Bilder
|
|
messageId,
|
|
timestamp: new Date().toISOString(),
|
|
isImage: isImage || false,
|
|
imageType: imageType || null,
|
|
imageUrl: imageUrl || null,
|
|
imageCode: isImage ? message : null // Code für Server-Referenz
|
|
};
|
|
|
|
// Debug-Logging für Bilder
|
|
if (isImage) {
|
|
console.log(`[Bild] Sende Bild von ${client.userName} an ${toUserName}, Größe: ${message ? message.length : 0} Zeichen, Typ: ${imageType || 'unbekannt'}`);
|
|
console.log(`[Bild] Absender Socket verbunden: ${socket.connected}, Empfänger Socket verbunden: ${receiver.socket ? receiver.socket.connected : 'null'}`);
|
|
}
|
|
|
|
// Prüfe, ob Absender noch verbunden ist
|
|
if (!socket.connected) {
|
|
console.error(`[Bild] Absender ${client.userName} Socket nicht mehr verbunden beim Senden!`);
|
|
return;
|
|
}
|
|
|
|
// Prüfe, ob Empfänger-Socket noch existiert und verbunden ist
|
|
if (!receiver.socket || !receiver.socket.connected) {
|
|
console.error(`[Bild] Empfänger ${toUserName} Socket nicht mehr verbunden beim Senden!`);
|
|
socket.emit('error', { message: 'Empfänger ist nicht mehr online' });
|
|
return;
|
|
}
|
|
|
|
try {
|
|
receiver.socket.emit('message', messagePayload);
|
|
|
|
// Bestätigung an Absender (nur wenn noch verbunden)
|
|
if (socket.connected) {
|
|
socket.emit('messageSent', {
|
|
messageId,
|
|
to: toUserName
|
|
});
|
|
}
|
|
} catch (error) {
|
|
console.error(`[Bild] Fehler beim Senden der Nachricht:`, error);
|
|
if (socket.connected) {
|
|
socket.emit('error', { message: 'Fehler beim Senden der Nachricht' });
|
|
}
|
|
}
|
|
|
|
// Aktualisiere ungelesene Nachrichten für den Empfänger
|
|
updateUnreadCount(receiver);
|
|
}
|
|
|
|
function handleRequestConversation(socket, client, data) {
|
|
if (!client.userName) {
|
|
socket.emit('error', { message: 'Nicht eingeloggt' });
|
|
return;
|
|
}
|
|
|
|
const { withUserName } = data;
|
|
const convKey = getConversationKey(client.userName, withUserName);
|
|
const conversation = conversations.get(convKey) || [];
|
|
|
|
// Markiere alle Nachrichten von diesem Benutzer als gelesen
|
|
for (const msg of conversation) {
|
|
if (msg.to === client.userName && msg.from === withUserName) {
|
|
msg.read = true;
|
|
}
|
|
}
|
|
|
|
// Aktualisiere ungelesene Nachrichten nach dem Markieren als gelesen
|
|
updateUnreadCount(client);
|
|
|
|
socket.emit('conversation', {
|
|
with: withUserName,
|
|
messages: conversation.map(msg => ({
|
|
from: msg.from,
|
|
message: msg.message,
|
|
timestamp: msg.timestamp,
|
|
isImage: msg.isImage || false,
|
|
imageType: msg.imageType || null
|
|
}))
|
|
});
|
|
}
|
|
|
|
function handleUserSearch(socket, client, data) {
|
|
if (!client.userName) {
|
|
socket.emit('error', { message: 'Nicht eingeloggt' });
|
|
return;
|
|
}
|
|
|
|
const { nameIncludes, minAge, maxAge, countries, genders } = data;
|
|
|
|
const results = [];
|
|
for (const [sid, c] of clients.entries()) {
|
|
if (!c.userName || c.userName === client.userName) continue;
|
|
|
|
// Name-Filter
|
|
if (nameIncludes && !c.userName.toLowerCase().includes(nameIncludes.toLowerCase())) {
|
|
continue;
|
|
}
|
|
|
|
// Alter-Filter
|
|
if (minAge && c.age < minAge) continue;
|
|
if (maxAge && c.age > maxAge) continue;
|
|
|
|
// Länder-Filter
|
|
if (countries && countries.length > 0 && !countries.includes(c.country)) {
|
|
continue;
|
|
}
|
|
|
|
// Geschlecht-Filter
|
|
if (genders && genders.length > 0 && !genders.includes(c.gender)) {
|
|
continue;
|
|
}
|
|
|
|
results.push(c.toJSON());
|
|
}
|
|
|
|
socket.emit('searchResults', {
|
|
results
|
|
});
|
|
}
|
|
|
|
function handleRequestHistory(socket, client) {
|
|
if (!client.userName) {
|
|
socket.emit('error', { message: 'Nicht eingeloggt' });
|
|
return;
|
|
}
|
|
|
|
const history = [];
|
|
for (const [convKey, messages] of conversations.entries()) {
|
|
const [user1, user2] = convKey.split(':');
|
|
if (user1 === client.userName || user2 === client.userName) {
|
|
const otherUser = user1 === client.userName ? user2 : user1;
|
|
history.push({
|
|
userName: otherUser,
|
|
lastMessage: messages.length > 0 ? messages[messages.length - 1] : null
|
|
});
|
|
}
|
|
}
|
|
|
|
socket.emit('historyResults', {
|
|
results: history
|
|
});
|
|
}
|
|
|
|
function handleRequestOpenConversations(socket, client) {
|
|
if (!client.userName) {
|
|
socket.emit('error', { message: 'Nicht eingeloggt' });
|
|
return;
|
|
}
|
|
|
|
const inbox = [];
|
|
for (const [convKey, messages] of conversations.entries()) {
|
|
const [user1, user2] = convKey.split(':');
|
|
if (user1 === client.userName || user2 === client.userName) {
|
|
const otherUser = user1 === client.userName ? user2 : user1;
|
|
const unreadCount = messages.filter(m =>
|
|
m.to === client.userName && !m.read
|
|
).length;
|
|
|
|
if (unreadCount > 0) {
|
|
inbox.push({
|
|
userName: otherUser,
|
|
unreadCount
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
socket.emit('inboxResults', {
|
|
results: inbox
|
|
});
|
|
}
|
|
|
|
function handleBlockUser(socket, client, data) {
|
|
if (!client.userName) {
|
|
socket.emit('error', { message: 'Nicht eingeloggt' });
|
|
return;
|
|
}
|
|
|
|
const { userName } = data;
|
|
client.blockedUsers.add(userName);
|
|
|
|
socket.emit('userBlocked', {
|
|
userName
|
|
});
|
|
}
|
|
|
|
function handleUnblockUser(socket, client, data) {
|
|
if (!client.userName) {
|
|
socket.emit('error', { message: 'Nicht eingeloggt' });
|
|
return;
|
|
}
|
|
|
|
const { userName } = data;
|
|
client.blockedUsers.delete(userName);
|
|
|
|
socket.emit('userUnblocked', {
|
|
userName
|
|
});
|
|
}
|
|
|
|
function updateUnreadCount(client) {
|
|
if (!client.userName || !client.socket || !client.socket.connected) {
|
|
return;
|
|
}
|
|
|
|
// Zähle Personen mit ungelesenen Nachrichten für diesen Benutzer
|
|
let unreadPersons = 0;
|
|
for (const [convKey, messages] of conversations.entries()) {
|
|
const [user1, user2] = convKey.split(':');
|
|
if (user1 === client.userName || user2 === client.userName) {
|
|
const unreadCount = messages.filter(m =>
|
|
m.to === client.userName && !m.read
|
|
).length;
|
|
if (unreadCount > 0) {
|
|
unreadPersons++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sende Update an Client
|
|
client.socket.emit('unreadChats', {
|
|
count: unreadPersons
|
|
});
|
|
}
|
|
|
|
function broadcastUserList() {
|
|
// Filtere nur eingeloggte Benutzer mit aktiver Verbindung
|
|
const userList = Array.from(clients.values())
|
|
.filter(c => {
|
|
const hasUserName = !!c.userName;
|
|
const hasSocket = !!c.socket;
|
|
const isConnected = c.socket && c.socket.connected;
|
|
if (hasUserName && (!hasSocket || !isConnected)) {
|
|
console.log('broadcastUserList - Client ohne Socket oder nicht verbunden:', c.userName, 'socket:', hasSocket, 'connected:', isConnected);
|
|
}
|
|
return hasUserName && hasSocket && isConnected;
|
|
})
|
|
.map(c => c.toJSON());
|
|
|
|
console.log('broadcastUserList - Sende Userliste mit', userList.length, 'Benutzern:', userList.map(u => u.userName));
|
|
|
|
// Sende an alle verbundenen Clients
|
|
io.emit('userList', {
|
|
users: userList
|
|
});
|
|
}
|
|
|
|
function broadcastToUser(userName, data) {
|
|
// Finde den Client mit diesem Benutzernamen und sende die Nachricht
|
|
for (const [sid, c] of clients.entries()) {
|
|
if (c.userName === userName && c.socket) {
|
|
c.socket.emit(data.type, data);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Timeout-Check alle 60 Sekunden
|
|
setInterval(() => {
|
|
for (const [sid, client] of clients.entries()) {
|
|
if (client.activitiesTimedOut()) {
|
|
console.log(`Client ${client.userName} hat Timeout erreicht`);
|
|
clients.delete(sid);
|
|
broadcastUserList();
|
|
}
|
|
}
|
|
}, 60000);
|
|
}
|