import crypto from 'crypto'; import { readFileSync, writeFileSync, appendFileSync } 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 } 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 }; } } 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); } 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) { try { const logPath = join(process.cwd(), 'logs', '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); } } function checkAndLogStart() { try { const logPath = join(process.cwd(), 'logs', 'starts.log'); const logEntry = `${new Date().toISOString()}\n`; appendFileSync(logPath, logEntry, 'utf-8'); } catch (error) { console.error('Fehler beim Loggen des Starts:', error); } } export function setupBroadcast(io) { // 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 // 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) => { const client = clients.get(sessionId); if (client) { // 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) { broadcastUserList(); } // Client bleibt in der Map, damit Session-Wiederherstellung funktioniert } }); 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); checkAndLogStart(); // 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 } = data; 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, messageId, timestamp: new Date().toISOString(), read: false, isImage: isImage || false, imageType: imageType || null }); // Sende an Empfänger (wenn online) receiver.socket.emit('message', { from: client.userName, message, messageId, timestamp: new Date().toISOString(), isImage: isImage || false, imageType: imageType || null }); // Bestätigung an Absender socket.emit('messageSent', { messageId, to: toUserName }); // 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); }