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 = '