'; } elseif ($isFa) { return ''; } elseif ($isCustomEmote) { // Fetch emote path static $ce_icons_cache; if ($ce_icons_cache === null) { try { $ce_icons_cache = db()->query("SELECT code, path FROM custom_emotes")->fetchAll(PDO::FETCH_KEY_PAIR); } catch (Exception $e) { $ce_icons_cache = []; } } if (isset($ce_icons_cache[$icon])) { return ''; } return '' . htmlspecialchars($icon) . ''; } else { return '' . htmlspecialchars($icon) . ''; } } // Helper to parse markdown in content function parse_markdown($text) { if (empty($text)) return ""; // First escape HTML $html = htmlspecialchars($text); // Code blocks: ```language\ncontent``` $code_blocks = []; $html = preg_replace_callback('/```(?:(\w+)\n)?([\s\S]*?)```/', function($matches) use (&$code_blocks) { $lang = $matches[1] ?? 'text'; $content = $matches[2]; $placeholder = "__CODE_BLOCK_" . count($code_blocks) . "__"; $code_blocks[] = '
' . $content . '
'; return $placeholder; }, $html); // Inline code: `content` $inline_codes = []; $html = preg_replace_callback('/`([^`\n]+)`/', function($matches) use (&$inline_codes) { $content = $matches[1]; $placeholder = "__INLINE_CODE_" . count($inline_codes) . "__"; $inline_codes[] = '' . $content . ''; return $placeholder; }, $html); // Bold: **text** $html = preg_replace('/\*\*([^*]+)\*\*/', '$1', $html); // Italics: *text* or _text_ $html = preg_replace('/\*([^*]+)\*/', '$1', $html); $html = preg_replace('/_([^_]+)_/', '$1', $html); // Underline: __text__ $html = preg_replace('/__([^_]+)__/', '$1', $html); // Strikethrough: ~~text~~ $html = preg_replace('/~~([^~]+)~~/', '$1', $html); // Spoiler: ||text|| $html = preg_replace('/\|\|([^|]+)\|\|/', '$1', $html); // Headers: # H1, ## H2, ### H3 (must be at start of line) $html = preg_replace('/^# (.*$)/m', '

$1

', $html); $html = preg_replace('/^## (.*$)/m', '

$1

', $html); $html = preg_replace('/^### (.*$)/m', '

$1

', $html); // Subtext: -# text (must be at start of line) $html = preg_replace('/^-# (.*$)/m', '$1', $html); // Blockquotes: > text or >>> text $html = preg_replace('/^> (.*$)/m', '
$1
', $html); $html = preg_replace('/^>>> ([\s\S]*$)/', '
$1
', $html); // Hyperlinks: [text](url) $html = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '$1', $html); // Pure links: $html = preg_replace('/<(https?:\/\/[^&]+)>/', '$1', $html); // Newlines to
(only those not inside placeholders) $html = nl2br($html); // Remove extra space around headers and blockquotes added by nl2br $html = preg_replace('/(\s*)\s*(|
)/i', '$2', $html); $html = preg_replace('/(<\/h[1-3]>|<\/blockquote>)\s*(\s*)/i', '$1', $html); // Re-insert inline code foreach ($inline_codes as $i => $code) { $html = str_replace("__INLINE_CODE_$i" . "__", $code, $html); } // Re-insert code blocks foreach ($code_blocks as $i => $block) { $html = str_replace("__CODE_BLOCK_$i" . "__", $block, $html); } return $html; } // Helper to parse emotes in content function parse_emotes($content, $username_to_mention = null) { static $custom_emotes_cache; if ($custom_emotes_cache === null) { try { $custom_emotes_cache = db()->query("SELECT name, path, code FROM custom_emotes")->fetchAll(); } catch (Exception $e) { $custom_emotes_cache = []; } } $result = parse_markdown($content); // Parse mentions if username provided if ($username_to_mention) { $mention_pattern = '/@' . preg_quote($username_to_mention, '/') . '\b/'; $result = preg_replace($mention_pattern, '@' . htmlspecialchars($username_to_mention) . '', $result); } foreach ($custom_emotes_cache as $ce) { $emote_html = '' . htmlspecialchars($ce['name']) . ''; $result = str_replace($ce['code'], $emote_html, $result); } return $result; } requireLogin(); $user = getCurrentUser(); $current_user_id = $user['id']; // Fetch servers user is member of $stmt = db()->prepare(" SELECT s.* FROM servers s JOIN server_members sm ON s.id = sm.server_id WHERE sm.user_id = ? LIMIT 20 "); $stmt->execute([$current_user_id]); $servers = $stmt->fetchAll(); // Automatic rotation of expired invite codes for owned servers foreach ($servers as &$s) { if ($s['owner_id'] == $current_user_id && (empty($s['invite_code_expires_at']) || strtotime($s['invite_code_expires_at']) < time())) { $new_code = generateSecureInviteCode(); $new_expiry = date('Y-m-d H:i:s', time() + 1800); $stmt = db()->prepare("UPDATE servers SET invite_code = ?, invite_code_expires_at = ? WHERE id = ?"); $stmt->execute([$new_code, $new_expiry, $s['id']]); $s['invite_code'] = $new_code; $s['invite_code_expires_at'] = $new_expiry; } } unset($s); $is_dm_view = (isset($_GET['server_id']) && $_GET['server_id'] == 'dms') || !isset($_GET['server_id']) && empty($servers); if ($is_dm_view) { $active_server_id = 'dms'; // Fetch DM channels $stmt = db()->prepare(" SELECT c.id, u.display_name as other_user, u.avatar_url, u.status, u.id as other_user_id FROM channels c JOIN channel_members cm1 ON c.id = cm1.channel_id JOIN channel_members cm2 ON c.id = cm2.channel_id JOIN users u ON cm2.user_id = u.id WHERE c.type = 'dm' AND cm1.user_id = ? AND cm2.user_id != ? "); $stmt->execute([$current_user_id, $current_user_id]); $dm_channels = $stmt->fetchAll(); $active_channel_id = $_GET['channel_id'] ?? ($dm_channels[0]['id'] ?? 0); $channel_theme = null; // DMs don't have custom themes for now if ($active_channel_id) { // Fetch DM messages $stmt = db()->prepare(" SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url FROM messages m JOIN users u ON m.user_id = u.id WHERE m.channel_id = ? ORDER BY m.created_at ASC LIMIT 50 "); $stmt->execute([$active_channel_id]); $messages = $stmt->fetchAll(); $current_channel_name = 'Direct Message'; foreach($dm_channels as $dm) { if ($dm['id'] == $active_channel_id) { $current_channel_name = $dm['other_user']; break; } } } else { $messages = []; $current_channel_name = 'Direct Messages'; } $channels = []; $members = []; // Members list is different for DMs or hidden } else { $active_server_id = $_GET['server_id'] ?? ($servers[0]['id'] ?? 1); // Fetch channels $stmt = db()->prepare("SELECT * FROM channels WHERE server_id = ? ORDER BY position ASC, id ASC"); $stmt->execute([$active_server_id]); $all_channels = $stmt->fetchAll(); require_once 'includes/permissions.php'; $channels = []; foreach($all_channels as $c) { if (Permissions::canViewChannel($current_user_id, $c['id'])) { $channels[] = $c; } } $active_channel_id = $_GET['channel_id'] ?? ($channels[0]['id'] ?? 0); // Fetch active channel details for theme $active_channel = null; foreach($channels as $c) { if($c['id'] == $active_channel_id) { $active_channel = $c; break; } } $is_owner = false; $can_manage_channels = false; $can_manage_server = false; $active_server = null; foreach($servers as $s) { if($s['id'] == $active_server_id) { $active_server = $s; $is_owner = ($s['owner_id'] == $current_user_id); $can_manage_channels = Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_CHANNELS) || $is_owner; $can_manage_server = Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_SERVER) || Permissions::hasPermission($current_user_id, $active_server_id, Permissions::ADMINISTRATOR) || $is_owner; break; } } $channel_theme = $active_server['theme_color'] ?? null; $channel_type = $active_channel['type'] ?? 'chat'; $active_thread_id = $_GET['thread_id'] ?? null; $active_thread = null; if ($active_thread_id) { $stmt = db()->prepare("SELECT t.*, u.display_name as username, u.username as login_name FROM forum_threads t JOIN users u ON t.user_id = u.id WHERE t.id = ?"); $stmt->execute([$active_thread_id]); $active_thread = $stmt->fetch(); if ($active_thread) { $stmt = db()->prepare(" SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url, (SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color, (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon FROM messages m JOIN users u ON m.user_id = u.id WHERE m.thread_id = ? ORDER BY m.created_at ASC "); $stmt->execute([$active_server_id, $active_server_id, $active_thread_id]); $messages = $stmt->fetchAll(); } } if (!$active_thread && $channel_type === 'rules') { $stmt = db()->prepare("SELECT * FROM channel_rules WHERE channel_id = ? ORDER BY position ASC"); $stmt->execute([$active_channel_id]); $rules = $stmt->fetchAll(); } elseif ($channel_type === 'autorole') { $stmt = db()->prepare("SELECT ca.*, r.name as role_name FROM channel_autoroles ca JOIN roles r ON ca.role_id = r.id WHERE ca.channel_id = ? ORDER BY ca.id ASC"); $stmt->execute([$active_channel_id]); $autoroles = $stmt->fetchAll(); } elseif ($channel_type === 'forum') { $filter_status = $_GET['status'] ?? 'all'; $status_where = ""; if ($filter_status === 'resolved') { $status_where = " AND t.solution_message_id IS NOT NULL"; } elseif ($filter_status === 'unresolved') { $status_where = " AND t.solution_message_id IS NULL"; } $stmt = db()->prepare(" SELECT t.*, u.display_name as username, u.username as login_name, u.avatar_url, (SELECT COUNT(*) FROM messages m WHERE m.thread_id = t.id) as message_count, (SELECT MAX(created_at) FROM messages m WHERE m.thread_id = t.id) as last_activity, (SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color, (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon, (SELECT GROUP_CONCAT(CONCAT(ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags FROM forum_threads t JOIN users u ON t.user_id = u.id WHERE t.channel_id = ? " . $status_where . " ORDER BY last_activity DESC, t.created_at DESC "); $stmt->execute([$active_server_id, $active_server_id, $active_channel_id]); $threads = $stmt->fetchAll(); } else { // Fetch messages $display_limit = !empty($active_channel['message_limit']) ? (int)$active_channel['message_limit'] : 50; $stmt = db()->prepare(" SELECT m.*, u.display_name as username, u.username as login_name, u.avatar_url, (SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color, (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon FROM messages m JOIN users u ON m.user_id = u.id WHERE m.channel_id = ? ORDER BY m.created_at ASC LIMIT " . $display_limit . " "); $stmt->execute([$active_server_id, $active_server_id, $active_channel_id]); $messages = $stmt->fetchAll(); } $current_channel_name = 'general'; foreach($channels as $c) if($c['id'] == $active_channel_id) $current_channel_name = $c['name']; // Fetch voice sessions for the sidebar $stmt_vs = db()->prepare(" SELECT vs.channel_id, vs.user_id, u.username, u.display_name, u.avatar_url FROM voice_sessions vs JOIN users u ON vs.user_id = u.id "); $stmt_vs->execute(); $voice_sessions = $stmt_vs->fetchAll(); $voice_users_by_channel = []; foreach($voice_sessions as $vs) { $voice_users_by_channel[$vs['channel_id']][] = $vs; } // Fetch members $stmt = db()->prepare(" SELECT u.id, u.display_name as username, u.username as login_name, u.avatar_url, u.status, (SELECT GROUP_CONCAT(r.id) FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ?) as role_ids, (SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color, (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon FROM users u JOIN server_members sm ON u.id = sm.user_id WHERE sm.server_id = ? "); $stmt->execute([$active_server_id, $active_server_id, $active_server_id, $active_server_id]); $all_server_members = $stmt->fetchAll(); $members = []; foreach($all_server_members as $m) { if (Permissions::canViewChannel($m['id'], $active_channel_id)) { $members[] = $m; } } // Fetch all server roles $stmt = db()->prepare("SELECT * FROM roles WHERE server_id = ? ORDER BY position DESC"); $stmt->execute([$active_server_id]); $server_roles = $stmt->fetchAll(); } // SEO & Env tags $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? 'Discord-like messaging app built with PHP'; $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; ?> #<?php echo htmlspecialchars($current_channel_name); ?> | <?php echo htmlspecialchars($projectDescription); ?>
Direct Messages'; } else { $active_server_name = 'Server'; foreach($servers as $s) { if($s['id'] == $active_server_id) { $active_server_name = $s['name']; break; } } echo '' . htmlspecialchars($active_server_name) . ''; ?>
PHP |
'; elseif ($active_channel['type'] === 'rules') echo ''; elseif ($active_channel['type'] === 'autorole') echo ''; elseif ($active_channel['type'] === 'forum') echo ''; elseif ($active_channel['type'] === 'voice') echo ''; else echo ''; if (!empty($active_channel['icon'])) { echo ' ' . renderRoleIcon($active_channel['icon'], '16px') . ''; } } ?>
← Back to Forum

Discussion:

prepare("SELECT ft.* FROM forum_tags ft JOIN thread_tags tt ON ft.id = tt.tag_id WHERE tt.thread_id = ?"); $stmt_t->execute([$active_thread['id']]); $thread_tags = $stmt_t->fetchAll(); if ($thread_tags): foreach ($thread_tags as $t): ?>
">
"> SOLUTION
prepare("SELECT emoji, COUNT(*) as count, GROUP_CONCAT(user_id) as users FROM message_reactions WHERE message_id = ? GROUP BY emoji"); $stmt_react->execute([$m['id']]); $reactions = $stmt_react->fetchAll(); foreach ($reactions as $r): $reacted = in_array($current_user_id, explode(',', $r['users'])); ?> +

📜

.
prepare("SELECT 1 FROM rule_acceptances WHERE user_id = ? AND channel_id = ?"); $stmtAcc->execute([$current_user_id, $active_channel_id]); $has_accepted = $stmtAcc->fetch(); ?>
Vous avez accepté les règles.

Veuillez accepter les règles pour obtenir l'accès complet.

🛡️

Cliquez sur un bouton pour vous attribuer ou vous retirer un rôle.

prepare("SELECT 1 FROM user_roles WHERE user_id = ? AND role_id = ?"); $stmtHasRole->execute([$current_user_id, $ar['role_id']]); $has_role = $stmtHasRole->fetch(); ?>

Welcome to #!

This is the start of the # channel.

">
"> Pinned
prepare("SELECT emoji, COUNT(*) as count, GROUP_CONCAT(user_id) as users FROM message_reactions WHERE message_id = ? GROUP BY emoji"); $stmt_react->execute([$m['id']]); $reactions = $stmt_react->fetchAll(); foreach ($reactions as $r): $reacted = in_array($current_user_id, explode(',', $r['users'])); ?> +
Vous ne disposez pas de la permission d\'envoyer des messages dans ce salon.
'; } else { $allow_files = true; foreach($channels as $c) { if($c['id'] == $active_channel_id) { $allow_files = (bool)$c['allow_file_sharing']; break; } } ?>
Members —
">
">