Files
AI_Aurora/chat1.php
T
2025-11-02 17:53:33 +01:00

3534 lines
127 KiB
PHP

<?php
/**
* ═══════════════════════════════════════════════════════════
* SECURE PRIVATE CHAT SYSTEM - VOLLSCHUTZ VERSION
* ═══════════════════════════════════════════════════════════
* Features:
* - Altersverifikation (U18/Ü18 Trennung)
* - User-ID System (Max#1234)
* - Keyword-Filter (Treffen, Adressen, etc.)
* - Rate-Limiting (Spam-Schutz)
* - Wortfilter (Schimpfwörter)
* - Link-Blockierung
* - Report-System
* - Block-Funktion
* - Admin-Panel
* - Auto-Flagging
* - Log-System
* - AGB/Disclaimer
* - Server-Sent Events (Echtzeit)
* ═══════════════════════════════════════════════════════════
*/
// ═══════════════════════════════════════════════════════════
// KONFIGURATION
// ═══════════════════════════════════════════════════════════
define('DB_FILE', __DIR__ . '/chat_secure.db');
define('MESSAGE_RETENTION_HOURS', 24);
define('LOG_RETENTION_MONTHS', 6);
define('ONLINE_TIMEOUT_SECONDS', 30);
define('SSE_RETRY_MS', 1000);
// Rate Limiting
define('MAX_MESSAGES_PER_MINUTE', 10);
define('MAX_MESSAGES_PER_HOUR', 100);
define('MAX_MESSAGES_PER_DAY_U18', 50);
// Admin Credentials (BITTE ÄNDERN!)
define('ADMIN_USERNAME', 'admin');
define('ADMIN_PASSWORD', password_hash('SecureAdmin2025!', PASSWORD_BCRYPT));
// Keyword Blacklist (Treffen, Adressen, etc.)
$KEYWORD_BLACKLIST = [
// Deutsch
'treffen', 'meet', 'date', 'treff', 'sehen', 'besuchen',
'adresse', 'wohnung', 'haus', 'straße', 'strasse',
'telefon', 'nummer', 'handy', 'whatsapp', 'snapchat', 'instagram',
'facebook', 'telegram', 'tiktok', 'discord',
'foto', 'bild', 'selfie', 'nackt', 'video',
'wo wohnst', 'wie alt', 'alter', 'geburtstag',
'schule', 'klasse', 'lehrer',
// Englisch
'address', 'phone', 'number', 'picture', 'pic', 'naked',
'where do you live', 'how old', 'school',
];
// Wortfilter (Schimpfwörter - TOP 50)
$PROFANITY_FILTER = [
// Deutsch (Beispiele - erweitere selbst)
'arsch', 'scheisse', 'scheiße', 'fick', 'hurensohn',
'wichser', 'fotze', 'schlampe', 'bastard', 'idiot',
'vollidiot', 'depp', 'trottel', 'schwachkopf',
// Englisch
'fuck', 'shit', 'ass', 'bitch', 'bastard',
'damn', 'crap', 'dick', 'pussy', 'cock',
];
// ═══════════════════════════════════════════════════════════
// DATENBANK SETUP
// ═══════════════════════════════════════════════════════════
function getDB() {
$db = new SQLite3(DB_FILE);
$db->busyTimeout(5000);
// Users Table (mit Geburtsdatum und User-ID)
$db->exec('
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
user_id TEXT UNIQUE NOT NULL,
birthdate DATE NOT NULL,
age_group TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
last_seen DATETIME DEFAULT CURRENT_TIMESTAMP,
is_banned INTEGER DEFAULT 0,
ban_reason TEXT
)
');
// Messages Table
$db->exec('
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
from_user_id INTEGER NOT NULL,
to_user_id INTEGER NOT NULL,
message TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
is_read INTEGER DEFAULT 0,
is_flagged INTEGER DEFAULT 0,
flag_reason TEXT,
FOREIGN KEY (from_user_id) REFERENCES users(id),
FOREIGN KEY (to_user_id) REFERENCES users(id)
)
');
// Online Status Table
$db->exec('
CREATE TABLE IF NOT EXISTS online_status (
user_id INTEGER PRIMARY KEY,
last_ping DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
// Reports Table
$db->exec('
CREATE TABLE IF NOT EXISTS reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
reporter_id INTEGER NOT NULL,
reported_user_id INTEGER NOT NULL,
reason TEXT NOT NULL,
message_id INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
status TEXT DEFAULT "pending",
FOREIGN KEY (reporter_id) REFERENCES users(id),
FOREIGN KEY (reported_user_id) REFERENCES users(id),
FOREIGN KEY (message_id) REFERENCES messages(id)
)
');
// Blocks Table
$db->exec('
CREATE TABLE IF NOT EXISTS blocks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
blocker_id INTEGER NOT NULL,
blocked_id INTEGER NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (blocker_id) REFERENCES users(id),
FOREIGN KEY (blocked_id) REFERENCES users(id),
UNIQUE(blocker_id, blocked_id)
)
');
// Rate Limiting Table
$db->exec('
CREATE TABLE IF NOT EXISTS rate_limits (
user_id INTEGER NOT NULL,
action_type TEXT NOT NULL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
// Logs Table (für Behörden)
$db->exec('
CREATE TABLE IF NOT EXISTS security_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER,
action TEXT NOT NULL,
details TEXT,
ip_address TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
)
');
// Admin Table
$db->exec('
CREATE TABLE IF NOT EXISTS admins (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password_hash TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
');
// Create default admin if not exists
$stmt = $db->prepare('SELECT COUNT(*) as count FROM admins WHERE username = :username');
$stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row['count'] == 0) {
$stmt = $db->prepare('INSERT INTO admins (username, password_hash) VALUES (:username, :password)');
$stmt->bindValue(':username', ADMIN_USERNAME, SQLITE3_TEXT);
$stmt->bindValue(':password', ADMIN_PASSWORD, SQLITE3_TEXT);
$stmt->execute();
}
// Create indexes
$db->exec('CREATE INDEX IF NOT EXISTS idx_messages_users ON messages(from_user_id, to_user_id)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_messages_timestamp ON messages(timestamp)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_users_age_group ON users(age_group)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_blocks ON blocks(blocker_id, blocked_id)');
$db->exec('CREATE INDEX IF NOT EXISTS idx_reports_status ON reports(status)');
return $db;
}
// ═══════════════════════════════════════════════════════════
// SECURITY FUNCTIONS
// ═══════════════════════════════════════════════════════════
function generateUserId() {
return str_pad(mt_rand(1000, 9999), 4, '0', STR_PAD_LEFT);
}
function calculateAge($birthdate) {
$birth = new DateTime($birthdate);
$today = new DateTime();
return $birth->diff($today)->y;
}
function getAgeGroup($birthdate) {
$age = calculateAge($birthdate);
return $age < 18 ? 'U18' : 'O18';
}
function checkKeywordBlacklist($message) {
global $KEYWORD_BLACKLIST;
$message_lower = mb_strtolower($message, 'UTF-8');
foreach ($KEYWORD_BLACKLIST as $keyword) {
if (stripos($message_lower, $keyword) !== false) {
return ['blocked' => true, 'keyword' => $keyword];
}
}
return ['blocked' => false];
}
function checkProfanityFilter($message) {
global $PROFANITY_FILTER;
$message_lower = mb_strtolower($message, 'UTF-8');
foreach ($PROFANITY_FILTER as $word) {
if (stripos($message_lower, $word) !== false) {
return ['blocked' => true, 'word' => $word];
}
}
return ['blocked' => false];
}
function checkLinkFilter($message) {
$patterns = [
'/https?:\/\//i',
'/www\./i',
'/[a-z0-9-]+\.(com|de|ch|net|org|info)/i'
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $message)) {
return ['blocked' => true];
}
}
return ['blocked' => false];
}
function checkRateLimit($userId, $ageGroup) {
$db = getDB();
// Check messages per minute
$stmt = $db->prepare("
SELECT COUNT(*) as count
FROM rate_limits
WHERE user_id = :user_id
AND action_type = 'message'
AND timestamp > datetime('now', '-1 minute')
");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row['count'] >= MAX_MESSAGES_PER_MINUTE) {
return ['allowed' => false, 'reason' => 'Zu viele Nachrichten pro Minute (max ' . MAX_MESSAGES_PER_MINUTE . ')'];
}
// Check messages per hour
$stmt = $db->prepare("
SELECT COUNT(*) as count
FROM rate_limits
WHERE user_id = :user_id
AND action_type = 'message'
AND timestamp > datetime('now', '-1 hour')
");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row['count'] >= MAX_MESSAGES_PER_HOUR) {
return ['allowed' => false, 'reason' => 'Zu viele Nachrichten pro Stunde (max ' . MAX_MESSAGES_PER_HOUR . ')'];
}
// Check messages per day for U18
if ($ageGroup === 'U18') {
$stmt = $db->prepare("
SELECT COUNT(*) as count
FROM rate_limits
WHERE user_id = :user_id
AND action_type = 'message'
AND timestamp > datetime('now', '-1 day')
");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row['count'] >= MAX_MESSAGES_PER_DAY_U18) {
return ['allowed' => false, 'reason' => 'Tages-Limit erreicht (max ' . MAX_MESSAGES_PER_DAY_U18 . ' für U18)'];
}
}
return ['allowed' => true];
}
function logRateLimit($userId) {
$db = getDB();
$stmt = $db->prepare("INSERT INTO rate_limits (user_id, action_type) VALUES (:user_id, 'message')");
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
}
function logSecurityEvent($userId, $action, $details = '') {
$db = getDB();
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$stmt = $db->prepare('
INSERT INTO security_logs (user_id, action, details, ip_address)
VALUES (:user_id, :action, :details, :ip)
');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->bindValue(':action', $action, SQLITE3_TEXT);
$stmt->bindValue(':details', $details, SQLITE3_TEXT);
$stmt->bindValue(':ip', $ip, SQLITE3_TEXT);
$stmt->execute();
}
function isBlocked($userId, $otherUserId) {
$db = getDB();
$stmt = $db->prepare('
SELECT COUNT(*) as count
FROM blocks
WHERE (blocker_id = :user1 AND blocked_id = :user2)
OR (blocker_id = :user2 AND blocked_id = :user1)
');
$stmt->bindValue(':user1', $userId, SQLITE3_INTEGER);
$stmt->bindValue(':user2', $otherUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
return $row['count'] > 0;
}
function cleanupOldData() {
$db = getDB();
// Delete old messages
$hours = MESSAGE_RETENTION_HOURS;
$db->exec("DELETE FROM messages WHERE timestamp < datetime('now', '-{$hours} hours')");
// Delete old rate limits
$db->exec("DELETE FROM rate_limits WHERE timestamp < datetime('now', '-1 day')");
// Delete old logs (keep 6 months)
$months = LOG_RETENTION_MONTHS;
$db->exec("DELETE FROM security_logs WHERE timestamp < datetime('now', '-{$months} months')");
}
// ═══════════════════════════════════════════════════════════
// SESSION & AUTH
// ═══════════════════════════════════════════════════════════
session_start();
function isLoggedIn() {
return isset($_SESSION['user_id']) && isset($_SESSION['username']);
}
function isAdmin() {
return isset($_SESSION['is_admin']) && $_SESSION['is_admin'] === true;
}
function getCurrentUserId() {
return $_SESSION['user_id'] ?? null;
}
function getCurrentUsername() {
return $_SESSION['username'] ?? null;
}
function getCurrentUserDisplayName() {
if (!isLoggedIn()) return null;
return $_SESSION['username'] . '#' . $_SESSION['user_display_id'];
}
function getCurrentAgeGroup() {
return $_SESSION['age_group'] ?? null;
}
function updateOnlineStatus($userId) {
$db = getDB();
$stmt = $db->prepare('
INSERT OR REPLACE INTO online_status (user_id, last_ping)
VALUES (:user_id, CURRENT_TIMESTAMP)
');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
$stmt = $db->prepare('UPDATE users SET last_seen = CURRENT_TIMESTAMP WHERE id = :user_id');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
}
// ═══════════════════════════════════════════════════════════
// AJAX API HANDLER
// ═══════════════════════════════════════════════════════════
if (isset($_POST['action']) || isset($_GET['action'])) {
header('Content-Type: application/json');
$action = $_POST['action'] ?? $_GET['action'];
// ───────────────────────────────────────────────────────
// REGISTER
// ───────────────────────────────────────────────────────
if ($action === 'register') {
$username = trim($_POST['username'] ?? '');
$birthdate = trim($_POST['birthdate'] ?? '');
$agreed_terms = isset($_POST['agreed_terms']) && $_POST['agreed_terms'] === 'true';
// Validierung
if (empty($username) || empty($birthdate)) {
echo json_encode(['success' => false, 'error' => 'Username und Geburtsdatum erforderlich']);
exit;
}
if (!$agreed_terms) {
echo json_encode(['success' => false, 'error' => 'Bitte akzeptiere die Nutzungsbedingungen']);
exit;
}
if (strlen($username) < 3 || strlen($username) > 15) {
echo json_encode(['success' => false, 'error' => 'Username muss 3-15 Zeichen lang sein']);
exit;
}
if (!preg_match('/^[a-zA-Z0-9_]+$/', $username)) {
echo json_encode(['success' => false, 'error' => 'Nur Buchstaben, Zahlen und _ erlaubt']);
exit;
}
// Verbotene Usernamen
$forbidden = ['admin', 'moderator', 'support', 'system', 'root'];
if (in_array(strtolower($username), $forbidden)) {
echo json_encode(['success' => false, 'error' => 'Dieser Username ist nicht erlaubt']);
exit;
}
// Alter prüfen
$age = calculateAge($birthdate);
if ($age < 13) {
echo json_encode(['success' => false, 'error' => 'Du musst mindestens 13 Jahre alt sein']);
logSecurityEvent(null, 'REGISTER_UNDERAGE', "Username: $username, Age: $age");
exit;
}
$ageGroup = getAgeGroup($birthdate);
$db = getDB();
// Generate unique user_id
$userId = generateUserId();
$attempts = 0;
while ($attempts < 10) {
$stmt = $db->prepare('SELECT COUNT(*) as count FROM users WHERE user_id = :user_id');
$stmt->bindValue(':user_id', $userId, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row['count'] == 0) break;
$userId = generateUserId();
$attempts++;
}
// Create user
$stmt = $db->prepare('
INSERT INTO users (username, user_id, birthdate, age_group)
VALUES (:username, :user_id, :birthdate, :age_group)
');
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
$stmt->bindValue(':user_id', $userId, SQLITE3_TEXT);
$stmt->bindValue(':birthdate', $birthdate, SQLITE3_TEXT);
$stmt->bindValue(':age_group', $ageGroup, SQLITE3_TEXT);
$stmt->execute();
$dbUserId = $db->lastInsertRowID();
$_SESSION['user_id'] = $dbUserId;
$_SESSION['username'] = $username;
$_SESSION['user_display_id'] = $userId;
$_SESSION['age_group'] = $ageGroup;
$_SESSION['birthdate'] = $birthdate;
updateOnlineStatus($dbUserId);
logSecurityEvent($dbUserId, 'REGISTER', "Age group: $ageGroup");
echo json_encode([
'success' => true,
'user_id' => $dbUserId,
'username' => $username,
'display_name' => $username . '#' . $userId,
'age_group' => $ageGroup
]);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN LOGIN
// ───────────────────────────────────────────────────────
if ($action === 'admin_login') {
$username = trim($_POST['username'] ?? '');
$password = trim($_POST['password'] ?? '');
$db = getDB();
$stmt = $db->prepare('SELECT * FROM admins WHERE username = :username');
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
$result = $stmt->execute();
$admin = $result->fetchArray(SQLITE3_ASSOC);
if ($admin && password_verify($password, $admin['password_hash'])) {
$_SESSION['is_admin'] = true;
$_SESSION['admin_id'] = $admin['id'];
$_SESSION['admin_username'] = $admin['username'];
logSecurityEvent(null, 'ADMIN_LOGIN', "Admin: $username");
echo json_encode(['success' => true]);
} else {
logSecurityEvent(null, 'ADMIN_LOGIN_FAILED', "Username: $username");
echo json_encode(['success' => false, 'error' => 'Ungültige Zugangsdaten']);
}
exit;
}
// ───────────────────────────────────────────────────────
// LOGOUT
// ───────────────────────────────────────────────────────
if ($action === 'logout') {
if (isLoggedIn()) {
logSecurityEvent(getCurrentUserId(), 'LOGOUT', '');
}
session_destroy();
echo json_encode(['success' => true]);
exit;
}
// All other actions require login
if (!isLoggedIn() && !isAdmin()) {
echo json_encode(['success' => false, 'error' => 'Nicht eingeloggt']);
exit;
}
// ───────────────────────────────────────────────────────
// PING (UPDATE ONLINE STATUS)
// ───────────────────────────────────────────────────────
if ($action === 'ping') {
updateOnlineStatus(getCurrentUserId());
cleanupOldData();
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// GET USERS
// ───────────────────────────────────────────────────────
if ($action === 'get_users') {
$db = getDB();
$currentUserId = getCurrentUserId();
$currentAgeGroup = getCurrentAgeGroup();
$query = '
SELECT
u.id,
u.username,
u.user_id as display_id,
u.age_group,
u.last_seen,
CASE
WHEN os.last_ping IS NOT NULL
AND (julianday("now") - julianday(os.last_ping)) * 86400 < ' . ONLINE_TIMEOUT_SECONDS . '
THEN 1
ELSE 0
END as is_online,
(
SELECT COUNT(*)
FROM messages
WHERE from_user_id = u.id
AND to_user_id = :current_user_id
AND is_read = 0
) as unread_count,
(
SELECT COUNT(*)
FROM blocks
WHERE blocker_id = :current_user_id
AND blocked_id = u.id
) as is_blocked_by_me,
(
SELECT COUNT(*)
FROM blocks
WHERE blocker_id = u.id
AND blocked_id = :current_user_id
) as has_blocked_me
FROM users u
LEFT JOIN online_status os ON u.id = os.user_id
WHERE u.id != :current_user_id
AND u.is_banned = 0
AND u.age_group = :age_group
ORDER BY is_online DESC, u.username ASC
';
$stmt = $db->prepare($query);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':age_group', $currentAgeGroup, SQLITE3_TEXT);
$result = $stmt->execute();
$users = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
// Don't show users who blocked me or I blocked
if ($row['is_blocked_by_me'] > 0 || $row['has_blocked_me'] > 0) {
continue;
}
$users[] = [
'id' => $row['id'],
'username' => $row['username'],
'display_id' => $row['display_id'],
'display_name' => $row['username'] . '#' . $row['display_id'],
'age_group' => $row['age_group'],
'is_online' => $row['is_online'],
'unread_count' => $row['unread_count']
];
}
echo json_encode(['success' => true, 'users' => $users]);
exit;
}
// ───────────────────────────────────────────────────────
// GET MESSAGES
// ───────────────────────────────────────────────────────
if ($action === 'get_messages') {
$otherUserId = intval($_GET['user_id'] ?? 0);
if ($otherUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
// Check if blocked
if (isBlocked(getCurrentUserId(), $otherUserId)) {
echo json_encode(['success' => false, 'error' => 'Chat nicht verfügbar']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$query = '
SELECT
m.id,
m.from_user_id,
m.to_user_id,
m.message,
m.timestamp,
m.is_read,
m.is_flagged,
u.username as from_username,
u.user_id as from_display_id
FROM messages m
JOIN users u ON m.from_user_id = u.id
WHERE
(m.from_user_id = :current_user_id AND m.to_user_id = :other_user_id)
OR
(m.from_user_id = :other_user_id AND m.to_user_id = :current_user_id)
ORDER BY m.timestamp ASC
';
$stmt = $db->prepare($query);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$messages = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$messages[] = [
'id' => $row['id'],
'from_user_id' => $row['from_user_id'],
'to_user_id' => $row['to_user_id'],
'message' => $row['message'],
'timestamp' => $row['timestamp'],
'is_read' => $row['is_read'],
'is_flagged' => $row['is_flagged'],
'from_username' => $row['from_username'],
'from_display_name' => $row['from_username'] . '#' . $row['from_display_id']
];
}
echo json_encode(['success' => true, 'messages' => $messages]);
exit;
}
// ───────────────────────────────────────────────────────
// SEND MESSAGE
// ───────────────────────────────────────────────────────
if ($action === 'send_message') {
$toUserId = intval($_POST['to_user_id'] ?? 0);
$message = trim($_POST['message'] ?? '');
if ($toUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
if (empty($message)) {
echo json_encode(['success' => false, 'error' => 'Nachricht darf nicht leer sein']);
exit;
}
if (strlen($message) > 1000) {
echo json_encode(['success' => false, 'error' => 'Nachricht zu lang (max 1000 Zeichen)']);
exit;
}
// Check if blocked
if (isBlocked(getCurrentUserId(), $toUserId)) {
echo json_encode(['success' => false, 'error' => 'Nachricht kann nicht gesendet werden']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$currentAgeGroup = getCurrentAgeGroup();
// Rate Limiting
$rateLimitCheck = checkRateLimit($currentUserId, $currentAgeGroup);
if (!$rateLimitCheck['allowed']) {
logSecurityEvent($currentUserId, 'RATE_LIMIT_EXCEEDED', $rateLimitCheck['reason']);
echo json_encode(['success' => false, 'error' => $rateLimitCheck['reason']]);
exit;
}
// Keyword Blacklist
$keywordCheck = checkKeywordBlacklist($message);
if ($keywordCheck['blocked']) {
logSecurityEvent($currentUserId, 'KEYWORD_BLOCKED', "Keyword: {$keywordCheck['keyword']}");
echo
echo json_encode([
'success' => false,
'error' => 'Deine Nachricht enthält nicht erlaubte Inhalte',
'details' => 'Verbotenes Wort erkannt: ' . $keywordCheck['keyword']
]);
exit;
}
// Profanity Filter
$profanityCheck = checkProfanityFilter($message);
if ($profanityCheck['blocked']) {
logSecurityEvent($currentUserId, 'PROFANITY_BLOCKED', "Word: {$profanityCheck['word']}");
echo json_encode([
'success' => false,
'error' => 'Deine Nachricht enthält Schimpfwörter',
'details' => 'Bitte verwende eine angemessene Sprache'
]);
exit;
}
// Link Filter
$linkCheck = checkLinkFilter($message);
if ($linkCheck['blocked']) {
logSecurityEvent($currentUserId, 'LINK_BLOCKED', "Message: $message");
echo json_encode([
'success' => false,
'error' => 'Links sind nicht erlaubt',
'details' => 'Aus Sicherheitsgründen können keine URLs gesendet werden'
]);
exit;
}
// Auto-Flagging (verdächtige Muster)
$isFlagged = 0;
$flagReason = '';
// Check for repeated characters (AAAAAAA)
if (preg_match('/(.)\1{5,}/', $message)) {
$isFlagged = 1;
$flagReason = 'Repeated characters';
}
// Check for all caps (min 20 chars)
if (strlen($message) > 20 && $message === strtoupper($message)) {
$isFlagged = 1;
$flagReason = 'All caps';
}
// Check for excessive emojis
$emojiCount = preg_match_all('/[\x{1F600}-\x{1F64F}]/u', $message);
if ($emojiCount > 10) {
$isFlagged = 1;
$flagReason = 'Excessive emojis';
}
// Insert message
$stmt = $db->prepare('
INSERT INTO messages (from_user_id, to_user_id, message, is_flagged, flag_reason)
VALUES (:from_user_id, :to_user_id, :message, :is_flagged, :flag_reason)
');
$stmt->bindValue(':from_user_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':to_user_id', $toUserId, SQLITE3_INTEGER);
$stmt->bindValue(':message', $message, SQLITE3_TEXT);
$stmt->bindValue(':is_flagged', $isFlagged, SQLITE3_INTEGER);
$stmt->bindValue(':flag_reason', $flagReason, SQLITE3_TEXT);
$stmt->execute();
$messageId = $db->lastInsertRowID();
// Log rate limit
logRateLimit($currentUserId);
if ($isFlagged) {
logSecurityEvent($currentUserId, 'MESSAGE_FLAGGED', "Reason: $flagReason, Message ID: $messageId");
}
echo json_encode([
'success' => true,
'message_id' => $messageId,
'timestamp' => date('Y-m-d H:i:s')
]);
exit;
}
// ───────────────────────────────────────────────────────
// MARK AS READ
// ───────────────────────────────────────────────────────
if ($action === 'mark_read') {
$otherUserId = intval($_POST['user_id'] ?? 0);
if ($otherUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
UPDATE messages
SET is_read = 1
WHERE from_user_id = :other_user_id
AND to_user_id = :current_user_id
AND is_read = 0
');
$stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$stmt->execute();
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// BLOCK USER
// ───────────────────────────────────────────────────────
if ($action === 'block_user') {
$blockedUserId = intval($_POST['user_id'] ?? 0);
if ($blockedUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
INSERT OR IGNORE INTO blocks (blocker_id, blocked_id)
VALUES (:blocker_id, :blocked_id)
');
$stmt->bindValue(':blocker_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':blocked_id', $blockedUserId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($currentUserId, 'USER_BLOCKED', "Blocked user ID: $blockedUserId");
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// UNBLOCK USER
// ───────────────────────────────────────────────────────
if ($action === 'unblock_user') {
$blockedUserId = intval($_POST['user_id'] ?? 0);
if ($blockedUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
DELETE FROM blocks
WHERE blocker_id = :blocker_id
AND blocked_id = :blocked_id
');
$stmt->bindValue(':blocker_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':blocked_id', $blockedUserId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($currentUserId, 'USER_UNBLOCKED', "Unblocked user ID: $blockedUserId");
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// REPORT USER
// ───────────────────────────────────────────────────────
if ($action === 'report_user') {
$reportedUserId = intval($_POST['user_id'] ?? 0);
$reason = trim($_POST['reason'] ?? '');
$messageId = intval($_POST['message_id'] ?? 0);
if ($reportedUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
if (empty($reason)) {
echo json_encode(['success' => false, 'error' => 'Bitte gib einen Grund an']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
INSERT INTO reports (reporter_id, reported_user_id, reason, message_id)
VALUES (:reporter_id, :reported_user_id, :reason, :message_id)
');
$stmt->bindValue(':reporter_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':reported_user_id', $reportedUserId, SQLITE3_INTEGER);
$stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
$stmt->bindValue(':message_id', $messageId > 0 ? $messageId : null, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($currentUserId, 'USER_REPORTED', "Reported user ID: $reportedUserId, Reason: $reason");
echo json_encode(['success' => true, 'message' => 'Meldung wurde erfolgreich gesendet']);
exit;
}
// ───────────────────────────────────────────────────────
// GET BLOCKED USERS
// ───────────────────────────────────────────────────────
if ($action === 'get_blocked_users') {
$db = getDB();
$currentUserId = getCurrentUserId();
$query = '
SELECT
u.id,
u.username,
u.user_id as display_id,
b.timestamp as blocked_at
FROM blocks b
JOIN users u ON b.blocked_id = u.id
WHERE b.blocker_id = :current_user_id
ORDER BY b.timestamp DESC
';
$stmt = $db->prepare($query);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$blockedUsers = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$blockedUsers[] = [
'id' => $row['id'],
'display_name' => $row['username'] . '#' . $row['display_id'],
'blocked_at' => $row['blocked_at']
];
}
echo json_encode(['success' => true, 'blocked_users' => $blockedUsers]);
exit;
}
// ═══════════════════════════════════════════════════════════
// ADMIN ACTIONS
// ═══════════════════════════════════════════════════════════
if (!isAdmin()) {
echo json_encode(['success' => false, 'error' => 'Admin-Rechte erforderlich']);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: GET REPORTS
// ───────────────────────────────────────────────────────
if ($action === 'admin_get_reports') {
$db = getDB();
$query = '
SELECT
r.id,
r.reason,
r.timestamp,
r.status,
r.message_id,
reporter.username as reporter_name,
reporter.user_id as reporter_display_id,
reported.username as reported_name,
reported.user_id as reported_display_id,
reported.id as reported_user_id,
m.message as message_content
FROM reports r
JOIN users reporter ON r.reporter_id = reporter.id
JOIN users reported ON r.reported_user_id = reported.id
LEFT JOIN messages m ON r.message_id = m.id
WHERE r.status = "pending"
ORDER BY r.timestamp DESC
LIMIT 50
';
$result = $db->query($query);
$reports = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$reports[] = [
'id' => $row['id'],
'reporter' => $row['reporter_name'] . '#' . $row['reporter_display_id'],
'reported' => $row['reported_name'] . '#' . $row['reported_display_id'],
'reported_user_id' => $row['reported_user_id'],
'reason' => $row['reason'],
'message' => $row['message_content'],
'timestamp' => $row['timestamp'],
'status' => $row['status']
];
}
echo json_encode(['success' => true, 'reports' => $reports]);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: GET FLAGGED MESSAGES
// ───────────────────────────────────────────────────────
if ($action === 'admin_get_flagged') {
$db = getDB();
$query = '
SELECT
m.id,
m.message,
m.flag_reason,
m.timestamp,
u.username,
u.user_id as display_id,
u.id as user_db_id
FROM messages m
JOIN users u ON m.from_user_id = u.id
WHERE m.is_flagged = 1
ORDER BY m.timestamp DESC
LIMIT 50
';
$result = $db->query($query);
$flagged = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$flagged[] = [
'id' => $row['id'],
'user' => $row['username'] . '#' . $row['display_id'],
'user_id' => $row['user_db_id'],
'message' => $row['message'],
'reason' => $row['flag_reason'],
'timestamp' => $row['timestamp']
];
}
echo json_encode(['success' => true, 'flagged' => $flagged]);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: GET STATISTICS
// ───────────────────────────────────────────────────────
if ($action === 'admin_get_stats') {
$db = getDB();
// Total users
$result = $db->query('SELECT COUNT(*) as count FROM users WHERE is_banned = 0');
$totalUsers = $result->fetchArray(SQLITE3_ASSOC)['count'];
// U18 users
$result = $db->query('SELECT COUNT(*) as count FROM users WHERE age_group = "U18" AND is_banned = 0');
$u18Users = $result->fetchArray(SQLITE3_ASSOC)['count'];
// O18 users
$result = $db->query('SELECT COUNT(*) as count FROM users WHERE age_group = "O18" AND is_banned = 0');
$o18Users = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Online users
$result = $db->query('
SELECT COUNT(*) as count
FROM online_status
WHERE (julianday("now") - julianday(last_ping)) * 86400 < ' . ONLINE_TIMEOUT_SECONDS
);
$onlineUsers = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Total messages (24h)
$result = $db->query('SELECT COUNT(*) as count FROM messages');
$totalMessages = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Flagged messages
$result = $db->query('SELECT COUNT(*) as count FROM messages WHERE is_flagged = 1');
$flaggedMessages = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Pending reports
$result = $db->query('SELECT COUNT(*) as count FROM reports WHERE status = "pending"');
$pendingReports = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Banned users
$result = $db->query('SELECT COUNT(*) as count FROM users WHERE is_banned = 1');
$bannedUsers = $result->fetchArray(SQLITE3_ASSOC)['count'];
echo json_encode([
'success' => true,
'stats' => [
'total_users' => $totalUsers,
'u18_users' => $u18Users,
'o18_users' => $o18Users,
'online_users' => $onlineUsers,
'total_messages' => $totalMessages,
'flagged_messages' => $flaggedMessages,
'pending_reports' => $pendingReports,
'banned_users' => $bannedUsers
]
]);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: BAN USER
// ───────────────────────────────────────────────────────
if ($action === 'admin_ban_user') {
$userId = intval($_POST['user_id'] ?? 0);
$reason = trim($_POST['reason'] ?? 'Verstoß gegen Nutzungsbedingungen');
if ($userId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$stmt = $db->prepare('
UPDATE users
SET is_banned = 1, ban_reason = :reason
WHERE id = :user_id
');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
$stmt->execute();
logSecurityEvent(null, 'ADMIN_BAN_USER', "User ID: $userId, Reason: $reason");
echo json_encode(['success' => true, 'message' => 'User wurde gesperrt']);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: UNBAN USER
// ───────────────────────────────────────────────────────
if ($action === 'admin_unban_user') {
$userId = intval($_POST['user_id'] ?? 0);
if ($userId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$stmt = $db->prepare('
UPDATE users
SET is_banned = 0, ban_reason = NULL
WHERE id = :user_id
');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent(null, 'ADMIN_UNBAN_USER', "User ID: $userId");
echo json_encode(['success' => true, 'message' => 'Sperre wurde aufgehoben']);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: RESOLVE REPORT
// ───────────────────────────────────────────────────────
if ($action === 'admin_resolve_report') {
$reportId = intval($_POST['report_id'] ?? 0);
$action_taken = trim($_POST['action_taken'] ?? 'resolved');
if ($reportId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige Report-ID']);
exit;
}
$db = getDB();
$stmt = $db->prepare('
UPDATE reports
SET status = :status
WHERE id = :report_id
');
$stmt->bindValue(':report_id', $reportId, SQLITE3_INTEGER);
$stmt->bindValue(':status', $action_taken, SQLITE3_TEXT);
$stmt->execute();
logSecurityEvent(null, 'ADMIN_RESOLVE_REPORT', "Report ID: $reportId, Action: $action_taken");
echo json_encode(['success' => true, 'message' => 'Report wurde bearbeitet']);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: DELETE MESSAGE
// ───────────────────────────────────────────────────────
if ($action === 'admin_delete_message') {
$messageId = intval($_POST['message_id'] ?? 0);
if ($messageId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige Message-ID']);
exit;
}
$db = getDB();
$stmt = $db->prepare('DELETE FROM messages WHERE id = :message_id');
$stmt->bindValue(':message_id', $messageId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent(null, 'ADMIN_DELETE_MESSAGE', "Message ID: $messageId");
echo json_encode(['success' => true, 'message' => 'Nachricht wurde gelöscht']);
exit;
}
echo json_encode(['success' => false, 'error' => 'Unbekannte Aktion']);
exit;
}
// ═══════════════════════════════════════════════════════════
// SSE STREAM (ECHTZEIT)
// ═══════════════════════════════════════════════════════════
if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
if (!isLoggedIn()) {
exit;
}
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
$currentUserId = getCurrentUserId();
$lastMessageId = intval($_GET['last_message_id'] ?? 0);
set_time_limit(0);
ob_implicit_flush(true);
ob_end_flush();
$db = getDB();
$stmt = $db->prepare('
SELECT
m.id,
m.from_user_id,
m.to_user_id,
m.message,
m.timestamp,
u.username as from_username,
u.user_id as from_display_id
FROM messages m
JOIN users u ON m.from_user_id = u.id
WHERE m.id > :last_message_id
AND (m.to_user_id = :current_user_id OR m.from_user_id = :current_user_id)
AND NOT EXISTS (
SELECT 1 FROM blocks
WHERE (blocker_id = :current_user_id AND blocked_id = m.from_user_id)
OR (blocker_id = m.from_user_id AND blocked_id = :current_user_id)
)
ORDER BY m.id ASC
');
$stmt->bindValue(':last_message_id', $lastMessageId, SQLITE3_INTEGER);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$messages = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$messages[] = [
'id' => $row['id'],
'from_user_id' => $row['from_user_id'],
'to_user_id' => $row['to_user_id'],
'message' => $row['message'],
'timestamp' => $row['timestamp'],
'from_username' => $row['from_username'],
'from_display_name' => $row['from_username'] . '#' . $row['from_display_id']
];
}
if (!empty($messages)) {
echo "data: " . json_encode(['type' => 'messages', 'messages' => $messages]) . "\n\n";
flush();
} else {
echo "data: " . json_encode(['type' => 'ping']) . "\n\n";
flush();
}
exit;
}
// ═══════════════════════════════════════════════════════════
// HTML OUTPUT
// ═══════════════════════════════════════════════════════════
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🔒 Sicherer Private Chat</title>
echo json_encode([
'success' => false,
'error' => 'Diese Nachricht enthält nicht erlaubte Inhalte: "' . $keywordCheck['keyword'] . '"',
'blocked_keyword' => true
]);
exit;
}
// Profanity Filter
$profanityCheck = checkProfanityFilter($message);
if ($profanityCheck['blocked']) {
logSecurityEvent($currentUserId, 'PROFANITY_BLOCKED', "Word: {$profanityCheck['word']}");
echo json_encode([
'success' => false,
'error' => 'Bitte verwende keine Schimpfwörter',
'blocked_profanity' => true
]);
exit;
}
// Link Filter
$linkCheck = checkLinkFilter($message);
if ($linkCheck['blocked']) {
logSecurityEvent($currentUserId, 'LINK_BLOCKED', "Message: $message");
echo json_encode([
'success' => false,
'error' => 'Links sind nicht erlaubt',
'blocked_link' => true
]);
exit;
}
// Insert message
$stmt = $db->prepare('
INSERT INTO messages (from_user_id, to_user_id, message)
VALUES (:from_user_id, :to_user_id, :message)
');
$stmt->bindValue(':from_user_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':to_user_id', $toUserId, SQLITE3_INTEGER);
$stmt->bindValue(':message', $message, SQLITE3_TEXT);
$stmt->execute();
$messageId = $db->lastInsertRowID();
// Log rate limit
logRateLimit($currentUserId);
echo json_encode([
'success' => true,
'message_id' => $messageId,
'timestamp' => date('Y-m-d H:i:s')
]);
exit;
}
// ───────────────────────────────────────────────────────
// MARK AS READ
// ───────────────────────────────────────────────────────
if ($action === 'mark_read') {
$otherUserId = intval($_POST['user_id'] ?? 0);
if ($otherUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
UPDATE messages
SET is_read = 1
WHERE from_user_id = :other_user_id
AND to_user_id = :current_user_id
AND is_read = 0
');
$stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$stmt->execute();
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// BLOCK USER
// ───────────────────────────────────────────────────────
if ($action === 'block_user') {
$blockUserId = intval($_POST['user_id'] ?? 0);
if ($blockUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
INSERT OR IGNORE INTO blocks (blocker_id, blocked_id)
VALUES (:blocker_id, :blocked_id)
');
$stmt->bindValue(':blocker_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':blocked_id', $blockUserId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($currentUserId, 'USER_BLOCKED', "Blocked user ID: $blockUserId");
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// UNBLOCK USER
// ───────────────────────────────────────────────────────
if ($action === 'unblock_user') {
$unblockUserId = intval($_POST['user_id'] ?? 0);
if ($unblockUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
DELETE FROM blocks
WHERE blocker_id = :blocker_id
AND blocked_id = :blocked_id
');
$stmt->bindValue(':blocker_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':blocked_id', $unblockUserId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($currentUserId, 'USER_UNBLOCKED', "Unblocked user ID: $unblockUserId");
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// REPORT USER
// ───────────────────────────────────────────────────────
if ($action === 'report_user') {
$reportedUserId = intval($_POST['user_id'] ?? 0);
$reason = trim($_POST['reason'] ?? '');
$messageId = intval($_POST['message_id'] ?? 0);
if ($reportedUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
if (empty($reason)) {
echo json_encode(['success' => false, 'error' => 'Bitte gib einen Grund an']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
INSERT INTO reports (reporter_id, reported_user_id, reason, message_id)
VALUES (:reporter_id, :reported_user_id, :reason, :message_id)
');
$stmt->bindValue(':reporter_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':reported_user_id', $reportedUserId, SQLITE3_INTEGER);
$stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
$stmt->bindValue(':message_id', $messageId > 0 ? $messageId : null, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($currentUserId, 'USER_REPORTED', "Reported user ID: $reportedUserId, Reason: $reason");
// Flag message if provided
if ($messageId > 0) {
$stmt = $db->prepare('
UPDATE messages
SET is_flagged = 1, flag_reason = :reason
WHERE id = :message_id
');
$stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
$stmt->bindValue(':message_id', $messageId, SQLITE3_INTEGER);
$stmt->execute();
}
echo json_encode(['success' => true, 'message' => 'Meldung wurde erfasst. Danke!']);
exit;
}
// ───────────────────────────────────────────────────────
// GET BLOCKED USERS
// ───────────────────────────────────────────────────────
if ($action === 'get_blocked_users') {
$db = getDB();
$currentUserId = getCurrentUserId();
$query = '
SELECT
u.id,
u.username,
u.user_id as display_id,
b.timestamp as blocked_at
FROM blocks b
JOIN users u ON b.blocked_id = u.id
WHERE b.blocker_id = :current_user_id
ORDER BY b.timestamp DESC
';
$stmt = $db->prepare($query);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$blocked = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$blocked[] = [
'id' => $row['id'],
'username' => $row['username'],
'display_id' => $row['display_id'],
'display_name' => $row['username'] . '#' . $row['display_id'],
'blocked_at' => $row['blocked_at']
];
}
echo json_encode(['success' => true, 'blocked' => $blocked]);
exit;
}
// ═══════════════════════════════════════════════════════════
// ADMIN ACTIONS
// ═══════════════════════════════════════════════════════════
if (!isAdmin()) {
echo json_encode(['success' => false, 'error' => 'Admin-Rechte erforderlich']);
exit;
}
// ───────────────────────────────────────────────────────
// GET ADMIN STATS
// ───────────────────────────────────────────────────────
if ($action === 'admin_stats') {
$db = getDB();
// Total users
$result = $db->query('SELECT COUNT(*) as count FROM users WHERE is_banned = 0');
$totalUsers = $result->fetchArray(SQLITE3_ASSOC)['count'];
// U18 users
$result = $db->query('SELECT COUNT(*) as count FROM users WHERE age_group = "U18" AND is_banned = 0');
$u18Users = $result->fetchArray(SQLITE3_ASSOC)['count'];
// O18 users
$result = $db->query('SELECT COUNT(*) as count FROM users WHERE age_group = "O18" AND is_banned = 0');
$o18Users = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Total messages today
$result = $db->query('SELECT COUNT(*) as count FROM messages WHERE DATE(timestamp) = DATE("now")');
$messagesToday = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Pending reports
$result = $db->query('SELECT COUNT(*) as count FROM reports WHERE status = "pending"');
$pendingReports = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Flagged messages
$result = $db->query('SELECT COUNT(*) as count FROM messages WHERE is_flagged = 1');
$flaggedMessages = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Banned users
$result = $db->query('SELECT COUNT(*) as count FROM users WHERE is_banned = 1');
$bannedUsers = $result->fetchArray(SQLITE3_ASSOC)['count'];
echo json_encode([
'success' => true,
'stats' => [
'total_users' => $totalUsers,
'u18_users' => $u18Users,
'o18_users' => $o18Users,
'messages_today' => $messagesToday,
'pending_reports' => $pendingReports,
'flagged_messages' => $flaggedMessages,
'banned_users' => $bannedUsers
]
]);
exit;
}
// ───────────────────────────────────────────────────────
// GET REPORTS
// ───────────────────────────────────────────────────────
if ($action === 'admin_get_reports') {
$db = getDB();
$query = '
SELECT
r.id,
r.reason,
r.timestamp,
r.status,
r.message_id,
reporter.username as reporter_name,
reporter.user_id as reporter_display_id,
reported.username as reported_name,
reported.user_id as reported_display_id,
reported.id as reported_user_id,
m.message as message_content
FROM reports r
JOIN users reporter ON r.reporter_id = reporter.id
JOIN users reported ON r.reported_user_id = reported.id
LEFT JOIN messages m ON r.message_id = m.id
ORDER BY r.timestamp DESC
LIMIT 50
';
$result = $db->query($query);
$reports = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$reports[] = [
'id' => $row['id'],
'reason' => $row['reason'],
'timestamp' => $row['timestamp'],
'status' => $row['status'],
'reporter_name' => $row['reporter_name'] . '#' . $row['reporter_display_id'],
'reported_name' => $row['reported_name'] . '#' . $row['reported_display_id'],
'reported_user_id' => $row['reported_user_id'],
'message_content' => $row['message_content']
];
}
echo json_encode(['success' => true, 'reports' => $reports]);
exit;
}
// ───────────────────────────────────────────────────────
// BAN USER
// ───────────────────────────────────────────────────────
if ($action === 'admin_ban_user') {
$userId = intval($_POST['user_id'] ?? 0);
$reason = trim($_POST['reason'] ?? 'Verstoß gegen Nutzungsbedingungen');
if ($userId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$stmt = $db->prepare('
UPDATE users
SET is_banned = 1, ban_reason = :reason
WHERE id = :user_id
');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
$stmt->execute();
logSecurityEvent(null, 'ADMIN_BAN_USER', "User ID: $userId, Reason: $reason");
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// UNBAN USER
// ───────────────────────────────────────────────────────
if ($action === 'admin_unban_user') {
$userId = intval($_POST['user_id'] ?? 0);
if ($userId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$stmt = $db->prepare('
UPDATE users
SET is_banned = 0, ban_reason = NULL
WHERE id = :user_id
');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent(null, 'ADMIN_UNBAN_USER', "User ID: $userId");
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// RESOLVE REPORT
// ───────────────────────────────────────────────────────
if ($action === 'admin_resolve_report') {
$reportId = intval($_POST['report_id'] ?? 0);
$status = $_POST['status'] ?? 'resolved';
if ($reportId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige Report-ID']);
exit;
}
$db = getDB();
$stmt = $db->prepare('UPDATE reports SET status = :status WHERE id = :report_id');
$stmt->bindValue(':status', $status, SQLITE3_TEXT);
$stmt->bindValue(':report_id', $reportId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent(null, 'ADMIN_RESOLVE_REPORT', "Report ID: $reportId, Status: $status");
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// GET SECURITY LOGS
// ───────────────────────────────────────────────────────
if ($action === 'admin_get_logs') {
$db = getDB();
$query = '
SELECT
l.id,
l.action,
l.details,
l.ip_address,
l.timestamp,
u.username,
u.user_id as display_id
FROM security_logs l
LEFT JOIN users u ON l.user_id = u.id
ORDER BY l.timestamp DESC
LIMIT 100
';
$result = $db->query($query);
$logs = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$logs[] = [
'id' => $row['id'],
'action' => $row['action'],
'details' => $row['details'],
'ip_address' => $row['ip_address'],
'timestamp' => $row['timestamp'],
'username' => $row['username'] ? $row['username'] . '#' . $row['display_id'] : 'System'
];
}
echo json_encode(['success' => true, 'logs' => $logs]);
exit;
}
echo json_encode(['success' => false, 'error' => 'Unbekannte Aktion']);
exit;
}
// ═══════════════════════════════════════════════════════════
// SSE STREAM (ECHTZEIT)
// ═══════════════════════════════════════════════════════════
if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
if (!isLoggedIn()) {
exit;
}
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
$currentUserId = getCurrentUserId();
$lastMessageId = intval($_GET['last_message_id'] ?? 0);
set_time_limit(0);
ob_implicit_flush(true);
ob_end_flush();
$db = getDB();
$stmt = $db->prepare('
SELECT
m.id,
m.from_user_id,
m.to_user_id,
m.message,
m.timestamp,
u.username as from_username,
u.user_id as from_display_id
FROM messages m
JOIN users u ON m.from_user_id = u.id
WHERE m.id > :last_message_id
AND (m.to_user_id = :current_user_id OR m.from_user_id = :current_user_id)
ORDER BY m.id ASC
');
$stmt->bindValue(':last_message_id', $lastMessageId, SQLITE3_INTEGER);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$messages = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$messages[] = [
'id' => $row['id'],
'from_user_id' => $row['from_user_id'],
'to_user_id' => $row['to_user_id'],
'message' => $row['message'],
'timestamp' => $row['timestamp'],
'from_username' => $row['from_username'],
'from_display_id' => $row['from_display_id'],
'from_display_name' => $row['from_username'] . '#' . $row['from_display_id']
];
}
if (!empty($messages)) {
echo "data: " . json_encode(['type' => 'messages', 'messages' => $messages]) . "\n\n";
flush();
} else {
echo "data: " . json_encode(['type' => 'ping']) . "\n\n";
flush();
}
exit;
}
// ═══════════════════════════════════════════════════════════
// HTML OUTPUT
// ═══════════════════════════════════════════════════════════
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>💬 Secure Private Chat</title>
<style>
/* ═══════════════════════════════════════════════════════════ */
/* CSS STYLING */
/* ═══════════════════════════════════════════════════════════ */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
/* ───────────────────────────────────────────────────────── */
/* REGISTRATION SCREEN */
/* ───────────────────────────────────────────────────────── */
.register-container {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 500px;
width: 100%;
}
.register-container h1 {
color: #667eea;
margin-bottom: 10px;
font-size: 32px;
text-align: center;
}
.register-container .subtitle {
color: #666;
margin-bottom: 30px;
text-align: center;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 600;
}
.form-group input {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 16px;
transition: border-color 0.3s;
}
.form-group input:focus {
outline: none;
border-color: #667eea;
}
.terms-box {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
max-height: 200px;
overflow-y: auto;
font-size: 14px;
line-height: 1.6;
color: #555;
}
.terms-box h3 {
color: #667eea;
margin-bottom: 10px;
}
.terms-box ul {
margin-left: 20px;
}
.terms-box li {
margin-bottom: 8px;
}
.checkbox-group {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.checkbox-group input[type="checkbox"] {
width: 20px;
height: 20px;
margin-right: 10px;
cursor: pointer;
}
.checkbox-group label {
color: #333;
cursor: pointer;
}
.btn-primary {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s;
}
.btn-primary:hover {
transform: translateY(-2px);
}
.btn-primary:active {
transform: translateY(0);
}
.btn-primary:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error-message {
background: #fee;
color: #c33;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
font-size: 14px;
}
.success-message {
background: #efe;
color: #3c3;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
font-size: 14px;
}
.info-box {
background: #e3f2fd;
padding: 15px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
color: #1976d2;
}
.age-warning {
background: #fff3cd;
color: #856404;
padding: 12px;
border-radius: 8px;
margin-top: 10px;
font-size: 13px;
}
/* ───────────────────────────────────────────────────────── */
/* ADMIN LOGIN */
/* ───────────────────────────────────────────────────────── */
.admin-link {
text-align: center;
margin-top: 20px;
font-size: 14px;
}
.admin-link a {
color: #667eea;
text-decoration: none;
}
.admin-link a:hover {
text-decoration: underline;
}
/* ───────────────────────────────────────────────────────── */
/* CHAT CONTAINER */
/* ───────────────────────────────────────────────────────── */
.chat-container {
display: none;
width: 95%;
max-width: 1400px;
height: 90vh;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
grid-template-columns: 350px 1fr;
grid-template-rows: 60px 1fr;
}
.chat-container.show {
display: grid;
}
/* Chat Header */
.chat-header {
grid-column: 1 / -1;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-header h1 {
font-size: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.chat-header .user-info {
display: flex;
align-items: center;
gap: 15px;
}
.chat-header .username {
font-weight: bold;
}
.chat-header .age-badge {
background: rgba(255,255,255,0.3);
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
}
.chat-header button {
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
.chat-header button:hover {
background: rgba(255,255,255,0.3);
}
/* Sidebar */
.sidebar {
background: #f5f5f5;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.sidebar-tabs {
display: flex;
background: white;
border-bottom: 1px solid #e0e0e0;
}
.sidebar-tab {
flex: 1;
padding: 12px;
text-align: center;
cursor: pointer;
border-bottom: 3px solid transparent;
transition: all 0.3s;
}
.sidebar-tab.active {
border-bottom-color: #667eea;
color: #667eea;
font-weight: bold;
}
.sidebar-search {
padding: 15px;
background: white;
border-bottom: 1px solid #e0e0e0;
}
.sidebar-search input {
width: 100%;
padding: 10px 15px;
border: 1px solid #e0e0e0;
border-radius: 20px;
font-size: 14px;
}
.sidebar-search input:focus {
outline: none;
border-color: #667eea;
}
.user-list {
flex: 1;
overflow-y: auto;
}
.user-item {
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
gap:
echo json_encode([
'success' => false,
'error' => 'Diese Nachricht enthält nicht erlaubte Inhalte. Gib niemals persönliche Daten weiter!',
'blocked_keyword' => $keywordCheck['keyword']
]);
exit;
}
// Profanity Filter
$profanityCheck = checkProfanityFilter($message);
if ($profanityCheck['blocked']) {
logSecurityEvent($currentUserId, 'PROFANITY_BLOCKED', "Word: {$profanityCheck['word']}");
echo json_encode([
'success' => false,
'error' => 'Bitte verwende keine Schimpfwörter oder Beleidigungen.'
]);
exit;
}
// Link Filter
$linkCheck = checkLinkFilter($message);
if ($linkCheck['blocked']) {
logSecurityEvent($currentUserId, 'LINK_BLOCKED', "Message: $message");
echo json_encode([
'success' => false,
'error' => 'Links und URLs sind nicht erlaubt. Teile keine externen Kontakte!'
]);
exit;
}
// Insert message
$stmt = $db->prepare('
INSERT INTO messages (from_user_id, to_user_id, message)
VALUES (:from_user_id, :to_user_id, :message)
');
$stmt->bindValue(':from_user_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':to_user_id', $toUserId, SQLITE3_INTEGER);
$stmt->bindValue(':message', $message, SQLITE3_TEXT);
$stmt->execute();
$messageId = $db->lastInsertRowID();
// Log rate limit
logRateLimit($currentUserId);
echo json_encode([
'success' => true,
'message_id' => $messageId,
'timestamp' => date('Y-m-d H:i:s')
]);
exit;
}
// ───────────────────────────────────────────────────────
// MARK AS READ
// ───────────────────────────────────────────────────────
if ($action === 'mark_read') {
$otherUserId = intval($_POST['user_id'] ?? 0);
if ($otherUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
UPDATE messages
SET is_read = 1
WHERE from_user_id = :other_user_id
AND to_user_id = :current_user_id
AND is_read = 0
');
$stmt->bindValue(':other_user_id', $otherUserId, SQLITE3_INTEGER);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$stmt->execute();
echo json_encode(['success' => true]);
exit;
}
// ───────────────────────────────────────────────────────
// BLOCK USER
// ───────────────────────────────────────────────────────
if ($action === 'block_user') {
$blockedUserId = intval($_POST['user_id'] ?? 0);
if ($blockedUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
INSERT OR IGNORE INTO blocks (blocker_id, blocked_id)
VALUES (:blocker_id, :blocked_id)
');
$stmt->bindValue(':blocker_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':blocked_id', $blockedUserId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($currentUserId, 'BLOCK_USER', "Blocked user ID: $blockedUserId");
echo json_encode(['success' => true, 'message' => 'User wurde blockiert']);
exit;
}
// ───────────────────────────────────────────────────────
// UNBLOCK USER
// ───────────────────────────────────────────────────────
if ($action === 'unblock_user') {
$blockedUserId = intval($_POST['user_id'] ?? 0);
if ($blockedUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
DELETE FROM blocks
WHERE blocker_id = :blocker_id
AND blocked_id = :blocked_id
');
$stmt->bindValue(':blocker_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':blocked_id', $blockedUserId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($currentUserId, 'UNBLOCK_USER', "Unblocked user ID: $blockedUserId");
echo json_encode(['success' => true, 'message' => 'User wurde entblockt']);
exit;
}
// ───────────────────────────────────────────────────────
// REPORT USER
// ───────────────────────────────────────────────────────
if ($action === 'report_user') {
$reportedUserId = intval($_POST['user_id'] ?? 0);
$reason = trim($_POST['reason'] ?? '');
$messageId = intval($_POST['message_id'] ?? 0);
if ($reportedUserId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
if (empty($reason)) {
echo json_encode(['success' => false, 'error' => 'Bitte gib einen Grund an']);
exit;
}
$db = getDB();
$currentUserId = getCurrentUserId();
$stmt = $db->prepare('
INSERT INTO reports (reporter_id, reported_user_id, reason, message_id)
VALUES (:reporter_id, :reported_user_id, :reason, :message_id)
');
$stmt->bindValue(':reporter_id', $currentUserId, SQLITE3_INTEGER);
$stmt->bindValue(':reported_user_id', $reportedUserId, SQLITE3_INTEGER);
$stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
$stmt->bindValue(':message_id', $messageId > 0 ? $messageId : null, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($currentUserId, 'REPORT_USER', "Reported user ID: $reportedUserId, Reason: $reason");
// Auto-flag if multiple reports
$stmt = $db->prepare('
SELECT COUNT(*) as count
FROM reports
WHERE reported_user_id = :reported_user_id
AND status = "pending"
');
$stmt->bindValue(':reported_user_id', $reportedUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
if ($row['count'] >= 3) {
// Auto-ban after 3 reports
$stmt = $db->prepare('
UPDATE users
SET is_banned = 1, ban_reason = "Mehrfache Meldungen"
WHERE id = :user_id
');
$stmt->bindValue(':user_id', $reportedUserId, SQLITE3_INTEGER);
$stmt->execute();
logSecurityEvent($reportedUserId, 'AUTO_BAN', "3+ reports");
}
echo json_encode(['success' => true, 'message' => 'Meldung wurde gespeichert. Danke!']);
exit;
}
// ───────────────────────────────────────────────────────
// GET BLOCKED USERS
// ───────────────────────────────────────────────────────
if ($action === 'get_blocked_users') {
$db = getDB();
$currentUserId = getCurrentUserId();
$query = '
SELECT
u.id,
u.username,
u.user_id as display_id,
b.timestamp as blocked_at
FROM blocks b
JOIN users u ON b.blocked_id = u.id
WHERE b.blocker_id = :current_user_id
ORDER BY b.timestamp DESC
';
$stmt = $db->prepare($query);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$blocked = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$blocked[] = [
'id' => $row['id'],
'username' => $row['username'],
'display_name' => $row['username'] . '#' . $row['display_id'],
'blocked_at' => $row['blocked_at']
];
}
echo json_encode(['success' => true, 'blocked_users' => $blocked]);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: GET REPORTS
// ───────────────────────────────────────────────────────
if ($action === 'admin_get_reports') {
if (!isAdmin()) {
echo json_encode(['success' => false, 'error' => 'Keine Berechtigung']);
exit;
}
$db = getDB();
$query = '
SELECT
r.id,
r.reason,
r.timestamp,
r.status,
u1.username as reporter_username,
u1.user_id as reporter_display_id,
u2.username as reported_username,
u2.user_id as reported_display_id,
u2.id as reported_user_id,
m.message as message_content
FROM reports r
JOIN users u1 ON r.reporter_id = u1.id
JOIN users u2 ON r.reported_user_id = u2.id
LEFT JOIN messages m ON r.message_id = m.id
WHERE r.status = "pending"
ORDER BY r.timestamp DESC
';
$result = $db->query($query);
$reports = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$reports[] = [
'id' => $row['id'],
'reason' => $row['reason'],
'timestamp' => $row['timestamp'],
'reporter' => $row['reporter_username'] . '#' . $row['reporter_display_id'],
'reported_user' => $row['reported_username'] . '#' . $row['reported_display_id'],
'reported_user_id' => $row['reported_user_id'],
'message_content' => $row['message_content']
];
}
echo json_encode(['success' => true, 'reports' => $reports]);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: BAN USER
// ───────────────────────────────────────────────────────
if ($action === 'admin_ban_user') {
if (!isAdmin()) {
echo json_encode(['success' => false, 'error' => 'Keine Berechtigung']);
exit;
}
$userId = intval($_POST['user_id'] ?? 0);
$reason = trim($_POST['reason'] ?? 'Verstoß gegen Nutzungsbedingungen');
if ($userId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige User-ID']);
exit;
}
$db = getDB();
$stmt = $db->prepare('
UPDATE users
SET is_banned = 1, ban_reason = :reason
WHERE id = :user_id
');
$stmt->bindValue(':user_id', $userId, SQLITE3_INTEGER);
$stmt->bindValue(':reason', $reason, SQLITE3_TEXT);
$stmt->execute();
logSecurityEvent($userId, 'ADMIN_BAN', "Reason: $reason, Admin: " . $_SESSION['admin_username']);
echo json_encode(['success' => true, 'message' => 'User wurde gesperrt']);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: RESOLVE REPORT
// ───────────────────────────────────────────────────────
if ($action === 'admin_resolve_report') {
if (!isAdmin()) {
echo json_encode(['success' => false, 'error' => 'Keine Berechtigung']);
exit;
}
$reportId = intval($_POST['report_id'] ?? 0);
if ($reportId <= 0) {
echo json_encode(['success' => false, 'error' => 'Ungültige Report-ID']);
exit;
}
$db = getDB();
$stmt = $db->prepare('UPDATE reports SET status = "resolved" WHERE id = :report_id');
$stmt->bindValue(':report_id', $reportId, SQLITE3_INTEGER);
$stmt->execute();
echo json_encode(['success' => true, 'message' => 'Meldung wurde bearbeitet']);
exit;
}
// ───────────────────────────────────────────────────────
// ADMIN: GET STATS
// ───────────────────────────────────────────────────────
if ($action === 'admin_get_stats') {
if (!isAdmin()) {
echo json_encode(['success' => false, 'error' => 'Keine Berechtigung']);
exit;
}
$db = getDB();
// Total users
$result = $db->query('SELECT COUNT(*) as count FROM users');
$totalUsers = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Online users
$result = $db->query("
SELECT COUNT(*) as count
FROM online_status
WHERE (julianday('now') - julianday(last_ping)) * 86400 < " . ONLINE_TIMEOUT_SECONDS
);
$onlineUsers = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Pending reports
$result = $db->query('SELECT COUNT(*) as count FROM reports WHERE status = "pending"');
$pendingReports = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Messages today
$result = $db->query("SELECT COUNT(*) as count FROM messages WHERE timestamp > datetime('now', '-1 day')");
$messagesToday = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Banned users
$result = $db->query('SELECT COUNT(*) as count FROM users WHERE is_banned = 1');
$bannedUsers = $result->fetchArray(SQLITE3_ASSOC)['count'];
// Blocked keywords today
$result = $db->query("
SELECT COUNT(*) as count
FROM security_logs
WHERE action = 'KEYWORD_BLOCKED'
AND timestamp > datetime('now', '-1 day')
");
$blockedKeywords = $result->fetchArray(SQLITE3_ASSOC)['count'];
echo json_encode([
'success' => true,
'stats' => [
'total_users' => $totalUsers,
'online_users' => $onlineUsers,
'pending_reports' => $pendingReports,
'messages_today' => $messagesToday,
'banned_users' => $bannedUsers,
'blocked_keywords_today' => $blockedKeywords
]
]);
exit;
}
echo json_encode(['success' => false, 'error' => 'Unbekannte Aktion']);
exit;
}
// ═══════════════════════════════════════════════════════════
// SSE STREAM (ECHTZEIT)
// ═══════════════════════════════════════════════════════════
if (isset($_GET['stream']) && $_GET['stream'] === 'events') {
if (!isLoggedIn()) {
exit;
}
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('X-Accel-Buffering: no');
$currentUserId = getCurrentUserId();
$lastMessageId = intval($_GET['last_message_id'] ?? 0);
set_time_limit(0);
ob_implicit_flush(true);
ob_end_flush();
$db = getDB();
$stmt = $db->prepare('
SELECT
m.id,
m.from_user_id,
m.to_user_id,
m.message,
m.timestamp,
u.username as from_username,
u.user_id as from_display_id
FROM messages m
JOIN users u ON m.from_user_id = u.id
WHERE m.id > :last_message_id
AND (m.to_user_id = :current_user_id OR m.from_user_id = :current_user_id)
ORDER BY m.id ASC
');
$stmt->bindValue(':last_message_id', $lastMessageId, SQLITE3_INTEGER);
$stmt->bindValue(':current_user_id', $currentUserId, SQLITE3_INTEGER);
$result = $stmt->execute();
$messages = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$messages[] = $row;
}
if (!empty($messages)) {
echo "data: " . json_encode(['type' => 'messages', 'messages' => $messages]) . "\n\n";
flush();
} else {
echo "data: " . json_encode(['type' => 'ping']) . "\n\n";
flush();
}
exit;
}
// ═══════════════════════════════════════════════════════════
// HTML STARTS HERE
// ═══════════════════════════════════════════════════════════
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>💬 Secure Private Chat</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
/* ═══════════════════════════════════════════════════════════ */
/* LOGIN/REGISTER SCREEN */
/* ═══════════════════════════════════════════════════════════ */
.auth-container {
background: white;
padding: 40px;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 500px;
width: 100%;
}
.auth-container h1 {
color: #667eea;
margin-bottom: 10px;
font-size: 32px;
text-align: center;
}
.auth-container .subtitle {
color: #666;
margin-bottom: 30px;
text-align: center;
font-size: 14px;
}
.auth-container .warning-box {
background: #fff3cd;
border: 2px solid #ffc107;
border-radius: 10px;
padding: 15px;
margin-bottom: 20px;
}
.auth-container .warning-box h3 {
color: #856404;
margin-bottom: 10px;
font-size: 16px;
}
.auth-container .warning-box ul {
color: #856404;
margin-left: 20px;
font-size: 13px;
line-height: 1.6;
}
.auth-container .form-group {
margin-bottom: 20px;
}
.auth-container label {
display: block;
color: #333;
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
}
.auth-container input[type="text"],
.auth-container input[type="date"],
.auth-container input[type="password"] {
width: 100%;
padding: 12px 15px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 15px;
transition: border-color 0.3s;
}
.auth-container input:focus {
outline: none;
border-color: #667eea;
}
.auth-container .checkbox-group {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 20px;
}
.auth-container .checkbox-group input[type="checkbox"] {
margin-top: 4px;
width: 18px;
height: 18px;
cursor: pointer;
}
.auth-container .checkbox-group label {
margin: 0;
font-weight: normal;
font-size: 13px;
cursor: pointer;
}
.auth-container .terms-text {
font-size: 12px;
color: #666;
line-height: 1.6;
margin-top: 10px;
padding: 10px;
background: #f8f9fa;
border-radius: 5px;
max-height: 150px;
overflow-y: auto;
}
.auth-container button {
width: 100%;
padding: 15px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: transform 0.2s;
}
.auth-container button:hover {
transform: translateY(-2px);
}
.auth-container button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.error-message {
background: #fee;
color: #c33;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
font-size: 14px;
}
.success-message {
background: #d4edda;
color: #155724;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
display: none;
font-size: 14px;
}
.admin-link {
text-align: center;
margin-top: 20px;
font-size: 13px;
}
.admin-link a {
color: #667eea;
text-decoration: none;
}
.admin-link a:hover {
text-decoration: underline;
}
/* ═══════════════════════════════════════════════════════════ */
/* CHAT CONTAINER */
/* ═══════════════════════════════════════════════════════════ */
.chat-container {
display: none;
width: 95%;
max-width: 1400px;
height: 90vh;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
grid-template-columns: 350px 1fr;
grid-template-rows: 60px 1fr;
}
.chat-header {
grid-column: 1 / -1;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
}
.chat-header h1 {
font-size: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.chat-header .user-info {
display: flex;
align-items: center;
gap: 15px;
}
.chat-header .username {
font-weight: bold;
font-size: 14px;
}
.chat-header .age-badge {
background: rgba(255,255,255,0.3);
padding: 4px 10px;
border-radius: 12px;
font-size: 12px;
}
.chat-header button {
background: rgba(255,255,255,0.2);
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
font-size: 13px;
}
.chat-header button:hover {
background: rgba(255,255,255,0.3);
}
/* SIDEBAR */
.sidebar {
background: #f5f5f5;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.sidebar-search {
padding: 15px;
background: white;
border-bottom: 1px solid #e0e0e0;
}
.sidebar-search input {
width: 100%;
padding: 10px 15px;
border: 1px solid #e0e0e0;
border-radius: 20px;
font-size: 14px;
}
.sidebar-search input:focus {
outline: none;
border-color: #667eea;
}
.user-list {
flex: 1;
overflow-y: auto;
}
.user-item {
padding: 15px 20px;
border-bottom: 1px solid #e0e0e0;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
gap: 12px;
position: relative;
}
.user-item:hover {
background: #e8e8e8;
}
.user-item.active {
background: #667eea;
color: white;
}
.user-avatar {
width: 45px;
height: 45px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 16px;
flex-shrink: 0;
position: relative;
}
.user-item.active .user-avatar {
background: white;
color: #667eea;
}
.online-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
background: #4caf50;
border: 2px solid white;
position: absolute;
bottom: 0;
right: 0;
}
.offline-indicator {
background: #999;
}
.user-info-text {
flex: 1;
min-width: 0;
}
.user-name {
font-weight: 600;
font-size: 14px;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-status {
font-size: 12px;
color: #999;
}
.user-item.active .user-status {
color: rgba(255,255,255,0.8);
}
.unread-badge {
background: #f44336;
color: white;
border-radius: 12px;
padding: 2px 8px;
font-size: 11px;
font-weight: bold;
min-width: 20px;
text-align: center;
}
/* CHAT AREA */
.chat-area {
display: flex;
flex-direction: column;
background: #e5ddd5;
}
.chat-welcome {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: #999;
font-size: 18px;
}
.chat-welcome-icon {
font-size: 80px;
margin-bottom: 20px;
}
.chat-messages-container {
display: none;
flex-direction: column;
height: 100%;
}
.chat-messages-header {
background: white;
padding: 15px 20px;
border-bottom: 1
echo json_encode([
'success' => false,
'error' => 'Nachricht enthält nicht erlaubte Inhalte',
'reason' => 'Bitte gib keine persönlichen Daten weiter (Adressen, Telefonnummern, etc.)'
]);
exit;
}
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
gap: 12px;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.message {
max-width: 65%;
padding: 10px 15px;
border-radius: 10px;
word-wrap: break-word;
animation: slideIn 0.3s ease;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.message-received {
align-self: flex-start;
background: white;
border-bottom-left-radius: 2px;
}
.message-sent {
align-self: flex-end;
background: #dcf8c6;
border-bottom-right-radius: 2px;
}
.message-text {
margin-bottom: 5px;
line-height: 1.4;
}
.message-time {
font-size: 11px;
color: #999;
text-align: right;
}
/* Chat Input */
.chat-input-container {
background: white;
padding: 15px 20px;
border-top: 1px solid #e0e0e0;
display: flex;
gap: 10px;
align-items: center;
}
.chat-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #e0e0e0;
border-radius: 25px;
font-size: 15px;
resize: none;
max-height: 100px;
}
.chat-input:focus {
outline: none;
border-color: #667eea;
}
.send-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 25px;
cursor: pointer;
font-weight: bold;
transition: transform 0.2s;
}
.send-button:hover {
transform: translateY(-2px);
}
.send-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
</head>
<body>
<?php if (!isLoggedIn()): ?>
<!-- REGISTRATION FORM -->
<div class="register-container">
<h1>💬 Secure Private Chat</h1>
<p class="subtitle">Sicherer Chat mit Altersverifikation</p>
<div class="error-message" id="errorMessage"></div>
<form id="registerForm">
<div class="form-group">
<label>Username (3-15 Zeichen)</label>
<input type="text" id="username" maxlength="15" required>
</div>
<div class="form-group">
<label>Geburtsdatum</label>
<input type="date" id="birthdate" required>
</div>
<div class="terms-box">
<h3>⚠️ Wichtige Regeln</h3>
<ul>
<li><strong>Gib NIEMALS persönliche Daten weiter</strong> (Adresse, Telefon, etc.)</li>
<li><strong>Treffe dich NICHT mit Fremden</strong></li>
<li>Bleibe respektvoll und freundlich</li>
<li>Keine Links oder externe Kontakte teilen</li>
<li>Bei verdächtigem Verhalten: Melde den User!</li>
</ul>
</div>
<div class="checkbox-group">
<input type="checkbox" id="agreeTerms" required>
<label for="agreeTerms">Ich akzeptiere die Nutzungsbedingungen</label>
</div>
<button type="submit" class="btn-primary">Chat beitreten</button>
</form>
<div class="admin-link">
<a href="?admin=1">Admin-Login</a>
</div>
</div>
<?php else: ?>
<!-- CHAT INTERFACE -->
<div class="chat-container" style="display: grid;">
<!-- Header -->
<div class="chat-header">
<h1>💬 Secure Private Chat</h1>
<div class="user-info">
<span class="username"><?php echo getCurrentUserDisplayName(); ?></span>
<span class="age-badge"><?php echo getCurrentAgeGroup(); ?></span>
<button id="logoutBtn">Logout</button>
</div>
</div>
<!-- Sidebar -->
<div class="sidebar">
<div class="sidebar-search">
<input type="text" id="userSearch" placeholder="🔍 Benutzer suchen...">
</div>
<div class="user-list" id="userList">
<!-- Users loaded via JS -->
</div>
</div>
<!-- Chat Area -->
<div class="chat-area">
<div class="chat-welcome" id="chatWelcome">
<div class="chat-welcome-icon">💬</div>
<div>Wähle einen Benutzer aus der Liste</div>
</div>
<div class="chat-messages-container" id="chatMessagesContainer">
<div class="chat-messages-header" id="chatMessagesHeader">
<!-- Populated via JS -->
</div>
<div class="chat-messages" id="chatMessages">
<!-- Messages loaded via JS -->
</div>
<div class="chat-input-container">
<textarea class="chat-input" id="chatInput" placeholder="Nachricht schreiben..." rows="1" maxlength="1000"></textarea>
<button class="send-button" id="sendButton">Senden</button>
</div>
</div>
</div>
</div>
<?php endif; ?>
<script>
// ═══════════════════════════════════════════════════════════
// JAVASCRIPT
// ═══════════════════════════════════════════════════════════
<?php if (!isLoggedIn()): ?>
// REGISTRATION
document.getElementById('registerForm').addEventListener('submit', async (e) => {
e.preventDefault();
const username = document.getElementById('username').value.trim();
const birthdate = document.getElementById('birthdate').value;
const agreedTerms = document.getElementById('agreeTerms').checked;
const formData = new FormData();
formData.append('action', 'register');
formData.append('username', username);
formData.append('birthdate', birthdate);
formData.append('agreed_terms', agreedTerms);
try {
const response = await fetch('', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
window.location.reload();
} else {
document.getElementById('errorMessage').textContent = result.error;
document.getElementById('errorMessage').style.display = 'block';
}
} catch (error) {
document.getElementById('errorMessage').textContent = 'Verbindungsfehler';
document.getElementById('errorMessage').style.display = 'block';
}
});
<?php else: ?>
// CHAT INTERFACE
const state = {
currentUserId: <?php echo getCurrentUserId(); ?>,
currentUsername: '<?php echo addslashes(getCurrentUsername()); ?>',
selectedUserId: null,
users: [],
messages: [],
lastMessageId: 0,
eventSource: null
};
// Load Users
async function loadUsers() {
const response = await fetch('?action=get_users');
const result = await response.json();
if (result.success) {
state.users = result.users;
renderUserList();
}
}
// Render User List
function renderUserList() {
const userList = document.getElementById('userList');
const searchTerm = document.getElementById('userSearch').value.toLowerCase();
const filtered = state.users.filter(u => u.display_name.toLowerCase().includes(searchTerm));
userList.innerHTML = filtered.map(user => `
<div class="user-item ${user.id === state.selectedUserId ? 'active' : ''}" onclick="selectUser(${user.id}, '${user.display_name}')">
<div class="user-avatar">
${user.username.charAt(0).toUpperCase()}
<div class="online-indicator ${user.is_online ? '' : 'offline-indicator'}"></div>
</div>
<div class="user-info-text">
<div class="user-name">${user.display_name}</div>
<div class="user-status">${user.is_online ? 'Online' : 'Offline'}</div>
</div>
${user.unread_count > 0 ? `<div class="unread-badge">${user.unread_count}</div>` : ''}
</div>
`).join('');
}
// Select User
function selectUser(userId, displayName) {
state.selectedUserId = userId;
document.getElementById('chatWelcome').style.display = 'none';
document.getElementById('chatMessagesContainer').style.display = 'flex';
document.getElementById('chatMessagesHeader').innerHTML = `
<div class="user-avatar">${displayName.charAt(0).toUpperCase()}</div>
<div><div class="user-name">${displayName}</div></div>
`;
loadMessages(userId);
renderUserList();
}
// Load Messages
async function loadMessages(userId) {
const response = await fetch(`?action=get_messages&user_id=${userId}`);
const result = await response.json();
if (result.success) {
state.messages = result.messages;
renderMessages();
markAsRead(userId);
if (result.messages.length > 0) {
state.lastMessageId = Math.max(...result.messages.map(m => m.id));
}
}
}
// Render Messages
function renderMessages() {
const container = document.getElementById('chatMessages');
container.innerHTML = state.messages.map(msg => {
const isSent = msg.from_user_id === state.currentUserId;
const time = new Date(msg.timestamp).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
return `
<div class="message ${isSent ? 'message-sent' : 'message-received'}">
<div class="message-text">${escapeHtml(msg.message)}</div>
<div class="message-time">${time}</div>
</div>
`;
}).join('');
container.scrollTop = container.scrollHeight;
}
// Send Message
async function sendMessage() {
const input = document.getElementById('chatInput');
const message = input.value.trim();
if (!message || !state.selectedUserId) return;
const formData = new FormData();
formData.append('action', 'send_message');
formData.append('to_user_id', state.selectedUserId);
formData.append('message', message);
const response = await fetch('', { method: 'POST', body: formData });
const result = await response.json();
if (result.success) {
input.value = '';
} else {
alert(result.error);
}
}
// Mark as Read
async function markAsRead(userId) {
const formData = new FormData();
formData.append('action', 'mark_read');
formData.append('user_id', userId);
await fetch('', { method: 'POST', body: formData });
loadUsers();
}
// SSE
function startSSE() {
state.eventSource = new EventSource(`?stream=events&last_message_id=${state.lastMessageId}`);
state.eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'messages' && data.messages) {
data.messages.forEach(msg => {
if (msg.id > state.lastMessageId) {
state.lastMessageId = msg.id;
if (state.selectedUserId &&
((msg.from_user_id === state.selectedUserId && msg.to_user_id === state.currentUserId) ||
(msg.from_user_id === state.currentUserId && msg.to_user_id === state.selectedUserId))) {
if (!state.messages.find(m => m.id === msg.id)) {
state.messages.push(msg);
renderMessages();
if (msg.to_user_id === state.currentUserId) {
markAsRead(msg.from_user_id);
}
}
}
}
});
loadUsers();
}
};
}
// Utility
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// Event Listeners
document.getElementById('sendButton').addEventListener('click', sendMessage);
document.getElementById('chatInput').addEventListener('keypress', (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
document.getElementById('userSearch').addEventListener('input', renderUserList);
document.getElementById('logoutBtn').addEventListener('click', async () => {
const formData = new FormData();
formData.append('action', 'logout');
await fetch('', { method: 'POST', body: formData });
window.location.reload();
});
// Auto-resize textarea
document.getElementById('chatInput').addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = Math.min(this.scrollHeight, 100) + 'px';
});
// Ping every 10s
setInterval(async () => {
const formData = new FormData();
formData.append('action', 'ping');
await fetch('', { method: 'POST', body: formData });
}, 10000);
// Init
loadUsers();
startSSE();
setInterval(loadUsers, 30000);
<?php endif; ?>
</script>
</body>
</html>