Files
singlechat/server/broadcast.js
Torsten Schulz (local) 10e6e7a80a
All checks were successful
Deploy SingleChat / deploy (push) Successful in 27s
videochat integriert
2026-06-17 12:53:03 +02:00

2322 lines
80 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
const MAX_ACTIVE_VIDEO_CONNECTIONS = 3;
const ACTIVE_VIDEO_SESSION_STATUSES = new Set(['ringing', 'connecting', 'active']);
const TERMINAL_VIDEO_SESSION_STATUSES = new Set(['rejected', 'cancelled', 'ended', 'failed']);
const VIDEO_CONNECTION_STATES = new Set(['new', 'connecting', 'connected', 'disconnected', 'failed', 'closed']);
function parseDelimitedEnvList(rawValue) {
return String(rawValue || '')
.split(/[,\n]/)
.map((entry) => entry.trim())
.filter(Boolean);
}
function buildVideoIceServers() {
const jsonConfig = String(process.env.VIDEO_ICE_SERVERS_JSON || '').trim();
if (jsonConfig) {
try {
const parsed = JSON.parse(jsonConfig);
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
console.error('Ungültiges VIDEO_ICE_SERVERS_JSON:', error.message);
return [];
}
}
const iceServers = [];
const stunUrls = parseDelimitedEnvList(process.env.VIDEO_STUN_URLS);
if (stunUrls.length > 0) {
iceServers.push({ urls: stunUrls });
}
const turnUrls = parseDelimitedEnvList(process.env.VIDEO_TURN_URLS);
const turnUsername = String(process.env.VIDEO_TURN_USERNAME || '').trim();
const turnCredential = String(process.env.VIDEO_TURN_CREDENTIAL || '').trim();
if (turnUrls.length > 0 && turnUsername && turnCredential) {
iceServers.push({
urls: turnUrls,
username: turnUsername,
credential: turnCredential
});
}
return iceServers;
}
const VIDEO_ICE_SERVERS = buildVideoIceServers();
const VIDEO_MEDIA_RELAY_CONFIGURED = VIDEO_ICE_SERVERS.some((server) => {
const urls = Array.isArray(server?.urls) ? server.urls : [server?.urls];
return urls.some((url) => String(url || '').startsWith('turn:') || String(url || '').startsWith('turns:'));
});
if (!VIDEO_MEDIA_RELAY_CONFIGURED) {
console.warn('Video-Medienpfad ist nicht vollständig konfiguriert. Setze VIDEO_TURN_URLS, VIDEO_TURN_USERNAME und VIDEO_TURN_CREDENTIAL.');
}
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)
let videoConversations = new Map(); // Key: "user1:user2"
let videoSessions = new Map(); // Key: callId
// 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 isSocketConnected(socket) {
return !!(socket && socket.connected);
}
function isClientOnline(client) {
return !!(client && client.userName && isSocketConnected(client.socket));
}
function getClientByUserName(userName) {
for (const client of clients.values()) {
if (client.userName === userName) {
return client;
}
}
return null;
}
function getOrCreateVideoConversation(user1, user2) {
const users = [user1, user2].sort();
const convKey = users.join(':');
let state = videoConversations.get(convKey);
if (!state) {
state = {
convKey,
users,
consents: {
[users[0]]: false,
[users[1]]: false
},
activeCallIds: new Set()
};
videoConversations.set(convKey, state);
} else {
for (const user of users) {
if (typeof state.consents[user] !== 'boolean') {
state.consents[user] = false;
}
}
if (!(state.activeCallIds instanceof Set)) {
state.activeCallIds = new Set(Array.isArray(state.activeCallIds) ? state.activeCallIds : []);
}
}
return state;
}
function getExistingVideoConversation(user1, user2) {
return videoConversations.get(getConversationKey(user1, user2)) || null;
}
function getVideoConversationForRead(user1, user2) {
const existing = getExistingVideoConversation(user1, user2);
if (existing) {
return existing;
}
const users = [user1, user2].sort();
return {
convKey: users.join(':'),
users,
consents: {
[users[0]]: false,
[users[1]]: false
},
activeCallIds: new Set()
};
}
function removeVideoConversationIfEmpty(user1, user2) {
const convKey = getConversationKey(user1, user2);
const state = videoConversations.get(convKey);
if (!state) return;
const hasConsent = Object.values(state.consents || {}).some(Boolean);
const hasActiveCalls = Array.from(state.activeCallIds || []).some((callId) => {
const session = videoSessions.get(callId);
return session && ACTIVE_VIDEO_SESSION_STATUSES.has(session.status);
});
if (!hasConsent && !hasActiveCalls) {
videoConversations.delete(convKey);
}
}
function getOtherParticipant(session, userName) {
return session.participants.find((participant) => participant !== userName) || null;
}
function buildVideoConsentPayload(viewerName, state) {
const otherUserName = state.users.find((user) => user !== viewerName) || null;
return {
withUserName: otherUserName,
localConsent: !!state.consents[viewerName],
remoteConsent: otherUserName ? !!state.consents[otherUserName] : false,
videoVisible: !!(otherUserName && state.consents[viewerName] && state.consents[otherUserName])
};
}
function emitVideoConsentStateToUser(userName, otherUserName) {
const client = getClientByUserName(userName);
if (!isClientOnline(client)) return;
const state = getVideoConversationForRead(userName, otherUserName);
client.socket.emit('videoConsent:update', buildVideoConsentPayload(userName, state));
}
function emitVideoConsentStateToParticipants(user1, user2) {
emitVideoConsentStateToUser(user1, user2);
emitVideoConsentStateToUser(user2, user1);
}
function buildVideoCallPayloadForUser(session, userName) {
const otherUserName = getOtherParticipant(session, userName);
const muteStates = session.muteStates || {};
const connectionStates = session.connectionStates || {};
const includeMedia = session.status === 'connecting' || session.status === 'active';
return {
callId: session.callId,
roomId: session.roomId,
withUserName: otherUserName,
initiatedBy: session.initiatedBy,
status: session.status,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
endedAt: session.endedAt || null,
reason: session.reason || null,
localMuted: !!muteStates[userName],
remoteMuted: otherUserName ? !!muteStates[otherUserName] : false,
connectionState: connectionStates[userName] || 'new',
remoteConnectionState: otherUserName ? (connectionStates[otherUserName] || 'new') : 'new',
media: includeMedia ? {
mode: 'webrtc-relay',
relayOnly: true,
iceTransportPolicy: 'relay',
iceServers: VIDEO_ICE_SERVERS,
isCaller: session.initiatedBy === userName
} : null
};
}
function emitVideoCallEventToUser(userName, eventName, session) {
const client = getClientByUserName(userName);
if (!isClientOnline(client)) return;
client.socket.emit(eventName, buildVideoCallPayloadForUser(session, userName));
}
function emitVideoCapacityToUser(userName) {
const client = getClientByUserName(userName);
if (!isClientOnline(client)) return;
const activeConnections = getActiveVideoSessionsForUser(userName).length;
client.socket.emit('videoCall:capacity', {
activeConnections,
maxConnections: MAX_ACTIVE_VIDEO_CONNECTIONS,
reachedMax: activeConnections >= MAX_ACTIVE_VIDEO_CONNECTIONS
});
}
function getActiveVideoSessionsForUser(userName) {
return Array.from(videoSessions.values()).filter((session) => (
session.participants.includes(userName) &&
ACTIVE_VIDEO_SESSION_STATUSES.has(session.status)
));
}
function hasVideoCapacity(userName) {
return getActiveVideoSessionsForUser(userName).length < MAX_ACTIVE_VIDEO_CONNECTIONS;
}
function findActiveVideoSessionBetween(user1, user2) {
return Array.from(videoSessions.values()).find((session) => (
ACTIVE_VIDEO_SESSION_STATUSES.has(session.status) &&
session.participants.includes(user1) &&
session.participants.includes(user2)
)) || null;
}
function createVideoSession(callerUserName, calleeUserName) {
const now = new Date().toISOString();
const callId = crypto.randomUUID();
return {
callId,
roomId: `video-${callId}`,
participants: [callerUserName, calleeUserName],
initiatedBy: callerUserName,
status: 'ringing',
muteStates: {
[callerUserName]: false,
[calleeUserName]: false
},
connectionStates: {
[callerUserName]: 'new',
[calleeUserName]: 'new'
},
createdAt: now,
updatedAt: now,
endedAt: null,
reason: null
};
}
function touchVideoSession(session, status, reason = null) {
session.status = status;
session.updatedAt = new Date().toISOString();
if (reason) {
session.reason = reason;
}
if (TERMINAL_VIDEO_SESSION_STATUSES.has(status)) {
session.endedAt = session.updatedAt;
}
}
function registerVideoSession(session) {
videoSessions.set(session.callId, session);
const state = getOrCreateVideoConversation(session.participants[0], session.participants[1]);
state.activeCallIds.add(session.callId);
return state;
}
function finalizeVideoSession(session) {
const state = getExistingVideoConversation(session.participants[0], session.participants[1]);
if (state) {
state.activeCallIds.delete(session.callId);
}
videoSessions.delete(session.callId);
removeVideoConversationIfEmpty(session.participants[0], session.participants[1]);
}
function sendVideoCallError(socket, code, message, details = {}) {
socket.emit('videoCall:error', {
code,
message,
...details
});
}
function emitVideoCallUpdateToParticipants(session) {
emitVideoCallEventToUser(session.participants[0], 'videoCall:update', session);
emitVideoCallEventToUser(session.participants[1], 'videoCall:update', session);
}
function buildVideoSignalPayload(callId, fromUserName, signalType, details = {}) {
return {
callId,
fromUserName,
signalType,
...details
};
}
function isRelayIceCandidate(candidate) {
if (!candidate || typeof candidate !== 'object') {
return false;
}
if (candidate.type) {
return String(candidate.type).toLowerCase() === 'relay';
}
const candidateLine = String(candidate.candidate || '');
return /\btyp relay\b/i.test(candidateLine);
}
function endVideoSession(session, reason = 'ended', initiatorUserName = null) {
if (!session || TERMINAL_VIDEO_SESSION_STATUSES.has(session.status)) {
return;
}
touchVideoSession(session, 'ended', reason);
for (const participant of session.participants) {
const payload = buildVideoCallPayloadForUser(session, participant);
if (initiatorUserName) {
payload.endedBy = initiatorUserName;
}
const client = getClientByUserName(participant);
if (isClientOnline(client)) {
client.socket.emit('videoCall:end', payload);
}
}
finalizeVideoSession(session);
for (const participant of session.participants) {
emitVideoCapacityToUser(participant);
}
}
function endVideoSessionsForPair(user1, user2, reason = 'ended', initiatorUserName = null) {
for (const session of Array.from(videoSessions.values())) {
if (
ACTIVE_VIDEO_SESSION_STATUSES.has(session.status) &&
session.participants.includes(user1) &&
session.participants.includes(user2)
) {
endVideoSession(session, reason, initiatorUserName);
}
}
}
function endAllVideoSessionsForUser(userName, reason = 'ended', initiatorUserName = null) {
for (const session of Array.from(videoSessions.values())) {
if (ACTIVE_VIDEO_SESSION_STATUSES.has(session.status) && session.participants.includes(userName)) {
endVideoSession(session, reason, initiatorUserName);
}
}
}
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 readStartsLogStats() {
const path = join(ensureLogsDir(__dirname), 'starts.log');
if (!existsSync(path)) {
return { count: 0, first: null, last: null };
}
const lines = readFileSync(path, 'utf-8')
.split('\n')
.map((l) => l.trim())
.filter(Boolean);
if (lines.length === 0) {
return { count: 0, first: null, last: null };
}
return { count: lines.length, first: lines[0], last: lines[lines.length - 1] };
}
/**
* Sammelt alle verfügbaren Kennzahlen: Live-Server, starts.log, vollständige Auswertung von logins.log
* (entspricht inhaltlich den /stat-Unterbefehlen in einer Tabelle).
*/
function buildFullAllStatsRows(records) {
const rows = [];
const push = (a, b) => rows.push([String(a), b == null ? '' : String(b)]);
const liveOnline = Array.from(clients.values()).filter(
(c) => c.userName && c.socket && c.socket.connected
).length;
const socketCount =
io.sockets && io.sockets.sockets && typeof io.sockets.sockets.size === 'number'
? io.sockets.sockets.size
: 0;
let totalMessages = 0;
for (const msgs of conversations.values()) {
totalMessages += Array.isArray(msgs) ? msgs.length : 0;
}
const starts = readStartsLogStats();
push('— Live (Server) —', '');
push('Nutzer mit Chat-Login und aktiver Verbindung', liveOnline);
push('Socket.IO-Verbindungen (/)', socketCount);
push('Client-Sitzungen im Speicher', clients.size);
push('Unterhaltungen im Speicher (Paare)', conversations.size);
push('Nachrichten-Einträge gesamt (RAM)', totalMessages);
push('Server-Starts protokolliert (starts.log)', starts.count);
if (starts.first) {
push('Erster Start (Timestamp)', starts.first);
}
if (starts.last) {
push('Letzter Start (Timestamp)', starts.last);
}
push('— Login-Protokoll (logins.log, UTC) —', '');
if (records.length === 0) {
push('Keine Einträge', '—');
return rows;
}
const sortedByDate = records.slice().sort((a, b) => a.date - b.date);
const firstTs = sortedByDate[0].timestamp;
const lastTs = sortedByDate[sortedByDate.length - 1].timestamp;
const daysSorted = records.map((r) => r.day).sort();
const dayMin = daysSorted[0];
const dayMax = daysSorted[daysSorted.length - 1];
const today = new Date().toISOString().slice(0, 10);
const dayRecords = records.filter((r) => r.day === today);
const latestByUserToday = new Map();
dayRecords
.slice()
.sort((a, b) => b.date - a.date)
.forEach((record) => {
if (!latestByUserToday.has(record.userName)) {
latestByUserToday.set(record.userName, record);
}
});
const uniqueToday = latestByUserToday.size;
const namesToday = Array.from(latestByUserToday.keys()).sort((a, b) => a.localeCompare(b, 'de'));
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);
push('Zeitraum (Kalendertage)', `${dayMin}${dayMax}`);
push('Erster Login (Timestamp)', firstTs);
push('Letzter Login (Timestamp)', lastTs);
push('Logins gesamt', records.length);
push(`Logins heute (${today} UTC)`, dayRecords.length);
push('Verschiedene Nutzer heute (UTC)', uniqueToday);
const listToday = namesToday.join(', ');
push(
'Nutzer heute (Liste)',
listToday.length > 500
? `${uniqueToday} Nutzer — Auszug: ${namesToday.slice(0, 8).join(', ')} … (vollständig: /stat today)`
: listToday || '—'
);
push('Verschiedene Namen (historisch)', uniqueNames);
push('Ø Logins pro Namen (historisch)', (records.length / uniqueNames).toFixed(2));
push('Jüngster Nutzer (erster Treffer im Log)', `${youngest.userName} (${youngest.age})`);
push('Ältester Nutzer (erster Treffer im Log)', `${oldest.userName} (${oldest.age})`);
const topGenders = aggregateTop(
records,
(r) => (r.gender || '').trim() || '?',
15
);
push('Geschlecht (Häufigkeit)', topGenders.map(([g, n]) => `${g}(${n})`).join(', ') || '—');
const topCountries = aggregateTop(records, (r) => r.country, 20);
push('Top 20 Länder', topCountries.map(([c, n]) => `${c}(${n})`).join(', ') || '—');
const topNames = aggregateTop(records, (r) => r.userName, 20);
const topNamesDetail = topNames
.map(([name, count]) => {
const latestRecord = records
.filter((r) => r.userName === name)
.sort((a, b) => b.date - a.date)[0];
return `${name}(${count}, zuletzt ${latestRecord.timestamp})`;
})
.join('; ');
push(
'Top 20 Namen (Anzahl, letzter Login)',
topNamesDetail.length > 1200 ? `${topNamesDetail.slice(0, 1197)}` : topNamesDetail
);
push('Hinweis', 'Tabellen je Tag/Zeitraum: /stat today | /stat date | /stat range');
return rows;
}
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', 'Alle Kennzahlen: Live-Server + starts.log + komplette logins.log-Auswertung']
]);
return;
}
if (sub === 'today') {
const day = new Date().toISOString().slice(0, 10);
const dayRecords = records.filter((r) => r.day === day);
const latestByUserToday = new Map();
dayRecords
.slice()
.sort((a, b) => b.date - a.date)
.forEach((record) => {
if (!latestByUserToday.has(record.userName)) {
latestByUserToday.set(record.userName, record);
}
});
const uniqueUsersToday = Array.from(latestByUserToday.values())
.sort((a, b) => a.userName.localeCompare(b.userName, 'de'))
.map((r) => `${r.userName} (${r.age})`);
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', 'Alter', 'Anzahl', 'Letzter Login'],
topNames.map(([name, count]) => {
const latestRecord = records
.filter((r) => r.userName === name)
.sort((a, b) => b.date - a.date)[0];
return [name, latestRecord ? latestRecord.age : '-', count, latestRecord ? latestRecord.timestamp : '-'];
})
);
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 rows = buildFullAllStatsRows(readLoginRecords());
sendCommandTable(socket, 'Statistik: Vollständige Übersicht (/all-stats)', ['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', 'Alle Kennzahlen: Live-Server + starts.log + komplette logins.log-Auswertung'],
['/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;
socketToSessionMap.set(socket.id, 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 currentSessionId = socket.data.sessionId || sessionId;
socketToSessionMap.delete(socket.id);
console.log(`[Disconnect] Socket getrennt für Session-ID: ${currentSessionId}, Grund: ${reason}`);
const client = clients.get(currentSessionId);
if (client) {
console.log(`[Disconnect] Client gefunden: ${client.userName || 'unbekannt'}, Socket war verbunden: ${client.socket ? client.socket.connected : 'null'}`);
if (client.userName) {
endAllVideoSessionsForUser(client.userName, 'disconnect', client.userName);
}
// 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;
socketToSessionMap.set(socket.id, expressSessionId);
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()
});
emitVideoCapacityToUser(existingClient.userName);
// 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 });
}
});
socket.on('videoConsent:set', async (data) => {
try {
const currentClient = clients.get(socket.data.sessionId);
if (!currentClient) {
socket.emit('error', { message: 'Client nicht gefunden' });
return;
}
currentClient.setActivity();
handleVideoConsentSet(socket, currentClient, data);
} catch (error) {
console.error('Fehler beim Verarbeiten von videoConsent:set:', error);
sendVideoCallError(socket, 'VIDEO_CONSENT_SET_FAILED', error.message);
}
});
socket.on('videoCall:invite', async (data) => {
try {
const currentClient = clients.get(socket.data.sessionId);
if (!currentClient) {
socket.emit('error', { message: 'Client nicht gefunden' });
return;
}
currentClient.setActivity();
handleVideoCallInvite(socket, currentClient, data);
} catch (error) {
console.error('Fehler beim Verarbeiten von videoCall:invite:', error);
sendVideoCallError(socket, 'VIDEO_INVITE_FAILED', error.message);
}
});
socket.on('videoCall:accept', async (data) => {
try {
const currentClient = clients.get(socket.data.sessionId);
if (!currentClient) {
socket.emit('error', { message: 'Client nicht gefunden' });
return;
}
currentClient.setActivity();
handleVideoCallAccept(socket, currentClient, data);
} catch (error) {
console.error('Fehler beim Verarbeiten von videoCall:accept:', error);
sendVideoCallError(socket, 'VIDEO_ACCEPT_FAILED', error.message);
}
});
socket.on('videoCall:reject', async (data) => {
try {
const currentClient = clients.get(socket.data.sessionId);
if (!currentClient) {
socket.emit('error', { message: 'Client nicht gefunden' });
return;
}
currentClient.setActivity();
handleVideoCallReject(socket, currentClient, data);
} catch (error) {
console.error('Fehler beim Verarbeiten von videoCall:reject:', error);
sendVideoCallError(socket, 'VIDEO_REJECT_FAILED', error.message);
}
});
socket.on('videoCall:cancel', async (data) => {
try {
const currentClient = clients.get(socket.data.sessionId);
if (!currentClient) {
socket.emit('error', { message: 'Client nicht gefunden' });
return;
}
currentClient.setActivity();
handleVideoCallCancel(socket, currentClient, data);
} catch (error) {
console.error('Fehler beim Verarbeiten von videoCall:cancel:', error);
sendVideoCallError(socket, 'VIDEO_CANCEL_FAILED', error.message);
}
});
socket.on('videoCall:end', async (data) => {
try {
const currentClient = clients.get(socket.data.sessionId);
if (!currentClient) {
socket.emit('error', { message: 'Client nicht gefunden' });
return;
}
currentClient.setActivity();
handleVideoCallEnd(socket, currentClient, data);
} catch (error) {
console.error('Fehler beim Verarbeiten von videoCall:end:', error);
sendVideoCallError(socket, 'VIDEO_END_FAILED', error.message);
}
});
socket.on('videoCall:muteState', async (data) => {
try {
const currentClient = clients.get(socket.data.sessionId);
if (!currentClient) {
socket.emit('error', { message: 'Client nicht gefunden' });
return;
}
currentClient.setActivity();
handleVideoCallMuteState(socket, currentClient, data);
} catch (error) {
console.error('Fehler beim Verarbeiten von videoCall:muteState:', error);
sendVideoCallError(socket, 'VIDEO_MUTE_STATE_FAILED', error.message);
}
});
socket.on('videoCall:signal', async (data) => {
try {
const currentClient = clients.get(socket.data.sessionId);
if (!currentClient) {
socket.emit('error', { message: 'Client nicht gefunden' });
return;
}
currentClient.setActivity();
handleVideoCallSignal(socket, currentClient, data);
} catch (error) {
console.error('Fehler beim Verarbeiten von videoCall:signal:', error);
sendVideoCallError(socket, 'VIDEO_SIGNAL_FAILED', error.message);
}
});
socket.on('videoCall:connectionState', async (data) => {
try {
const currentClient = clients.get(socket.data.sessionId);
if (!currentClient) {
socket.emit('error', { message: 'Client nicht gefunden' });
return;
}
currentClient.setActivity();
handleVideoCallConnectionState(socket, currentClient, data);
} catch (error) {
console.error('Fehler beim Verarbeiten von videoCall:connectionState:', error);
sendVideoCallError(socket, 'VIDEO_CONNECTION_STATE_FAILED', 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()
});
emitVideoCapacityToUser(client.userName);
}
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
}))
});
emitVideoConsentStateToUser(client.userName, withUserName);
const session = findActiveVideoSessionBetween(client.userName, withUserName);
if (session) {
socket.emit('videoCall:update', buildVideoCallPayloadForUser(session, client.userName));
}
}
function handleVideoConsentSet(socket, client, data) {
if (!client.userName) {
socket.emit('error', { message: 'Nicht eingeloggt' });
return;
}
const withUserName = String(data?.withUserName || '').trim();
const allowed = !!data?.allowed;
if (!withUserName || withUserName === client.userName) {
sendVideoCallError(socket, 'VIDEO_INVALID_PARTNER', 'Ungültiger Gesprächspartner.');
return;
}
const state = getOrCreateVideoConversation(client.userName, withUserName);
state.consents[client.userName] = allowed;
if (!allowed) {
endVideoSessionsForPair(client.userName, withUserName, 'consent_revoked', client.userName);
}
emitVideoConsentStateToParticipants(client.userName, withUserName);
}
function assertCanUseVideoWith(socket, client, targetUserName) {
if (!client.userName) {
sendVideoCallError(socket, 'VIDEO_NOT_LOGGED_IN', 'Nicht eingeloggt.');
return null;
}
const targetClient = getClientByUserName(targetUserName);
if (!targetClient || !targetClient.userName) {
sendVideoCallError(socket, 'VIDEO_PARTNER_NOT_FOUND', 'Gesprächspartner nicht gefunden.', { withUserName: targetUserName });
return null;
}
if (!isClientOnline(targetClient)) {
sendVideoCallError(socket, 'VIDEO_PARTNER_OFFLINE', 'Gesprächspartner ist nicht online.', { withUserName: targetUserName });
return null;
}
if (targetClient.blockedUsers.has(client.userName)) {
sendVideoCallError(socket, 'VIDEO_BLOCKED_BY_PARTNER', 'Du wurdest von diesem Benutzer blockiert.', { withUserName: targetUserName });
return null;
}
if (client.blockedUsers.has(targetUserName)) {
sendVideoCallError(socket, 'VIDEO_PARTNER_BLOCKED', 'Du hast diesen Benutzer blockiert.', { withUserName: targetUserName });
return null;
}
const state = getOrCreateVideoConversation(client.userName, targetUserName);
if (!state.consents[client.userName] || !state.consents[targetUserName]) {
sendVideoCallError(socket, 'VIDEO_CONSENT_REQUIRED', 'Videochat ist erst nach beidseitiger Freigabe sichtbar.', { withUserName: targetUserName });
return null;
}
return { targetClient, state };
}
function handleVideoCallInvite(socket, client, data) {
const withUserName = String(data?.withUserName || '').trim();
if (!withUserName || withUserName === client.userName) {
sendVideoCallError(socket, 'VIDEO_INVALID_PARTNER', 'Ungültiger Gesprächspartner.');
return;
}
if (!VIDEO_MEDIA_RELAY_CONFIGURED) {
sendVideoCallError(socket, 'VIDEO_MEDIA_NOT_CONFIGURED', 'Video-Medienserver ist derzeit nicht konfiguriert.');
return;
}
const check = assertCanUseVideoWith(socket, client, withUserName);
if (!check) return;
if (!hasVideoCapacity(client.userName)) {
emitVideoCapacityToUser(client.userName);
sendVideoCallError(socket, 'VIDEO_CAPACITY_REACHED', 'Maximal drei Videoverbindungen gleichzeitig erlaubt.', {
withUserName,
activeConnections: getActiveVideoSessionsForUser(client.userName).length,
maxConnections: MAX_ACTIVE_VIDEO_CONNECTIONS
});
return;
}
if (!hasVideoCapacity(withUserName)) {
emitVideoCapacityToUser(withUserName);
sendVideoCallError(socket, 'VIDEO_PARTNER_CAPACITY_REACHED', 'Der Gesprächspartner hat bereits die maximale Anzahl an Videoverbindungen erreicht.', {
withUserName
});
return;
}
const existingSession = findActiveVideoSessionBetween(client.userName, withUserName);
if (existingSession) {
sendVideoCallError(socket, 'VIDEO_CALL_ALREADY_EXISTS', 'Für diesen Gesprächspartner existiert bereits ein laufender Videochat.', {
withUserName,
callId: existingSession.callId
});
return;
}
const session = createVideoSession(client.userName, withUserName);
registerVideoSession(session);
emitVideoCallEventToUser(client.userName, 'videoCall:invite', session);
emitVideoCallEventToUser(withUserName, 'videoCall:incoming', session);
emitVideoCapacityToUser(client.userName);
emitVideoCapacityToUser(withUserName);
}
function handleVideoCallAccept(socket, client, data) {
const callId = String(data?.callId || '').trim();
const session = videoSessions.get(callId);
if (!session || session.status !== 'ringing' || !session.participants.includes(client.userName)) {
sendVideoCallError(socket, 'VIDEO_CALL_NOT_FOUND', 'Videoanruf nicht gefunden.', { callId });
return;
}
if (session.initiatedBy === client.userName) {
sendVideoCallError(socket, 'VIDEO_CALL_INVALID_ACCEPT', 'Der Anrufer kann den eigenen Anruf nicht annehmen.', { callId });
return;
}
if (!VIDEO_MEDIA_RELAY_CONFIGURED) {
sendVideoCallError(socket, 'VIDEO_MEDIA_NOT_CONFIGURED', 'Video-Medienserver ist derzeit nicht konfiguriert.', { callId });
return;
}
const otherUserName = getOtherParticipant(session, client.userName);
const check = assertCanUseVideoWith(socket, client, otherUserName);
if (!check) {
touchVideoSession(session, 'failed', 'preconditions_failed');
emitVideoCallEventToUser(session.participants[0], 'videoCall:end', session);
emitVideoCallEventToUser(session.participants[1], 'videoCall:end', session);
finalizeVideoSession(session);
emitVideoCapacityToUser(session.participants[0]);
emitVideoCapacityToUser(session.participants[1]);
return;
}
touchVideoSession(session, 'connecting');
emitVideoCallEventToUser(session.participants[0], 'videoCall:start', session);
emitVideoCallEventToUser(session.participants[1], 'videoCall:start', session);
emitVideoCapacityToUser(session.participants[0]);
emitVideoCapacityToUser(session.participants[1]);
}
function handleVideoCallReject(socket, client, data) {
const callId = String(data?.callId || '').trim();
const session = videoSessions.get(callId);
if (!session || !ACTIVE_VIDEO_SESSION_STATUSES.has(session.status) || !session.participants.includes(client.userName)) {
sendVideoCallError(socket, 'VIDEO_CALL_NOT_FOUND', 'Videoanruf nicht gefunden.', { callId });
return;
}
if (session.status !== 'ringing' || session.initiatedBy === client.userName) {
sendVideoCallError(socket, 'VIDEO_CALL_INVALID_REJECT', 'Nur der Angerufene kann einen klingelnden Anruf ablehnen.', { callId });
return;
}
touchVideoSession(session, 'rejected', 'rejected');
emitVideoCallEventToUser(session.participants[0], 'videoCall:reject', session);
emitVideoCallEventToUser(session.participants[1], 'videoCall:reject', session);
finalizeVideoSession(session);
emitVideoCapacityToUser(session.participants[0]);
emitVideoCapacityToUser(session.participants[1]);
}
function handleVideoCallCancel(socket, client, data) {
const callId = String(data?.callId || '').trim();
const session = videoSessions.get(callId);
if (!session || !ACTIVE_VIDEO_SESSION_STATUSES.has(session.status) || !session.participants.includes(client.userName)) {
sendVideoCallError(socket, 'VIDEO_CALL_NOT_FOUND', 'Videoanruf nicht gefunden.', { callId });
return;
}
if (session.status !== 'ringing') {
sendVideoCallError(socket, 'VIDEO_CALL_INVALID_CANCEL', 'Nur klingelnde Anrufe können abgebrochen werden.', { callId });
return;
}
if (session.initiatedBy !== client.userName) {
sendVideoCallError(socket, 'VIDEO_CALL_INVALID_CANCEL', 'Nur der Anrufer kann einen klingelnden Anruf abbrechen.', { callId });
return;
}
touchVideoSession(session, 'cancelled', 'cancelled');
emitVideoCallEventToUser(session.participants[0], 'videoCall:cancel', session);
emitVideoCallEventToUser(session.participants[1], 'videoCall:cancel', session);
finalizeVideoSession(session);
emitVideoCapacityToUser(session.participants[0]);
emitVideoCapacityToUser(session.participants[1]);
}
function handleVideoCallEnd(socket, client, data) {
const callId = String(data?.callId || '').trim();
const session = videoSessions.get(callId);
if (!session || !ACTIVE_VIDEO_SESSION_STATUSES.has(session.status) || !session.participants.includes(client.userName)) {
sendVideoCallError(socket, 'VIDEO_CALL_NOT_FOUND', 'Videoanruf nicht gefunden.', { callId });
return;
}
endVideoSession(session, 'ended', client.userName);
}
function handleVideoCallMuteState(socket, client, data) {
const callId = String(data?.callId || '').trim();
const muted = !!data?.muted;
const session = videoSessions.get(callId);
if (!session || !ACTIVE_VIDEO_SESSION_STATUSES.has(session.status) || !session.participants.includes(client.userName)) {
sendVideoCallError(socket, 'VIDEO_CALL_NOT_FOUND', 'Videoanruf nicht gefunden.', { callId });
return;
}
session.muteStates = session.muteStates || {};
session.muteStates[client.userName] = muted;
session.updatedAt = new Date().toISOString();
emitVideoCallEventToUser(session.participants[0], 'videoCall:muteState', session);
emitVideoCallEventToUser(session.participants[1], 'videoCall:muteState', session);
}
function handleVideoCallSignal(socket, client, data) {
const callId = String(data?.callId || '').trim();
const signalType = String(data?.signalType || '').trim();
const session = videoSessions.get(callId);
if (!session || !ACTIVE_VIDEO_SESSION_STATUSES.has(session.status) || !session.participants.includes(client.userName)) {
sendVideoCallError(socket, 'VIDEO_CALL_NOT_FOUND', 'Videoanruf nicht gefunden.', { callId });
return;
}
const otherUserName = getOtherParticipant(session, client.userName);
const targetClient = getClientByUserName(otherUserName);
if (!isClientOnline(targetClient)) {
endVideoSession(session, 'partner_disconnected', client.userName);
return;
}
if (signalType === 'description') {
const description = data?.description;
const descriptionType = String(description?.type || '').trim();
const sdp = String(description?.sdp || '').trim();
if (!descriptionType || !sdp || !['offer', 'answer'].includes(descriptionType)) {
sendVideoCallError(socket, 'VIDEO_SIGNAL_INVALID_DESCRIPTION', 'Ungültige Video-Signalisierung.', { callId });
return;
}
targetClient.socket.emit('videoCall:signal', buildVideoSignalPayload(callId, client.userName, signalType, {
description: {
type: descriptionType,
sdp
}
}));
return;
}
if (signalType === 'candidate') {
const candidate = data?.candidate;
if (!isRelayIceCandidate(candidate)) {
sendVideoCallError(socket, 'VIDEO_SIGNAL_NON_RELAY_CANDIDATE', 'Nur Relay-Kandidaten sind für Videochat erlaubt.', { callId });
return;
}
targetClient.socket.emit('videoCall:signal', buildVideoSignalPayload(callId, client.userName, signalType, { candidate }));
return;
}
sendVideoCallError(socket, 'VIDEO_SIGNAL_UNSUPPORTED', 'Nicht unterstütztes Video-Signalisierungsformat.', { callId });
}
function handleVideoCallConnectionState(socket, client, data) {
const callId = String(data?.callId || '').trim();
const nextState = String(data?.connectionState || '').trim().toLowerCase();
const session = videoSessions.get(callId);
if (!session || !ACTIVE_VIDEO_SESSION_STATUSES.has(session.status) || !session.participants.includes(client.userName)) {
sendVideoCallError(socket, 'VIDEO_CALL_NOT_FOUND', 'Videoanruf nicht gefunden.', { callId });
return;
}
if (!VIDEO_CONNECTION_STATES.has(nextState)) {
sendVideoCallError(socket, 'VIDEO_CONNECTION_STATE_INVALID', 'Ungültiger Video-Verbindungsstatus.', { callId });
return;
}
session.connectionStates = session.connectionStates || {};
session.connectionStates[client.userName] = nextState;
session.updatedAt = new Date().toISOString();
if (nextState === 'failed' || nextState === 'closed') {
endVideoSession(session, 'media_connection_failed', client.userName);
return;
}
if (
session.status === 'connecting' &&
session.participants.every((participant) => session.connectionStates[participant] === 'connected')
) {
touchVideoSession(session, 'active');
}
emitVideoCallUpdateToParticipants(session);
}
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);
getOrCreateVideoConversation(client.userName, userName).consents[client.userName] = false;
endVideoSessionsForPair(client.userName, userName, 'blocked', client.userName);
emitVideoConsentStateToParticipants(client.userName, 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);
emitVideoConsentStateToParticipants(client.userName, 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`);
if (client.userName) {
endAllVideoSessionsForUser(client.userName, 'timeout', client.userName);
}
if (client.socket?.id) {
socketToSessionMap.delete(client.socket.id);
}
clients.delete(sid);
broadcastUserList();
}
}
}, 60000);
}