bind('forum_id', $forumId); $createTopic->bind('user_id', $userId); $createTopic->bind('topic_title', $title); $createTopic->bind('topic_type', $type); return $createTopic->execute() ? \Misuzu\DB::lastId() : 0; } function forum_topic_update(int $topicId, ?string $title, ?int $type = null): bool { if($topicId < 1) { return false; } // make sure it's null and not some other kinda empty if(empty($title)) { $title = null; } if($type !== null && !forum_topic_is_valid_type($type)) { return false; } $updateTopic = \Misuzu\DB::prepare(' UPDATE `msz_forum_topics` SET `topic_title` = COALESCE(:topic_title, `topic_title`), `topic_type` = COALESCE(:topic_type, `topic_type`) WHERE `topic_id` = :topic_id '); $updateTopic->bind('topic_id', $topicId); $updateTopic->bind('topic_title', $title); $updateTopic->bind('topic_type', $type); return $updateTopic->execute(); } function forum_topic_get(int $topicId, bool $allowDeleted = false): array { $getTopic = \Misuzu\DB::prepare(sprintf( ' SELECT t.`topic_id`, t.`forum_id`, t.`topic_title`, t.`topic_type`, t.`topic_locked`, t.`topic_created`, f.`forum_archived` AS `topic_archived`, t.`topic_deleted`, t.`topic_bumped`, f.`forum_type`, fp.`topic_id` AS `author_post_id`, fp.`user_id` AS `author_user_id`, ( SELECT COUNT(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` AND `post_deleted` IS NULL ) AS `topic_count_posts`, ( SELECT COUNT(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` AND `post_deleted` IS NOT NULL ) AS `topic_count_posts_deleted` FROM `msz_forum_topics` AS t LEFT JOIN `msz_forum_categories` AS f ON f.`forum_id` = t.`forum_id` LEFT JOIN `msz_forum_posts` AS fp ON fp.`post_id` = ( SELECT MIN(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` ) WHERE t.`topic_id` = :topic_id %s ', $allowDeleted ? '' : 'AND t.`topic_deleted` IS NULL' )); $getTopic->bind('topic_id', $topicId); return $getTopic->fetch(); } function forum_topic_redir_info(int $topicId): ?object { $getTopicRedir = \Misuzu\DB::prepare(' SELECT topic_id, user_id, topic_redir_url, UNIX_TIMESTAMP(topic_redir_created) AS topic_redir_created FROM msz_forum_topics_redirects WHERE topic_id = :topic_id '); $getTopicRedir->bind('topic_id', $topicId); return $getTopicRedir->fetchObject(); } function forum_topic_redir_count(): int { return \Misuzu\DB::query('SELECT COUNT(*) FROM msz_forum_topics_redirects')->fetchColumn() ?? 0; } function forum_topic_redir_all(int $offset, int $take): array { $getTopicRedirs = \Misuzu\DB::prepare(' SELECT topic_id, user_id, topic_redir_url, UNIX_TIMESTAMP(topic_redir_created) AS topic_redir_created FROM msz_forum_topics_redirects LIMIT :offset, :take '); $getTopicRedirs->bind('offset', $offset); $getTopicRedirs->bind('take', $take); return $getTopicRedirs->fetchObjects(); } function forum_topic_redir_create(int $topicId, int $userId, string $url): void { if($topicId < 1 || empty($url)) return; if($userId < 1) $userId = null; $createTopicRedir = \Misuzu\DB::prepare(' INSERT INTO msz_forum_topics_redirects (topic_id, user_id, topic_redir_url) VALUES (:topic_id, :user_id, :redir_url) '); $createTopicRedir->bind('topic_id', $topicId); $createTopicRedir->bind('user_id', $userId); $createTopicRedir->bind('redir_url', $url); $createTopicRedir->execute(); } function forum_topic_redir_remove(int $topicId): void { $removeTopicRedir = \Misuzu\DB::prepare(' DELETE FROM msz_forum_topics_redirects WHERE topic_id = :topic_id '); $removeTopicRedir->bind('topic_id', $topicId); $removeTopicRedir->execute(); } function forum_topic_bump(int $topicId): bool { $bumpTopic = \Misuzu\DB::prepare(' UPDATE `msz_forum_topics` SET `topic_bumped` = NOW() WHERE `topic_id` = :topic_id AND `topic_deleted` IS NULL '); $bumpTopic->bind('topic_id', $topicId); return $bumpTopic->execute(); } function forum_topic_views_increment(int $topicId): void { if($topicId < 1) { return; } $bumpViews = \Misuzu\DB::prepare(' UPDATE `msz_forum_topics` SET `topic_count_views` = `topic_count_views` + 1 WHERE `topic_id` = :topic_id '); $bumpViews->bind('topic_id', $topicId); $bumpViews->execute(); } function forum_topic_mark_read(int $userId, int $topicId, int $forumId): void { if($userId < 1) { return; } // previously a TRIGGER was used to achieve this behaviour, // but those explode when running on a lot of queries (like forum_mark_read() does) // so instead we get to live with this garbage now try { $markAsRead = \Misuzu\DB::prepare(' INSERT INTO `msz_forum_topics_track` (`user_id`, `topic_id`, `forum_id`, `track_last_read`) VALUES (:user_id, :topic_id, :forum_id, NOW()) '); $markAsRead->bind('user_id', $userId); $markAsRead->bind('topic_id', $topicId); $markAsRead->bind('forum_id', $forumId); if($markAsRead->execute()) { forum_topic_views_increment($topicId); } } catch(PDOException $ex) { if($ex->getCode() != '23000') { throw $ex; } $markAsRead = \Misuzu\DB::prepare(' UPDATE `msz_forum_topics_track` SET `track_last_read` = NOW(), `forum_id` = :forum_id WHERE `user_id` = :user_id AND `topic_id` = :topic_id '); $markAsRead->bind('user_id', $userId); $markAsRead->bind('topic_id', $topicId); $markAsRead->bind('forum_id', $forumId); $markAsRead->execute(); } } function forum_topic_listing( int $forumId, int $userId, int $offset = 0, int $take = 0, bool $showDeleted = false, bool $sortByPriority = false ): array { $hasPagination = $offset >= 0 && $take > 0; $getTopics = \Misuzu\DB::prepare(sprintf( ' SELECT :user_id AS `target_user_id`, t.`topic_id`, t.`topic_title`, t.`topic_locked`, t.`topic_type`, t.`topic_created`, t.`topic_bumped`, t.`topic_deleted`, t.`topic_count_views`, f.`forum_type`, au.`user_id` AS `author_id`, au.`username` AS `author_name`, COALESCE(au.`user_colour`, ar.`role_colour`) AS `author_colour`, lp.`post_id` AS `response_id`, lp.`post_created` AS `response_created`, lu.`user_id` AS `respondent_id`, lu.`username` AS `respondent_name`, COALESCE(lu.`user_colour`, lr.`role_colour`) AS `respondent_colour`, ( SELECT COUNT(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` %5$s ) AS `topic_count_posts`, ( SELECT CEIL(COUNT(`post_id`) / %6$d) FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` %5$s ) AS `topic_pages`, ( SELECT `target_user_id` > 0 AND t.`topic_bumped` > NOW() - INTERVAL 1 MONTH AND ( SELECT COUNT(ti.`topic_id`) < 1 FROM `msz_forum_topics_track` AS tt RIGHT JOIN `msz_forum_topics` AS ti ON ti.`topic_id` = tt.`topic_id` WHERE ti.`topic_id` = t.`topic_id` AND tt.`user_id` = `target_user_id` AND `track_last_read` >= `topic_bumped` ) ) AS `topic_unread`, ( SELECT COUNT(`post_id`) > 0 FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` AND `user_id` = `target_user_id` LIMIT 1 ) AS `topic_participated` FROM `msz_forum_topics` AS t LEFT JOIN `msz_forum_categories` AS f ON f.`forum_id` = t.`forum_id` LEFT JOIN `msz_users` AS au ON t.`user_id` = au.`user_id` LEFT JOIN `msz_roles` AS ar ON ar.`role_id` = au.`display_role` LEFT JOIN `msz_forum_posts` AS lp ON lp.`post_id` = ( SELECT `post_id` FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` %5$s ORDER BY `post_id` DESC LIMIT 1 ) LEFT JOIN `msz_users` AS lu ON lu.`user_id` = lp.`user_id` LEFT JOIN `msz_roles` AS lr ON lr.`role_id` = lu.`display_role` WHERE ( t.`forum_id` = :forum_id OR t.`topic_type` = %3$d ) %1$s GROUP BY t.`topic_id` ORDER BY FIELD(t.`topic_type`, %4$s) DESC, t.`topic_bumped` DESC %2$s ', $showDeleted ? '' : 'AND t.`topic_deleted` IS NULL', $hasPagination ? 'LIMIT :offset, :take' : '', MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT, implode(',', array_reverse(MSZ_TOPIC_TYPE_ORDER)), $showDeleted ? '' : 'AND `post_deleted` IS NULL', MSZ_FORUM_POSTS_PER_PAGE )); $getTopics->bind('forum_id', $forumId); $getTopics->bind('user_id', $userId); if($hasPagination) { $getTopics->bind('offset', $offset); $getTopics->bind('take', $take); } return $getTopics->fetchAll(); } function forum_topic_count_user(int $authorId, int $userId, bool $showDeleted = false): int { $getTopics = \Misuzu\DB::prepare(sprintf( ' SELECT COUNT(`topic_id`) FROM `msz_forum_topics` AS t WHERE t.`user_id` = :author_id %1$s ', $showDeleted ? '' : 'AND t.`topic_deleted` IS NULL' )); $getTopics->bind('author_id', $authorId); //$getTopics->bind('user_id', $userId); return (int)$getTopics->fetchColumn(); } // Remove unneccesary stuff from the sql stmt function forum_topic_listing_user( int $authorId, int $userId, int $offset = 0, int $take = 0, bool $showDeleted = false ): array { $hasPagination = $offset >= 0 && $take > 0; $getTopics = \Misuzu\DB::prepare(sprintf( ' SELECT :user_id AS `target_user_id`, t.`topic_id`, t.`topic_title`, t.`topic_locked`, t.`topic_type`, t.`topic_created`, t.`topic_bumped`, t.`topic_deleted`, t.`topic_count_views`, f.`forum_type`, au.`user_id` AS `author_id`, au.`username` AS `author_name`, COALESCE(au.`user_colour`, ar.`role_colour`) AS `author_colour`, lp.`post_id` AS `response_id`, lp.`post_created` AS `response_created`, lu.`user_id` AS `respondent_id`, lu.`username` AS `respondent_name`, COALESCE(lu.`user_colour`, lr.`role_colour`) AS `respondent_colour`, ( SELECT COUNT(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` %5$s ) AS `topic_count_posts`, ( SELECT CEIL(COUNT(`post_id`) / %6$d) FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` %5$s ) AS `topic_pages`, ( SELECT `target_user_id` > 0 AND t.`topic_bumped` > NOW() - INTERVAL 1 MONTH AND ( SELECT COUNT(ti.`topic_id`) < 1 FROM `msz_forum_topics_track` AS tt RIGHT JOIN `msz_forum_topics` AS ti ON ti.`topic_id` = tt.`topic_id` WHERE ti.`topic_id` = t.`topic_id` AND tt.`user_id` = `target_user_id` AND `track_last_read` >= `topic_bumped` ) ) AS `topic_unread`, ( SELECT COUNT(`post_id`) > 0 FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` AND `user_id` = `target_user_id` LIMIT 1 ) AS `topic_participated` FROM `msz_forum_topics` AS t LEFT JOIN `msz_forum_categories` AS f ON f.`forum_id` = t.`forum_id` LEFT JOIN `msz_users` AS au ON t.`user_id` = au.`user_id` LEFT JOIN `msz_roles` AS ar ON ar.`role_id` = au.`display_role` LEFT JOIN `msz_forum_posts` AS lp ON lp.`post_id` = ( SELECT `post_id` FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` %5$s ORDER BY `post_id` DESC LIMIT 1 ) LEFT JOIN `msz_users` AS lu ON lu.`user_id` = lp.`user_id` LEFT JOIN `msz_roles` AS lr ON lr.`role_id` = lu.`display_role` WHERE au.`user_id` = :author_id %1$s ORDER BY FIELD(t.`topic_type`, %4$s) DESC, t.`topic_bumped` DESC %2$s ', $showDeleted ? '' : 'AND t.`topic_deleted` IS NULL', $hasPagination ? 'LIMIT :offset, :take' : '', MSZ_TOPIC_TYPE_GLOBAL_ANNOUNCEMENT, implode(',', array_reverse(MSZ_TOPIC_TYPE_ORDER)), $showDeleted ? '' : 'AND `post_deleted` IS NULL', MSZ_FORUM_POSTS_PER_PAGE )); $getTopics->bind('author_id', $authorId); $getTopics->bind('user_id', $userId); if($hasPagination) { $getTopics->bind('offset', $offset); $getTopics->bind('take', $take); } return $getTopics->fetchAll(); } function forum_topic_listing_search(string $query, int $userId): array { $getTopics = \Misuzu\DB::prepare(sprintf( ' SELECT :user_id AS `target_user_id`, t.`topic_id`, t.`topic_title`, t.`topic_locked`, t.`topic_type`, t.`topic_created`, t.`topic_bumped`, t.`topic_deleted`, t.`topic_count_views`, f.`forum_type`, au.`user_id` AS `author_id`, au.`username` AS `author_name`, COALESCE(au.`user_colour`, ar.`role_colour`) AS `author_colour`, lp.`post_id` AS `response_id`, lp.`post_created` AS `response_created`, lu.`user_id` AS `respondent_id`, lu.`username` AS `respondent_name`, COALESCE(lu.`user_colour`, lr.`role_colour`) AS `respondent_colour`, ( SELECT COUNT(`post_id`) FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` AND `post_deleted` IS NULL ) AS `topic_count_posts`, ( SELECT CEIL(COUNT(`post_id`) / %2$d) FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` AND `post_deleted` IS NULL ) AS `topic_pages`, ( SELECT `target_user_id` > 0 AND t.`topic_bumped` > NOW() - INTERVAL 1 MONTH AND ( SELECT COUNT(ti.`topic_id`) < 1 FROM `msz_forum_topics_track` AS tt RIGHT JOIN `msz_forum_topics` AS ti ON ti.`topic_id` = tt.`topic_id` WHERE ti.`topic_id` = t.`topic_id` AND tt.`user_id` = `target_user_id` AND `track_last_read` >= `topic_bumped` ) ) AS `topic_unread`, ( SELECT COUNT(`post_id`) > 0 FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` AND `user_id` = `target_user_id` LIMIT 1 ) AS `topic_participated` FROM `msz_forum_topics` AS t LEFT JOIN `msz_forum_categories` AS f ON f.`forum_id` = t.`forum_id` LEFT JOIN `msz_users` AS au ON t.`user_id` = au.`user_id` LEFT JOIN `msz_roles` AS ar ON ar.`role_id` = au.`display_role` LEFT JOIN `msz_forum_posts` AS lp ON lp.`post_id` = ( SELECT `post_id` FROM `msz_forum_posts` WHERE `topic_id` = t.`topic_id` AND `post_deleted` IS NULL ORDER BY `post_id` DESC LIMIT 1 ) LEFT JOIN `msz_users` AS lu ON lu.`user_id` = lp.`user_id` LEFT JOIN `msz_roles` AS lr ON lr.`role_id` = lu.`display_role` WHERE MATCH(`topic_title`) AGAINST (:query IN NATURAL LANGUAGE MODE) AND t.`topic_deleted` IS NULL ORDER BY FIELD(t.`topic_type`, %1$s) DESC, t.`topic_bumped` DESC ', implode(',', array_reverse(MSZ_TOPIC_TYPE_ORDER)), MSZ_FORUM_POSTS_PER_PAGE )); $getTopics->bind('query', $query); $getTopics->bind('user_id', $userId); return $getTopics->fetchAll(); } function forum_topic_lock(int $topicId): bool { if($topicId < 1) { return false; } $markLocked = \Misuzu\DB::prepare(' UPDATE `msz_forum_topics` SET `topic_locked` = NOW() WHERE `topic_id` = :topic AND `topic_locked` IS NULL '); $markLocked->bind('topic', $topicId); return $markLocked->execute(); } function forum_topic_unlock(int $topicId): bool { if($topicId < 1) { return false; } $markUnlocked = \Misuzu\DB::prepare(' UPDATE `msz_forum_topics` SET `topic_locked` = NULL WHERE `topic_id` = :topic AND `topic_locked` IS NOT NULL '); $markUnlocked->bind('topic', $topicId); return $markUnlocked->execute(); } define('MSZ_E_FORUM_TOPIC_DELETE_OK', 0); // deleting is fine define('MSZ_E_FORUM_TOPIC_DELETE_USER', 1); // invalid user define('MSZ_E_FORUM_TOPIC_DELETE_TOPIC', 2); // topic doesn't exist define('MSZ_E_FORUM_TOPIC_DELETE_DELETED', 3); // topic is already marked as deleted define('MSZ_E_FORUM_TOPIC_DELETE_OWNER', 4); // you may only delete your own topics define('MSZ_E_FORUM_TOPIC_DELETE_OLD', 5); // topic has existed for too long to be deleted define('MSZ_E_FORUM_TOPIC_DELETE_PERM', 6); // you aren't allowed to delete topics define('MSZ_E_FORUM_TOPIC_DELETE_POSTS', 7); // the topic already has replies // only allow topics made within a day of posting to be deleted by normal users define('MSZ_FORUM_TOPIC_DELETE_TIME_LIMIT', 60 * 60 * 24); // only allow topics with a single post to be deleted, includes soft deleted posts define('MSZ_FORUM_TOPIC_DELETE_POST_LIMIT', 1); // set $userId to null for system request, make sure this is NEVER EVER null on user request // $topicId can also be a the return value of forum_topic_get if you already grabbed it once before function forum_topic_can_delete($topicId, ?int $userId = null): int { if($userId !== null && $userId < 1) { return MSZ_E_FORUM_TOPIC_DELETE_USER; } if(is_array($topicId)) { $topic = $topicId; } else { $topic = forum_topic_get((int)$topicId, true); } if(empty($topic)) { return MSZ_E_FORUM_TOPIC_DELETE_TOPIC; } $isSystemReq = $userId === null; $perms = $isSystemReq ? 0 : forum_perms_get_user($topic['forum_id'], $userId)[MSZ_FORUM_PERMS_GENERAL]; $canDeleteAny = $isSystemReq ? true : perms_check($perms, MSZ_FORUM_PERM_DELETE_ANY_POST); $canViewPost = $isSystemReq ? true : perms_check($perms, MSZ_FORUM_PERM_VIEW_FORUM); $postIsDeleted = !empty($topic['topic_deleted']); if(!$canViewPost) { return MSZ_E_FORUM_TOPIC_DELETE_TOPIC; } if($postIsDeleted) { return $canDeleteAny ? MSZ_E_FORUM_TOPIC_DELETE_DELETED : MSZ_E_FORUM_TOPIC_DELETE_TOPIC; } if($isSystemReq) { return MSZ_E_FORUM_TOPIC_DELETE_OK; } if(!$canDeleteAny) { if(!perms_check($perms, MSZ_FORUM_PERM_DELETE_POST)) { return MSZ_E_FORUM_TOPIC_DELETE_PERM; } if($topic['author_user_id'] !== $userId) { return MSZ_E_FORUM_TOPIC_DELETE_OWNER; } if(strtotime($topic['topic_created']) <= (time() - MSZ_FORUM_TOPIC_DELETE_TIME_LIMIT)) { return MSZ_E_FORUM_TOPIC_DELETE_OLD; } $totalReplies = $topic['topic_count_posts'] + $topic['topic_count_posts_deleted']; if($totalReplies > MSZ_E_FORUM_TOPIC_DELETE_POSTS) { return MSZ_E_FORUM_TOPIC_DELETE_POSTS; } } return MSZ_E_FORUM_TOPIC_DELETE_OK; } function forum_topic_delete(int $topicId): bool { if($topicId < 1) { return false; } $markTopicDeleted = \Misuzu\DB::prepare(' UPDATE `msz_forum_topics` SET `topic_deleted` = NOW() WHERE `topic_id` = :topic AND `topic_deleted` IS NULL '); $markTopicDeleted->bind('topic', $topicId); if(!$markTopicDeleted->execute()) { return false; } $markPostsDeleted = \Misuzu\DB::prepare(' UPDATE `msz_forum_posts` as p SET p.`post_deleted` = ( SELECT `topic_deleted` FROM `msz_forum_topics` WHERE `topic_id` = p.`topic_id` ) WHERE p.`topic_id` = :topic AND p.`post_deleted` IS NULL '); $markPostsDeleted->bind('topic', $topicId); return $markPostsDeleted->execute(); } function forum_topic_restore(int $topicId): bool { if($topicId < 1) { return false; } $markPostsRestored = \Misuzu\DB::prepare(' UPDATE `msz_forum_posts` as p SET p.`post_deleted` = NULL WHERE p.`topic_id` = :topic AND p.`post_deleted` = ( SELECT `topic_deleted` FROM `msz_forum_topics` WHERE `topic_id` = p.`topic_id` ) '); $markPostsRestored->bind('topic', $topicId); if(!$markPostsRestored->execute()) { return false; } $markTopicRestored = \Misuzu\DB::prepare(' UPDATE `msz_forum_topics` SET `topic_deleted` = NULL WHERE `topic_id` = :topic AND `topic_deleted` IS NOT NULL '); $markTopicRestored->bind('topic', $topicId); return $markTopicRestored->execute(); } function forum_topic_nuke(int $topicId): bool { if($topicId < 1) { return false; } $nukeTopic = \Misuzu\DB::prepare(' DELETE FROM `msz_forum_topics` WHERE `topic_id` = :topic '); $nukeTopic->bind('topic', $topicId); return $nukeTopic->execute(); } function forum_get_user_most_active_topic_info(int $userId): ?object { if($userId < 1) return null; global $cfg; $getActiveForum = \Misuzu\DB::prepare(sprintf( 'SELECT topic_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = :user AND post_deleted IS NULL AND forum_id NOT IN (%s) GROUP BY topic_id ORDER BY post_count DESC LIMIT 1', implode(',', $cfg->getValue('forum_leader.unranked.forum', \Misuzu\Config\IConfig::T_ARR)) )); $getActiveForum->bind('user', $userId); return $getActiveForum->fetchObject(); }