This repository has been archived on 2025-01-28. You can view files and clone it, but cannot push or open issues or pull requests.
misuzu-interim/src/Forum/ForumTopics.php

537 lines
21 KiB
PHP
Raw Normal View History

<?php
namespace Misuzu\Forum;
use InvalidArgumentException;
use RuntimeException;
use stdClass;
2024-10-05 02:40:29 +00:00
use Index\Db\{DbConnection,DbStatementCache,DbTools};
use Misuzu\Pagination;
use Misuzu\Users\UserInfo;
class ForumTopics {
2024-10-05 02:40:29 +00:00
private DbConnection $dbConn;
private DbStatementCache $cache;
2024-10-05 02:40:29 +00:00
public function __construct(DbConnection $dbConn) {
$this->dbConn = $dbConn;
$this->cache = new DbStatementCache($dbConn);
}
2024-12-02 21:33:15 +00:00
/** @param ForumCategoryInfo|string|null|array<ForumCategoryInfo|string|int> $categoryInfo */
public function countTopics(
ForumCategoryInfo|string|array|null $categoryInfo = null,
UserInfo|string|null $userInfo = null,
?bool $global = null,
?bool $deleted = null
): int {
if($categoryInfo instanceof ForumCategoryInfo)
$categoryInfo = $categoryInfo->id;
if($userInfo instanceof UserInfo)
2024-11-30 04:20:20 +00:00
$userInfo = $userInfo->id;
$hasCategoryInfo = $categoryInfo !== null;
$hasUserInfo = $userInfo !== null;
$hasGlobal = $global !== null;
$hasDeleted = $deleted !== null;
$args = 0;
$query = 'SELECT COUNT(*) FROM msz_forum_topics';
if($hasCategoryInfo || $hasGlobal) {
++$args;
// wow this sucks
$hasGlobalAndCategory = $hasCategoryInfo && $hasGlobal;
$query .= ' WHERE ';
if($hasGlobalAndCategory)
$query .= '(';
if($hasCategoryInfo) {
if(is_array($categoryInfo))
$query .= sprintf('forum_id IN (%s)', DbTools::prepareListString($categoryInfo));
else
$query .= 'forum_id = ?';
}
if($hasGlobalAndCategory)
$query .= ' OR ';
if($hasGlobal) // not sure why you would ever set this to false, but consistency!
$query .= sprintf('topic_type %s %d', $global ? '=' : '<>', ForumTopicInfo::TYPE_GLOBAL);
if($hasGlobalAndCategory)
$query .= ')';
}
if($hasUserInfo)
$query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
if($hasDeleted)
$query .= sprintf(' %s topic_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS');
$args = 0;
$stmt = $this->cache->get($query);
if($hasCategoryInfo) {
if(is_array($categoryInfo)) {
foreach($categoryInfo as $categoryInfoEntry)
$stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->id : (string)$categoryInfoEntry);
} else
$stmt->addParameter(++$args, $categoryInfo);
}
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() ? $result->getInteger(0) : 0;
}
2024-12-02 21:33:15 +00:00
/**
* @param ForumCategoryInfo|string|null|array<ForumCategoryInfo|string|int> $categoryInfo
* @param ?array{type?: string, author?: string, after?: string, query_string?: string} $searchQuery
* @return \Iterator<int, ForumTopicInfo>|ForumTopicInfo[]
*/
public function getTopics(
ForumCategoryInfo|string|array|null $categoryInfo = null,
UserInfo|string|null $userInfo = null,
?array $searchQuery = null,
?bool $global = null,
?bool $deleted = null,
?Pagination $pagination = null
2024-02-07 00:04:45 +00:00
): iterable {
// remove this hack when search server
$hasSearchQuery = $searchQuery !== null;
$hasAfterTopicId = false;
$afterTopicId = null;
$doSearchOrder = false;
if($hasSearchQuery) {
if(!empty($searchQuery['type'])
&& $searchQuery['type'] !== 'forum'
&& $searchQuery['type'] !== 'forum:topic')
return [];
$deleted = false;
$pagination = null;
$doSearchOrder = true;
if(!empty($searchQuery['author']))
$userInfo = $searchQuery['author'];
if(!empty($searchQuery['after'])) {
$hasAfterTopicId = true;
$afterTopicId = $searchQuery['after'];
}
$searchQuery = $searchQuery['query_string'];
$hasSearchQuery = !empty($searchQuery);
}
if($categoryInfo instanceof ForumCategoryInfo)
$categoryInfo = $categoryInfo->id;
if($userInfo instanceof UserInfo)
2024-11-30 04:20:20 +00:00
$userInfo = $userInfo->id;
$hasCategoryInfo = $categoryInfo !== null;
$hasUserInfo = $userInfo !== null;
$hasGlobal = $global !== null;
$hasDeleted = $deleted !== null;
$hasPagination = $pagination !== null;
$args = 0;
$query = 'SELECT topic_id, forum_id, user_id, topic_type, topic_title, topic_count_views, UNIX_TIMESTAMP(topic_created), UNIX_TIMESTAMP(topic_bumped), UNIX_TIMESTAMP(topic_deleted), UNIX_TIMESTAMP(topic_locked), (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NULL) AS topic_count_posts, (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NOT NULL) AS topic_count_posts_deleted FROM msz_forum_topics AS ft';
if($hasCategoryInfo || $hasGlobal) {
++$args;
// wow this sucks
$hasGlobalAndCategory = $hasCategoryInfo && $hasGlobal;
$query .= ' WHERE ';
if($hasGlobalAndCategory)
$query .= '(';
if($hasCategoryInfo) {
if(is_array($categoryInfo))
$query .= sprintf('forum_id IN (%s)', DbTools::prepareListString($categoryInfo));
else
$query .= 'forum_id = ?';
}
if($hasGlobalAndCategory)
$query .= ' OR ';
if($hasGlobal) // not sure why you would ever set this to false, but consistency!
$query .= sprintf('topic_type %s %d', $global ? '=' : '<>', ForumTopicInfo::TYPE_GLOBAL);
if($hasGlobalAndCategory)
$query .= ')';
}
if($hasUserInfo)
$query .= sprintf(' %s user_id = ?', ++$args > 1 ? 'AND' : 'WHERE');
if($hasAfterTopicId)
$query .= sprintf(' %s topic_id > ?', ++$args > 1 ? 'AND' : 'WHERE');
if($hasSearchQuery)
$query .= sprintf(' %s MATCH(topic_title) AGAINST (? IN NATURAL LANGUAGE MODE)', ++$args > 1 ? 'AND' : 'WHERE');
if($hasDeleted)
$query .= sprintf(' %s topic_deleted %s NULL', ++$args > 1 ? 'AND' : 'WHERE', $deleted ? 'IS NOT' : 'IS');
if($doSearchOrder) {
$query .= ' ORDER BY topic_id ASC LIMIT 20';
} else {
$query .= ' ORDER BY topic_type DESC, topic_bumped DESC';
if($hasPagination)
$query .= ' LIMIT ? OFFSET ?';
}
$args = 0;
$stmt = $this->cache->get($query);
if($hasCategoryInfo) {
if(is_array($categoryInfo)) {
foreach($categoryInfo as $categoryInfoEntry)
$stmt->addParameter(++$args, $categoryInfoEntry instanceof ForumCategoryInfo ? $categoryInfoEntry->id : (string)$categoryInfoEntry);
} else
$stmt->addParameter(++$args, $categoryInfo);
}
if($hasUserInfo)
$stmt->addParameter(++$args, $userInfo);
if($hasAfterTopicId)
$stmt->addParameter(++$args, $afterTopicId);
if($hasSearchQuery)
$stmt->addParameter(++$args, $searchQuery);
if($hasPagination) {
2024-12-19 01:22:26 +00:00
$stmt->addParameter(++$args, $pagination->range);
$stmt->addParameter(++$args, $pagination->offset);
}
$stmt->execute();
2024-02-07 00:04:45 +00:00
return $stmt->getResult()->getIterator(ForumTopicInfo::fromResult(...));
}
public function getTopic(
?string $topicId = null,
ForumPostInfo|string|null $postInfo = null,
?bool $deleted = null
): ForumTopicInfo {
$hasTopicId = $topicId !== null;
$hasPostInfo = $postInfo !== null;
$hasDeleted = $deleted !== null;
if(!$hasTopicId && !$hasPostInfo)
throw new InvalidArgumentException('At least one argument must be specified.');
if($hasTopicId && $hasPostInfo)
throw new InvalidArgumentException('Only one argument may be specified.');
$value = null;
$query = 'SELECT topic_id, forum_id, user_id, topic_type, topic_title, topic_count_views, UNIX_TIMESTAMP(topic_created), UNIX_TIMESTAMP(topic_bumped), UNIX_TIMESTAMP(topic_deleted), UNIX_TIMESTAMP(topic_locked), (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NULL) AS topic_count_posts, (SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ft.topic_id AND post_deleted IS NOT NULL) AS topic_count_posts_deleted FROM msz_forum_topics AS ft';
if($hasTopicId) {
$query .= ' WHERE topic_id = ?';
$value = $topicId;
}
if($hasPostInfo) {
if($postInfo instanceof ForumPostInfo) {
$query .= ' WHERE topic_id = ?';
$value = $postInfo->topicId;
} else {
$query .= ' WHERE topic_id = (SELECT topic_id FROM msz_forum_posts WHERE post_id = ?)';
$value = $postInfo;
}
}
if($hasDeleted)
$query .= sprintf(' AND topic_deleted %s NULL', $deleted ? 'IS NOT' : 'IS');
$stmt = $this->cache->get($query);
$stmt->addParameter(1, $value);
$stmt->execute();
$result = $stmt->getResult();
if(!$result->next())
throw new RuntimeException('Forum topic not found.');
2024-02-07 00:04:45 +00:00
return ForumTopicInfo::fromResult($result);
}
public function createTopic(
ForumCategoryInfo|string $categoryInfo,
UserInfo|string|null $userInfo,
string $title,
string|int $type = ForumTopicInfo::TYPE_DISCUSSION
): ForumTopicInfo {
if(is_string($type)) {
if(!array_key_exists($type, ForumTopicInfo::TYPE_ALIASES))
throw new InvalidArgumentException('$type is not a valid alias.');
$type = ForumTopicInfo::TYPE_ALIASES[$type];
}
if($categoryInfo instanceof ForumCategoryInfo)
$categoryInfo = $categoryInfo->id;
if($userInfo instanceof UserInfo)
2024-11-30 04:20:20 +00:00
$userInfo = $userInfo->id;
$stmt = $this->cache->get('INSERT INTO msz_forum_topics (forum_id, user_id, topic_type, topic_title) VALUES (?, ?, ?, ?)');
$stmt->addParameter(1, $categoryInfo);
$stmt->addParameter(2, $userInfo);
$stmt->addParameter(3, $type);
$stmt->addParameter(4, $title);
$stmt->execute();
return $this->getTopic(topicId: (string)$this->dbConn->getLastInsertId());
}
public function updateTopic(
ForumTopicInfo|string $topicInfo,
?string $title = null,
string|int|null $type = null
): void {
if($topicInfo instanceof ForumTopicInfo)
$topicInfo = $topicInfo->id;
$fields = [];
$values = [];
if($title !== null) {
$fields[] = 'topic_title = ?';
$values[] = $title;
}
if($type !== null) {
if(is_string($type)) {
if(!array_key_exists($type, ForumTopicInfo::TYPE_ALIASES))
throw new InvalidArgumentException('$type is not a valid type alias.');
$type = ForumTopicInfo::TYPE_ALIASES[$type];
}
$fields[] = 'topic_type = ?';
$values[] = $type;
}
if(empty($fields))
return;
$args = 0;
$stmt = $this->cache->get(sprintf('UPDATE msz_forum_topics SET %s WHERE topic_id = ?', implode(', ', $fields)));
foreach($values as $value)
$stmt->addParameter(++$args, $value);
$stmt->addParameter(++$args, $topicInfo);
$stmt->execute();
}
public function incrementTopicViews(ForumTopicInfo|string $topicInfo): void {
if($topicInfo instanceof ForumTopicInfo)
$topicInfo = $topicInfo->id;
$stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_count_views = topic_count_views + 1 WHERE topic_id = ?');
$stmt->addParameter(1, $topicInfo);
$stmt->execute();
}
public function bumpTopic(ForumTopicInfo|string $topicInfo): void {
if($topicInfo instanceof ForumTopicInfo)
$topicInfo = $topicInfo->id;
$stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_bumped = NOW() WHERE topic_id = ?');
$stmt->addParameter(1, $topicInfo);
$stmt->execute();
}
public function lockTopic(ForumTopicInfo|string $topicInfo): void {
if($topicInfo instanceof ForumTopicInfo)
$topicInfo = $topicInfo->id;
$stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NOW() WHERE topic_id = ?');
$stmt->addParameter(1, $topicInfo);
$stmt->execute();
}
public function unlockTopic(ForumTopicInfo|string $topicInfo): void {
if($topicInfo instanceof ForumTopicInfo)
$topicInfo = $topicInfo->id;
$stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_locked = NULL WHERE topic_id = ?');
$stmt->addParameter(1, $topicInfo);
$stmt->execute();
}
public function deleteTopic(ForumTopicInfo|string $topicInfo): void {
if($topicInfo instanceof ForumTopicInfo)
$topicInfo = $topicInfo->id;
$stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_deleted = COALESCE(topic_deleted, NOW()) WHERE topic_id = ? AND topic_deleted IS NULL');
$stmt->addParameter(1, $topicInfo);
$stmt->execute();
$stmt = $this->cache->get('UPDATE msz_forum_posts AS fp SET post_deleted = (SELECT topic_deleted FROM msz_forum_topics WHERE topic_id = fp.topic_id) WHERE topic_id = ? AND post_deleted = NULL');
$stmt->addParameter(1, $topicInfo);
$stmt->execute();
}
public function restoreTopic(ForumTopicInfo|string $topicInfo): void {
if($topicInfo instanceof ForumTopicInfo)
$topicInfo = $topicInfo->id;
$stmt = $this->cache->get('UPDATE msz_forum_posts AS fp SET post_deleted = NULL WHERE topic_id = ? AND post_deleted = (SELECT topic_deleted FROM msz_forum_topics WHERE topic_id = fp.topic_id)');
$stmt->addParameter(1, $topicInfo);
$stmt->execute();
$stmt = $this->cache->get('UPDATE msz_forum_topics SET topic_deleted = NULL WHERE topic_id = ?');
$stmt->addParameter(1, $topicInfo);
$stmt->execute();
}
public function nukeTopic(ForumTopicInfo|string $topicInfo): void {
if($topicInfo instanceof ForumTopicInfo)
$topicInfo = $topicInfo->id;
$stmt = $this->cache->get('DELETE FROM msz_forum_topics WHERE topic_id = ?');
$stmt->addParameter(1, $topicInfo);
$stmt->execute();
}
public function checkTopicParticipated(
ForumTopicInfo|string $topicInfo,
UserInfo|string|null $userInfo
): bool {
if($userInfo === null)
return false;
if($topicInfo instanceof ForumTopicInfo)
$topicInfo = $topicInfo->id;
if($userInfo instanceof UserInfo)
2024-11-30 04:20:20 +00:00
$userInfo = $userInfo->id;
$stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_posts WHERE topic_id = ? AND user_id = ?');
$stmt->addParameter(1, $topicInfo);
$stmt->addParameter(2, $userInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() && $result->getInteger(0) > 0;
}
public function checkTopicUnread(
ForumTopicInfo|string $topicInfo,
UserInfo|string|null $userInfo
): bool {
if($userInfo === null)
return false;
$topicInfoIsInstance = $topicInfo instanceof ForumTopicInfo;
if($topicInfoIsInstance && !$topicInfo->active)
return false;
$query = 'SELECT UNIX_TIMESTAMP(track_last_read) FROM msz_forum_topics_track AS ftt WHERE user_id = ? AND topic_id = ?';
if(!$topicInfoIsInstance)
$query .= ' AND track_last_read = (SELECT topic_bumped FROM msz_forum_topics WHERE topic_id = ftt.topic_id AND topic_bumped >= NOW() - INTERVAL 1 MONTH)';
$stmt = $this->cache->get($query);
2024-11-30 04:20:20 +00:00
$stmt->addParameter(1, $userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->addParameter(2, $topicInfoIsInstance ? $topicInfo->id : $topicInfo);
$stmt->execute();
$result = $stmt->getResult();
// user has never read this topic, return unread
if(!$result->next())
return true;
return $result->getInteger(0) < $topicInfo->bumpedTime;
}
2024-12-02 21:33:15 +00:00
/**
* @param array<ForumCategoryInfo|string|int> $exceptCategoryInfos
* @param array<ForumTopicInfo|string|int> $exceptTopicInfos
* @return object{success: false}|object{success: true, topicId: string, categoryId: string, postCount: int}
*/
public function getMostActiveTopicInfo(
UserInfo|string $userInfo,
array $exceptCategoryInfos = [],
array $exceptTopicInfos = [],
?bool $deleted = null
): object {
if($userInfo instanceof UserInfo)
2024-11-30 04:20:20 +00:00
$userInfo = $userInfo->id;
$hasExceptCategoryInfos = !empty($exceptCategoryInfos);
$hasExceptTopicInfos = !empty($exceptTopicInfos);
$hasDeleted = $deleted !== null;
$query = 'SELECT topic_id, forum_id, COUNT(*) AS post_count FROM msz_forum_posts WHERE user_id = ?';
if($hasDeleted)
$query .= sprintf(' AND post_deleted %s NULL', $deleted ? 'IS NOT' : 'IS');
if($hasExceptCategoryInfos)
$query .= sprintf(' AND forum_id NOT IN (%s)', DbTools::prepareListString($exceptCategoryInfos));
if($hasExceptTopicInfos)
$query .= sprintf(' AND topic_id NOT IN (%s)', DbTools::prepareListString($exceptTopicInfos));
$query .= ' GROUP BY topic_id ORDER BY post_count DESC LIMIT 1';
$args = 0;
$stmt = $this->cache->get($query);
$stmt->addParameter(++$args, $userInfo);
foreach($exceptCategoryInfos as $categoryInfo) {
if($categoryInfo instanceof ForumCategoryInfo)
$stmt->addParameter(++$args, $categoryInfo->id);
elseif(is_string($categoryInfo) || is_int($categoryInfo))
$stmt->addParameter(++$args, (string)$categoryInfo);
else
throw new InvalidArgumentException('$exceptCategoryInfos may only contain string ids or instances of ForumCategoryInfo.');
}
foreach($exceptTopicInfos as $topicInfo) {
if($topicInfo instanceof ForumTopicInfo)
$stmt->addParameter(++$args, $topicInfo->id);
elseif(is_string($topicInfo) || is_int($topicInfo))
$stmt->addParameter(++$args, (string)$topicInfo);
else
throw new InvalidArgumentException('$exceptTopicInfos may only contain string ids or instances of ForumTopicInfo.');
}
$stmt->execute();
$result = $stmt->getResult();
$info = new stdClass;
$info->success = $result->next();
if($info->success) {
$info->topicId = $result->getString(0);
$info->categoryId = $result->getString(1);
$info->postCount = $result->getInteger(2);
}
return $info;
}
public function checkUserHasReadTopic(
UserInfo|string|null $userInfo,
ForumTopicInfo|string $topicInfo
): bool {
// this method is primarily used to check if we should increment the view count
// guests shouldn't increment it so we just
if($userInfo === null)
return true;
$stmt = $this->cache->get('SELECT COUNT(*) FROM msz_forum_topics_track WHERE topic_id = ? AND user_id = ?');
$stmt->addParameter(1, $topicInfo instanceof ForumTopicInfo ? $topicInfo->id : $topicInfo);
2024-11-30 04:20:20 +00:00
$stmt->addParameter(2, $userInfo instanceof UserInfo ? $userInfo->id : $userInfo);
$stmt->execute();
$result = $stmt->getResult();
return $result->next() && $result->getInteger(0) > 0;
}
public function updateUserReadTopic(
UserInfo|string|null $userInfo,
ForumTopicInfo|string $topicInfo,
ForumCategoryInfo|string|null $categoryInfo = null
): void {
if($userInfo === null)
return;
if($userInfo instanceof UserInfo)
2024-11-30 04:20:20 +00:00
$userInfo = $userInfo->id;
if($topicInfo instanceof ForumTopicInfo) {
$categoryInfo = $topicInfo->categoryId;
$topicInfo = $topicInfo->id;
} else {
if($categoryInfo === null)
throw new InvalidArgumentException('$categoryInfo must be specified if $topicInfo is not an instance of ForumTopicInfo.');
if($categoryInfo instanceof ForumCategoryInfo)
$categoryInfo = $categoryInfo->id;
}
$stmt = $this->cache->get('REPLACE INTO msz_forum_topics_track (user_id, topic_id, forum_id, track_last_read) VALUES (?, ?, ?, NOW())');
$stmt->addParameter(1, $userInfo);
$stmt->addParameter(2, $topicInfo);
$stmt->addParameter(3, $categoryInfo);
$stmt->execute();
}
}