Compare commits

...

65 Commits

Author SHA1 Message Date
Flatlogic Bot
af9afaf499 Autosave: 20260218-010816 2026-02-18 01:08:16 +00:00
Flatlogic Bot
63ee7f6c2b Autosave: 20260218-002012 2026-02-18 00:20:13 +00:00
Flatlogic Bot
55896a11bd Autosave: 20260217-235801 2026-02-17 23:58:01 +00:00
Flatlogic Bot
fcda68cf07 Autosave: 20260217-234438 2026-02-17 23:44:38 +00:00
Flatlogic Bot
5dff56179a Final V2 2026-02-17 20:16:00 +00:00
Flatlogic Bot
bdc0ccf63d Autosave: 20260217-191914 2026-02-17 19:19:14 +00:00
Flatlogic Bot
099f307a09 Final V2 2026-02-17 19:15:53 +00:00
Flatlogic Bot
09fa2a7096 final v1 fonctionnel 2026-02-17 15:50:11 +00:00
Flatlogic Bot
c11359b2d2 Autosave: 20260217-152131 2026-02-17 15:21:31 +00:00
Flatlogic Bot
24671bdbc7 VOX a moitié fonctionnel 2026-02-17 15:07:03 +00:00
Flatlogic Bot
04cad1c49b PTT semi focntionnel 2026-02-17 14:47:12 +00:00
Flatlogic Bot
08664dda0d Autosave: 20260217-125051 2026-02-17 12:50:51 +00:00
Flatlogic Bot
d8c5bbb218 Autosave: 20260217-122132 2026-02-17 12:21:32 +00:00
Flatlogic Bot
920e26ada3 + a coté du nom de serveur 2026-02-17 10:15:53 +00:00
Flatlogic Bot
95cfa227e9 Autosave: 20260217-082815 2026-02-17 08:28:15 +00:00
Flatlogic Bot
75e3425c41 Flux RSS v1.0 2026-02-17 08:21:23 +00:00
Flatlogic Bot
35c2bad3b7 final v0.8 2026-02-17 01:03:52 +00:00
Flatlogic Bot
afb642ce41 Flux RSS v0.6 2026-02-17 00:40:25 +00:00
Flatlogic Bot
eb7cbe5ace flux rss v0.5 2026-02-17 00:29:34 +00:00
Flatlogic Bot
e678bcf5aa flux rss final 0.3 2026-02-16 23:49:17 +00:00
Flatlogic Bot
f46a1c7e4b flux rss v1 2026-02-16 23:42:16 +00:00
Flatlogic Bot
794f971b73 Autosave: 20260216-233738 2026-02-16 23:37:38 +00:00
Flatlogic Bot
e3e1dc3456 Final 0.2 2026-02-16 23:16:16 +00:00
Flatlogic Bot
5083b2794c version focntionnelle 02 2026-02-16 23:01:50 +00:00
Flatlogic Bot
f55113bf56 Version Fonctionnelle 1 2026-02-16 22:54:07 +00:00
Flatlogic Bot
e387e07cc6 Version stable 01 2026-02-16 22:09:05 +00:00
Flatlogic Bot
c0b4015a24 DARK LIGHT 2026-02-16 20:49:23 +00:00
Flatlogic Bot
5b5ac99cae Autosave: 20260216-202956 2026-02-16 20:29:56 +00:00
Flatlogic Bot
f26d0b6abc fermeture ouverture categorie 2026-02-16 18:51:39 +00:00
Flatlogic Bot
f9c70d9be2 Version secure A 2026-02-16 18:44:13 +00:00
Flatlogic Bot
2bda3a08f3 role emote 2026-02-16 18:01:22 +00:00
Flatlogic Bot
7241b4052b Autosave: 20260216-173823 2026-02-16 17:38:23 +00:00
Flatlogic Bot
171804c16a emotes roles V2 2026-02-16 17:09:01 +00:00
Flatlogic Bot
cd2b57b27d emote autoroles 2026-02-16 16:36:18 +00:00
Flatlogic Bot
e6233598d6 Autosave: 20260216-130929 2026-02-16 13:09:29 +00:00
Flatlogic Bot
77269fa65c Autosave: 20260216-032143 2026-02-16 03:21:43 +00:00
Flatlogic Bot
f20e908050 autoroles 2026-02-16 03:19:06 +00:00
Flatlogic Bot
41fa76eec3 v15 2026-02-16 02:59:13 +00:00
Flatlogic Bot
1a0b8da2ba V15 2026-02-16 02:58:09 +00:00
Flatlogic Bot
7aa3b7d910 membres roles canaux affichage 2026-02-16 00:36:16 +00:00
Flatlogic Bot
79d65ef265 acceptation regles 2026-02-16 00:26:21 +00:00
Flatlogic Bot
f41686b17d regles v2 2026-02-16 00:04:01 +00:00
Flatlogic Bot
a35fd4aafb règles v1 2026-02-15 23:59:02 +00:00
Flatlogic Bot
c987b0caba Autosave: 20260215-235418 2026-02-15 23:54:18 +00:00
Flatlogic Bot
b5ae307f55 categories 2026-02-15 23:38:38 +00:00
Flatlogic Bot
f604713529 barre de séparation 2026-02-15 22:48:16 +00:00
Flatlogic Bot
a757fa13ed roles dans le popup des membres a droite 2026-02-15 22:37:21 +00:00
Flatlogic Bot
c08cfebf52 roles++ 2026-02-15 22:26:04 +00:00
Flatlogic Bot
5494f1e4ee # et roles 2026-02-15 21:22:03 +00:00
Flatlogic Bot
e3984686cb Autosave: 20260215-194817 2026-02-15 19:48:17 +00:00
Flatlogic Bot
98888d0370 emoji sur les roles 2026-02-15 19:21:41 +00:00
Flatlogic Bot
652014e524 Autosave: 20260215-190103 2026-02-15 19:01:03 +00:00
Flatlogic Bot
dfd640b430 chat défilement 2026-02-15 18:28:21 +00:00
Flatlogic Bot
1440c83ccf Autosave: 20260215-170412 2026-02-15 17:04:12 +00:00
Flatlogic Bot
001690b707 Canaux ok 2026-02-15 16:49:18 +00:00
Flatlogic Bot
7a52251131 Autosave: 20260215-162152 2026-02-15 16:21:52 +00:00
Flatlogic Bot
5d6fd46690 v12 2026-02-15 16:11:50 +00:00
Flatlogic Bot
40f605d106 Autosave: 20260215-151332 2026-02-15 15:13:32 +00:00
Flatlogic Bot
9c07e1ee23 v8 2026-02-15 13:13:16 +00:00
Flatlogic Bot
1e73419ffb v6 2026-02-15 11:24:55 +00:00
Flatlogic Bot
0911f86785 V4 2026-02-15 11:01:34 +00:00
Flatlogic Bot
c49abcc049 Autosave: 20260215-105502 2026-02-15 10:55:02 +00:00
Flatlogic Bot
ef520f4259 v3 2026-02-15 10:33:41 +00:00
Flatlogic Bot
4883125cda v2 2026-02-15 10:30:17 +00:00
Flatlogic Bot
2642f97c8b v1 2026-02-15 10:25:49 +00:00
85 changed files with 11230 additions and 145 deletions

139
api/emotes.php Normal file
View File

@ -0,0 +1,139 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
$action = $_GET['action'] ?? '';
if ($action === 'list') {
try {
$stmt = db()->query("SELECT * FROM custom_emotes ORDER BY created_at DESC");
echo json_encode(['success' => true, 'emotes' => $stmt->fetchAll()]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($action === 'upload' && $_SERVER['REQUEST_METHOD'] === 'POST') {
if (!isset($_FILES['emote'])) {
echo json_encode(['success' => false, 'error' => 'Aucun fichier reçu (emote)']);
exit;
}
if ($_FILES['emote']['error'] !== UPLOAD_ERR_OK) {
$errorMsg = 'Erreur d\'upload PHP: ' . $_FILES['emote']['error'];
if ($_FILES['emote']['error'] === 1) $errorMsg = 'Fichier trop volumineux (limit PHP)';
if ($_FILES['emote']['error'] === 4) $errorMsg = 'Aucun fichier sélectionné';
echo json_encode(['success' => false, 'error' => $errorMsg]);
exit;
}
$name = preg_replace('/[^a-z0-9_]/', '', strtolower($_POST['name'] ?? 'emote'));
if (empty($name)) {
echo json_encode(['success' => false, 'error' => 'Nom invalide']);
exit;
}
$file = $_FILES['emote'];
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
if (!in_array(strtolower($ext), ['png', 'jpg', 'jpeg'])) {
echo json_encode(['success' => false, 'error' => 'Format non supporté (PNG uniquement recommandé)']);
exit;
}
$uploadDir = __DIR__ . '/../assets/images/custom_emotes/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$fileName = time() . '_' . $name . '.png';
$targetPath = $uploadDir . $fileName;
// Process image: Resize to 48x48
$srcImage = null;
if (strtolower($ext) === 'png') $srcImage = imagecreatefrompng($file['tmp_name']);
else if (in_array(strtolower($ext), ['jpg', 'jpeg'])) $srcImage = imagecreatefromjpeg($file['tmp_name']);
if (!$srcImage) {
echo json_encode(['success' => false, 'error' => 'Fichier image corrompu']);
exit;
}
$width = imagesx($srcImage);
$height = imagesy($srcImage);
$newSize = 48;
$dstImage = imagecreatetruecolor($newSize, $newSize);
// Transparency for PNG
imagealphablending($dstImage, false);
imagesavealpha($dstImage, true);
$transparent = imagecolorallocatealpha($dstImage, 255, 255, 255, 127);
imagefilledrectangle($dstImage, 0, 0, $newSize, $newSize, $transparent);
imagecopyresampled($dstImage, $srcImage, 0, 0, 0, 0, $newSize, $newSize, $width, $height);
imagepng($dstImage, $targetPath);
imagedestroy($srcImage);
imagedestroy($dstImage);
$relativePath = 'assets/images/custom_emotes/' . $fileName;
$code = ':' . $name . ':';
try {
$stmt = db()->prepare("INSERT INTO custom_emotes (name, path, code) VALUES (?, ?, ?)");
$stmt->execute([$name, $relativePath, $code]);
echo json_encode([
'success' => true,
'emote' => [
'id' => db()->lastInsertId(),
'name' => $name,
'path' => $relativePath,
'code' => $code
]
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($action === 'rename' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? 0;
$newName = preg_replace('/[^a-z0-9_]/', '', strtolower($_POST['name'] ?? ''));
if (empty($newName)) {
echo json_encode(['success' => false, 'error' => 'Nom invalide']);
exit;
}
try {
$code = ':' . $newName . ':';
$stmt = db()->prepare("UPDATE custom_emotes SET name = ?, code = ? WHERE id = ?");
$stmt->execute([$newName, $code, $id]);
echo json_encode(['success' => true, 'name' => $newName, 'code' => $code]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($action === 'delete' && $_SERVER['REQUEST_METHOD'] === 'POST') {
$id = $_POST['id'] ?? 0;
try {
$stmt = db()->prepare("SELECT path FROM custom_emotes WHERE id = ?");
$stmt->execute([$id]);
$emote = $stmt->fetch();
if ($emote) {
$filePath = __DIR__ . '/../' . $emote['path'];
if (file_exists($filePath)) unlink($filePath);
$stmt = db()->prepare("DELETE FROM custom_emotes WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Emote non trouvée']);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}

26
api/pexels.php Normal file
View File

@ -0,0 +1,26 @@
<?php
header('Content-Type: application/json');
require_once __DIR__.'/../includes/pexels.php';
$action = $_GET['action'] ?? 'search';
if ($action === 'search') {
$q = $_GET['query'] ?? 'avatar';
$url = 'https://api.pexels.com/v1/search?query=' . urlencode($q) . '&per_page=12&page=1';
$data = pexels_get($url);
if (!$data) {
echo json_encode(['error' => 'Failed to fetch images']);
exit;
}
$results = [];
foreach ($data['photos'] as $photo) {
$results[] = [
'id' => $photo['id'],
'url' => $photo['src']['medium'],
'photographer' => $photo['photographer']
];
}
echo json_encode($results);
exit;
}

59
api_v1_accept_rules.php Normal file
View File

@ -0,0 +1,59 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$json = json_decode(file_get_contents('php://input'), true);
$channel_id = $json['channel_id'] ?? 0;
$user_id = $_SESSION['user_id'];
if (!$channel_id) {
echo json_encode(['success' => false, 'error' => 'ID de canal manquant']);
exit;
}
// Fetch channel details to get rules_role_id
$stmt = db()->prepare("SELECT * FROM channels WHERE id = ? AND type = 'rules'");
$stmt->execute([$channel_id]);
$channel = $stmt->fetch();
if (!$channel) {
echo json_encode(['success' => false, 'error' => 'Canal de règles introuvable']);
exit;
}
if (empty($channel['rules_role_id'])) {
echo json_encode(['success' => false, 'error' => 'Aucun rôle n\'est configuré pour ce canal']);
exit;
}
$role_id = $channel['rules_role_id'];
try {
db()->beginTransaction();
// 1. Record acceptance
$stmtAcc = db()->prepare("INSERT IGNORE INTO rule_acceptances (user_id, channel_id) VALUES (?, ?)");
$stmtAcc->execute([$user_id, $channel_id]);
// 2. Assign role
// Check if user already has this role
$stmtRoleCheck = db()->prepare("SELECT 1 FROM user_roles WHERE user_id = ? AND role_id = ?");
$stmtRoleCheck->execute([$user_id, $role_id]);
if (!$stmtRoleCheck->fetch()) {
$stmtRole = db()->prepare("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)");
$stmtRole->execute([$user_id, $role_id]);
}
db()->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success' => false, 'error' => 'Erreur lors de l\'attribution du rôle : ' . $e->getMessage()]);
}
exit;
}
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);

102
api_v1_autoroles.php Normal file
View File

@ -0,0 +1,102 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user_id = $_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$json = json_decode(file_get_contents('php://input'), true);
$action = $_POST['action'] ?? ($json['action'] ?? '');
if ($action === 'create') {
$channel_id = $_POST['channel_id'] ?? 0;
$server_id = $_POST['server_id'] ?? 0;
$icon = $_POST['icon'] ?? '';
$title = $_POST['title'] ?? '';
$role_id = $_POST['role_id'] ?? 0;
if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("INSERT INTO channel_autoroles (channel_id, icon, title, role_id) VALUES (?, ?, ?, ?)");
$stmt->execute([$channel_id, $icon, $title, $role_id]);
}
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
exit;
}
if ($action === 'update') {
$id = $_POST['id'] ?? 0;
$channel_id = $_POST['channel_id'] ?? 0;
$server_id = $_POST['server_id'] ?? 0;
$icon = $_POST['icon'] ?? '';
$title = $_POST['title'] ?? '';
$role_id = $_POST['role_id'] ?? 0;
if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("UPDATE channel_autoroles SET icon = ?, title = ?, role_id = ? WHERE id = ?");
$stmt->execute([$icon, $title, $role_id, $id]);
}
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
exit;
}
if ($action === 'delete') {
$id = $_POST['id'] ?? 0;
$channel_id = $_POST['channel_id'] ?? 0;
$server_id = $_POST['server_id'] ?? 0;
if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("DELETE FROM channel_autoroles WHERE id = ?");
$stmt->execute([$id]);
}
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
exit;
}
if ($action === 'toggle') {
// This will be called via AJAX
$role_id = $json['role_id'] ?? 0;
if (!$role_id) {
echo json_encode(['success' => false, 'error' => 'Invalid role']);
exit;
}
// Find the server for this role
$stmt = db()->prepare("SELECT server_id FROM roles WHERE id = ?");
$stmt->execute([$role_id]);
$role = $stmt->fetch();
if (!$role) {
echo json_encode(['success' => false, 'error' => 'Role not found']);
exit;
}
// Check if user is member of server
$stmt = db()->prepare("SELECT 1 FROM server_members WHERE server_id = ? AND user_id = ?");
$stmt->execute([$role['server_id'], $user_id]);
if (!$stmt->fetch()) {
echo json_encode(['success' => false, 'error' => 'Not a member of this server']);
exit;
}
// Toggle role
$stmt = db()->prepare("SELECT 1 FROM user_roles WHERE user_id = ? AND role_id = ?");
$stmt->execute([$user_id, $role_id]);
$has_role = $stmt->fetch();
if ($has_role) {
$stmt = db()->prepare("DELETE FROM user_roles WHERE user_id = ? AND role_id = ?");
$stmt->execute([$user_id, $role_id]);
$added = false;
} else {
$stmt = db()->prepare("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)");
$stmt->execute([$user_id, $role_id]);
$added = true;
}
echo json_encode(['success' => true, 'added' => $added]);
exit;
}
}

View File

@ -0,0 +1,131 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
requireLogin();
$user_id = $_SESSION['user_id'];
$data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$channel_id = $_GET['channel_id'] ?? 0;
// Get server_id for this channel
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$channel = $stmt->fetch();
$server_id = $channel['server_id'] ?? 0;
// Ensure @everyone role exists for this server
$stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (LOWER(name) = '@everyone' OR LOWER(name) = 'everyone') LIMIT 1");
$stmt->execute([$server_id]);
$everyone = $stmt->fetch();
if (!$everyone && $server_id) {
$stmt = db()->prepare("INSERT INTO roles (server_id, name, color, permissions, position) VALUES (?, '@everyone', '#99aab5', 0, 0)");
$stmt->execute([$server_id]);
$everyone_role_id = db()->lastInsertId();
} else {
$everyone_role_id = $everyone['id'] ?? 0;
}
// Fetch permissions for this channel
$stmt = db()->prepare("
SELECT cp.*, r.name as role_name, r.color as role_color
FROM channel_permissions cp
JOIN roles r ON cp.role_id = r.id
WHERE cp.channel_id = ?
");
$stmt->execute([$channel_id]);
$permissions = $stmt->fetchAll();
// Check if @everyone is in permissions, if not add it manually to show up by default
$has_everyone = false;
foreach($permissions as $p) {
if ($p['role_id'] == $everyone_role_id) {
$has_everyone = true;
break;
}
}
if (!$has_everyone && $everyone_role_id > 0) {
$stmt = db()->prepare("SELECT name, color FROM roles WHERE id = ?");
$stmt->execute([$everyone_role_id]);
$r = $stmt->fetch();
if ($r) {
array_unshift($permissions, [
'channel_id' => (int)$channel_id,
'role_id' => (int)$everyone_role_id,
'allow_permissions' => 0,
'deny_permissions' => 0,
'role_name' => $r['name'],
'role_color' => $r['color']
]);
}
}
echo json_encode(['success' => true, 'permissions' => $permissions]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$channel_id = $data['channel_id'] ?? 0;
$role_id = $data['role_id'] ?? 0;
$allow = $data['allow'] ?? 0;
$deny = $data['deny'] ?? 0;
// Check permissions: Owner or MANAGE_CHANNELS or ADMINISTRATOR
require_once 'includes/permissions.php';
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$ch = $stmt->fetch();
$server_id = $ch['server_id'] ?? 0;
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();
$is_owner = ($server && $server['owner_id'] == $user_id);
$can_manage = Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS) ||
Permissions::hasPermission($user_id, $server_id, Permissions::ADMINISTRATOR);
if ($is_owner || $can_manage) {
$stmt = db()->prepare("
INSERT INTO channel_permissions (channel_id, role_id, allow_permissions, deny_permissions)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE allow_permissions = VALUES(allow_permissions), deny_permissions = VALUES(deny_permissions)
");
$stmt->execute([$channel_id, $role_id, $allow, $deny]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$channel_id = $data['channel_id'] ?? 0;
$role_id = $data['role_id'] ?? 0;
// Check permissions
require_once 'includes/permissions.php';
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$ch = $stmt->fetch();
$server_id = $ch['server_id'] ?? 0;
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();
$is_owner = ($server && $server['owner_id'] == $user_id);
$can_manage = Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS) ||
Permissions::hasPermission($user_id, $server_id, Permissions::ADMINISTRATOR);
if ($is_owner || $can_manage) {
$stmt = db()->prepare("DELETE FROM channel_permissions WHERE channel_id = ? AND role_id = ?");
$stmt->execute([$channel_id, $role_id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
}
exit;
}

131
api_v1_channels.php Normal file
View File

@ -0,0 +1,131 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$server_id = $_GET['server_id'] ?? 0;
if (!$server_id) {
echo json_encode([]);
exit;
}
$stmt = db()->prepare("SELECT c.*, (SELECT MAX(id) FROM messages WHERE channel_id = c.id) as last_message_id FROM channels c WHERE server_id = ?");
$stmt->execute([$server_id]);
echo json_encode($stmt->fetchAll());
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Handle JSON input
$json = json_decode(file_get_contents('php://input'), true);
if ($json) {
$action = $json['action'] ?? '';
if ($action === 'reorder') {
$server_id = $json['server_id'] ?? 0;
$orders = $json['orders'] ?? []; // Array of {id, position, category_id}
$user_id = $_SESSION['user_id'];
// Debug log
file_put_contents('debug_reorder.log', date('Y-m-d H:i:s') . " - Server: $server_id - Orders: " . json_encode($orders) . "\n", FILE_APPEND);
if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("UPDATE channels SET position = ?, category_id = ? WHERE id = ? AND server_id = ?");
foreach ($orders as $o) {
$stmt->execute([$o['position'], $o['category_id'] ?: null, $o['id'], $server_id]);
}
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Permission denied']);
}
exit;
}
}
$action = $_POST['action'] ?? 'create';
$server_id = $_POST['server_id'] ?? 0;
$user_id = $_SESSION['user_id'];
if ($action === 'update') {
$channel_id = $_POST['channel_id'] ?? 0;
$name = $_POST['name'] ?? '';
$type = $_POST['type'] ?? 'chat';
$status = $_POST['status'] ?? null;
$allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0;
$message_limit = !empty($_POST['message_limit']) ? (int)$_POST['message_limit'] : null;
$icon = $_POST['icon'] ?? null;
if ($icon === '') $icon = null;
$category_id = !empty($_POST['category_id']) ? (int)$_POST['category_id'] : null;
$rules_role_id = !empty($_POST['rules_role_id']) ? (int)$_POST['rules_role_id'] : null;
// Check if user has permission to manage channels
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$chan = $stmt->fetch();
if ($chan && Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
if ($type === 'separator' && !$name) $name = 'separator';
// Allow spaces, accents and mixed case
$name = trim($name);
// Explicitly exclude position from update to prevent jumping to bottom
$stmt = db()->prepare("UPDATE channels SET name = ?, type = ?, status = ?, allow_file_sharing = ?, message_limit = ?, icon = ?, category_id = ?, rules_role_id = ? WHERE id = ?");
$stmt->execute([$name, $type, $status, $allow_file_sharing, $message_limit, $icon, $category_id, $rules_role_id, $channel_id]);
if ($message_limit !== null) {
require_once 'db/config.php';
enforceChannelLimit($channel_id);
}
}
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
exit;
}
if ($action === 'delete') {
$channel_id = $_POST['channel_id'] ?? 0;
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$chan = $stmt->fetch();
if ($chan && Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("DELETE FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
}
header('Location: index.php?server_id=' . ($chan['server_id'] ?? ''));
exit;
}
$name = $_POST['name'] ?? '';
$type = $_POST['type'] ?? 'text';
$user_id = $_SESSION['user_id'];
// Check if user has permission to manage channels
if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_CHANNELS) && ($name || $type === 'separator')) {
try {
if ($type === 'separator' && !$name) $name = 'separator';
// Allow spaces, accents and mixed case
$name = trim($name);
$allow_file_sharing = isset($_POST['allow_file_sharing']) ? 1 : 0;
$message_limit = !empty($_POST['message_limit']) ? (int)$_POST['message_limit'] : null;
$icon = $_POST['icon'] ?? null;
if ($icon === '') $icon = null;
$category_id = !empty($_POST['category_id']) ? (int)$_POST['category_id'] : null;
$rules_role_id = !empty($_POST['rules_role_id']) ? (int)$_POST['rules_role_id'] : null;
// Get next position
$stmtPos = db()->prepare("SELECT MAX(position) as max_pos FROM channels WHERE server_id = ?");
$stmtPos->execute([$server_id]);
$maxPos = $stmtPos->fetch();
$nextPos = ($maxPos['max_pos'] ?? -1) + 1;
$stmt = db()->prepare("INSERT INTO channels (server_id, name, type, allow_file_sharing, message_limit, icon, category_id, position, rules_role_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt->execute([$server_id, $name, $type, $allow_file_sharing, $message_limit, $icon, $category_id, $nextPos, $rules_role_id]);
$channel_id = db()->lastInsertId();
header('Location: index.php?server_id=' . $server_id . '&channel_id=' . $channel_id);
exit;
} catch (Exception $e) {
die("Error creating channel: " . $e->getMessage());
}
}
}
header('Location: index.php');

46
api_v1_clear_channel.php Normal file
View File

@ -0,0 +1,46 @@
<?php
require_once __DIR__ . "/db/config.php";
require_once __DIR__ . "/includes/permissions.php";
session_start();
header("Content-Type: application/json");
if (!isset($_SESSION["user_id"])) {
echo json_encode(["success" => false, "error" => "Unauthorized"]);
exit;
}
$channel_id = $_POST["channel_id"] ?? null;
if (!$channel_id) {
echo json_encode(["success" => false, "error" => "Missing channel ID"]);
exit;
}
// Get server_id for this channel
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$channel = $stmt->fetch();
if (!$channel) {
echo json_encode(["success" => false, "error" => "Channel not found"]);
exit;
}
$server_id = $channel["server_id"];
// Check if user is owner or admin (minimal check for now)
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();
if (!Permissions::hasPermission($_SESSION["user_id"], $server_id, Permissions::MANAGE_CHANNELS)) {
echo json_encode(["success" => false, "error" => "Only moderators or admins can clear history"]);
exit;
}
try {
$stmt = db()->prepare("DELETE FROM messages WHERE channel_id = ?");
$stmt->execute([$channel_id]);
echo json_encode(["success" => true]);
} catch (Exception $e) {
echo json_encode(["success" => false, "error" => $e->getMessage()]);
}

67
api_v1_dms.php Normal file
View File

@ -0,0 +1,67 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
requireLogin();
$current_user_id = $_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$target_user_id = $_POST['user_id'] ?? 0;
if ($target_user_id == $current_user_id) {
echo json_encode(['success' => false, 'error' => 'You cannot message yourself']);
exit;
}
try {
// Check if DM channel already exists between these two users
$stmt = db()->prepare("
SELECT c.id
FROM channels c
JOIN channel_members cm1 ON c.id = cm1.channel_id
JOIN channel_members cm2 ON c.id = cm2.channel_id
WHERE c.type = 'dm' AND cm1.user_id = ? AND cm2.user_id = ?
");
$stmt->execute([$current_user_id, $target_user_id]);
$existing = $stmt->fetch();
if ($existing) {
echo json_encode(['success' => true, 'channel_id' => $existing['id']]);
exit;
}
// Create new DM channel
$stmt = db()->prepare("INSERT INTO channels (server_id, name, type) VALUES (NULL, 'dm', 'dm')");
$stmt->execute();
$channel_id = db()->lastInsertId();
// Add both users to the channel
$stmt = db()->prepare("INSERT INTO channel_members (channel_id, user_id) VALUES (?, ?), (?, ?)");
$stmt->execute([$channel_id, $current_user_id, $channel_id, $target_user_id]);
echo json_encode(['success' => true, 'channel_id' => $channel_id]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
// Fetch all DM channels for current user
try {
$stmt = db()->prepare("
SELECT c.id, u.display_name as other_user, u.username as login_name, 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]);
$dms = $stmt->fetchAll();
echo json_encode(['success' => true, 'dms' => $dms]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}

271
api_v1_messages.php Normal file
View File

@ -0,0 +1,271 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/opengraph.php';
require_once 'includes/ai_filtering.php';
require_once 'includes/permissions.php';
// Check for Bot token in headers
$headers = getallheaders();
$bot_token = null;
if (isset($headers['Authorization']) && preg_match('/Bot\s+(\S+)/', $headers['Authorization'], $matches)) {
$bot_token = $matches[1];
}
$user_id = null;
if ($bot_token) {
$stmt = db()->prepare("SELECT id FROM users WHERE bot_token = ? AND is_bot = TRUE");
$stmt->execute([$bot_token]);
$bot = $stmt->fetch();
if ($bot) {
$user_id = $bot['id'];
} else {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Invalid Bot Token']);
exit;
}
} elseif (isset($_SESSION['user_id'])) {
$user_id = $_SESSION['user_id'];
} else {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$channel_id = $_GET['channel_id'] ?? 0;
$pinned = isset($_GET['pinned']) && $_GET['pinned'] == 1;
if ($pinned) {
try {
// Get server_id for the channel
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$server_id = $stmt->fetchColumn();
$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 = ? AND m.is_pinned = 1
ORDER BY m.created_at DESC
");
$stmt->execute([$server_id ?: 0, $server_id ?: 0, $channel_id]);
$msgs = $stmt->fetchAll();
foreach ($msgs as &$m) {
$m['time'] = date('H:i', strtotime($m['created_at']));
$m['metadata'] = $m['metadata'] ? json_decode($m['metadata']) : null;
}
echo json_encode(['success' => true, 'messages' => $msgs]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if (isset($_GET['after_id'])) {
try {
$after_id = (int)$_GET['after_id'];
// Get server_id for the channel
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$server_id = $stmt->fetchColumn();
$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 = ? AND m.id > ?
ORDER BY m.id ASC
");
$stmt->execute([$server_id ?: 0, $server_id ?: 0, $channel_id, $after_id]);
$msgs = $stmt->fetchAll();
foreach ($msgs as &$m) {
$m['time'] = date('H:i', strtotime($m['created_at']));
$m['metadata'] = $m['metadata'] ? json_decode($m['metadata']) : null;
}
echo json_encode(['success' => true, 'messages' => $msgs]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
}
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$data = json_decode(file_get_contents('php://input'), true);
$message_id = $data['id'] ?? 0;
$content = $data['content'] ?? '';
$action = $data['action'] ?? 'edit';
try {
if ($action === 'pin') {
$stmt = db()->prepare("UPDATE messages SET is_pinned = 1 WHERE id = ?");
$stmt->execute([$message_id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'unpin') {
$stmt = db()->prepare("UPDATE messages SET is_pinned = 0 WHERE id = ?");
$stmt->execute([$message_id]);
echo json_encode(['success' => true]);
exit;
}
if (empty($content)) {
echo json_encode(['success' => false, 'error' => 'Content cannot be empty']);
exit;
}
$stmt = db()->prepare("UPDATE messages SET content = ? WHERE id = ? AND user_id = ?");
$stmt->execute([$content, $message_id, $user_id]);
if ($stmt->rowCount() > 0) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Message not found or unauthorized']);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$data = json_decode(file_get_contents('php://input'), true);
$message_id = $data['id'] ?? 0;
try {
$stmt = db()->prepare("DELETE FROM messages WHERE id = ? AND user_id = ?");
$stmt->execute([$message_id, $user_id]);
if ($stmt->rowCount() > 0) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Message not found or unauthorized']);
}
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
$content = '';
$channel_id = 0;
$thread_id = null;
$attachment_url = null;
if (strpos($_SERVER['CONTENT_TYPE'] ?? '', 'application/json') !== false) {
$data = json_decode(file_get_contents('php://input'), true);
$content = $data['content'] ?? '';
$channel_id = $data['channel_id'] ?? 0;
$thread_id = !empty($data['thread_id']) ? (int)$data['thread_id'] : null;
} else {
$content = $_POST['content'] ?? '';
$channel_id = $_POST['channel_id'] ?? 0;
$thread_id = !empty($_POST['thread_id']) ? (int)$_POST['thread_id'] : null;
// Check if file sharing is allowed in this channel
$stmt = db()->prepare("SELECT allow_file_sharing FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$channel = $stmt->fetch();
$can_share_files = $channel ? (bool)$channel['allow_file_sharing'] : true;
if (isset($_FILES['file']) && $_FILES['file']['error'] === UPLOAD_ERR_OK) {
if (!$can_share_files) {
echo json_encode(['success' => false, 'error' => 'File sharing is disabled in this channel.']);
exit;
}
$upload_dir = 'assets/uploads/';
if (!is_dir($upload_dir)) mkdir($upload_dir, 0775, true);
$filename = time() . '_' . basename($_FILES['file']['name']);
$target_file = $upload_dir . $filename;
if (move_uploaded_file($_FILES['file']['tmp_name'], $target_file)) {
$attachment_url = $target_file;
}
}
}
if (empty($content) && empty($attachment_url)) {
echo json_encode(['success' => false, 'error' => 'Empty content and no attachment']);
exit;
}
// Check granular permissions
if (!Permissions::canSendInChannel($user_id, $channel_id)) {
echo json_encode(['success' => false, 'error' => 'You do not have permission to send messages in this channel.']);
exit;
}
if (!empty($content)) {
$moderation = moderateContent($content);
if (!$moderation['is_safe']) {
echo json_encode(['success' => false, 'error' => 'Message flagged as inappropriate: ' . ($moderation['reason'] ?? 'Violation of community standards')]);
exit;
}
}
$metadata = null;
if (!empty($content)) {
$urls = extractUrls($content);
if (!empty($urls)) {
// Fetch OG data for the first URL
$ogData = fetchOpenGraphData($urls[0]);
if ($ogData) {
$metadata = json_encode($ogData);
}
}
}
try {
$stmt = db()->prepare("INSERT INTO messages (channel_id, thread_id, user_id, content, attachment_url, metadata) VALUES (?, ?, ?, ?, ?, ?)");
$stmt->execute([$channel_id, $thread_id, $user_id, $content, $attachment_url, $metadata]);
$last_id = db()->lastInsertId();
// Enforce message limit if set
enforceChannelLimit($channel_id);
// Get server_id for the channel
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$server_id = $stmt->fetchColumn();
// Fetch message with username and role color for the response
$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.id = ?
");
$stmt->execute([$server_id ?: 0, $server_id ?: 0, $last_id]);
$msg = $stmt->fetch();
echo json_encode([
'success' => true,
'message' => [
'id' => $msg['id'],
'user_id' => $msg['user_id'],
'username' => $msg['username'], 'login_name' => $msg['login_name'],
'avatar_url' => $msg['avatar_url'],
'role_color' => $msg['role_color'],
'role_icon' => $msg['role_icon'],
'content' => $msg['content'],
'attachment_url' => $msg['attachment_url'],
'metadata' => $msg['metadata'] ? json_decode($msg['metadata']) : null,
'time' => date('H:i', strtotime($msg['created_at']))
]
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

58
api_v1_reactions.php Normal file
View File

@ -0,0 +1,58 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$user_id = $_SESSION['user_id'];
$data = json_decode(file_get_contents('php://input'), true);
$message_id = $data['message_id'] ?? 0;
$emoji = $data['emoji'] ?? '';
$action = $data['action'] ?? 'toggle'; // 'toggle', 'add', 'remove'
if (!$message_id || !$emoji) {
echo json_encode(['success' => false, 'error' => 'Missing message_id or emoji']);
exit;
}
try {
if ($action === 'toggle') {
$stmt = db()->prepare("SELECT id FROM message_reactions WHERE message_id = ? AND user_id = ? AND emoji = ?");
$stmt->execute([$message_id, $user_id, $emoji]);
if ($stmt->fetch()) {
$stmt = db()->prepare("DELETE FROM message_reactions WHERE message_id = ? AND user_id = ? AND emoji = ?");
$stmt->execute([$message_id, $user_id, $emoji]);
$res_action = 'removed';
} else {
$stmt = db()->prepare("INSERT INTO message_reactions (message_id, user_id, emoji) VALUES (?, ?, ?)");
$stmt->execute([$message_id, $user_id, $emoji]);
$res_action = 'added';
}
} elseif ($action === 'add') {
$stmt = db()->prepare("INSERT IGNORE INTO message_reactions (message_id, user_id, emoji) VALUES (?, ?, ?)");
$stmt->execute([$message_id, $user_id, $emoji]);
$res_action = 'added';
} else {
$stmt = db()->prepare("DELETE FROM message_reactions WHERE message_id = ? AND user_id = ? AND emoji = ?");
$stmt->execute([$message_id, $user_id, $emoji]);
$res_action = 'removed';
}
// Get updated reactions for this message
$stmt = db()->prepare("SELECT emoji, COUNT(*) as count, GROUP_CONCAT(user_id) as users FROM message_reactions WHERE message_id = ? GROUP BY emoji");
$stmt->execute([$message_id]);
$reactions = $stmt->fetchAll();
echo json_encode([
'success' => true,
'action' => $res_action,
'message_id' => $message_id,
'reactions' => $reactions
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

175
api_v1_roles.php Normal file
View File

@ -0,0 +1,175 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user_id = $_SESSION['user_id'];
$data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$server_id = $_GET['server_id'] ?? 0;
if (!$server_id) {
echo json_encode(['success' => false, 'error' => 'Missing server_id']);
exit;
}
// Verify user is in server
$stmt = db()->prepare("SELECT * FROM server_members WHERE server_id = ? AND user_id = ?");
$stmt->execute([$server_id, $user_id]);
if (!$stmt->fetch()) {
echo json_encode(['success' => false, 'error' => 'Access denied']);
exit;
}
$stmt = db()->prepare("SELECT * FROM roles WHERE server_id = ? ORDER BY position DESC");
$stmt->execute([$server_id]);
$roles = $stmt->fetchAll();
// Fetch members and their roles
$stmt = db()->prepare("
SELECT u.id, u.display_name as username, u.username as login_name, u.avatar_url,
GROUP_CONCAT(r.id) as role_ids,
GROUP_CONCAT(r.name) as role_names,
(SELECT r2.color FROM roles r2 JOIN user_roles ur2 ON r2.id = ur2.role_id WHERE ur2.user_id = u.id AND r2.server_id = ? ORDER BY r2.position DESC LIMIT 1) as role_color,
(SELECT r2.icon_url FROM roles r2 JOIN user_roles ur2 ON r2.id = ur2.role_id WHERE ur2.user_id = u.id AND r2.server_id = ? ORDER BY r2.position DESC LIMIT 1) as role_icon
FROM users u
JOIN server_members sm ON u.id = sm.user_id
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id AND r.server_id = ?
WHERE sm.server_id = ?
GROUP BY u.id
");
$stmt->execute([$server_id, $server_id, $server_id, $server_id]);
$members = $stmt->fetchAll();
$filtered_members = null;
$channel_id = $_GET['channel_id'] ?? 0;
if ($channel_id) {
$filtered_members = [];
foreach ($members as $m) {
if (Permissions::canViewChannel($m['id'], $channel_id)) {
$filtered_members[] = $m;
}
}
}
echo json_encode([
'success' => true,
'roles' => $roles,
'members' => $members,
'filtered_members' => $filtered_members,
'permissions_list' => [
['value' => 1, 'name' => 'View Channels'],
['value' => 2, 'name' => 'Send Messages'],
['value' => 4, 'name' => 'Manage Messages'],
['value' => 8, 'name' => 'Manage Channels'],
['value' => 16, 'name' => 'Manage Server'],
['value' => 32, 'name' => 'Administrator']
]
]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $data['action'] ?? '';
$server_id = $data['server_id'] ?? 0;
// Permissions check: Owner or MANAGE_SERVER
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();
$is_owner = ($server && $server['owner_id'] == $user_id);
$can_manage = Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_SERVER) || Permissions::hasPermission($user_id, $server_id, Permissions::ADMINISTRATOR);
if (!$is_owner && !$can_manage) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($action === 'create') {
$name = $data['name'] ?? 'New Role';
$color = $data['color'] ?? '#99aab5';
$perms = $data['permissions'] ?? 0;
$stmt = db()->prepare("INSERT INTO roles (server_id, name, color, permissions) VALUES (?, ?, ?, ?)");
$stmt->execute([$server_id, $name, $color, $perms]);
echo json_encode(['success' => true, 'role_id' => db()->lastInsertId()]);
} elseif ($action === 'update') {
$role_id = $data['id'] ?? 0;
$name = $data['name'] ?? '';
$color = $data['color'] ?? '';
$icon_url = $data['icon_url'] ?? null;
$perms = $data['permissions'] ?? 0;
$stmt = db()->prepare("UPDATE roles SET name = ?, color = ?, icon_url = ?, permissions = ? WHERE id = ? AND server_id = ?");
$stmt->execute([$name, $color, $icon_url, $perms, $role_id, $server_id]);
echo json_encode(['success' => true]);
} elseif ($action === 'delete') {
$role_id = $data['id'] ?? 0;
$stmt = db()->prepare("DELETE FROM roles WHERE id = ? AND server_id = ?");
$stmt->execute([$role_id, $server_id]);
echo json_encode(['success' => true]);
} elseif ($action === 'assign') {
$target_user_id = $data['user_id'] ?? 0;
$role_id = $data['role_id'] ?? 0;
// Verify role belongs to server
$stmt = db()->prepare("SELECT id FROM roles WHERE id = ? AND server_id = ?");
$stmt->execute([$role_id, $server_id]);
if (!$stmt->fetch()) {
echo json_encode(['success' => false, 'error' => 'Invalid role']);
exit;
}
$stmt = db()->prepare("INSERT IGNORE INTO user_roles (user_id, role_id) VALUES (?, ?)");
$stmt->execute([$target_user_id, $role_id]);
echo json_encode(['success' => true]);
} elseif ($action === 'unassign') {
$target_user_id = $data['user_id'] ?? 0;
$role_id = $data['role_id'] ?? 0;
$stmt = db()->prepare("DELETE ur FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = ? AND ur.role_id = ? AND r.server_id = ?");
$stmt->execute([$target_user_id, $role_id, $server_id]);
echo json_encode(['success' => true]);
} elseif ($action === 'reorder') {
$orders = $data['orders'] ?? [];
foreach ($orders as $order) {
$stmt = db()->prepare("UPDATE roles SET position = ? WHERE id = ? AND server_id = ?");
$stmt->execute([$order['position'], $order['id'], $server_id]);
}
echo json_encode(['success' => true]);
} elseif ($action === 'set_user_roles') {
$target_user_id = $data['user_id'] ?? 0;
$role_ids = $data['role_ids'] ?? [];
// Begin transaction
$db = db();
$db->beginTransaction();
try {
// Remove all existing roles for this user in this server
$stmt = $db->prepare("DELETE ur FROM user_roles ur JOIN roles r ON ur.role_id = r.id WHERE ur.user_id = ? AND r.server_id = ?");
$stmt->execute([$target_user_id, $server_id]);
// Add new roles
if (!empty($role_ids)) {
$stmt = $db->prepare("INSERT INTO user_roles (user_id, role_id) VALUES (?, ?)");
foreach ($role_ids as $rid) {
// Verify role belongs to server
$check = $db->prepare("SELECT id FROM roles WHERE id = ? AND server_id = ?");
$check->execute([$rid, $server_id]);
if ($check->fetch()) {
$stmt->execute([$target_user_id, $rid]);
}
}
}
$db->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
$db->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
}
exit;
}

189
api_v1_rss.php Normal file
View File

@ -0,0 +1,189 @@
<?php
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user = getCurrentUser();
$action = $_REQUEST['action'] ?? '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$channel_id = $_POST['channel_id'] ?? 0;
// Permission check: must have manage channels permission
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$chan = $stmt->fetch();
if (!$chan || !Permissions::hasPermission($user['id'], $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($action === 'add') {
$url = $_POST['url'] ?? '';
if (!filter_var($url, FILTER_VALIDATE_URL)) {
echo json_encode(['success' => false, 'error' => 'Invalid URL']);
exit;
}
$stmt = db()->prepare("INSERT INTO channel_rss_feeds (channel_id, url) VALUES (?, ?)");
$stmt->execute([$channel_id, $url]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'delete') {
$feed_id = $_POST['feed_id'] ?? 0;
$stmt = db()->prepare("DELETE FROM channel_rss_feeds WHERE id = ? AND channel_id = ?");
$stmt->execute([$feed_id, $channel_id]);
echo json_encode(['success' => true]);
exit;
}
if ($action === 'sync') {
// Cooldown check: only sync if last sync was > 5 minutes ago
// Or if it's a forced sync from the settings UI
$is_auto = isset($_POST['auto']) && $_POST['auto'] == 1;
// Fetch feeds for this channel
$stmt = db()->prepare("SELECT * FROM channel_rss_feeds WHERE channel_id = ?");
$stmt->execute([$channel_id]);
$feeds = $stmt->fetchAll();
if ($is_auto) {
$last_fetch = 0;
foreach ($feeds as $f) {
if ($f['last_fetched_at']) {
$ts = strtotime($f['last_fetched_at']);
if ($ts > $last_fetch) $last_fetch = $ts;
}
}
if (time() - $last_fetch < 120) { // 2 minutes to match JS
echo json_encode(['success' => true, 'new_items' => 0, 'skipped' => true]);
exit;
}
}
$new_items_count = 0;
foreach ($feeds as $feed) {
$rss_content = @file_get_contents($feed['url']);
if (!$rss_content) continue;
$xml = @simplexml_load_string($rss_content);
if (!$xml) continue;
$items = [];
if (isset($xml->channel->item)) { // RSS 2.0
$items = $xml->channel->item;
} elseif (isset($xml->entry)) { // Atom
$items = $xml->entry;
}
$feed_items = [];
foreach ($items as $item) {
$feed_items[] = $item;
}
$feed_items = array_reverse($feed_items);
foreach ($feed_items as $item) {
$guid = (string)($item->guid ?? ($item->id ?? $item->link));
if (empty($guid) && isset($item->link['href'])) {
$guid = (string)$item->link['href'];
}
$title = (string)$item->title;
$link = (string)($item->link['href'] ?? $item->link);
if (empty($link) && isset($item->link)) {
foreach($item->link as $l) {
if ($l['rel'] == 'alternate' || !$l['rel']) {
$link = (string)$l['href'];
break;
}
}
}
$description = (string)($item->description ?? ($item->summary ?? $item->content));
$description = strip_tags($description);
// Extract additional fields
$category = (string)($item->category ?? ($item->category['term'] ?? ''));
$pubDate = (string)($item->pubDate ?? ($item->published ?? ($item->updated ?? '')));
// Format date nicely if possible
if ($pubDate) {
$timestamp = strtotime($pubDate);
if ($timestamp) {
$pubDate = date('d/m/Y H:i', $timestamp);
}
}
$author = (string)($item->author->name ?? ($item->author ?? ''));
if (!$author) {
$dc = $item->children('http://purl.org/dc/elements/1.1/');
if (isset($dc->creator)) {
$author = (string)$dc->creator;
}
}
// Check if already exists in processed items (prevents re-posting if deleted by retention policy)
$stmt_check = db()->prepare("SELECT id FROM rss_processed_items WHERE channel_id = ? AND rss_guid = ?");
$stmt_check->execute([$channel_id, $guid]);
if ($stmt_check->fetch()) continue;
// Insert into processed items first to be safe
$stmt_processed = db()->prepare("INSERT IGNORE INTO rss_processed_items (channel_id, rss_guid) VALUES (?, ?)");
$stmt_processed->execute([$channel_id, $guid]);
// Insert as message from a special "RSS Bot" user or system
$stmt_bot = db()->prepare("SELECT id FROM users WHERE username = 'RSS Bot' AND is_bot = 1");
$stmt_bot->execute();
$bot = $stmt_bot->fetch();
if (!$bot) {
$stmt_create_bot = db()->prepare("INSERT INTO users (username, display_name, is_bot, status, avatar_url, email, password_hash) VALUES ('RSS Bot', 'RSS Bot', 1, 'online', 'https://cdn-icons-png.flaticon.com/512/3607/3607436.png', 'rss-bot@system.internal', 'bot-no-password')");
$stmt_create_bot->execute();
$bot_id = db()->lastInsertId();
} else {
$bot_id = $bot['id'];
}
// Format content for traditional view
$content = "[" . $title . "](" . $link . ")\n";
if ($category || $pubDate || $author) {
$parts = [];
if ($category) $parts[] = $category;
if ($pubDate) $parts[] = $pubDate;
if ($author) $parts[] = $author;
$content .= implode(" · ", $parts);
}
$metadata = json_encode([
'title' => $title,
'description' => mb_substr($description, 0, 500) . (mb_strlen($description) > 500 ? '...' : ''),
'url' => $link,
'category' => $category,
'date' => $pubDate,
'author' => $author,
'is_rss' => true,
'site_name' => parse_url($feed['url'], PHP_URL_HOST)
]);
$stmt_msg = db()->prepare("INSERT INTO messages (channel_id, user_id, content, metadata, rss_guid) VALUES (?, ?, ?, ?, ?)");
$stmt_msg->execute([$channel_id, $bot_id, $content, $metadata, $guid]);
enforceChannelLimit($channel_id);
$new_items_count++;
}
$stmt_update_feed = db()->prepare("UPDATE channel_rss_feeds SET last_fetched_at = CURRENT_TIMESTAMP WHERE id = ?");
$stmt_update_feed->execute([$feed['id']]);
}
echo json_encode(['success' => true, 'new_items' => $new_items_count, 'channel_id' => $channel_id]);
exit;
}
} else {
// GET: List feeds
$channel_id = $_GET['channel_id'] ?? 0;
$stmt = db()->prepare("SELECT * FROM channel_rss_feeds WHERE channel_id = ? ORDER BY created_at DESC");
$stmt->execute([$channel_id]);
echo json_encode(['success' => true, 'feeds' => $stmt->fetchAll()]);
exit;
}

125
api_v1_rules.php Normal file
View File

@ -0,0 +1,125 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user_id = $_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$channel_id = $_POST['channel_id'] ?? 0;
$content = $_POST['content'] ?? '';
// Check if user has permission to manage channels
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$chan = $stmt->fetch();
if (!$chan || !Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
try {
// Get max position
$stmt = db()->prepare("SELECT MAX(position) FROM channel_rules WHERE channel_id = ?");
$stmt->execute([$channel_id]);
$pos = (int)$stmt->fetchColumn() + 1;
$stmt = db()->prepare("INSERT INTO channel_rules (channel_id, content, position) VALUES (?, ?, ?)");
$stmt->execute([$channel_id, $content, $pos]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$id = $_GET['id'] ?? 0;
$stmt = db()->prepare("SELECT c.server_id FROM channels c JOIN channel_rules r ON c.id = r.channel_id WHERE r.id = ?");
$stmt->execute([$id]);
$res = $stmt->fetch();
if ($res && Permissions::hasPermission($user_id, $res['server_id'], Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("DELETE FROM channel_rules WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PATCH') {
$data = json_decode(file_get_contents('php://input'), true);
if (isset($data['order'])) {
// Bulk reorder
foreach ($data['order'] as $index => $id) {
// Basic permission check (optional but recommended: verify all rules belong to same server user can manage)
if ($index === 0) {
$stmt = db()->prepare("SELECT c.server_id FROM channels c JOIN channel_rules r ON c.id = r.channel_id WHERE r.id = ?");
$stmt->execute([$id]);
$res = $stmt->fetch();
if (!$res || !Permissions::hasPermission($user_id, $res['server_id'], Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
}
$stmt = db()->prepare("UPDATE channel_rules SET position = ? WHERE id = ?");
$stmt->execute([$index + 1, $id]);
}
echo json_encode(['success' => true]);
exit;
}
$id = $data['id'] ?? 0;
$dir = $data['dir'] ?? 'up';
// Check permission
$stmt = db()->prepare("SELECT c.server_id, r.channel_id, r.position FROM channels c JOIN channel_rules r ON c.id = r.channel_id WHERE r.id = ?");
$stmt->execute([$id]);
$current = $stmt->fetch();
if ($current && Permissions::hasPermission($user_id, $current['server_id'], Permissions::MANAGE_CHANNELS)) {
$channel_id = $current['channel_id'];
$pos = $current['position'];
if ($dir === 'up') {
$stmt = db()->prepare("SELECT id, position FROM channel_rules WHERE channel_id = ? AND position < ? ORDER BY position DESC LIMIT 1");
} else {
$stmt = db()->prepare("SELECT id, position FROM channel_rules WHERE channel_id = ? AND position > ? ORDER BY position ASC LIMIT 1");
}
$stmt->execute([$channel_id, $pos]);
$other = $stmt->fetch();
if ($other) {
db()->prepare("UPDATE channel_rules SET position = ? WHERE id = ?")->execute([$other['position'], $id]);
db()->prepare("UPDATE channel_rules SET position = ? WHERE id = ?")->execute([$pos, $other['id']]);
}
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Rule not found']);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$data = json_decode(file_get_contents('php://input'), true);
$id = $data['id'] ?? 0;
$content = $data['content'] ?? '';
$stmt = db()->prepare("SELECT c.server_id FROM channels c JOIN channel_rules r ON c.id = r.channel_id WHERE r.id = ?");
$stmt->execute([$id]);
$res = $stmt->fetch();
if ($res && Permissions::hasPermission($user_id, $res['server_id'], Permissions::MANAGE_CHANNELS)) {
$stmt = db()->prepare("UPDATE channel_rules SET content = ? WHERE id = ?");
$stmt->execute([$content, $id]);
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
}
exit;
}

57
api_v1_search.php Normal file
View File

@ -0,0 +1,57 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
requireLogin();
$user_id = $_SESSION['user_id'];
$query = $_GET['q'] ?? '';
$type = $_GET['type'] ?? 'messages'; // messages or users
$channel_id = $_GET['channel_id'] ?? 0;
if (empty($query)) {
echo json_encode(['success' => true, 'results' => []]);
exit;
}
try {
if ($type === 'users') {
$stmt = db()->prepare("
SELECT id, display_name as username, username as login_name, avatar_url, status
FROM users
WHERE username LIKE ? OR display_name LIKE ?
LIMIT 20
");
$stmt->execute(["%" . $query . "%", "%" . $query . "%"]);
$results = $stmt->fetchAll();
} else {
$sql = "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.content LIKE ? ";
$params = ["%" . $query . "%"];
if ($channel_id > 0) {
$sql .= " AND m.channel_id = ?";
$params[] = $channel_id;
} else {
// Search in all channels user has access to
$sql .= " AND m.channel_id IN (
SELECT c.id FROM channels c
LEFT JOIN server_members sm ON c.server_id = sm.server_id
LEFT JOIN channel_members cm ON c.id = cm.channel_id
WHERE sm.user_id = ? OR cm.user_id = ?
)";
$params[] = $user_id;
$params[] = $user_id;
}
$sql .= " ORDER BY m.created_at DESC LIMIT 50";
$stmt = db()->prepare($sql);
$stmt->execute($params);
$results = $stmt->fetchAll();
}
echo json_encode(['success' => true, 'results' => $results]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

85
api_v1_servers.php Normal file
View File

@ -0,0 +1,85 @@
<?php
require_once 'auth/session.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$action = $_POST['action'] ?? 'create';
$user_id = $_SESSION['user_id'];
if ($action === 'join') {
$invite_code = $_POST['invite_code'] ?? '';
$stmt = db()->prepare("SELECT id FROM servers WHERE invite_code = ?");
$stmt->execute([$invite_code]);
$server = $stmt->fetch();
if ($server) {
$stmt = db()->prepare("INSERT IGNORE INTO server_members (server_id, user_id) VALUES (?, ?)");
$stmt->execute([$server['id'], $user_id]);
header('Location: index.php?server_id=' . $server['id']);
exit;
} else {
die("Invalid invite code.");
}
}
if ($action === 'update') {
$server_id = $_POST['server_id'] ?? 0;
$name = $_POST['name'] ?? '';
$icon_url = $_POST['icon_url'] ?? '';
$theme_color = $_POST['theme_color'] ?? null;
if ($theme_color === '') $theme_color = null;
require_once 'includes/permissions.php';
if (Permissions::hasPermission($user_id, $server_id, Permissions::MANAGE_SERVER)) {
$stmt = db()->prepare("UPDATE servers SET name = ?, icon_url = ?, theme_color = ? WHERE id = ?");
$stmt->execute([$name, $icon_url, $theme_color, $server_id]);
}
header('Location: index.php?server_id=' . $server_id);
exit;
}
if ($action === 'delete') {
$server_id = $_POST['server_id'] ?? 0;
$stmt = db()->prepare("DELETE FROM servers WHERE id = ? AND owner_id = ?");
$stmt->execute([$server_id, $user_id]);
header('Location: index.php');
exit;
}
$name = $_POST['name'] ?? '';
$icon_url = $_POST['icon_url'] ?? '';
if ($name) {
try {
$db = db();
$db->beginTransaction();
// Create server
$invite_code = substr(strtoupper(md5(uniqid())), 0, 8);
$stmt = $db->prepare("INSERT INTO servers (name, owner_id, invite_code, icon_url) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $user_id, $invite_code, $icon_url]);
$server_id = $db->lastInsertId();
// Add owner as member
$stmt = $db->prepare("INSERT INTO server_members (server_id, user_id) VALUES (?, ?)");
$stmt->execute([$server_id, $user_id]);
// Create default channel
$stmt = $db->prepare("INSERT INTO channels (server_id, name, type) VALUES (?, 'general', 'text')");
$stmt->execute([$server_id]);
// Create default @everyone role
$stmt = $db->prepare("INSERT INTO roles (server_id, name, color, permissions, position) VALUES (?, '@everyone', '#99aab5', 0, 0)");
$stmt->execute([$server_id]);
$db->commit();
header('Location: index.php?server_id=' . $server_id);
exit;
} catch (Exception $e) {
$db->rollBack();
die("Error creating server: " . $e->getMessage());
}
}
}
header('Location: index.php');

74
api_v1_stats.php Normal file
View File

@ -0,0 +1,74 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
$server_id = $_GET['server_id'] ?? 0;
if (!$server_id) {
echo json_encode(['success' => false, 'error' => 'Server ID required']);
exit;
}
$user_id = $_SESSION['user_id'];
// Check if user is member of the server
$stmt = db()->prepare("SELECT 1 FROM server_members WHERE server_id = ? AND user_id = ?");
$stmt->execute([$server_id, $user_id]);
if (!$stmt->fetch()) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
try {
// Total members
$stmt = db()->prepare("SELECT COUNT(*) as count FROM server_members WHERE server_id = ?");
$stmt->execute([$server_id]);
$total_members = $stmt->fetch()['count'];
// Total messages in all channels of this server
$stmt = db()->prepare("
SELECT COUNT(*) as count
FROM messages m
JOIN channels c ON m.channel_id = c.id
WHERE c.server_id = ?
");
$stmt->execute([$server_id]);
$total_messages = $stmt->fetch()['count'];
// Messages per day (last 7 days)
$stmt = db()->prepare("
SELECT DATE(m.created_at) as date, COUNT(*) as count
FROM messages m
JOIN channels c ON m.channel_id = c.id
WHERE c.server_id = ? AND m.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE(m.created_at)
ORDER BY date ASC
");
$stmt->execute([$server_id]);
$history = $stmt->fetchAll();
// Top active users
$stmt = db()->prepare("
SELECT u.display_name as username, u.username as login_name, COUNT(*) as message_count
FROM messages m
JOIN channels c ON m.channel_id = c.id
JOIN users u ON m.user_id = u.id
WHERE c.server_id = ?
GROUP BY m.user_id
ORDER BY message_count DESC
LIMIT 5
");
$stmt->execute([$server_id]);
$top_users = $stmt->fetchAll();
echo json_encode([
'success' => true,
'stats' => [
'total_members' => $total_members,
'total_messages' => $total_messages,
'history' => $history,
'top_users' => $top_users
]
]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}

54
api_v1_tags.php Normal file
View File

@ -0,0 +1,54 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
require_once 'includes/permissions.php';
requireLogin();
$user_id = $_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$channel_id = $_GET['channel_id'] ?? 0;
if (!$channel_id) {
echo json_encode(['success' => false, 'error' => 'Missing channel_id']);
exit;
}
$stmt = db()->prepare("SELECT * FROM forum_tags WHERE channel_id = ?");
$stmt->execute([$channel_id]);
echo json_encode(['success' => true, 'tags' => $stmt->fetchAll()]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
if (!$data) $data = $_POST;
$action = $data['action'] ?? 'create';
$channel_id = $data['channel_id'] ?? 0;
// Check permissions
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$chan = $stmt->fetch();
if (!$chan || !Permissions::hasPermission($user_id, $chan['server_id'], Permissions::MANAGE_CHANNELS)) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
if ($action === 'create') {
$name = $data['name'] ?? '';
$color = $data['color'] ?? '#7289da';
if (!$name) {
echo json_encode(['success' => false, 'error' => 'Missing name']);
exit;
}
$stmt = db()->prepare("INSERT INTO forum_tags (channel_id, name, color) VALUES (?, ?, ?)");
$stmt->execute([$channel_id, $name, $color]);
echo json_encode(['success' => true, 'tag_id' => db()->lastInsertId()]);
} elseif ($action === 'delete') {
$tag_id = $data['tag_id'] ?? 0;
$stmt = db()->prepare("DELETE FROM forum_tags WHERE id = ? AND channel_id = ?");
$stmt->execute([$tag_id, $channel_id]);
echo json_encode(['success' => true]);
}
exit;
}

86
api_v1_threads.php Normal file
View File

@ -0,0 +1,86 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$channel_id = $_POST['channel_id'] ?? 0;
$title = $_POST['title'] ?? '';
$user_id = $_SESSION['user_id'];
if (!$channel_id || !$title) {
echo json_encode(['success' => false, 'error' => 'Missing data']);
exit;
}
require_once 'includes/permissions.php';
if (!Permissions::canSendInChannel($user_id, $channel_id)) {
echo json_encode(['success' => false, 'error' => 'You do not have permission to create threads in this channel.']);
exit;
}
$tag_ids = $_POST['tag_ids'] ?? [];
if (is_string($tag_ids)) {
$tag_ids = array_filter(explode(',', $tag_ids));
}
try {
db()->beginTransaction();
$stmt = db()->prepare("INSERT INTO forum_threads (channel_id, user_id, title) VALUES (?, ?, ?)");
$stmt->execute([$channel_id, $user_id, $title]);
$thread_id = db()->lastInsertId();
if (!empty($tag_ids)) {
$stmtTag = db()->prepare("INSERT INTO thread_tags (thread_id, tag_id) VALUES (?, ?)");
foreach ($tag_ids as $tag_id) {
if ($tag_id) $stmtTag->execute([$thread_id, $tag_id]);
}
}
db()->commit();
echo json_encode(['success' => true, 'thread_id' => $thread_id]);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'PATCH' || (isset($_GET['action']) && $_GET['action'] === 'solve')) {
$data = json_decode(file_get_contents('php://input'), true) ?? $_POST;
$thread_id = $data['thread_id'] ?? 0;
$message_id = $data['message_id'] ?? null; // null to unsolve
$user_id = $_SESSION['user_id'];
if (!$thread_id) {
echo json_encode(['success' => false, 'error' => 'Missing thread_id']);
exit;
}
// Verify permission (thread owner or admin)
$stmt = db()->prepare("SELECT t.*, c.server_id FROM forum_threads t JOIN channels c ON t.channel_id = c.id WHERE t.id = ?");
$stmt->execute([$thread_id]);
$thread = $stmt->fetch();
if (!$thread) {
echo json_encode(['success' => false, 'error' => 'Thread not found']);
exit;
}
$stmtServer = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmtServer->execute([$thread['server_id']]);
$server = $stmtServer->fetch();
if ($thread['user_id'] != $user_id && $server['owner_id'] != $user_id) {
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
try {
$stmt = db()->prepare("UPDATE forum_threads SET solution_message_id = ? WHERE id = ?");
$stmt->execute([$message_id, $thread_id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}

48
api_v1_user.php Normal file
View File

@ -0,0 +1,48 @@
<?php
require_once 'auth/session.php';
header('Content-Type: application/json');
// Detailed log
$log = [
'date' => date('Y-m-d H:i:s'),
'method' => $_SERVER['REQUEST_METHOD'],
'post' => $_POST,
'session' => $_SESSION
];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$user = getCurrentUser();
if (!$user) {
$log['error'] = 'Unauthorized';
file_put_contents('requests.log', json_encode($log) . "\n", FILE_APPEND);
echo json_encode(['success' => false, 'error' => 'Unauthorized']);
exit;
}
$log['user_id'] = $user['id'];
$display_name = !empty($_POST['display_name']) ? $_POST['display_name'] : $user['display_name'];
$avatar_url = isset($_POST['avatar_url']) ? $_POST['avatar_url'] : $user['avatar_url'];
$dnd_mode = isset($_POST['dnd_mode']) ? (int)$_POST['dnd_mode'] : 0;
$sound_notifications = isset($_POST['sound_notifications']) ? (int)$_POST['sound_notifications'] : 0;
$theme = !empty($_POST['theme']) ? $_POST['theme'] : $user['theme'];
$voice_mode = !empty($_POST['voice_mode']) ? $_POST['voice_mode'] : ($user['voice_mode'] ?? 'vox');
$voice_ptt_key = !empty($_POST['voice_ptt_key']) ? $_POST['voice_ptt_key'] : ($user['voice_ptt_key'] ?? 'v');
$voice_vox_threshold = isset($_POST['voice_vox_threshold']) ? (float)$_POST['voice_vox_threshold'] : ($user['voice_vox_threshold'] ?? 0.1);
try {
$stmt = db()->prepare("UPDATE users SET display_name = ?, avatar_url = ?, dnd_mode = ?, sound_notifications = ?, theme = ?, voice_mode = ?, voice_ptt_key = ?, voice_vox_threshold = ? WHERE id = ?");
$success = $stmt->execute([$display_name, $avatar_url, $dnd_mode, $sound_notifications, $theme, $voice_mode, $voice_ptt_key, $voice_vox_threshold, $user['id']]);
$log['db_success'] = $success;
file_put_contents('requests.log', json_encode($log) . "\n", FILE_APPEND);
echo json_encode(['success' => true]);
} catch (Exception $e) {
$log['db_error'] = $e->getMessage();
file_put_contents('requests.log', json_encode($log) . "\n", FILE_APPEND);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
echo json_encode(['success' => false, 'error' => 'Invalid request']);

239
api_v1_voice.php Normal file
View File

@ -0,0 +1,239 @@
<?php // Vocal secours — WebRTC P2P + signalisation PHP + participants + PTT + VOX (gating via GainNode)
// Mutualisé OVH : nécessite ./data écrivable.
declare(strict_types=1);
require_once "auth/session.php";
header("X-Content-Type-Options: nosniff");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: POST, GET, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
$DATA_DIR = __DIR__ . "/data";
if (!is_dir($DATA_DIR)) @mkdir($DATA_DIR, 0775, true);
$user = getCurrentUser();
$current_user_id = $user ? (int)$user["id"] : 0;
function room_id(string $s): string {
$s = preg_replace("~[^a-zA-Z0-9_\-]~", "", $s);
return $s !== "" ? $s : "secours";
}
function peer_id(): string {
return bin2hex(random_bytes(8));
}
function room_log_file(string $room): string {
return __DIR__ . "/data/" . $room . ".log";
}
function room_participants_file(string $room): string {
return __DIR__ . "/data/" . $room . ".participants.json";
}
function chat_log_file_for_today(): string {
// Un fichier par jour : YYYY-MM-DD.chat.log
$d = date("Y-m-d");
return __DIR__ . "/data/" . $d . ".chat.log";
}
function now_ms(): int {
return (int) floor(microtime(true) * 1000);
}
function json_out($data, int $code = 200): void {
http_response_code($code);
header("Content-Type: application/json; charset=utf-8");
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
function read_json_file(string $path): array {
if (!file_exists($path)) return [];
$raw = @file_get_contents($path);
if ($raw === false || $raw === "") return [];
$j = json_decode($raw, true);
return is_array($j) ? $j : [];
}
function write_json_file(string $path, array $data): void {
file_put_contents($path, json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
}
function tail_lines(string $path, int $maxLines = 120): array {
if (!is_file($path)) return [];
$fp = fopen($path, "rb");
if (!$fp) return [];
$lines = [];
fseek($fp, 0, SEEK_END);
$pos = ftell($fp);
$buffer = "";
while ($pos > 0 && count($lines) < $maxLines) {
$readSize = min($pos, 4096);
$pos -= $readSize;
fseek($fp, $pos);
$chunk = fread($fp, $readSize);
$buffer = $chunk . $buffer;
$chunkLines = explode("\n", $buffer);
$buffer = array_shift($chunkLines);
while (!empty($chunkLines)) {
$line = array_pop($chunkLines);
if (trim($line) !== "") {
array_unshift($lines, trim($line));
if (count($lines) >= $maxLines) break;
}
}
}
fclose($fp);
return $lines;
}
// Logic for signaling
$action = $_REQUEST["action"] ?? "";
$room = room_id($_REQUEST["room"] ?? "secours");
$my_id = $_REQUEST["peer_id"] ?? "";
if ($action === "join") {
$name = $_REQUEST["name"] ?? "User";
$p_file = room_participants_file($room);
$ps = read_json_file($p_file);
// Cleanup old participants (> 10s)
$stale_time = now_ms() - 10000;
foreach ($ps as $id => $p) {
if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]);
}
$new_id = peer_id();
$ps[$new_id] = [
"id" => $new_id,
"user_id" => $current_user_id,
"name" => $name,
"avatar_url" => $user["avatar_url"] ?? "",
"last_seen" => now_ms()
];
write_json_file($p_file, $ps);
// DB Integration for sidebar
if ($current_user_id > 0) {
try {
$stmt = db()->prepare("INSERT INTO voice_sessions (user_id, channel_id, last_seen) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE channel_id = ?, last_seen = ?");
$stmt->execute([$current_user_id, $room, now_ms(), $room, now_ms()]);
} catch (Exception $e) {}
}
json_out(["success" => true, "peer_id" => $new_id, "participants" => $ps]);
}
if ($action === "poll") {
if (!$my_id) json_out(["error" => "Missing peer_id"], 400);
$p_file = room_participants_file($room);
$ps = read_json_file($p_file);
if (isset($ps[$my_id])) {
$ps[$my_id]["last_seen"] = now_ms();
}
$stale_time = now_ms() - 10000;
foreach ($ps as $id => $p) {
if (($p["last_seen"] ?? 0) < $stale_time) unset($ps[$id]);
}
write_json_file($p_file, $ps);
// Update DB last_seen
if ($current_user_id > 0) {
try {
$stmt = db()->prepare("UPDATE voice_sessions SET last_seen = ? WHERE user_id = ?");
$stmt->execute([now_ms(), $current_user_id]);
} catch (Exception $e) {}
}
// Read signals
$log_file = room_log_file($room);
$signals = [];
if (file_exists($log_file)) {
$lines = file($log_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$remaining = [];
$now = now_ms();
foreach ($lines as $line) {
$sig = json_decode($line, true);
if ($sig && isset($sig["to"]) && $sig["to"] === $my_id) {
$signals[] = $sig;
} elseif ($sig && ($now - ($sig["time"] ?? 0) < 30000)) {
$remaining[] = $line;
}
}
file_put_contents($log_file, implode("\n", $remaining) . (empty($remaining) ? "" : "\n"), LOCK_EX);
}
json_out(["success" => true, "participants" => $ps, "signals" => $signals]);
}
if ($action === "signal") {
if (!$my_id) json_out(["error" => "Missing peer_id"], 400);
$to = $_REQUEST["to"] ?? "";
$data = $_REQUEST["data"] ?? "";
if (!$to || !$data) json_out(["error" => "Missing to/data"], 400);
$sig = [
"from" => $my_id,
"to" => $to,
"data" => json_decode($data, true),
"time" => now_ms()
];
file_put_contents(room_log_file($room), json_encode($sig) . "\n", FILE_APPEND | LOCK_EX);
json_out(["success" => true]);
}
if ($action === "list_all") {
// Periodic cleanup of the DB table (stale sessions > 15s)
if (rand(1, 10) === 1) {
try {
$stale_db_time = now_ms() - 15000;
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE last_seen < ?");
$stmt->execute([$stale_db_time]);
} catch (Exception $e) {}
}
try {
$stmt = 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
WHERE vs.last_seen > ?
");
$stale_db_time = now_ms() - 15000;
$stmt->execute([$stale_db_time]);
$sessions = $stmt->fetchAll(PDO::FETCH_ASSOC);
$by_channel = [];
foreach ($sessions as $s) {
$by_channel[$s['channel_id']][] = $s;
}
json_out(["success" => true, "channels" => $by_channel]);
} catch (Exception $e) {
json_out(["error" => $e->getMessage()], 500);
}
}
if ($action === "leave") {
if ($my_id) {
$p_file = room_participants_file($room);
$ps = read_json_file($p_file);
unset($ps[$my_id]);
write_json_file($p_file, $ps);
}
if ($current_user_id > 0) {
try {
$stmt = db()->prepare("DELETE FROM voice_sessions WHERE user_id = ?");
$stmt->execute([$current_user_id]);
} catch (Exception $e) {}
}
json_out(["success" => true]);
}
json_out(["error" => "Unknown action"], 404);

85
api_v1_webhook.php Normal file
View File

@ -0,0 +1,85 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
// Check for execution (no session needed, just token)
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_GET['token'])) {
require_once 'db/config.php';
$token = $_GET['token'] ?? '';
$data = json_decode(file_get_contents('php://input'), true);
$content = $data['content'] ?? '';
$stmt = db()->prepare("SELECT * FROM webhooks WHERE token = ?");
$stmt->execute([$token]);
$webhook = $stmt->fetch();
if (!$webhook) {
http_response_code(401);
echo json_encode(['success' => false, 'error' => 'Invalid token']);
exit;
}
if (empty($content)) {
http_response_code(400);
echo json_encode(['success' => false, 'error' => 'Empty content']);
exit;
}
try {
$stmt = db()->prepare("INSERT INTO messages (channel_id, user_id, content) VALUES (?, ?, ?)");
$stmt->execute([$webhook['channel_id'], 1, $content]); // 1 is system/bot user
enforceChannelLimit($webhook['channel_id']);
echo json_encode(['success' => true]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
// Manage webhooks (session needed)
requireLogin();
$user_id = $_SESSION['user_id'];
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$server_id = $_GET['server_id'] ?? 0;
$stmt = db()->prepare("
SELECT w.*, c.name as channel_name
FROM webhooks w
JOIN channels c ON w.channel_id = c.id
WHERE c.server_id = ?
");
$stmt->execute([$server_id]);
$webhooks = $stmt->fetchAll();
echo json_encode(['success' => true, 'webhooks' => $webhooks]);
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
$channel_id = $data['channel_id'] ?? 0;
$name = $data['name'] ?? 'New Webhook';
$token = bin2hex(random_bytes(16));
try {
$stmt = db()->prepare("INSERT INTO webhooks (channel_id, name, token) VALUES (?, ?, ?)");
$stmt->execute([$channel_id, $name, $token]);
echo json_encode(['success' => true, 'webhook' => ['id' => db()->lastInsertId(), 'token' => $token]]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
$data = json_decode(file_get_contents('php://input'), true);
$id = $data['id'] ?? 0;
try {
$stmt = db()->prepare("DELETE FROM webhooks WHERE id = ?");
$stmt->execute([$id]);
echo json_encode(['success' => true]);
} catch (Exception $e) {
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
}
exit;
}

50
api_v1_withdraw_rules.php Normal file
View File

@ -0,0 +1,50 @@
<?php
header('Content-Type: application/json');
require_once 'auth/session.php';
requireLogin();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$json = json_decode(file_get_contents('php://input'), true);
$channel_id = $json['channel_id'] ?? 0;
$user_id = $_SESSION['user_id'];
if (!$channel_id) {
echo json_encode(['success' => false, 'error' => 'ID de canal manquant']);
exit;
}
// Fetch channel details to get rules_role_id
$stmt = db()->prepare("SELECT * FROM channels WHERE id = ? AND type = 'rules'");
$stmt->execute([$channel_id]);
$channel = $stmt->fetch();
if (!$channel) {
echo json_encode(['success' => false, 'error' => 'Canal de règles introuvable']);
exit;
}
$role_id = $channel['rules_role_id'];
try {
db()->beginTransaction();
// 1. Remove acceptance record
$stmtAcc = db()->prepare("DELETE FROM rule_acceptances WHERE user_id = ? AND channel_id = ?");
$stmtAcc->execute([$user_id, $channel_id]);
// 2. Remove role if it was configured
if ($role_id) {
$stmtRole = db()->prepare("DELETE FROM user_roles WHERE user_id = ? AND role_id = ?");
$stmtRole->execute([$user_id, $role_id]);
}
db()->commit();
echo json_encode(['success' => true]);
} catch (Exception $e) {
db()->rollBack();
echo json_encode(['success' => false, 'error' => 'Erreur lors du retrait : ' . $e->getMessage()]);
}
exit;
}
echo json_encode(['success' => false, 'error' => 'Méthode non autorisée']);

1433
assets/css/discord.css Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

2741
assets/js/main.js Normal file

File diff suppressed because it is too large Load Diff

670
assets/js/voice.js Normal file
View File

@ -0,0 +1,670 @@
console.log('voice.js loaded');
class VoiceChannel {
constructor(ws, settings) {
this.ws = ws;
this.settings = settings || { mode: 'vox', pttKey: 'v', voxThreshold: 0.1 };
console.log('VoiceChannel constructor called with settings:', this.settings);
this.localStream = null;
this.analysisStream = null;
this.peers = {}; // userId -> RTCPeerConnection
this.participants = {}; // userId -> {name}
this.currentChannelId = null;
this.myPeerId = null;
this.pollInterval = null;
this.remoteAudios = {}; // userId -> Audio element
this.pendingCandidates = {}; // userId -> array of candidates
this.audioContext = null;
this.analyser = null;
this.microphone = null;
this.scriptProcessor = null;
this.delayNode = null;
this.voxDestination = null;
this.isTalking = false;
this.pttPressed = false;
this.voxActive = false;
this.lastVoiceTime = 0;
this.voxHoldTime = 500;
// Track who is speaking to persist across UI refreshes
this.speakingUsers = new Set();
this.setupPTTListeners();
window.addEventListener('beforeunload', () => this.leave());
}
setupPTTListeners() {
window.addEventListener('keydown', (e) => {
// Ignore if in input field
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (this.settings.mode !== 'ptt') return;
const isMatch = e.key.toLowerCase() === this.settings.pttKey.toLowerCase() ||
(e.code && e.code.toLowerCase() === this.settings.pttKey.toLowerCase()) ||
(this.settings.pttKey === '0' && e.code === 'Numpad0');
if (isMatch) {
if (!this.pttPressed) {
console.log('PTT Key Pressed:', e.key, e.code, 'Expected:', this.settings.pttKey);
this.pttPressed = true;
this.updateMuteState();
}
}
});
window.addEventListener('keyup', (e) => {
if (this.settings.mode !== 'ptt') return;
const isMatch = e.key.toLowerCase() === this.settings.pttKey.toLowerCase() ||
(e.code && e.code.toLowerCase() === this.settings.pttKey.toLowerCase()) ||
(this.settings.pttKey === '0' && e.code === 'Numpad0');
if (isMatch) {
console.log('PTT Key Released:', e.key, e.code, 'Expected:', this.settings.pttKey);
this.pttPressed = false;
this.updateMuteState();
}
});
}
async join(channelId) {
console.log('VoiceChannel.join process started for channel:', channelId);
if (this.currentChannelId === channelId) {
console.log('Already in this channel');
return;
}
if (this.currentChannelId) {
console.log('Leaving previous channel:', this.currentChannelId);
this.leave();
}
this.currentChannelId = channelId;
try {
console.log('Requesting microphone access...');
this.localStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: false });
console.log('Microphone access granted');
// Always setup VOX logic for volume meter and detection
this.setupVOX();
// Initial mute (on the buffered stream if it exists)
this.setMute(true);
// Join via PHP
console.log('Calling API join...');
const url = `api_v1_voice.php?action=join&room=${channelId}&name=${encodeURIComponent(window.currentUsername || 'Unknown')}`;
const resp = await fetch(url);
const data = await resp.json();
console.log('API join response:', data);
if (data.success) {
this.myPeerId = data.peer_id;
console.log('Joined room with peer_id:', this.myPeerId);
// Start polling
this.startPolling();
this.updateVoiceUI();
} else {
console.error('API join failed:', data.error);
}
} catch (e) {
console.error('Failed to join voice:', e);
alert('Microphone access required for voice channels. Error: ' + e.message);
this.currentChannelId = null;
}
}
startPolling() {
if (this.pollInterval) clearInterval(this.pollInterval);
this.pollInterval = setInterval(() => this.poll(), 1000);
this.poll(); // Initial poll
}
async poll() {
if (!this.myPeerId || !this.currentChannelId) return;
try {
const resp = await fetch(`api_v1_voice.php?action=poll&room=${this.currentChannelId}&peer_id=${this.myPeerId}`);
const data = await resp.json();
if (data.success) {
// Update participants
const oldPs = Object.keys(this.participants);
this.participants = data.participants;
const newPs = Object.keys(this.participants);
// If new people joined, initiate offer if I'm the "older" one (prevent glare by comparing IDs)
newPs.forEach(pid => {
if (pid !== this.myPeerId && !this.peers[pid]) {
// Only initiate if my ID is greater than theirs (lexicographical comparison)
if (this.myPeerId > pid) {
console.log('Initiating offer to new peer:', pid);
this.createPeerConnection(pid, true);
} else {
console.log('Waiting for offer from peer:', pid);
// Just create the PC without offering, it will handle the incoming offer
this.createPeerConnection(pid, false);
}
}
});
// Cleanup left peers
oldPs.forEach(pid => {
if (!this.participants[pid] && this.peers[pid]) {
console.log('Peer left:', pid);
try {
this.peers[pid].close();
} catch(e) {}
delete this.peers[pid];
if (this.remoteAudios[pid]) {
try {
this.remoteAudios[pid].pause();
this.remoteAudios[pid].srcObject = null;
this.remoteAudios[pid].remove();
} catch(e) {}
delete this.remoteAudios[pid];
}
// Also cleanup by class just in case
document.querySelectorAll(`.voice-remote-audio[data-peer-id="${pid}"]`).forEach(el => el.remove());
}
});
// Handle incoming signals
if (data.signals && data.signals.length > 0) {
for (const sig of data.signals) {
await this.handleSignaling(sig);
}
}
this.updateVoiceUI();
}
} catch (e) {
console.error('Polling error:', e);
}
}
async sendSignal(to, data) {
if (!this.myPeerId || !this.currentChannelId) return;
await fetch(`api_v1_voice.php?action=signal&room=${this.currentChannelId}&peer_id=${this.myPeerId}&to=${to}&data=${encodeURIComponent(JSON.stringify(data))}`);
}
async handleSignaling(sig) {
const { from, data } = sig;
console.log('Handling signaling from:', from, data.type);
switch (data.type) {
case 'offer':
await this.handleOffer(from, data.offer);
break;
case 'answer':
await this.handleAnswer(from, data.answer);
break;
case 'ice_candidate':
await this.handleCandidate(from, data.candidate);
break;
case 'voice_speaking':
this.updateSpeakingUI(data.user_id, data.speaking);
break;
}
}
createPeerConnection(userId, isOfferor) {
if (this.peers[userId]) return this.peers[userId];
console.log('Creating PeerConnection for:', userId, 'as offeror:', isOfferor);
const pc = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun1.l.google.com:19302' }
]
});
this.peers[userId] = pc;
this.pendingCandidates[userId] = [];
pc.oniceconnectionstatechange = () => {
console.log(`ICE Connection State with ${userId}: ${pc.iceConnectionState}`);
};
if (this.localStream) {
const streamToShare = (this.voxDestination && this.voxDestination.stream) ? this.voxDestination.stream : this.localStream;
streamToShare.getTracks().forEach(track => {
console.log(`Adding track ${track.kind} to peer ${userId}`);
pc.addTrack(track, streamToShare);
});
}
pc.onicecandidate = (event) => {
if (event.candidate) {
this.sendSignal(userId, { type: 'ice_candidate', candidate: event.candidate });
}
};
pc.ontrack = (event) => {
console.log('Received remote track from:', userId, event);
const stream = event.streams[0] || new MediaStream([event.track]);
if (this.remoteAudios[userId]) {
console.log('Updating existing remote audio for:', userId);
this.remoteAudios[userId].srcObject = stream;
} else {
console.log('Creating new remote audio for:', userId);
const remoteAudio = new Audio();
remoteAudio.classList.add('voice-remote-audio');
remoteAudio.dataset.peerId = userId;
remoteAudio.autoplay = true;
remoteAudio.playsInline = true;
remoteAudio.srcObject = stream;
document.body.appendChild(remoteAudio);
this.remoteAudios[userId] = remoteAudio;
}
this.remoteAudios[userId].play().then(() => {
console.log('Audio playing for peer:', userId);
}).catch(e => {
console.warn('Autoplay prevented or failed for:', userId, e);
// Try again on any click
const retry = () => {
this.remoteAudios[userId].play();
window.removeEventListener('click', retry);
};
window.addEventListener('click', retry);
});
};
if (isOfferor) {
pc.createOffer().then(offer => {
return pc.setLocalDescription(offer);
}).then(() => {
this.sendSignal(userId, { type: 'offer', offer: pc.localDescription });
});
}
return pc;
}
async handleOffer(from, offer) {
console.log('Received offer from:', from);
const pc = this.createPeerConnection(from, false);
try {
await pc.setRemoteDescription(new RTCSessionDescription(offer));
console.log('Remote description set for offer from:', from);
const answer = await pc.createAnswer();
await pc.setLocalDescription(answer);
console.log('Local description (answer) set for:', from);
this.sendSignal(from, { type: 'answer', answer: pc.localDescription });
await this.processPendingCandidates(from);
} catch (e) {
console.error('Error handling offer from', from, e);
}
}
async handleAnswer(from, answer) {
console.log('Received answer from:', from);
const pc = this.peers[from];
if (pc) {
try {
await pc.setRemoteDescription(new RTCSessionDescription(answer));
console.log('Remote description (answer) set for:', from);
await this.processPendingCandidates(from);
} catch (e) {
console.error('Error handling answer from', from, e);
}
} else {
console.warn('Received answer but no peer connection for:', from);
}
}
async handleCandidate(from, candidate) {
console.log('Received ICE candidate from:', from);
const pc = this.peers[from];
if (pc && pc.remoteDescription) {
try {
await pc.addIceCandidate(new RTCIceCandidate(candidate));
console.log('Added ICE candidate for:', from);
} catch (e) {
console.error('Error adding ice candidate from', from, e);
}
} else if (pc) {
console.log('Queuing ICE candidate for:', from);
this.pendingCandidates[from].push(candidate);
} else {
console.warn('Received ICE candidate but no peer connection for:', from);
}
}
async processPendingCandidates(userId) {
const pc = this.peers[userId];
const candidates = this.pendingCandidates[userId];
if (pc && pc.remoteDescription && candidates && candidates.length > 0) {
console.log(`Processing ${candidates.length} pending candidates for ${userId}`);
while (candidates.length > 0) {
const cand = candidates.shift();
try {
await pc.addIceCandidate(new RTCIceCandidate(cand));
} catch (e) {
console.error('Error processing pending candidate', e);
}
}
}
}
setupVOX() {
if (!this.localStream) {
console.warn('Cannot setup VOX: no localStream');
return;
}
console.log('Setting up VOX logic...');
try {
if (!this.audioContext) {
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
// Re-ensure context is running
if (this.audioContext.state === 'suspended') {
this.audioContext.resume().then(() => console.log('AudioContext resumed'));
}
// Cleanup old nodes
if (this.scriptProcessor) {
this.scriptProcessor.onaudioprocess = null;
try { this.scriptProcessor.disconnect(); } catch(e) {}
}
if (this.microphone) {
try { this.microphone.disconnect(); } catch(e) {}
}
this.analyser = this.audioContext.createAnalyser();
this.analyser.fftSize = 512;
// Use a cloned stream for analysis so VOX works even when localStream is muted/disabled
if (this.analysisStream) {
this.analysisStream.getTracks().forEach(t => t.stop());
}
this.analysisStream = this.localStream.clone();
this.analysisStream.getAudioTracks().forEach(t => t.enabled = true); // Ensure analysis stream is NOT muted
this.microphone = this.audioContext.createMediaStreamSource(this.analysisStream);
this.scriptProcessor = this.audioContext.createScriptProcessor(2048, 1, 1);
// Setup Delay Buffer for VOX
this.delayNode = this.audioContext.createDelay(1.0);
this.delayNode.delayTime.value = 0.3; // 300ms buffer
this.voxDestination = this.audioContext.createMediaStreamDestination();
this.microphone.connect(this.delayNode);
this.delayNode.connect(this.voxDestination);
this.microphone.connect(this.analyser);
this.analyser.connect(this.scriptProcessor);
// Avoid feedback: connect to a gain node with 0 volume then to destination
const silence = this.audioContext.createGain();
silence.gain.value = 0;
this.scriptProcessor.connect(silence);
silence.connect(this.audioContext.destination);
this.voxActive = false;
this.currentVolume = 0;
this.scriptProcessor.onaudioprocess = () => {
const array = new Uint8Array(this.analyser.frequencyBinCount);
this.analyser.getByteFrequencyData(array);
let values = 0;
for (let i = 0; i < array.length; i++) values += array[i];
const average = values / array.length;
this.currentVolume = average / 255;
if (this.settings.mode !== 'vox') {
this.voxActive = false;
return;
}
if (this.currentVolume > this.settings.voxThreshold) {
this.lastVoiceTime = Date.now();
if (!this.voxActive) {
this.voxActive = true;
this.updateMuteState();
}
} else {
if (this.voxActive && Date.now() - this.lastVoiceTime > this.voxHoldTime) {
this.voxActive = false;
this.updateMuteState();
}
}
};
console.log('VOX logic setup complete');
} catch (e) {
console.error('Failed to setup VOX:', e);
}
}
getVolume() {
return this.currentVolume || 0;
}
updateMuteState() {
if (!this.currentChannelId || !this.localStream) return;
let shouldTalk = (this.settings.mode === 'ptt') ? this.pttPressed : this.voxActive;
console.log('updateMuteState: shouldTalk =', shouldTalk, 'mode =', this.settings.mode);
if (this.isTalking !== shouldTalk) {
this.isTalking = shouldTalk;
this.setMute(!shouldTalk);
this.updateSpeakingUI(window.currentUserId, shouldTalk);
// Notify others
const msg = { type: 'voice_speaking', channel_id: this.currentChannelId, user_id: window.currentUserId, speaking: shouldTalk };
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(msg));
} else {
Object.keys(this.peers).forEach(pid => {
this.sendSignal(pid, msg);
});
}
}
}
setMute(mute) {
// We mute the destination stream (delayed) instead of the localStream source
// to ensure the delay buffer keeps filling with live audio.
const streamToMute = (this.voxDestination && this.voxDestination.stream) ? this.voxDestination.stream : this.localStream;
if (streamToMute) {
console.log('Setting mute to:', mute, 'on stream:', streamToMute.id);
streamToMute.getAudioTracks().forEach(track => { track.enabled = !mute; });
}
}
leave() {
if (!this.currentChannelId) return;
const roomToLeave = this.currentChannelId;
const myIdToLeave = this.myPeerId;
console.log('Leaving voice channel:', roomToLeave);
// 1. Clear interval and set state to null immediately to stop any ongoing poll/logic
if (this.pollInterval) clearInterval(this.pollInterval);
this.pollInterval = null;
this.currentChannelId = null;
this.myPeerId = null;
// 2. Notify server
const leaveUrl = `api_v1_voice.php?action=leave&room=${roomToLeave}&peer_id=${myIdToLeave}`;
if (navigator.sendBeacon) {
navigator.sendBeacon(leaveUrl);
} else {
fetch(leaveUrl);
}
// 3. Stop all local audio tracks
if (this.localStream) {
console.log('Stopping localStream tracks');
this.localStream.getTracks().forEach(track => {
track.enabled = false;
track.stop();
});
this.localStream = null;
}
if (this.analysisStream) {
this.analysisStream.getTracks().forEach(track => track.stop());
this.analysisStream = null;
}
// 4. Cleanup VOX/AudioContext nodes
if (this.scriptProcessor) {
try {
this.scriptProcessor.onaudioprocess = null;
this.scriptProcessor.disconnect();
} catch(e) {}
this.scriptProcessor = null;
}
if (this.microphone) {
try { this.microphone.disconnect(); } catch(e) {}
this.microphone = null;
}
if (this.delayNode) {
try { this.delayNode.disconnect(); } catch(e) {}
this.delayNode = null;
}
this.voxDestination = null;
if (this.audioContext && this.audioContext.state !== 'closed') {
try { this.audioContext.suspend(); } catch(e) {}
}
// 5. Close all peer connections
console.log('Closing all peer connections:', Object.keys(this.peers));
Object.keys(this.peers).forEach(pid => {
try {
this.peers[pid].onicecandidate = null;
this.peers[pid].ontrack = null;
this.peers[pid].oniceconnectionstatechange = null;
this.peers[pid].close();
} catch(e) { console.error('Error closing peer:', pid, e); }
delete this.peers[pid];
});
this.peers = {};
// 6. Remove all remote audio elements
document.querySelectorAll('.voice-remote-audio').forEach(audio => {
try {
audio.pause();
audio.srcObject = null;
audio.remove();
} catch(e) {}
});
this.remoteAudios = {};
this.pendingCandidates = {};
this.participants = {};
this.speakingUsers.clear();
this.isTalking = false;
this.voxActive = false;
// Final notify speaking false
if (roomToLeave) {
const msg = { type: 'voice_speaking', channel_id: roomToLeave, user_id: window.currentUserId, speaking: false };
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
try { this.ws.send(JSON.stringify(msg)); } catch(e) {}
}
}
// 7. Update UI
this.updateVoiceUI();
}
updateVoiceUI() {
// We now use a global update mechanism for all channels
VoiceChannel.refreshAllVoiceUsers();
if (this.currentChannelId) {
if (!document.querySelector('.voice-controls')) {
const controls = document.createElement('div');
controls.className = 'voice-controls p-2 d-flex justify-content-between align-items-center border-top bg-dark';
controls.style.backgroundColor = '#232428';
controls.innerHTML = `
<div class="d-flex align-items-center">
<div class="voice-status-icon text-success me-2" style="font-size: 8px;"></div>
<div class="small fw-bold" style="font-size: 11px; color: #248046;">Voice (${this.settings.mode.toUpperCase()})</div>
</div>
<div>
<button class="btn btn-sm text-muted" id="btn-voice-leave" title="Disconnect">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.68 13.31a16 16 0 0 0 3.41 2.6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7 2 2 0 0 1 1.72 2v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.42 19.42 0 0 1-3.33-2.67m-2.67-3.34a19.79 19.79 0 0 1-3.07-8.63A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91"></path><line x1="23" y1="1" x2="1" y2="23"></line></svg>
</button>
</div>
`;
const sidebar = document.querySelector('.channels-sidebar');
if (sidebar) sidebar.appendChild(controls);
const btnLeave = document.getElementById('btn-voice-leave');
if (btnLeave) btnLeave.onclick = () => this.leave();
}
} else {
const controls = document.querySelector('.voice-controls');
if (controls) controls.remove();
}
}
updateSpeakingUI(userId, isSpeaking) {
if (isSpeaking) {
this.speakingUsers.add(userId);
} else {
this.speakingUsers.delete(userId);
}
const userEls = document.querySelectorAll(`.voice-user[data-user-id="${userId}"]`);
userEls.forEach(el => {
const avatar = el.querySelector('.message-avatar');
if (avatar) {
avatar.style.boxShadow = isSpeaking ? '0 0 0 2px #23a559' : 'none';
}
});
}
static async refreshAllVoiceUsers() {
try {
const resp = await fetch('api_v1_voice.php?action=list_all');
const data = await resp.json();
if (data.success) {
// Clear all lists first
document.querySelectorAll('.voice-users-list').forEach(el => el.innerHTML = '');
// Populate based on data
Object.keys(data.channels).forEach(channelId => {
// Fix: The voice-users-list is a sibling of the container of the voice-item
const voiceItem = document.querySelector(`.voice-item[data-channel-id="${channelId}"]`);
if (voiceItem) {
const container = voiceItem.closest('.channel-item-container');
if (container) {
const listEl = container.querySelector('.voice-users-list');
if (listEl) {
data.channels[channelId].forEach(p => {
const isSpeaking = window.voiceHandler && window.voiceHandler.speakingUsers.has(p.user_id);
VoiceChannel.renderUserToUI(listEl, p.user_id, p.display_name || p.username, p.avatar_url, isSpeaking);
});
}
}
}
});
}
} catch (e) {
console.error('Failed to refresh voice users:', e);
}
}
static renderUserToUI(container, userId, username, avatarUrl, isSpeaking = false) {
const userEl = document.createElement('div');
userEl.className = 'voice-user small text-muted d-flex align-items-center mb-1';
userEl.dataset.userId = userId;
userEl.style.paddingLeft = '8px';
const avatarStyle = avatarUrl ? `background-image: url('${avatarUrl}'); background-size: cover;` : "background-color: #555;";
const boxShadow = isSpeaking ? 'box-shadow: 0 0 0 2px #23a559;' : '';
userEl.innerHTML = `
<div class="message-avatar me-2" style="width: 16px; height: 16px; border-radius: 50%; transition: box-shadow 0.2s; ${avatarStyle} ${boxShadow}"></div>
<span style="font-size: 13px;">${username}</span>
`;
container.appendChild(userEl);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

69
auth/login.php Normal file
View File

@ -0,0 +1,69 @@
<?php
require_once __DIR__ . '/session.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
if ($email && $password) {
$stmt = db()->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
$user = $stmt->fetch();
if ($user && password_verify($password, $user['password_hash'])) {
$_SESSION['user_id'] = $user['id'];
header('Location: ../index.php');
exit;
} else {
$error = "Invalid email or password.";
}
} else {
$error = "Please fill all fields.";
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login | Discord Clone</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/discord.css">
<style>
body { background-color: #313338; display: flex; align-items: center; justify-content: center; height: 100vh; }
.auth-card { background-color: #2b2d31; padding: 32px; border-radius: 8px; width: 100%; max-width: 480px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); }
.form-label { color: #b5bac1; font-size: 12px; font-weight: bold; text-transform: uppercase; }
.form-control { background-color: #1e1f22; border: none; color: #dbdee1; padding: 10px; }
.form-control:focus { background-color: #1e1f22; color: #dbdee1; box-shadow: none; }
.btn-blurple { background-color: #5865f2; color: white; width: 100%; font-weight: bold; margin-top: 20px; }
.btn-blurple:hover { background-color: #4752c4; color: white; }
.auth-footer { color: #949ba4; font-size: 14px; margin-top: 10px; }
.auth-footer a { color: #00a8fc; text-decoration: none; }
</style>
</head>
<body>
<div class="auth-card">
<h3 class="text-center mb-1">Welcome back!</h3>
<p class="text-center mb-4" style="color: #b5bac1;">We're so excited to see you again!</p>
<?php if($error): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<form method="POST">
<div class="mb-3">
<label class="form-label">Email or Phone Number</label>
<input type="email" name="email" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" required>
</div>
<a href="#" style="color: #00a8fc; font-size: 14px; text-decoration: none;">Forgot your password?</a>
<button type="submit" class="btn btn-blurple">Log In</button>
<div class="auth-footer">
Need an account? <a href="register.php">Register</a>
</div>
</form>
</div>
</body>
</html>

5
auth/logout.php Normal file
View File

@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header('Location: login.php');
exit;

120
auth/register.php Normal file
View File

@ -0,0 +1,120 @@
<?php
header("Cache-Control: no-cache, no-store, must-revalidate");
header("Pragma: no-cache");
header("Expires: 0");
require_once __DIR__ . '/session.php';
$error = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = $_POST['username'] ?? '';
$email = $_POST['email'] ?? '';
$password = $_POST['password'] ?? '';
$inviteCode = $_POST['invite_code'] ?? '';
if ($username && $email && $password) {
// Strict invite code validation (Private by default)
$requireInvite = true;
if (defined('REQUIRE_INVITE_CODE')) {
$requireInvite = REQUIRE_INVITE_CODE;
}
if ($requireInvite) {
if (empty($inviteCode)) {
$error = "An invitation code is required to register.";
} else {
$stmt = db()->prepare("SELECT id FROM servers WHERE invite_code = ?");
$stmt->execute([$inviteCode]);
$server = $stmt->fetch();
if (!$server) {
$error = "Invalid invitation code.";
}
}
}
if (!$error) {
$hash = password_hash($password, PASSWORD_DEFAULT);
try {
$stmt = db()->prepare("INSERT INTO users (username, display_name, email, password_hash) VALUES (?, ?, ?, ?)");
$stmt->execute([$username, $username, $email, $hash]);
$userId = db()->lastInsertId();
// Add to default server or the one from invite code
$serverId = 1; // Default
if (isset($server) && $server) {
$serverId = $server['id'];
}
$stmt = db()->prepare("INSERT IGNORE INTO server_members (server_id, user_id) VALUES (?, ?)");
$stmt->execute([$serverId, $userId]);
$_SESSION['user_id'] = $userId;
header('Location: ../index.php');
exit;
} catch (Exception $e) {
$error = "Registration failed: " . $e->getMessage();
}
}
} else {
$error = "Please fill all fields.";
}
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register | Discord Clone</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="../assets/css/discord.css">
<style>
body { background-color: #313338; display: flex; align-items: center; justify-content: center; height: 100vh; }
.auth-card { background-color: #2b2d31; padding: 32px; border-radius: 8px; width: 100%; max-width: 480px; box-shadow: 0 2px 10px rgba(0,0,0,0.2); border: 3px solid #f04747; }
.form-label { color: #b5bac1; font-size: 12px; font-weight: bold; text-transform: uppercase; }
.form-control { background-color: #1e1f22; border: none; color: #dbdee1; padding: 10px; }
.form-control:focus { background-color: #1e1f22; color: #dbdee1; box-shadow: none; }
.btn-blurple { background-color: #5865f2; color: white; width: 100%; font-weight: bold; margin-top: 20px; }
.btn-blurple:hover { background-color: #4752c4; color: white; }
.auth-footer { color: #949ba4; font-size: 14px; margin-top: 10px; }
.auth-footer a { color: #00a8fc; text-decoration: none; }
@keyframes blink { 0% { border-color: #f04747; } 50% { border-color: transparent; } 100% { border-color: #f04747; } }
.invite-box { border: 4px solid #f04747; padding: 15px; border-radius: 8px; margin-bottom: 25px; background: rgba(240, 71, 71, 0.1); animation: blink 1s infinite; }
</style>
</head>
<body>
<div class="auth-card">
<h3 class="text-center mb-4">Create an account</h3>
<?php if($error): ?>
<div class="alert alert-danger"><?php echo htmlspecialchars($error); ?></div>
<?php endif; ?>
<form method="POST">
<!-- INVITE CODE FIELD - CRITICAL -->
<div class="invite-box">
<label class="form-label" style="color: #f04747 !important; font-size: 16px;">REQUIRED: INVITE CODE</label>
<div class="small mb-1" style="color: #fff !important; font-weight: bold;">Enter the code from Server Settings.</div>
<input type="text" name="invite_code" class="form-control" placeholder="INVITE CODE HERE" required style="border: 2px solid #f04747; font-size: 20px; text-align: center; font-weight: bold;">
</div>
<div class="mb-3">
<label class="form-label">Username</label>
<input type="text" name="username" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" name="email" class="form-control" required>
</div>
<div class="mb-3">
<label class="form-label">Password</label>
<input type="password" name="password" class="form-control" required>
</div>
<button type="submit" class="btn btn-blurple">Continue</button>
<div class="text-center mt-2" style="color: #4e5058; font-size: 10px;">Version: <?php echo time(); ?></div>
<div class="auth-footer">
Already have an account? <a href="login.php">Login</a>
</div>
</form>
</div>
</body>
</html>

20
auth/session.php Normal file
View File

@ -0,0 +1,20 @@
<?php
session_start();
require_once __DIR__ . '/../db/config.php';
function getCurrentUser() {
if (!isset($_SESSION['user_id'])) {
return null;
}
$stmt = db()->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$_SESSION['user_id']]);
return $stmt->fetch();
}
function requireLogin() {
if (!isset($_SESSION['user_id'])) {
header('Location: auth/login.php');
exit;
}
}

1
auth/test_check.php Normal file
View File

@ -0,0 +1 @@
<?php echo "FILE SYSTEM SYNC TEST - SUCCESS - TIME: " . time(); ?>

0
data/22.log Normal file
View File

View File

@ -0,0 +1 @@
{"de65a0b0b1a29c9a":{"id":"de65a0b0b1a29c9a","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771343410040}}

0
data/3.log Normal file
View File

1
data/3.participants.json Normal file
View File

@ -0,0 +1 @@
{"920464469dc771ed":{"id":"920464469dc771ed","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771343410598}}

10
data/6.log Normal file
View File

@ -0,0 +1,10 @@
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"offer","offer":{"type":"offer","sdp":"v=0\r\no=mozilla...THIS_IS_SDPARTA-99.0 1705174900585877835 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=sendrecv\r\na=fingerprint:sha-256 AA:37:B9:FA:14:15:DC:34:29:97:DC:55:77:28:8E:74:C0:94:15:08:DF:5B:E9:CC:36:81:E5:D9:5C:49:FB:46\r\na=group:BUNDLE 0\r\na=ice-options:trickle\r\na=msid-semantic:WMS *\r\nm=audio 9 UDP\/TLS\/RTP\/SAVPF 109 9 0 8 101\r\nc=IN IP4 0.0.0.0\r\na=sendrecv\r\na=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\na=extmap:2\/recvonly urn:ietf:params:rtp-hdrext:csrc-audio-level\r\na=extmap:3 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap-allow-mixed\r\na=fmtp:109 maxplaybackrate=48000;stereo=1;useinbandfec=1\r\na=fmtp:101 0-15\r\na=ice-pwd:075627a25c9f07d3908305504803057d\r\na=ice-ufrag:03d0add1\r\na=mid:0\r\na=msid:{7a2c5afc-d053-43fd-9196-3caf4510feb3} {8439b857-ead8-4cb1-8111-cb3400348461}\r\na=rtcp-mux\r\na=rtpmap:109 opus\/48000\/2\r\na=rtpmap:9 G722\/8000\/1\r\na=rtpmap:0 PCMU\/8000\r\na=rtpmap:8 PCMA\/8000\r\na=rtpmap:101 telephone-event\/8000\r\na=setup:actpass\r\na=ssrc:1663488241 cname:{7dcb8038-2b50-4e42-827c-c4617a225c01}\r\n"}},"time":1771336456644}
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 1 UDP 2122252543 192.168.26.26 62572 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456645}
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 1 TCP 2105524479 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456647}
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:2 2 TCP 2105524478 192.168.26.26 9 typ host tcptype active","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456652}
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:0 2 UDP 2122252542 192.168.26.26 62573 typ host","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456654}
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052351 78.246.210.10 31184 typ srflx raddr 192.168.26.26 rport 62572","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456720}
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052350 78.246.210.10 31185 typ srflx raddr 192.168.26.26 rport 62573","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456757}
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 1 UDP 1686052863 78.246.210.10 31184 typ srflx raddr 192.168.26.26 rport 62572","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456776}
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"candidate:1 2 UDP 1686052862 78.246.210.10 31185 typ srflx raddr 192.168.26.26 rport 62573","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456802}
{"from":"75b3938d276e81e1","to":"0cce3619c0f299fa","data":{"type":"ice_candidate","candidate":{"candidate":"","sdpMLineIndex":0,"sdpMid":"0","usernameFragment":"03d0add1"}},"time":1771336456805}

1
data/6.participants.json Normal file
View File

@ -0,0 +1 @@
{"0cce3619c0f299fa":{"id":"0cce3619c0f299fa","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771336452806},"75b3938d276e81e1":{"id":"75b3938d276e81e1","user_id":2,"name":"swefpifh ᵇʰᶠʳ","avatar_url":"","last_seen":1771336462293}}

View File

@ -0,0 +1 @@
{"0fbf720bc2f110c0":{"id":"0fbf720bc2f110c0","name":"AI","last_seen":1771336229774}}

1
data/test.txt Normal file
View File

@ -0,0 +1 @@
hello

1
data/test_www.txt Normal file
View File

@ -0,0 +1 @@
hello

94
database/schema.sql Normal file
View File

@ -0,0 +1,94 @@
-- Initial schema for Discord-like app
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password_hash VARCHAR(255) NOT NULL,
avatar_url VARCHAR(255),
status VARCHAR(20) DEFAULT 'offline',
is_bot BOOLEAN DEFAULT FALSE,
bot_token VARCHAR(64) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS servers (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
owner_id INT NOT NULL,
icon_url VARCHAR(255),
invite_code VARCHAR(10) UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (owner_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS channels (
id INT AUTO_INCREMENT PRIMARY KEY,
server_id INT NOT NULL,
name VARCHAR(100) NOT NULL,
type ENUM('text', 'voice') DEFAULT 'text',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS messages (
id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS webhooks (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
token VARCHAR(64) NOT NULL UNIQUE,
channel_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS roles (
id INT AUTO_INCREMENT PRIMARY KEY,
server_id INT NOT NULL,
name VARCHAR(50) NOT NULL,
color VARCHAR(7) DEFAULT '#99aab5',
permissions INT DEFAULT 0,
position INT DEFAULT 0,
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS user_roles (
user_id INT NOT NULL,
role_id INT NOT NULL,
PRIMARY KEY (user_id, role_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS server_members (
server_id INT NOT NULL,
user_id INT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (server_id, user_id),
FOREIGN KEY (server_id) REFERENCES servers(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Seed initial data
INSERT IGNORE INTO users (id, username, email, password_hash, status) VALUES
(1, 'System', 'system@local', '$2y$10$xyz', 'online');
INSERT IGNORE INTO servers (id, name, owner_id, invite_code) VALUES
(1, 'General Community', 1, 'GEN-123'),
(2, 'Flatlogic Devs', 1, 'DEV-456');
INSERT IGNORE INTO server_members (server_id, user_id) VALUES (1, 1), (2, 1);
INSERT IGNORE INTO channels (id, server_id, name, type) VALUES
(1, 1, 'general', 'text'),
(2, 1, 'random', 'text'),
(3, 1, 'Voice General', 'voice'),
(4, 2, 'announcements', 'text'),
(5, 2, 'coding-help', 'text');

View File

@ -1,9 +1,12 @@
<?php
// Generated by setup_mariadb_project.sh — edit as needed.
define('DB_HOST', '127.0.0.1');
define('DB_NAME', 'app_38443');
define('DB_USER', 'app_38443');
define('DB_PASS', '888f6481-a87b-421a-a4bd-c80fa3c5a57b');
define('DB_NAME', 'app_38527');
define('DB_USER', 'app_38527');
define('DB_PASS', 'ded9b4d6-94d1-4a22-b636-1d581ddcb411');
// Registration settings
define('REQUIRE_INVITE_CODE', true); // Set to true to make registration private, false for public
function db() {
static $pdo;
@ -15,3 +18,26 @@ function db() {
}
return $pdo;
}
function enforceChannelLimit($channel_id) {
$stmt = db()->prepare("SELECT message_limit FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$channel = $stmt->fetch();
if ($channel && !empty($channel['message_limit'])) {
$limit = (int)$channel['message_limit'];
// Delete oldest messages that exceed the limit
$stmt = db()->prepare("
DELETE FROM messages
WHERE channel_id = ?
AND id NOT IN (
SELECT id FROM (
SELECT id FROM messages
WHERE channel_id = ?
ORDER BY created_at DESC, id DESC
LIMIT " . $limit . "
) as tmp
)
");
$stmt->execute([$channel_id, $channel_id]);
}
}

View File

@ -0,0 +1,7 @@
CREATE TABLE IF NOT EXISTS custom_emotes (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
path VARCHAR(255) NOT NULL,
code VARCHAR(60) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

View File

@ -0,0 +1,13 @@
-- Migration to add attachments and reactions
ALTER TABLE messages ADD COLUMN attachment_url VARCHAR(255) AFTER content;
CREATE TABLE IF NOT EXISTS message_reactions (
id INT AUTO_INCREMENT PRIMARY KEY,
message_id INT NOT NULL,
user_id INT NOT NULL,
emoji VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY (message_id, user_id, emoji),
FOREIGN KEY (message_id) REFERENCES messages(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,2 @@
-- Migration: Add status to channels for voice channels
ALTER TABLE channels ADD COLUMN status VARCHAR(255) DEFAULT NULL;

View File

@ -0,0 +1,26 @@
-- Migration to support DMs and Message Editing
ALTER TABLE messages ADD COLUMN updated_at TIMESTAMP NULL ON UPDATE CURRENT_TIMESTAMP;
-- Support for DMs in channels table
ALTER TABLE channels MODIFY COLUMN server_id INT NULL;
ALTER TABLE channels MODIFY COLUMN type ENUM('text', 'voice', 'dm') DEFAULT 'text';
-- Track members in channels (especially for DMs)
CREATE TABLE IF NOT EXISTS channel_members (
channel_id INT NOT NULL,
user_id INT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (channel_id, user_id),
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
-- Notifications: Track last read message per channel per user
CREATE TABLE IF NOT EXISTS channel_last_read (
channel_id INT NOT NULL,
user_id INT NOT NULL,
last_read_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (channel_id, user_id),
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,3 @@
-- Add solution support to forum threads
ALTER TABLE forum_threads ADD COLUMN solution_message_id INT NULL DEFAULT NULL;
ALTER TABLE forum_threads ADD FOREIGN KEY (solution_message_id) REFERENCES messages(id) ON DELETE SET NULL;

View File

@ -0,0 +1,16 @@
-- Add forum tags support
CREATE TABLE IF NOT EXISTS forum_tags (
id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
name VARCHAR(50) NOT NULL,
color VARCHAR(20) DEFAULT '#7289da',
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS thread_tags (
thread_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (thread_id, tag_id),
FOREIGN KEY (thread_id) REFERENCES forum_threads(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES forum_tags(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,12 @@
-- Migration: Add channel permissions and user theme preference
CREATE TABLE IF NOT EXISTS channel_permissions (
channel_id INT NOT NULL,
role_id INT NOT NULL,
allow_permissions INT DEFAULT 0,
deny_permissions INT DEFAULT 0,
PRIMARY KEY (channel_id, role_id),
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
ALTER TABLE users ADD COLUMN IF NOT EXISTS theme VARCHAR(20) DEFAULT 'dark';

View File

@ -0,0 +1,12 @@
-- Migration: Add RSS support for announcement channels
CREATE TABLE IF NOT EXISTS channel_rss_feeds (
id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
url VARCHAR(255) NOT NULL,
last_fetched_at TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);
ALTER TABLE messages ADD COLUMN IF NOT EXISTS rss_guid VARCHAR(255) NULL;
CREATE INDEX IF NOT EXISTS idx_rss_guid ON messages(rss_guid);

View File

@ -0,0 +1,11 @@
-- Add autorole channels support
CREATE TABLE IF NOT EXISTS channel_autoroles (
id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
icon VARCHAR(50) NOT NULL,
title VARCHAR(255) NOT NULL,
role_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,12 @@
-- Add rules_role_id to channels and create rule_acceptances table
ALTER TABLE channels ADD COLUMN rules_role_id INT NULL;
CREATE TABLE IF NOT EXISTS rule_acceptances (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
channel_id INT NOT NULL,
accepted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY user_channel (user_id, channel_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,9 @@
-- Migration: Track processed RSS items to prevent re-posting after they are deleted by retention policy
CREATE TABLE IF NOT EXISTS rss_processed_items (
id INT AUTO_INCREMENT PRIMARY KEY,
channel_id INT NOT NULL,
rss_guid VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY idx_channel_guid (channel_id, rss_guid),
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,10 @@
-- Unread messages tracking
CREATE TABLE IF NOT EXISTS channel_reads (
user_id INT NOT NULL,
channel_id INT NOT NULL,
last_read_message_id INT NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, channel_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,12 @@
-- Add voice settings to users and create voice sessions table
ALTER TABLE users ADD COLUMN voice_mode ENUM('vox', 'ptt') DEFAULT 'vox';
ALTER TABLE users ADD COLUMN voice_ptt_key VARCHAR(20) DEFAULT 'v';
ALTER TABLE users ADD COLUMN voice_vox_threshold FLOAT DEFAULT 0.1;
CREATE TABLE IF NOT EXISTS voice_sessions (
user_id INT PRIMARY KEY,
channel_id INT NOT NULL,
joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE
);

View File

@ -0,0 +1,2 @@
ALTER TABLE users ADD COLUMN display_name VARCHAR(50) AFTER username;
UPDATE users SET display_name = username WHERE display_name IS NULL OR display_name = "";

10
db_init.php Normal file
View File

@ -0,0 +1,10 @@
<?php
require_once 'db/config.php';
try {
$sql = file_get_contents('database/schema.sql');
db()->exec($sql);
echo "Database initialized successfully.\n";
} catch (Exception $e) {
echo "Error initializing database: " . $e->getMessage() . "\n";
}

19
debug_reorder.log Normal file
View File

@ -0,0 +1,19 @@
2026-02-15 23:32:05 - Server: 1 - Orders: [{"id":"1","position":0,"category_id":null},{"id":"6","position":1,"category_id":null},{"id":"10","position":2,"category_id":null},{"id":"2","position":3,"category_id":null},{"id":"9","position":4,"category_id":null},{"id":"3","position":5,"category_id":null}]
2026-02-15 23:35:48 - Server: 1 - Orders: [{"id":"10","position":0,"category_id":null},{"id":"1","position":1,"category_id":null},{"id":"6","position":2,"category_id":null},{"id":"2","position":3,"category_id":null},{"id":"9","position":4,"category_id":null},{"id":"3","position":5,"category_id":null}]
2026-02-15 23:36:25 - Server: 1 - Orders: [{"id":"10","position":0,"category_id":null},{"id":"1","position":1,"category_id":"10"},{"id":"6","position":2,"category_id":"10"},{"id":"2","position":3,"category_id":null},{"id":"9","position":4,"category_id":null},{"id":"3","position":5,"category_id":null}]
2026-02-15 23:36:28 - Server: 1 - Orders: [{"id":"10","position":0,"category_id":null},{"id":"1","position":1,"category_id":"10"},{"id":"6","position":2,"category_id":"10"},{"id":"2","position":3,"category_id":"10"},{"id":"9","position":4,"category_id":null},{"id":"3","position":5,"category_id":null}]
2026-02-15 23:39:25 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"10","position":1,"category_id":null},{"id":"1","position":2,"category_id":"10"},{"id":"6","position":3,"category_id":"10"},{"id":"2","position":4,"category_id":"10"},{"id":"9","position":5,"category_id":null},{"id":"3","position":6,"category_id":null}]
2026-02-15 23:39:45 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"12","position":1,"category_id":null},{"id":"10","position":2,"category_id":null},{"id":"1","position":3,"category_id":"10"},{"id":"6","position":4,"category_id":"10"},{"id":"2","position":5,"category_id":"10"},{"id":"9","position":6,"category_id":null},{"id":"3","position":7,"category_id":null}]
2026-02-15 23:40:11 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"12","position":1,"category_id":null},{"id":"10","position":2,"category_id":null},{"id":"1","position":3,"category_id":"10"},{"id":"6","position":4,"category_id":"10"},{"id":"2","position":5,"category_id":"10"},{"id":"13","position":6,"category_id":null},{"id":"9","position":7,"category_id":null},{"id":"3","position":8,"category_id":null}]
2026-02-15 23:40:20 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"12","position":1,"category_id":null},{"id":"10","position":2,"category_id":null},{"id":"1","position":3,"category_id":"10"},{"id":"6","position":4,"category_id":"10"},{"id":"2","position":5,"category_id":"10"},{"id":"14","position":6,"category_id":null},{"id":"13","position":7,"category_id":null},{"id":"9","position":8,"category_id":null},{"id":"3","position":9,"category_id":null}]
2026-02-16 00:15:14 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"12","position":1,"category_id":null},{"id":"10","position":2,"category_id":null},{"id":"1","position":3,"category_id":"10"},{"id":"6","position":4,"category_id":"10"},{"id":"15","position":5,"category_id":"10"},{"id":"2","position":6,"category_id":"10"},{"id":"14","position":7,"category_id":null},{"id":"13","position":8,"category_id":null},{"id":"9","position":9,"category_id":null},{"id":"3","position":10,"category_id":null}]
2026-02-16 00:17:23 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"12","position":1,"category_id":null},{"id":"10","position":2,"category_id":null},{"id":"1","position":3,"category_id":"10"},{"id":"6","position":4,"category_id":"10"},{"id":"15","position":5,"category_id":"10"},{"id":"2","position":6,"category_id":"10"},{"id":"14","position":7,"category_id":null},{"id":"13","position":8,"category_id":null},{"id":"9","position":9,"category_id":null},{"id":"3","position":10,"category_id":null}]
2026-02-16 00:17:31 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"12","position":1,"category_id":null},{"id":"10","position":2,"category_id":null},{"id":"1","position":3,"category_id":"10"},{"id":"6","position":4,"category_id":"10"},{"id":"15","position":5,"category_id":"10"},{"id":"2","position":6,"category_id":"10"},{"id":"14","position":7,"category_id":null},{"id":"13","position":8,"category_id":null},{"id":"9","position":9,"category_id":null},{"id":"3","position":10,"category_id":null}]
2026-02-16 03:07:52 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"16","position":1,"category_id":null},{"id":"12","position":2,"category_id":null},{"id":"10","position":3,"category_id":null},{"id":"1","position":4,"category_id":"10"},{"id":"6","position":5,"category_id":"10"},{"id":"15","position":6,"category_id":"10"},{"id":"2","position":7,"category_id":"10"},{"id":"14","position":8,"category_id":null},{"id":"13","position":9,"category_id":null},{"id":"9","position":10,"category_id":null},{"id":"3","position":11,"category_id":null}]
2026-02-16 03:08:33 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"17","position":1,"category_id":null},{"id":"12","position":2,"category_id":null},{"id":"10","position":3,"category_id":null},{"id":"1","position":4,"category_id":"10"},{"id":"6","position":5,"category_id":"10"},{"id":"15","position":6,"category_id":"10"},{"id":"2","position":7,"category_id":"10"},{"id":"14","position":8,"category_id":null},{"id":"13","position":9,"category_id":null},{"id":"9","position":10,"category_id":null},{"id":"3","position":11,"category_id":null}]
2026-02-16 03:09:18 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"17","position":1,"category_id":null},{"id":"12","position":2,"category_id":null},{"id":"10","position":3,"category_id":null},{"id":"1","position":4,"category_id":"10"},{"id":"6","position":5,"category_id":"10"},{"id":"15","position":6,"category_id":"10"},{"id":"2","position":7,"category_id":"10"},{"id":"18","position":8,"category_id":"10"},{"id":"14","position":9,"category_id":null},{"id":"13","position":10,"category_id":null},{"id":"9","position":11,"category_id":null},{"id":"3","position":12,"category_id":null}]
2026-02-16 18:43:44 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"17","position":1,"category_id":null},{"id":"12","position":2,"category_id":null},{"id":"19","position":3,"category_id":null},{"id":"10","position":4,"category_id":null},{"id":"1","position":5,"category_id":"10"},{"id":"6","position":6,"category_id":"10"},{"id":"15","position":7,"category_id":"10"},{"id":"2","position":8,"category_id":"10"},{"id":"18","position":9,"category_id":"10"},{"id":"14","position":10,"category_id":null},{"id":"13","position":11,"category_id":null},{"id":"9","position":12,"category_id":null},{"id":"3","position":13,"category_id":null}]
2026-02-16 23:37:00 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"12","position":1,"category_id":null},{"id":"17","position":2,"category_id":null},{"id":"19","position":3,"category_id":null},{"id":"10","position":4,"category_id":null},{"id":"1","position":5,"category_id":"10"},{"id":"6","position":6,"category_id":"10"},{"id":"15","position":7,"category_id":"10"},{"id":"2","position":8,"category_id":"10"},{"id":"18","position":9,"category_id":"10"},{"id":"14","position":10,"category_id":null},{"id":"13","position":11,"category_id":null},{"id":"9","position":12,"category_id":null},{"id":"3","position":13,"category_id":null}]
2026-02-16 23:37:02 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"17","position":1,"category_id":null},{"id":"12","position":2,"category_id":null},{"id":"19","position":3,"category_id":null},{"id":"10","position":4,"category_id":null},{"id":"1","position":5,"category_id":"10"},{"id":"6","position":6,"category_id":"10"},{"id":"15","position":7,"category_id":"10"},{"id":"2","position":8,"category_id":"10"},{"id":"18","position":9,"category_id":"10"},{"id":"14","position":10,"category_id":null},{"id":"13","position":11,"category_id":null},{"id":"9","position":12,"category_id":null},{"id":"3","position":13,"category_id":null}]
2026-02-17 00:31:00 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"17","position":1,"category_id":null},{"id":"12","position":2,"category_id":null},{"id":"20","position":3,"category_id":null},{"id":"19","position":4,"category_id":null},{"id":"10","position":5,"category_id":null},{"id":"1","position":6,"category_id":"10"},{"id":"6","position":7,"category_id":"10"},{"id":"15","position":8,"category_id":"10"},{"id":"2","position":9,"category_id":"10"},{"id":"18","position":10,"category_id":"10"},{"id":"14","position":11,"category_id":null},{"id":"13","position":12,"category_id":null},{"id":"9","position":13,"category_id":null},{"id":"3","position":14,"category_id":null}]
2026-02-17 08:19:05 - Server: 1 - Orders: [{"id":"11","position":0,"category_id":null},{"id":"17","position":1,"category_id":null},{"id":"20","position":2,"category_id":null},{"id":"21","position":3,"category_id":null},{"id":"19","position":4,"category_id":null},{"id":"10","position":5,"category_id":null},{"id":"1","position":6,"category_id":"10"},{"id":"6","position":7,"category_id":"10"},{"id":"15","position":8,"category_id":"10"},{"id":"2","position":9,"category_id":"10"},{"id":"18","position":10,"category_id":"10"},{"id":"14","position":11,"category_id":null},{"id":"13","position":12,"category_id":null},{"id":"9","position":13,"category_id":null},{"id":"3","position":14,"category_id":null}]

29
includes/ai_filtering.php Normal file
View File

@ -0,0 +1,29 @@
<?php
require_once __DIR__ . '/../ai/LocalAIApi.php';
function moderateContent($content) {
if (empty(trim($content))) return ['is_safe' => true];
// Bypass moderation for video platforms as they are handled by their own safety measures
// and often trigger false positives in AI moderation due to "lack of context".
if (preg_match('/(?:https?:\/\/)?(?:www\.)?(?:youtube\.com|youtu\.be|dailymotion\.com|dai\.ly|vimeo\.com)\//i', $content)) {
return ['is_safe' => true];
}
$resp = LocalAIApi::createResponse([
'input' => [
['role' => 'system', 'content' => 'You are a content moderator. Analyze the message and return a JSON object with "is_safe" (boolean) and "reason" (string, optional). Safe means no hate speech, extreme violence, or explicit sexual content. Do not flag URLs as unsafe simply because you cannot see the content behind them.'],
['role' => 'user', 'content' => $content],
],
]);
if (!empty($resp['success'])) {
$result = LocalAIApi::decodeJsonFromResponse($resp);
if ($result && isset($result['is_safe'])) {
return $result;
}
}
// Default to safe if AI fails, to avoid blocking users
return ['is_safe' => true];
}

62
includes/opengraph.php Normal file
View File

@ -0,0 +1,62 @@
<?php
function fetchOpenGraphData($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
'Accept-Language: en-US,en;q=0.9',
'Cache-Control: no-cache',
'Pragma: no-cache',
'Upgrade-Insecure-Requests: 1'
]);
$html = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if (!$html || $info['http_code'] !== 200) return null;
if (!class_exists('DOMDocument')) return null;
$doc = new DOMDocument();
@$doc->loadHTML($html);
$metas = $doc->getElementsByTagName('meta');
$data = [
'title' => '',
'description' => '',
'image' => '',
'url' => $url,
'site_name' => ''
];
// Try title tag if og:title is missing
$titles = $doc->getElementsByTagName('title');
if ($titles->length > 0) {
$data['title'] = $titles->item(0)->nodeValue;
}
foreach ($metas as $meta) {
$property = $meta->getAttribute('property');
$name = $meta->getAttribute('name');
$content = $meta->getAttribute('content');
if ($property === 'og:title' || $name === 'twitter:title') $data['title'] = $content;
if ($property === 'og:description' || $name === 'description' || $name === 'twitter:description') $data['description'] = $content;
if ($property === 'og:image' || $name === 'twitter:image') $data['image'] = $content;
if ($property === 'og:site_name') $data['site_name'] = $content;
}
// Filter out empty results
if (empty($data['title']) && empty($data['description'])) return null;
return $data;
}
function extractUrls($text) {
$pattern = '/https?:\/\/[^\s<]+/';
preg_match_all($pattern, $text, $matches);
return $matches[0];
}

131
includes/permissions.php Normal file
View File

@ -0,0 +1,131 @@
<?php
class Permissions {
const VIEW_CHANNEL = 1;
const SEND_MESSAGES = 2;
const MANAGE_MESSAGES = 4;
const MANAGE_CHANNELS = 8;
const MANAGE_SERVER = 16;
const ADMINISTRATOR = 32;
public static function hasPermission($user_id, $server_id, $permission) {
$stmt = db()->prepare("SELECT is_admin FROM users WHERE id = ?");
$stmt->execute([$user_id]);
$user = $stmt->fetch();
if ($user && $user['is_admin']) return true;
$stmt = db()->prepare("SELECT owner_id FROM servers WHERE id = ?");
$stmt->execute([$server_id]);
$server = $stmt->fetch();
if ($server && $server['owner_id'] == $user_id) return true;
$stmt = db()->prepare("
SELECT SUM(r.permissions) as total_perms
FROM roles r
JOIN user_roles ur ON r.id = ur.role_id
WHERE ur.user_id = ? AND r.server_id = ?
");
$stmt->execute([$user_id, $server_id]);
$row = $stmt->fetch();
$perms = (int)($row['total_perms'] ?? 0);
if ($perms & self::ADMINISTRATOR) return true;
return ($perms & $permission) === $permission;
}
public static function canViewChannel($user_id, $channel_id) {
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$c = $stmt->fetch();
if (!$c) return false;
$server_id = $c['server_id'];
// Check if owner or admin
if (self::hasPermission($user_id, $server_id, self::ADMINISTRATOR)) return true;
// Fetch overrides for all roles the user has in this server
$stmt = db()->prepare("
SELECT cp.allow_permissions, cp.deny_permissions
FROM channel_permissions cp
JOIN user_roles ur ON cp.role_id = ur.role_id
WHERE ur.user_id = ? AND cp.channel_id = ?
");
$stmt->execute([$user_id, $channel_id]);
$overrides = $stmt->fetchAll();
// Check @everyone override specifically (even if user has no roles assigned)
$stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1");
$stmt->execute([$server_id]);
$everyone_role = $stmt->fetch();
if ($everyone_role) {
$stmt = db()->prepare("SELECT allow_permissions, deny_permissions FROM channel_permissions WHERE channel_id = ? AND role_id = ?");
$stmt->execute([$channel_id, $everyone_role['id']]);
$eo = $stmt->fetch();
if ($eo) {
$overrides[] = $eo;
}
}
if (empty($overrides)) {
return true; // Default to yes
}
$allow = false;
$deny = false;
foreach($overrides as $o) {
if ($o['allow_permissions'] & self::VIEW_CHANNEL) $allow = true;
if ($o['deny_permissions'] & self::VIEW_CHANNEL) $deny = true;
}
if ($allow) return true;
if ($deny) return false;
return true; // Default to yes
}
public static function canSendInChannel($user_id, $channel_id) {
$stmt = db()->prepare("SELECT server_id FROM channels WHERE id = ?");
$stmt->execute([$channel_id]);
$c = $stmt->fetch();
if (!$c) return false;
$server_id = $c['server_id'];
// Check if owner or admin
if (self::hasPermission($user_id, $server_id, self::ADMINISTRATOR)) return true;
// Check overrides
$stmt = db()->prepare("
SELECT cp.allow_permissions, cp.deny_permissions
FROM channel_permissions cp
JOIN user_roles ur ON cp.role_id = ur.role_id
WHERE ur.user_id = ? AND cp.channel_id = ?
");
$stmt->execute([$user_id, $channel_id]);
$overrides = $stmt->fetchAll();
// Check @everyone override
$stmt = db()->prepare("SELECT id FROM roles WHERE server_id = ? AND (name = '@everyone' OR name = 'Everyone') LIMIT 1");
$stmt->execute([$server_id]);
$everyone_role = $stmt->fetch();
if ($everyone_role) {
$stmt = db()->prepare("SELECT allow_permissions, deny_permissions FROM channel_permissions WHERE channel_id = ? AND role_id = ?");
$stmt->execute([$channel_id, $everyone_role['id']]);
$eo = $stmt->fetch();
if ($eo) {
$overrides[] = $eo;
}
}
$allow = false;
$deny = false;
foreach($overrides as $o) {
if ($o['allow_permissions'] & self::SEND_MESSAGES) $allow = true;
if ($o['deny_permissions'] & self::SEND_MESSAGES) $deny = true;
}
if ($allow) return true;
if ($deny) return false;
return self::hasPermission($user_id, $server_id, self::SEND_MESSAGES);
}
}

28
includes/pexels.php Normal file
View File

@ -0,0 +1,28 @@
<?php
// includes/pexels.php
function pexels_key() {
$k = getenv('PEXELS_KEY');
return $k && strlen($k) > 0 ? $k : 'Vc99rnmOhHhJAbgGQoKLZtsaIVfkeownoQNbTj78VemUjKh08ZYRbf18';
}
function pexels_get($url) {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [ 'Authorization: '. pexels_key() ],
CURLOPT_TIMEOUT => 15,
]);
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($code >= 200 && $code < 300 && $resp) return json_decode($resp, true);
return null;
}
function download_to($srcUrl, $destPath) {
$data = file_get_contents($srcUrl);
if ($data === false) return false;
if (!is_dir(dirname($destPath))) mkdir(dirname($destPath), 0775, true);
return file_put_contents($destPath, $data) !== false;
}

2602
index.php

File diff suppressed because it is too large Load Diff

696
requests.log Normal file
View File

@ -0,0 +1,696 @@
2026-02-15 23:22:04 - GET /?fl_project=38443 - POST: []
2026-02-15 23:27:59 - GET / - POST: []
2026-02-15 23:28:03 - HEAD / - POST: []
2026-02-15 23:28:16 - GET /?fl_project=38443 - POST: []
2026-02-15 23:31:59 - GET /index.php?server_id=1&channel_id=10 - POST: []
2026-02-15 23:32:07 - GET /index.php?server_id=1&channel_id=10 - POST: []
2026-02-15 23:32:12 - GET /index.php?server_id=1&channel_id=10 - POST: []
2026-02-15 23:36:00 - GET /index.php?server_id=1&channel_id=10 - POST: []
2026-02-15 23:36:17 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-15 23:36:35 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:38:20 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-15 23:38:22 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-15 23:39:19 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:39:43 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-15 23:39:49 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-15 23:40:05 - GET /index.php?server_id=1&channel_id=13 - POST: []
2026-02-15 23:40:17 - GET /index.php?server_id=1&channel_id=14 - POST: []
2026-02-15 23:40:21 - GET /index.php?server_id=1&channel_id=13 - POST: []
2026-02-15 23:40:49 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:42:44 - GET /?fl_project=38443 - POST: []
2026-02-15 23:42:55 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:43:28 - GET /index.php - POST: []
2026-02-15 23:44:37 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:44:49 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-15 23:44:52 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:44:58 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-15 23:45:04 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:45:05 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-15 23:45:11 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:45:12 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:45:13 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-15 23:45:17 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-15 23:45:41 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-15 23:46:38 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-15 23:46:40 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:46:42 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-15 23:46:50 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-15 23:47:06 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:47:16 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:47:23 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:47:45 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-15 23:48:03 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:49:16 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-15 23:52:12 - GET /?fl_project=38443 - POST: []
2026-02-15 23:52:32 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:52:37 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-15 23:52:45 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:52:53 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:52:56 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:53:22 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:54:12 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:55:22 - GET /?fl_project=38443 - POST: []
2026-02-15 23:55:42 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:55:45 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:56:14 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:56:41 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:57:05 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:57:30 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:57:46 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-15 23:57:49 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-15 23:59:43 - GET /?fl_project=38443 - POST: []
2026-02-16 00:02:15 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:02:20 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:06:51 - GET /?fl_project=38443 - POST: []
2026-02-16 00:11:43 - GET /?fl_project=38443 - POST: []
2026-02-16 00:13:38 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:13:47 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:13:53 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:14:45 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:15:11 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:15:42 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:15:56 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:16:00 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:16:00 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:17:19 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:17:27 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:17:35 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 00:17:46 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:17:58 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 00:18:04 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:18:12 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 00:18:15 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 00:18:17 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:18:19 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:18:25 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 00:18:27 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:19:28 - GET /?fl_project=38443 - POST: []
2026-02-16 00:23:03 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:23:08 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:23:14 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:23:19 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:24:37 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 00:24:58 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 00:25:16 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 00:25:18 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:25:27 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:28:38 - GET / - POST: []
2026-02-16 00:28:56 - GET /?fl_project=38443 - POST: []
2026-02-16 00:29:17 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:29:44 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:29:47 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:30:05 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 00:30:37 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:30:39 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:30:55 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:30:57 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:30:59 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:31:02 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 00:31:04 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 00:31:06 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:31:09 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:31:11 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:31:13 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 00:31:15 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 00:31:16 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:31:59 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:32:08 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:32:15 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:32:19 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 00:32:21 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:32:45 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:34:01 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:34:31 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 00:34:34 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:34:36 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 00:34:39 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 00:36:39 - GET /index.php?server_id=dms - POST: []
2026-02-16 00:36:43 - GET /index.php?server_id=dms&channel_id=7 - POST: []
2026-02-16 00:36:46 - GET /index.php?server_id=dms&channel_id=7 - POST: []
2026-02-16 00:36:48 - GET /index.php?server_id=1 - POST: []
2026-02-16 00:36:56 - GET /index.php?server_id=1 - POST: []
2026-02-16 00:37:04 - GET /index.php?server_id=dms - POST: []
2026-02-16 00:37:05 - GET /index.php?server_id=1 - POST: []
2026-02-16 00:38:00 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 00:39:58 - GET /index.php?server_id=1 - POST: []
2026-02-16 02:38:56 - GET / - POST: []
2026-02-16 02:54:58 - GET /?fl_project=38443 - POST: []
2026-02-16 02:56:39 - GET /index.php?server_id=1 - POST: []
2026-02-16 02:57:05 - GET /index.php?server_id=1 - POST: []
2026-02-16 02:57:17 - GET /index.php?server_id=1 - POST: []
2026-02-16 02:58:22 - GET /?fl_project=38443 - POST: []
2026-02-16 02:59:03 - GET /?fl_project=38443 - POST: []
2026-02-16 03:05:28 - GET / - POST: []
2026-02-16 03:06:27 - GET /?fl_project=38443 - POST: []
2026-02-16 03:07:23 - GET /index.php?server_id=1 - POST: []
2026-02-16 03:07:43 - GET /index.php?server_id=1&channel_id=16 - POST: []
2026-02-16 03:08:00 - GET /index.php?server_id=1&channel_id=16 - POST: []
2026-02-16 03:08:21 - GET /index.php?server_id=1 - POST: []
2026-02-16 03:08:30 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:09:15 - GET /index.php?server_id=1&channel_id=18 - POST: []
2026-02-16 03:09:47 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 03:09:51 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:09:54 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:10:22 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:10:27 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:10:35 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:10:58 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:15:47 - GET /?fl_project=38443 - POST: []
2026-02-16 03:16:11 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:16:19 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:16:26 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:17:11 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:20:43 - GET /?fl_project=38443 - POST: []
2026-02-16 03:21:26 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:21:33 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:21:35 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:21:38 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:26:28 - GET /?fl_project=38443 - POST: []
2026-02-16 03:26:35 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:28:36 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:30:31 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 03:30:36 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 03:30:41 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:32:55 - GET / - POST: []
2026-02-16 03:32:59 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:33:09 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:33:11 - GET /?fl_project=38443 - POST: []
2026-02-16 03:33:12 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:33:14 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:33:34 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:33:37 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 03:35:52 - GET /?fl_project=38443 - POST: []
2026-02-16 08:35:40 - GET /?fl_project=38443 - POST: []
2026-02-16 08:37:05 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 08:54:29 - GET / - POST: []
2026-02-16 08:54:39 - GET /?fl_project=38443 - POST: []
2026-02-16 12:42:27 - GET /?fl_project=38443 - POST: []
2026-02-16 12:47:00 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 12:47:29 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 12:47:34 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 12:48:16 - GET / - POST: []
2026-02-16 12:48:18 - HEAD / - POST: []
2026-02-16 12:48:24 - GET /?fl_project=38443 - POST: []
2026-02-16 12:48:45 - GET /?fl_project=38443 - POST: []
2026-02-16 12:51:34 - GET /?fl_project=38443 - POST: []
2026-02-16 12:53:59 - GET / - POST: []
2026-02-16 12:54:11 - GET /?fl_project=38443 - POST: []
2026-02-16 12:54:33 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 12:54:52 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 12:56:37 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 12:56:38 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 13:01:21 - GET / - POST: []
2026-02-16 13:01:28 - GET /?fl_project=38443 - POST: []
2026-02-16 13:03:56 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 13:06:06 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 13:06:15 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 13:06:20 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 13:06:25 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 13:06:41 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 13:06:43 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 13:06:45 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 13:11:31 - GET /?fl_project=38443 - POST: []
2026-02-16 13:13:37 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 13:18:51 - GET /?fl_project=38443 - POST: []
2026-02-16 13:19:22 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 13:25:42 - GET /?fl_project=38443 - POST: []
2026-02-16 13:38:52 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 13:44:04 - GET /?fl_project=38443 - POST: []
2026-02-16 14:10:45 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 14:10:52 - GET /index.php - POST: []
2026-02-16 14:10:57 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 14:11:06 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 14:13:29 - GET /?fl_project=38443 - POST: []
2026-02-16 16:20:04 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 16:20:11 - GET /index.php - POST: []
2026-02-16 16:20:31 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 16:20:47 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 16:23:59 - GET / - POST: []
2026-02-16 16:24:07 - GET /?fl_project=38443 - POST: []
2026-02-16 16:24:48 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 16:24:51 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 16:25:36 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 16:32:41 - GET / - POST: []
2026-02-16 16:32:49 - GET /?fl_project=38443 - POST: []
2026-02-16 16:32:51 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 16:33:28 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 16:38:10 - GET / - POST: []
2026-02-16 16:38:17 - GET /?fl_project=38443 - POST: []
2026-02-16 16:58:08 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 16:58:15 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 17:01:50 - GET /?fl_project=38443 - POST: []
2026-02-16 17:03:56 - GET / - POST: []
2026-02-16 17:04:02 - GET /?fl_project=38443 - POST: []
2026-02-16 17:08:30 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 17:09:29 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 17:09:33 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 17:09:34 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 17:09:38 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 17:09:50 - GET /index.php - POST: []
2026-02-16 17:09:55 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 17:09:57 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 17:09:59 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 17:41:16 - GET / - POST: []
2026-02-16 17:41:28 - GET /?fl_project=38443 - POST: []
2026-02-16 18:00:22 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 18:03:21 - GET /?fl_project=38443 - POST: []
2026-02-16 18:23:09 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 18:29:00 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 18:39:35 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 18:39:37 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 18:39:39 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 18:39:40 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 18:40:37 - GET /index.php?server_id=1&channel_id=18 - POST: []
2026-02-16 18:40:39 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 18:41:42 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 18:41:48 - GET /index.php?server_id=1&channel_id=2 - POST: []
2026-02-16 18:41:51 - GET /index.php?server_id=1&channel_id=18 - POST: []
2026-02-16 18:42:47 - GET /index.php?server_id=1 - POST: []
2026-02-16 18:42:50 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 18:43:03 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 18:43:06 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 18:43:30 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 18:43:38 - GET /index.php?server_id=1&channel_id=19 - POST: []
2026-02-16 18:47:12 - GET / - POST: []
2026-02-16 18:47:41 - GET /?fl_project=38443 - POST: []
2026-02-16 18:48:13 - GET /index.php?server_id=1&channel_id=19 - POST: []
2026-02-16 18:48:31 - GET /index.php?server_id=1&channel_id=19 - POST: []
2026-02-16 18:48:34 - GET /index.php?server_id=1&channel_id=19 - POST: []
2026-02-16 18:48:39 - GET /index.php?server_id=1&channel_id=19 - POST: []
2026-02-16 18:50:31 - GET /?fl_project=38443 - POST: []
2026-02-16 18:50:41 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 18:51:08 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 18:54:57 - GET /?fl_project=38443 - POST: []
2026-02-16 18:54:58 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 18:55:39 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 18:56:06 - GET /?fl_project=38443 - POST: []
2026-02-16 18:56:52 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 19:05:49 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 19:06:59 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 19:09:05 - GET / - POST: []
2026-02-16 19:09:11 - GET /?fl_project=38443 - POST: []
2026-02-16 19:09:19 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 19:09:23 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 19:11:40 - GET /?fl_project=38443 - POST: []
2026-02-16 19:15:47 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 19:15:54 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 19:16:02 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 19:19:15 - GET /?fl_project=38443 - POST: []
2026-02-16 20:15:59 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 20:16:11 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 20:18:56 - GET /?fl_project=38443 - POST: []
2026-02-16 20:29:33 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 20:29:40 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 20:35:04 - GET /?fl_project=38443 - POST: []
2026-02-16 20:49:01 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 21:20:57 - GET /?fl_project=38443 - POST: []
2026-02-16 21:30:31 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 21:30:38 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 21:31:19 - GET /?fl_project=38443 - POST: []
2026-02-16 21:33:59 - GET /?fl_project=38443 - POST: []
2026-02-16 21:35:15 - GET /?fl_project=38443 - POST: []
2026-02-16 21:46:25 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 21:46:33 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 21:48:54 - api_v1_user.php - - POST: {"username":"testuser","theme":"light"}
2026-02-16 21:48:58 - api_v1_user.php - POST - POST: {"username":"testuser","theme":"light"}
2026-02-16 21:53:22 - GET /?fl_project=38443 - POST: []
2026-02-16 21:56:55 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 21:57:11 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 21:57:15 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 21:57:23 - GET /index.php?server_id=1&channel_id=6 - POST: []
{"date":"2026-02-16 22:01:01","method":"POST","post":{"username":"DebugUser","theme":"light"},"session":{"user_id":1},"user_id":1,"db_success":true}
2026-02-16 22:01:28 - GET /?fl_project=38443 - POST: []
2026-02-16 22:01:51 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 22:01:56 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 22:02:03 - GET /index.php?server_id=1&channel_id=6 - POST: []
{"date":"2026-02-16 22:02:07","method":"POST","post":{"avatar_url":"","username":"swefpifh","dnd_mode":"1","sound_notifications":"1","theme":"dark"},"session":{"user_id":2,"username":"swefpifh"},"user_id":2,"db_success":true}
2026-02-16 22:02:08 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 22:02:09 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 22:02:28 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 22:02:30 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:02:33 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-16 22:02:52 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:03:17 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:03:19 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:08:26 - GET /?fl_project=38443 - POST: []
2026-02-16 22:08:27 - GET /index.php?server_id=1&channel_id=17 - POST: []
{"date":"2026-02-16 22:08:44","method":"POST","post":{"avatar_url":"","username":"swefpifh","dnd_mode":"1","sound_notifications":"1","theme":"light"},"session":{"user_id":2,"username":"swefpifh"},"user_id":2,"db_success":true}
2026-02-16 22:08:45 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:08:46 - GET /index.php?server_id=1&channel_id=17 - POST: []
{"date":"2026-02-16 22:08:52","method":"POST","post":{"avatar_url":"","username":"swefpifh","dnd_mode":"1","sound_notifications":"1","theme":"dark"},"session":{"user_id":2,"username":"swefpifh"},"user_id":2,"db_success":true}
2026-02-16 22:08:52 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:09:17 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:53:00 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 22:53:02 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 22:53:49 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:54:14 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:57:26 - GET / - POST: []
2026-02-16 22:57:44 - GET /?fl_project=38443 - POST: []
2026-02-16 22:58:55 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 22:59:59 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:00:03 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:00:08 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:00:09 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:00:18 - GET /index.php - POST: []
2026-02-16 23:00:20 - GET /index.php - POST: []
2026-02-16 23:00:24 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:00:26 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:00:28 - GET /index.php?server_id=1&channel_id=17 - POST: []
{"date":"2026-02-16 23:00:56","method":"POST","post":{"avatar_url":"","username":"swef","theme":"light","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
2026-02-16 23:00:57 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:00:59 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:01:02 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:01:08 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 23:01:09 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 23:01:23 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 23:04:26 - GET /?fl_project=38443 - POST: []
2026-02-16 23:05:06 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:05:13 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-16 23:05:15 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 23:05:29 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-16 23:13:30 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:18:05 - GET /?fl_project=38443 - POST: []
2026-02-16 23:18:47 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:26:02 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:26:50 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:27:48 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:28:42 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:28:44 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:30:14 - GET /?fl_project=38443 - POST: []
2026-02-16 23:30:27 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:31:15 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:31:17 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:31:45 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:31:46 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:32:05 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:32:07 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:32:49 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 23:32:53 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:33:23 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:33:24 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:33:50 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:34:25 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:34:50 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:34:54 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:35:43 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:35:58 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:36:07 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:36:08 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:36:54 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:37:08 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:37:26 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:39:35 - GET /?fl_project=38443 - POST: []
2026-02-16 23:39:49 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:39:52 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-16 23:39:53 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:40:18 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:40:19 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:44:06 - GET /?fl_project=38443 - POST: []
2026-02-16 23:45:42 - GET /?fl_project=38443 - POST: []
2026-02-16 23:45:45 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-16 23:45:46 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:46:19 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:46:41 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:47:02 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-16 23:48:40 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-17 00:21:52 - GET /?fl_project=38443 - POST: []
2026-02-17 00:22:06 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-17 00:22:15 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-17 00:22:19 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-17 00:30:57 - GET /index.php?server_id=1&channel_id=20 - POST: []
2026-02-17 00:32:51 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-17 00:33:04 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-17 00:34:28 - GET /index.php?server_id=1&channel_id=12 - POST: []
2026-02-17 00:34:40 - GET /index.php?server_id=1 - POST: []
2026-02-17 00:34:45 - GET /index.php?server_id=1&channel_id=20 - POST: []
2026-02-17 00:35:23 - GET /index.php?server_id=1&channel_id=20 - POST: []
2026-02-17 00:43:23 - - POST: []
2026-02-17 00:44:01 - GET /index.php - POST: []
2026-02-17 00:44:15 - GET /?fl_project=38443 - POST: []
2026-02-17 00:45:12 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-17 00:45:14 - GET /index.php?server_id=1&channel_id=20 - POST: []
2026-02-17 00:45:21 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-17 00:45:23 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 00:48:04 - GET / - POST: []
2026-02-17 00:48:39 - GET /?fl_project=38443 - POST: []
2026-02-17 00:48:50 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 00:57:46 - GET / - POST: []
2026-02-17 00:58:08 - GET /?fl_project=38443 - POST: []
2026-02-17 00:58:11 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 00:58:14 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 01:02:05 - GET / - POST: []
2026-02-17 01:02:20 - GET /?fl_project=38443 - POST: []
2026-02-17 01:02:36 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 08:06:32 - GET /index.php?server_id=1&channel_id=20 - POST: []
2026-02-17 08:06:36 - GET /index.php - POST: []
2026-02-17 08:06:39 - GET /index.php?server_id=1&channel_id=20 - POST: []
2026-02-17 08:10:08 - GET /?fl_project=38443 - POST: []
2026-02-17 08:15:51 - GET /?fl_project=38443 - POST: []
2026-02-17 08:17:13 - GET /?fl_project=38443 - POST: []
2026-02-17 08:18:43 - GET /index.php?server_id=1&channel_id=20 - POST: []
2026-02-17 08:19:02 - GET /index.php?server_id=1&channel_id=21 - POST: []
2026-02-17 08:19:06 - GET /index.php?server_id=1&channel_id=20 - POST: []
2026-02-17 08:19:16 - GET /index.php?server_id=1 - POST: []
2026-02-17 08:19:29 - GET /index.php?server_id=1&channel_id=21 - POST: []
2026-02-17 08:20:44 - GET /index.php?server_id=1&channel_id=17 - POST: []
2026-02-17 08:20:46 - GET /index.php?server_id=1&channel_id=11 - POST: []
2026-02-17 08:20:48 - GET /index.php?server_id=1&channel_id=21 - POST: []
2026-02-17 08:27:06 - GET /?fl_project=38443 - POST: []
2026-02-17 08:31:06 - GET / - POST: []
2026-02-17 08:31:24 - GET /?fl_project=38443 - POST: []
2026-02-17 08:46:59 - GET /index.php?server_id=1&channel_id=21 - POST: []
{"date":"2026-02-17 08:47:14","method":"POST","post":{"avatar_url":"","display_name":"swefpifh\u00b2","dnd_mode":"1","sound_notifications":"1","theme":"dark"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 08:47:14 - GET /index.php?server_id=1&channel_id=21 - POST: []
2026-02-17 08:47:19 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 09:19:30 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 09:19:32 - GET /index.php - POST: []
{"date":"2026-02-17 09:20:03","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
2026-02-17 09:20:03 - GET /index.php - POST: []
{"date":"2026-02-17 09:20:46","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","dnd_mode":"1","sound_notifications":"1","theme":"dark"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 09:20:47 - GET /index.php?server_id=1&channel_id=6 - POST: []
{"date":"2026-02-17 09:21:25","method":"POST","post":{"avatar_url":"","display_name":"\u1d47\u02b0\u1da0\u02b3 swefpifh","dnd_mode":"1","sound_notifications":"1","theme":"dark"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 09:21:25 - GET /index.php?server_id=1&channel_id=6 - POST: []
{"date":"2026-02-17 09:22:02","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","dnd_mode":"1","sound_notifications":"1","theme":"dark"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 09:22:02 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 09:22:13 - GET /index.php - POST: []
2026-02-17 09:22:18 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-17 09:22:20 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 09:22:25 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-17 09:22:29 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 09:36:55 - GET /?fl_project=38443 - POST: []
2026-02-17 09:48:03 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 09:56:28 - GET /?fl_project=38443 - POST: []
2026-02-17 09:57:00 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 11:37:20 - GET /index.php?server_id=1&channel_id=21 - POST: []
2026-02-17 11:43:18 - GET /?fl_project=38443 - POST: []
2026-02-17 11:43:46 - GET /?fl_project=38443 - POST: []
2026-02-17 11:46:49 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 11:46:58 - GET /index.php?server_id=1&channel_id=3 - POST: []
2026-02-17 11:49:14 - GET /?fl_project=38443 - POST: []
2026-02-17 11:56:37 - GET /?fl_project=38443 - POST: []
2026-02-17 11:56:49 - GET /index.php?server_id=1&channel_id=3 - POST: []
2026-02-17 11:56:58 - GET /index.php?server_id=1&channel_id=3 - POST: []
2026-02-17 11:57:08 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 11:57:14 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:01:03 - GET /?fl_project=38443 - POST: []
2026-02-17 12:08:31 - GET /?fl_project=38443 - POST: []
2026-02-17 12:08:36 - GET /index.php?server_id=1&channel_id=6 - POST: []
{"date":"2026-02-17 12:09:05","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 12:09:05 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:14:02 - GET /?fl_project=38443 - POST: []
2026-02-17 12:14:47 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:15:05 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:18:43 - GET /?fl_project=38443 - POST: []
2026-02-17 12:22:00 - GET /?fl_project=38443 - POST: []
2026-02-17 12:23:42 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:24:23 - GET /?fl_project=38443 - POST: []
2026-02-17 12:25:06 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:25:50 - GET /?fl_project=38443 - POST: []
2026-02-17 12:27:17 - GET /?fl_project=38443 - POST: []
2026-02-17 12:28:56 - GET /?fl_project=38443 - POST: []
2026-02-17 12:32:39 - GET /?fl_project=38443 - POST: []
2026-02-17 12:34:47 - GET /?fl_project=38443 - POST: []
2026-02-17 12:38:46 - GET /?fl_project=38443 - POST: []
2026-02-17 12:44:53 - GET /?fl_project=38443 - POST: []
2026-02-17 12:45:22 - GET /?fl_project=38443 - POST: []
2026-02-17 12:50:07 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 12:51:03 - GET /?fl_project=38443 - POST: []
2026-02-17 13:29:32 - GET /?fl_project=38443 - POST: []
2026-02-17 13:34:34 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 13:35:23 - - POST: []
2026-02-17 13:35:28 - GET / - POST: []
2026-02-17 13:44:18 - GET /?fl_project=38443 - POST: []
2026-02-17 13:44:19 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 13:44:44 - GET /?fl_project=38443 - POST: []
2026-02-17 13:46:48 - GET /?fl_project=38443 - POST: []
2026-02-17 13:47:13 - GET /index.php - POST: []
2026-02-17 13:47:48 - GET /?fl_project=38443 - POST: []
2026-02-17 13:47:48 - GET /?fl_project=38443 - POST: []
2026-02-17 13:49:26 - GET /?fl_project=38443 - POST: []
2026-02-17 13:52:42 - GET /?fl_project=38443 - POST: []
2026-02-17 13:53:30 - GET /?fl_project=38443 - POST: []
2026-02-17 13:53:31 - GET /?fl_project=38443 - POST: []
2026-02-17 13:53:33 - GET /?fl_project=38443 - POST: []
2026-02-17 13:53:34 - GET /?fl_project=38443 - POST: []
2026-02-17 13:53:44 - GET /?fl_project=38443 - POST: []
2026-02-17 13:53:50 - GET /index.php - POST: []
2026-02-17 13:53:52 - GET /index.php - POST: []
2026-02-17 13:54:13 - GET /index.php?server_id=1&channel_id=15 - POST: []
2026-02-17 13:54:23 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 13:54:34 - GET /index.php?server_id=1&channel_id=3 - POST: []
2026-02-17 13:54:40 - GET /index.php?server_id=1&channel_id=22 - POST: []
{"date":"2026-02-17 13:54:53","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 13:54:54 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 13:55:35 - GET /index.php?server_id=1&channel_id=6 - POST: []
{"date":"2026-02-17 13:56:01","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
2026-02-17 13:56:02 - GET /index.php?server_id=1&channel_id=6 - POST: []
2026-02-17 13:56:22 - GET /?fl_project=38443 - POST: []
2026-02-17 13:56:32 - GET /index.php - POST: []
2026-02-17 14:03:44 - GET /?fl_project=38443 - POST: []
2026-02-17 14:03:45 - GET /?fl_project=38443 - POST: []
2026-02-17 14:04:57 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 14:05:10 - GET /index.php - POST: []
2026-02-17 14:05:34 - GET /index.php - POST: []
2026-02-17 14:05:36 - GET /index.php - POST: []
2026-02-17 14:12:18 - GET /index.php?server_id=1&channel_id=22 - POST: []
{"date":"2026-02-17 14:12:41","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:12:42 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 14:12:48 - GET /index.php?server_id=1&channel_id=22 - POST: []
{"date":"2026-02-17 14:12:58","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:12:59 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 14:23:18 - GET / - POST: []
2026-02-17 14:23:53 - GET /?fl_project=38443 - POST: []
2026-02-17 14:23:54 - GET /?fl_project=38443 - POST: []
2026-02-17 14:34:38 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 14:34:58 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 14:35:14 - GET /index.php - POST: []
{"date":"2026-02-17 14:36:59","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:37:00 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 14:42:23 - GET /?fl_project=38443 - POST: []
2026-02-17 14:42:24 - GET /?fl_project=38443 - POST: []
2026-02-17 14:43:06 - GET /index.php?server_id=1&channel_id=22 - POST: []
{"date":"2026-02-17 14:43:18","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:43:19 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 14:43:51 - GET /index.php - POST: []
{"date":"2026-02-17 14:44:07","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.1","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
2026-02-17 14:44:07 - GET /index.php - POST: []
{"date":"2026-02-17 14:44:43","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:44:43 - GET /index.php?server_id=1&channel_id=22 - POST: []
{"date":"2026-02-17 14:45:25","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:45:26 - GET /index.php?server_id=1&channel_id=22 - POST: []
{"date":"2026-02-17 14:46:17","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:46:17 - GET /index.php?server_id=1&channel_id=22 - POST: []
{"date":"2026-02-17 14:46:39","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:46:40 - GET /index.php?server_id=1&channel_id=22 - POST: []
{"date":"2026-02-17 14:48:43","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:48:44 - GET /index.php?server_id=1&channel_id=22 - POST: []
{"date":"2026-02-17 14:48:59","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"1","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 14:48:59 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 14:59:20 - GET / - POST: []
2026-02-17 14:59:50 - GET /?fl_project=38443 - POST: []
2026-02-17 14:59:50 - GET /?fl_project=38443 - POST: []
2026-02-17 15:02:24 - GET /index.php?server_id=1&channel_id=22 - POST: []
2026-02-17 15:02:30 - GET /index.php?server_id=1&channel_id=1 - POST: []
{"date":"2026-02-17 15:02:45","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 15:02:57 - GET /index.php - POST: []
2026-02-17 15:03:02 - GET /index.php - POST: []
{"date":"2026-02-17 15:03:19","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
2026-02-17 15:03:33 - GET /index.php?server_id=1&channel_id=1 - POST: []
{"date":"2026-02-17 15:03:41","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.01","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-17 15:03:44","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.01","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-17 15:04:13","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.38","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
2026-02-17 15:04:30 - GET /index.php?server_id=1&channel_id=1 - POST: []
{"date":"2026-02-17 15:04:48","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 15:04:52 - GET /index.php?server_id=1&channel_id=1 - POST: []
{"date":"2026-02-17 15:05:02","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-17 15:05:57","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 15:06:05 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-17 15:06:09 - GET /index.php?server_id=1&channel_id=1 - POST: []
{"date":"2026-02-17 15:07:56","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-17 15:08:11","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 15:14:44 - GET /?fl_project=38443 - POST: []
2026-02-17 15:14:44 - GET /?fl_project=38443 - POST: []
2026-02-17 15:18:05 - GET /?fl_project=38443 - POST: []
2026-02-17 15:18:05 - GET /?fl_project=38443 - POST: []
2026-02-17 15:19:12 - GET /index.php?server_id=1&channel_id=1 - POST: []
{"date":"2026-02-17 15:19:29","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"0","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-17 15:19:34","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"0","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-17 15:19:53","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"0","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 15:19:56 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-17 15:20:01 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-17 15:23:33 - GET /?fl_project=38443 - POST: []
2026-02-17 15:23:33 - GET /?fl_project=38443 - POST: []
2026-02-17 15:32:11 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-17 15:32:29 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-17 15:33:22 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-17 15:35:42 - GET / - POST: []
2026-02-17 15:36:35 - GET /?fl_project=38443 - POST: []
2026-02-17 15:36:35 - GET /?fl_project=38443 - POST: []
2026-02-17 15:46:33 - GET /index.php?server_id=1&channel_id=1 - POST: []
2026-02-17 15:46:36 - GET /index.php - POST: []
{"date":"2026-02-17 15:47:00","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
{"date":"2026-02-17 15:47:36","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"ptt","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
{"date":"2026-02-17 15:48:00","method":"POST","post":{"avatar_url":"","display_name":"swefheim","theme":"light","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0","dnd_mode":"0","sound_notifications":"0"},"session":{"user_id":3},"user_id":3,"db_success":true}
{"date":"2026-02-17 15:48:41","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.06","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true}
2026-02-17 16:55:24 - GET / - POST: []
2026-02-17 17:03:39 - GET /?fl_project=38527 - POST: []
2026-02-17 17:03:47 - GET / - POST: []
2026-02-17 17:09:03 - GET /?fl_project=38527 - POST: []
2026-02-17 18:06:34 - GET /?fl_project=38527 - POST: []
2026-02-17 18:36:12 - GET /?fl_project=38527 - POST: []
2026-02-17 18:56:15 - GET /?fl_project=38527 - POST: []
2026-02-17 18:58:00 - GET /?fl_project=38527 - POST: []
2026-02-17 19:13:31 - GET /?fl_project=38527 - POST: []
2026-02-17 19:14:39 - GET /?fl_project=38527 - POST: []
2026-02-17 19:15:28 - GET /?fl_project=38527 - POST: []
2026-02-17 19:16:15 - GET /?fl_project=38527 - POST: []
2026-02-17 19:20:01 - GET /?fl_project=38527 - POST: []
2026-02-17 20:01:09 - GET / - POST: []
2026-02-17 20:15:23 - GET /?fl_project=38527 - POST: []
2026-02-17 20:53:22 - GET / - POST: []
2026-02-17 23:02:47 - GET /?fl_project=38527 - POST: []
2026-02-17 23:06:00 - GET /?fl_project=38527 - POST: []
2026-02-17 23:09:01 - GET /?fl_project=38527 - POST: []
2026-02-17 23:13:51 - GET /?fl_project=38527 - POST: []
2026-02-17 23:16:33 - GET /?fl_project=38527 - POST: []
2026-02-17 23:21:30 - GET /?fl_project=38527 - POST: []
2026-02-17 23:23:30 - GET /?fl_project=38527 - POST: []
2026-02-17 23:26:01 - GET /?fl_project=38527 - POST: []
2026-02-17 23:27:10 - GET /?fl_project=38527 - POST: []
2026-02-17 23:30:39 - GET /?fl_project=38527 - POST: []
2026-02-17 23:34:19 - GET /?fl_project=38527 - POST: []
2026-02-17 23:43:17 - GET /?fl_project=38527 - POST: []
2026-02-17 23:44:16 - GET /?fl_project=38527 - POST: []
2026-02-17 23:44:50 - GET /?fl_project=38527 - POST: []
2026-02-17 23:46:12 - GET /?fl_project=38527 - POST: []
2026-02-17 23:47:06 - GET /?fl_project=38527 - POST: []
2026-02-17 23:48:35 - GET /?fl_project=38527 - POST: []
2026-02-17 23:52:07 - GET /?fl_project=38527 - POST: []
2026-02-17 23:52:31 - GET /?fl_project=38527 - POST: []
2026-02-17 23:53:31 - GET /?fl_project=38527 - POST: []
2026-02-17 23:54:48 - GET /?fl_project=38527 - POST: []
2026-02-17 23:56:45 - GET /?fl_project=38527 - POST: []
2026-02-17 23:57:33 - GET /?fl_project=38527 - POST: []
2026-02-17 23:57:44 - GET /?fl_project=38527 - POST: []
2026-02-17 23:59:06 - GET /?fl_project=38527 - POST: []
2026-02-18 00:01:35 - GET /?fl_project=38527 - POST: []
2026-02-18 00:03:11 - GET /?fl_project=38527 - POST: []
2026-02-18 00:03:37 - GET /?fl_project=38527 - POST: []
2026-02-18 00:04:31 - GET /?fl_project=38527 - POST: []
2026-02-18 00:06:20 - GET /?fl_project=38527 - POST: []
2026-02-18 00:15:34 - GET /?fl_project=38527 - POST: []
2026-02-18 00:15:59 - GET /?fl_project=38527 - POST: []
2026-02-18 00:16:09 - GET /?fl_project=38527 - POST: []
2026-02-18 00:18:47 - GET /?fl_project=38527 - POST: []
2026-02-18 00:21:02 - GET /?fl_project=38527 - POST: []
2026-02-18 00:21:20 - GET /?fl_project=38527 - POST: []
2026-02-18 00:24:47 - GET /?fl_project=38527 - POST: []
2026-02-18 00:25:20 - GET /?fl_project=38527 - POST: []
2026-02-18 00:26:40 - GET /?fl_project=38527 - POST: []
2026-02-18 00:26:58 - GET /?fl_project=38527 - POST: []
2026-02-18 00:27:08 - GET /?fl_project=38527 - POST: []
2026-02-18 00:37:29 - GET /?fl_project=38527 - POST: []
2026-02-18 00:44:21 - GET /?fl_project=38527 - POST: []
2026-02-18 01:00:09 - GET /?fl_project=38527 - POST: []
2026-02-18 01:02:09 - GET / - POST: []
2026-02-18 01:07:48 - GET /?fl_project=38527 - POST: []

9
run_migration.php Normal file
View File

@ -0,0 +1,9 @@
<?php
require_once 'db/config.php';
try {
$sql = file_get_contents('db/migrations/20260217_unread_indicators.sql');
db()->exec($sql);
echo "Migration successful\n";
} catch (Exception $e) {
echo "Migration failed: " . $e->getMessage() . "\n";
}

17
test_reorder.php Normal file
View File

@ -0,0 +1,17 @@
<?php
require 'db/config.php';
$orders = [
['id' => 1, 'position' => 1, 'category_id' => null],
['id' => 10, 'position' => 0, 'category_id' => null],
['id' => 6, 'position' => 2, 'category_id' => null],
['id' => 2, 'position' => 3, 'category_id' => null],
['id' => 9, 'position' => 4, 'category_id' => null],
['id' => 3, 'position' => 5, 'category_id' => null]
];
$server_id = 1;
$stmt = db()->prepare("UPDATE channels SET position = ?, category_id = ? WHERE id = ? AND server_id = ?");
foreach ($orders as $o) {
$stmt->execute([$o['position'], $o['category_id'], $o['id'], $server_id]);
echo "Updated ID {$o['id']} to position {$o['position']}\n";
}
?>

7
test_save.php Normal file
View File

@ -0,0 +1,7 @@
<?php
require_once 'auth/session.php';
$_SERVER['REQUEST_METHOD'] = 'POST';
$_SESSION['user_id'] = 1; // Assuming user 1 exists
$_POST['username'] = 'testuser';
$_POST['theme'] = 'light';
require_once 'api_v1_user.php';

3
ws/server.log Normal file
View File

@ -0,0 +1,3 @@
Server started on 0.0.0.0:8080
New client connected
Client disconnected

116
ws/server.php Normal file
View File

@ -0,0 +1,116 @@
<?php
// Very basic WebSocket server in pure PHP
$host = '0.0.0.0';
$port = 8080;
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);
socket_bind($socket, $host, $port);
socket_listen($socket);
$clients = [$socket];
echo "Server started on $host:$port\n";
while (true) {
$read = $clients;
$write = $except = null;
socket_select($read, $write, $except, null);
if (in_array($socket, $read)) {
$new_socket = socket_accept($socket);
$clients[] = $new_socket;
$header = socket_read($new_socket, 1024);
perform_handshake($header, $new_socket, $host, $port);
echo "New client connected\n";
$key = array_search($socket, $read);
unset($read[$key]);
}
foreach ($read as $client_socket) {
$data = socket_read($client_socket, 65536);
if ($data === false || strlen($data) === 0) {
$key = array_search($client_socket, $clients);
unset($clients[$key]);
socket_close($client_socket);
echo "Client disconnected\n";
continue;
}
$decoded_data = unmask($data);
if ($decoded_data) {
$payload = json_decode($decoded_data, true);
if ($payload) {
// If it's already a structured message, just broadcast it
$response = mask($decoded_data);
} else {
// Fallback for raw text
$response = mask(json_encode(['type' => 'message', 'data' => $decoded_data]));
}
foreach ($clients as $client) {
if ($client != $socket && $client != $client_socket) {
@socket_write($client, $response, strlen($response));
}
}
}
}
}
function perform_handshake($receved_header, $client_conn, $host, $port) {
$headers = array();
$lines = preg_split("/\r\n/", $receved_header);
foreach ($lines as $line) {
$line = chop($line);
if (preg_match('/\A(\S+): (.*)\z/', $line, $matches)) {
$headers[$matches[1]] = $matches[2];
}
}
if (!isset($headers['Sec-WebSocket-Key'])) return;
$secKey = $headers['Sec-WebSocket-Key'];
$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
$upgrade = "HTTP/1.1 101 Switching Protocols\r\n" .
"Upgrade: websocket\r\n" .
"Connection: Upgrade\r\n" .
"Sec-WebSocket-Accept: $secAccept\r\n\r\n";
socket_write($client_conn, $upgrade, strlen($upgrade));
}
function unmask($text) {
if (strlen($text) < 2) return null;
$length = ord($text[1]) & 127;
if ($length == 126) {
if (strlen($text) < 8) return null;
$masks = substr($text, 4, 4);
$data = substr($text, 8);
} elseif ($length == 127) {
if (strlen($text) < 14) return null;
$masks = substr($text, 10, 4);
$data = substr($text, 14);
} else {
if (strlen($text) < 6) return null;
$masks = substr($text, 2, 4);
$data = substr($text, 6);
}
$decoded = "";
for ($i = 0; $i < strlen($data); ++$i) {
$decoded .= $data[$i] ^ $masks[$i % 4];
}
return $decoded;
}
function mask($text) {
$b1 = 0x81; // FIN + Opcode 1 (text)
$length = strlen($text);
if ($length <= 125)
$header = pack('CC', $b1, $length);
elseif ($length > 125 && $length < 65536)
$header = pack('CCn', $b1, 126, $length);
elseif ($length >= 65536)
$header = pack('CCNN', $b1, 127, $length);
return $header . $text;
}

1
ws_output.log Normal file
View File

@ -0,0 +1 @@
Server started on 0.0.0.0:8080