From 430d57545fbe622e2f7daf6761fd374205286633 Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Thu, 19 Feb 2026 12:19:59 +0000 Subject: [PATCH] Autosave: 20260219-121958 --- api_v1_messages.php | 18 +++++ api_v1_threads.php | 51 ++++++++++++-- assets/js/main.js | 56 ++++++++++++++++ data/22.participants.json | 2 +- db/migrations/20260219_forum_enhancements.sql | 7 ++ index.php | 62 +++++++++++++---- requests.log | 67 +++++++++++++++++++ 7 files changed, 242 insertions(+), 21 deletions(-) create mode 100644 db/migrations/20260219_forum_enhancements.sql diff --git a/api_v1_messages.php b/api_v1_messages.php index c5d1302..f6e92ba 100644 --- a/api_v1_messages.php +++ b/api_v1_messages.php @@ -206,6 +206,18 @@ if (!Permissions::canSendInChannel($user_id, $channel_id)) { exit; } +// Check if thread is locked +if ($thread_id) { + $stmtThread = db()->prepare("SELECT is_locked, channel_id, server_id FROM forum_threads t JOIN channels c ON t.channel_id = c.id WHERE t.id = ?"); + $stmtThread->execute([$thread_id]); + $threadData = $stmtThread->fetch(); + if ($threadData && $threadData['is_locked']) { + // Strict lock: no one can post, not even admins. Admins must unlock first. + echo json_encode(['success' => false, 'error' => 'This thread is locked. Messages are disabled.']); + exit; + } +} + if (!empty($content)) { $moderation = moderateContent($content); if (!$moderation['is_safe']) { @@ -231,6 +243,12 @@ try { $stmt->execute([$channel_id, $thread_id, $user_id, $content, $attachment_url, $metadata]); $last_id = db()->lastInsertId(); + // Update last activity for forum threads + if ($thread_id) { + $stmtActivity = db()->prepare("UPDATE forum_threads SET last_activity_at = CURRENT_TIMESTAMP WHERE id = ?"); + $stmtActivity->execute([$thread_id]); + } + // Enforce message limit if set enforceChannelLimit($channel_id); diff --git a/api_v1_threads.php b/api_v1_threads.php index f2610cd..49e7855 100644 --- a/api_v1_threads.php +++ b/api_v1_threads.php @@ -1,6 +1,7 @@ false, 'error' => 'You do not have permission to create threads in this channel.']); exit; @@ -45,10 +45,16 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { exit; } -if ($_SERVER['REQUEST_METHOD'] === 'PATCH' || (isset($_GET['action']) && $_GET['action'] === 'solve')) { +if ($_SERVER['REQUEST_METHOD'] === 'PATCH' || $_SERVER['REQUEST_METHOD'] === 'DELETE' || (isset($_GET['action']) && in_array($_GET['action'], ['solve', 'pin', 'unpin', 'lock', 'unlock', 'delete']))) { $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 + $thread_id = $data['thread_id'] ?? $_GET['thread_id'] ?? 0; + $message_id = $data['message_id'] ?? null; + $action = $_GET['action'] ?? $data['action'] ?? 'solve'; + + if ($_SERVER['REQUEST_METHOD'] === 'DELETE') { + $action = 'delete'; + } + $user_id = $_SESSION['user_id']; if (!$thread_id) { @@ -70,14 +76,45 @@ if ($_SERVER['REQUEST_METHOD'] === 'PATCH' || (isset($_GET['action']) && $_GET[' $stmtServer->execute([$thread['server_id']]); $server = $stmtServer->fetch(); - if ($thread['user_id'] != $user_id && $server['owner_id'] != $user_id) { + $is_admin = Permissions::hasPermission($user_id, $thread['server_id'], Permissions::ADMINISTRATOR) || + Permissions::hasPermission($user_id, $thread['server_id'], Permissions::MANAGE_SERVER) || + Permissions::hasPermission($user_id, $thread['server_id'], Permissions::MANAGE_MESSAGES) || + $server['owner_id'] == $user_id; + + if ($thread['user_id'] != $user_id && !$is_admin) { 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]); + if ($action === 'solve') { + $stmt = db()->prepare("UPDATE forum_threads SET solution_message_id = ? WHERE id = ?"); + $stmt->execute([$message_id, $thread_id]); + } elseif ($action === 'pin') { + $stmt = db()->prepare("UPDATE forum_threads SET is_pinned = 1 WHERE id = ?"); + $stmt->execute([$thread_id]); + } elseif ($action === 'unpin') { + $stmt = db()->prepare("UPDATE forum_threads SET is_pinned = 0 WHERE id = ?"); + $stmt->execute([$thread_id]); + } elseif ($action === 'lock') { + $stmt = db()->prepare("UPDATE forum_threads SET is_locked = 1 WHERE id = ?"); + $stmt->execute([$thread_id]); + } elseif ($action === 'unlock') { + $stmt = db()->prepare("UPDATE forum_threads SET is_locked = 0 WHERE id = ?"); + $stmt->execute([$thread_id]); + } elseif ($action === 'delete') { + db()->beginTransaction(); + // Delete associated tags + $stmt = db()->prepare("DELETE FROM thread_tags WHERE thread_id = ?"); + $stmt->execute([$thread_id]); + // Delete associated messages + $stmt = db()->prepare("DELETE FROM messages WHERE thread_id = ?"); + $stmt->execute([$thread_id]); + // Delete thread + $stmt = db()->prepare("DELETE FROM forum_threads WHERE id = ?"); + $stmt->execute([$thread_id]); + db()->commit(); + } echo json_encode(['success' => true]); } catch (Exception $e) { echo json_encode(['success' => false, 'error' => $e->getMessage()]); diff --git a/assets/js/main.js b/assets/js/main.js index edaef00..7bbfdd7 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -2219,6 +2219,62 @@ document.addEventListener('DOMContentLoaded', () => { manageTagsModal.show(); }); + // Forum Thread Actions (Pin/Lock) + const pinThreadBtn = document.getElementById('toggle-pin-thread'); + pinThreadBtn?.addEventListener('click', async () => { + const id = pinThreadBtn.dataset.id; + const pinned = pinThreadBtn.dataset.pinned == '1'; + const action = pinned ? 'unpin' : 'pin'; + try { + const resp = await fetch(`api_v1_threads.php?action=${action}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ thread_id: id }) + }); + const result = await resp.json(); + if (result.success) location.reload(); + else alert(result.error || 'Failed to update thread'); + } catch (e) { console.error(e); } + }); + + const lockThreadBtn = document.getElementById('toggle-lock-thread'); + lockThreadBtn?.addEventListener('click', async () => { + const id = lockThreadBtn.dataset.id; + const locked = lockThreadBtn.dataset.locked == '1'; + const action = locked ? 'unlock' : 'lock'; + try { + const resp = await fetch(`api_v1_threads.php?action=${action}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ thread_id: id }) + }); + const result = await resp.json(); + if (result.success) location.reload(); + else alert(result.error || 'Failed to update thread'); + } catch (e) { console.error(e); } + }); + + const deleteThreadBtn = document.getElementById('delete-thread-btn'); + deleteThreadBtn?.addEventListener('click', async () => { + if (!confirm('Are you sure you want to delete this thread? This action cannot be undone.')) return; + const id = deleteThreadBtn.dataset.id; + const channelId = deleteThreadBtn.dataset.channelId; + const serverId = deleteThreadBtn.dataset.serverId; + try { + const resp = await fetch(`api_v1_threads.php?action=delete`, { + method: 'DELETE', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ thread_id: id }) + }); + const result = await resp.json(); + if (result.success) { + location.href = `?server_id=${serverId}&channel_id=${channelId}`; + } else { + alert(result.error || 'Failed to delete thread'); + } + } catch (e) { console.error(e); } + }); + async function loadForumAdminTags() { const list = document.getElementById('forum-tags-admin-list'); list.innerHTML = '
Loading tags...
'; diff --git a/data/22.participants.json b/data/22.participants.json index 810e8c2..0637a08 100644 --- a/data/22.participants.json +++ b/data/22.participants.json @@ -1 +1 @@ -{"1707c6672074b09b":{"id":"1707c6672074b09b","user_id":3,"name":"swefheim","avatar_url":"","last_seen":1771447993891,"is_muted":1,"is_deafened":0}} \ No newline at end of file +[] \ No newline at end of file diff --git a/db/migrations/20260219_forum_enhancements.sql b/db/migrations/20260219_forum_enhancements.sql new file mode 100644 index 0000000..e46fc9a --- /dev/null +++ b/db/migrations/20260219_forum_enhancements.sql @@ -0,0 +1,7 @@ +-- Add pinned, locked and activity tracking to forum threads +ALTER TABLE forum_threads ADD COLUMN is_pinned TINYINT(1) NOT NULL DEFAULT 0; +ALTER TABLE forum_threads ADD COLUMN is_locked TINYINT(1) NOT NULL DEFAULT 0; +ALTER TABLE forum_threads ADD COLUMN last_activity_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP; + +-- Initialize last_activity_at with created_at for existing threads +UPDATE forum_threads SET last_activity_at = created_at; diff --git a/index.php b/index.php index db5c865..bf0e1e0 100644 --- a/index.php +++ b/index.php @@ -228,7 +228,10 @@ if ($is_dm_view) { $active_server = $s; $is_owner = ($s['owner_id'] == $current_user_id); $can_manage_channels = Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_CHANNELS) || $is_owner; - $can_manage_server = Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_SERVER) || Permissions::hasPermission($current_user_id, $active_server_id, Permissions::ADMINISTRATOR) || $is_owner; + $can_manage_server = Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_SERVER) || + Permissions::hasPermission($current_user_id, $active_server_id, Permissions::MANAGE_MESSAGES) || + Permissions::hasPermission($current_user_id, $active_server_id, Permissions::ADMINISTRATOR) || + $is_owner; break; } } @@ -279,14 +282,13 @@ if ($is_dm_view) { $stmt = db()->prepare(" SELECT t.*, u.display_name as username, u.username as login_name, u.avatar_url, (SELECT COUNT(*) FROM messages m WHERE m.thread_id = t.id) as message_count, - (SELECT MAX(created_at) FROM messages m WHERE m.thread_id = t.id) as last_activity, (SELECT r.color FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_color, (SELECT r.icon_url FROM roles r JOIN user_roles ur ON r.id = ur.role_id WHERE ur.user_id = u.id AND r.server_id = ? ORDER BY r.position DESC LIMIT 1) as role_icon, (SELECT GROUP_CONCAT(CONCAT(ft.name, ':', ft.color) SEPARATOR '|') FROM thread_tags tt JOIN forum_tags ft ON tt.tag_id = ft.id WHERE tt.thread_id = t.id) as tags FROM forum_threads t JOIN users u ON t.user_id = u.id WHERE t.channel_id = ? " . $status_where . " - ORDER BY last_activity DESC, t.created_at DESC + ORDER BY t.is_pinned DESC, t.last_activity_at DESC, t.created_at DESC "); $stmt->execute([$active_server_id, $active_server_id, $active_channel_id]); $threads = $stmt->fetchAll(); @@ -708,8 +710,27 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
- ← Back to Forum -

Discussion:

+
+ ← Back to Forum + +
+ + + +
+ +
+

+ + + Discussion: +

prepare("SELECT ft.* FROM forum_tags ft JOIN thread_tags tt ON ft.id = tt.tag_id WHERE tt.thread_id = ?"); @@ -917,13 +938,24 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
No discussions yet. Start one!
- -
💬
+
+
+ + + + + + 💬 + +
+ + +
- - Last active: + + Last active:
@@ -1097,17 +1129,21 @@ $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
-
diff --git a/requests.log b/requests.log index fff1b7d..aa9be90 100644 --- a/requests.log +++ b/requests.log @@ -715,3 +715,70 @@ 2026-02-18 20:58:54 - GET /index.php?server_id=1&channel_id=15 - POST: [] 2026-02-18 21:00:09 - GET /?fl_project=38443 - POST: [] 2026-02-18 21:08:02 - GET /?fl_project=38443 - POST: [] +2026-02-18 22:31:51 - GET /index.php?server_id=1&channel_id=15 - POST: [] +2026-02-18 22:32:17 - GET /index.php - POST: [] +{"date":"2026-02-18 22:32:48","method":"POST","post":{"avatar_url":"","display_name":"swefpifh \u1d47\u02b0\u1da0\u02b3","theme":"dark","voice_input_device":"OPTIyYzx3bNI0gtW62vaTCb7SxzY5rNnwOw5G42w36M=","voice_output_device":"znHy1zh6U7iZkBs7ovKSXvb3r4k0jk0DBbg\/TtaWmwk=","voice_input_volume":"1","voice_output_volume":"1","voice_echo_cancellation":"1","voice_noise_suppression":"1","voice_mode":"vox","voice_ptt_key":"v","voice_vox_threshold":"0.17","dnd_mode":"1","sound_notifications":"1"},"session":{"user_id":2},"user_id":2,"db_success":true} +2026-02-19 00:21:00 - GET /index.php - POST: [] +2026-02-19 11:31:34 - GET /index.php - POST: [] +2026-02-19 11:33:16 - GET /?fl_project=38443 - POST: [] +2026-02-19 11:45:34 - GET /index.php - POST: [] +2026-02-19 11:45:58 - GET /index.php?server_id=3 - POST: [] +2026-02-19 11:54:36 - GET /?fl_project=38443 - POST: [] +2026-02-19 11:58:54 - GET /?fl_project=38443 - POST: [] +2026-02-19 12:03:16 - GET / - POST: [] +2026-02-19 12:03:36 - GET /?fl_project=38443 - POST: [] +2026-02-19 12:03:56 - GET /index.php - POST: [] +2026-02-19 12:03:58 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:04:13 - GET /index.php?server_id=1&channel_id=13&thread_id=1 - POST: [] +2026-02-19 12:04:26 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:04:36 - GET /index.php?server_id=1&channel_id=13&thread_id=1 - POST: [] +2026-02-19 12:04:45 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:05:21 - GET /index.php?server_id=1&channel_id=13&thread_id=1 - POST: [] +2026-02-19 12:05:25 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:05:28 - GET /index.php?server_id=1&channel_id=13&thread_id=1 - POST: [] +2026-02-19 12:05:33 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:05:47 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:05:49 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:06:00 - GET /index.php?server_id=1&channel_id=13&thread_id=1 - POST: [] +2026-02-19 12:06:08 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:06:31 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:06:44 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:06:52 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:07:05 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:07:19 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:07:39 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:07:41 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:13:43 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:15:17 - GET / - POST: [] +2026-02-19 12:15:50 - GET /?fl_project=38443 - POST: [] +2026-02-19 12:15:53 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:15:57 - GET /index.php?server_id=1&channel_id=13&thread_id=1 - POST: [] +2026-02-19 12:16:00 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:16:03 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:16:05 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:16:07 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:16:27 - GET /index.php?server_id=1&channel_id=13&thread_id=3 - POST: [] +2026-02-19 12:16:30 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:16:44 - GET /index.php?server_id=1&channel_id=13&thread_id=4 - POST: [] +2026-02-19 12:16:48 - GET /index.php?server_id=1&channel_id=13&thread_id=4 - POST: [] +2026-02-19 12:16:49 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:16:55 - GET /index.php?server_id=1&channel_id=13&thread_id=4 - POST: [] +2026-02-19 12:16:57 - GET /index.php?server_id=1&channel_id=13&thread_id=4 - POST: [] +2026-02-19 12:16:58 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:17:01 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:17:05 - GET /index.php?server_id=1&channel_id=13&thread_id=4 - POST: [] +2026-02-19 12:17:12 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:17:13 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:17:38 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:17:49 - GET /index.php?server_id=1&channel_id=18 - POST: [] +2026-02-19 12:17:50 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:18:00 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:18:02 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:18:04 - GET /index.php?server_id=1&channel_id=13&thread_id=3 - POST: [] +2026-02-19 12:18:09 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:18:11 - GET /index.php?server_id=1&channel_id=13&thread_id=4 - POST: [] +2026-02-19 12:18:15 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:18:29 - GET /index.php?server_id=1&channel_id=13&thread_id=2 - POST: [] +2026-02-19 12:18:33 - GET /index.php?server_id=1&channel_id=13 - POST: [] +2026-02-19 12:18:58 - GET /index.php?server_id=1&channel_id=13&thread_id=3 - POST: [] +2026-02-19 12:19:02 - GET /index.php?server_id=1&channel_id=13 - POST: []