// SPDX-License-Identifier: AGPL-3.0-or-later namespace OCA\Forum\Db; use OCA\Forum\AppInfo\Application; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; /** * @template-extends QBMapper */ class ForumUserMapper extends QBMapper { public function __construct( IDBConnection $db, ) { parent::__construct($db, Application::tableName('forum_users'), ForumUser::class); } /** * Find forum user by user ID * * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws DoesNotExistException */ public function find(string $userId): ForumUser { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) ->where( $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) ); return $this->findEntity($qb); } /** * Find all forum users * * @return array */ public function findAll(): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) ->orderBy('post_count', 'DESC'); return $this->findEntities($qb); } /** * Find forum users by multiple user IDs * * @param array $userIds * @return array */ public function findByUserIds(array $userIds): array { if (empty($userIds)) { return []; } $qb = $this->db->getQueryBuilder(); $qb->select('*') ->from($this->getTableName()) ->where($qb->expr()->in('user_id', $qb->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY))); return $this->findEntities($qb); } /** * Create or update forum user (upsert pattern) * This is used when we need to ensure a forum user record exists */ public function createOrUpdate(string $userId): ForumUser { try { return $this->find($userId); } catch (DoesNotExistException $e) { $user = new ForumUser(); $user->setUserId($userId); $user->setPostCount(0); $user->setThreadCount(0); $user->setCreatedAt(time()); $user->setUpdatedAt(time()); /** @var ForumUser */ return $this->insert($user); } } /** * Increment post count for a user * Auto-creates record if user doesn't exist */ public function incrementPostCount(string $userId, int $amount = 1): void { $user = $this->createOrUpdate($userId); $user->setPostCount($user->getPostCount() + $amount); $user->setLastPostAt(time()); $user->setUpdatedAt(time()); $this->update($user); } /** * Increment thread count for a user * Auto-creates record if user doesn't exist */ public function incrementThreadCount(string $userId, int $amount = 1): void { $user = $this->createOrUpdate($userId); $user->setThreadCount($user->getThreadCount() + $amount); $user->setUpdatedAt(time()); $this->update($user); } /** * Decrement post count for a user */ public function decrementPostCount(string $userId, int $amount = 1): void { try { $user = $this->find($userId); $user->setPostCount(max(0, $user->getPostCount() - $amount)); $user->setUpdatedAt(time()); $this->update($user); } catch (DoesNotExistException $e) { // User record doesn't exist, nothing to decrement } } /** * Decrement thread count for a user */ public function decrementThreadCount(string $userId, int $amount = 1): void { try { $user = $this->find($userId); $user->setThreadCount(max(0, $user->getThreadCount() - $amount)); $user->setUpdatedAt(time()); $this->update($user); } catch (DoesNotExistException $e) { // User record doesn't exist, nothing to decrement } } /** * Get top contributors by total activity (posts + threads) * * @return array */ public function getTopContributors(int $limit = 10): array { $qb = $this->db->getQueryBuilder(); $qb->select('user_id', 'post_count', 'thread_count') ->from($this->getTableName()) ->where($qb->expr()->isNull('deleted_at')) ->andWhere( $qb->expr()->orX( $qb->expr()->gt('post_count', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)), $qb->expr()->gt('thread_count', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)) ) ); $result = $qb->executeQuery(); $rows = $result->fetchAll(); $result->closeCursor(); // Calculate total and sort in PHP $contributors = array_map(fn ($row) => [ 'userId' => $row['user_id'], 'postCount' => (int)$row['post_count'], 'threadCount' => (int)$row['thread_count'], 'total' => (int)$row['post_count'] + (int)$row['thread_count'], ], $rows); // Sort by total descending, then by thread count descending (when totals are equal) usort($contributors, fn ($a, $b) => $b['total'] <=> $a['total'] ?: $b['threadCount'] <=> $a['threadCount'] ); // Return top N (remove the total field as it was just for sorting) return array_slice(array_map(fn ($c) => [ 'userId' => $c['userId'], 'postCount' => $c['postCount'], 'threadCount' => $c['threadCount'], ], $contributors), 0, $limit); } /** * Get top contributors for a specific time period by counting posts/threads directly * * @return array */ public function getTopContributorsSince(int $timestamp, int $limit = 10): array { // Count posts per user since timestamp (excluding first posts which are counted as threads) $postsQb = $this->db->getQueryBuilder(); $postsQb->select('author_id') ->selectAlias($postsQb->func()->count('*'), 'count') ->from(Application::tableName('forum_posts')) ->where($postsQb->expr()->gte('created_at', $postsQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))) ->andWhere($postsQb->expr()->isNull('deleted_at')) ->andWhere($postsQb->expr()->eq('is_first_post', $postsQb->createNamedParameter(0, IQueryBuilder::PARAM_INT))) ->groupBy('author_id'); $postsResult = $postsQb->executeQuery(); $postsRows = $postsResult->fetchAll(); $postsResult->closeCursor(); $postsByUser = []; foreach ($postsRows as $row) { $postsByUser[$row['author_id']] = (int)$row['count']; } // Count threads per user since timestamp $threadsQb = $this->db->getQueryBuilder(); $threadsQb->select('author_id') ->selectAlias($threadsQb->func()->count('*'), 'count') ->from(Application::tableName('forum_threads')) ->where($threadsQb->expr()->gte('created_at', $threadsQb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))) ->andWhere($threadsQb->expr()->isNull('deleted_at')) ->groupBy('author_id'); $threadsResult = $threadsQb->executeQuery(); $threadsRows = $threadsResult->fetchAll(); $threadsResult->closeCursor(); $threadsByUser = []; foreach ($threadsRows as $row) { $threadsByUser[$row['author_id']] = (int)$row['count']; } // Combine and calculate totals $allUserIds = array_unique(array_merge(array_keys($postsByUser), array_keys($threadsByUser))); $contributors = []; foreach ($allUserIds as $userId) { $postCount = $postsByUser[$userId] ?? 0; $threadCount = $threadsByUser[$userId] ?? 0; $total = $postCount + $threadCount; if ($total > 0) { $contributors[] = [ 'userId' => $userId, 'postCount' => $postCount, 'threadCount' => $threadCount, 'total' => $total, ]; } } // Sort by total descending, then by thread count descending (when totals are equal) usort($contributors, fn ($a, $b) => $b['total'] <=> $a['total'] ?: $b['threadCount'] <=> $a['threadCount'] ); // Return top N (remove the total field as it was just for sorting) return array_slice(array_map(fn ($c) => [ 'userId' => $c['userId'], 'postCount' => $c['postCount'], 'threadCount' => $c['threadCount'], ], $contributors), 0, $limit); } /** * Count all users (excluding deleted) */ public function countAll(): int { $qb = $this->db->getQueryBuilder(); $qb->select($qb->func()->count('*', 'count')) ->from($this->getTableName()) ->where($qb->expr()->isNull('deleted_at')); $result = $qb->executeQuery(); $row = $result->fetch(); $result->closeCursor(); return (int)($row['count'] ?? 0); } /** * Count users who posted since a timestamp */ public function countSince(int $timestamp): int { $qb = $this->db->getQueryBuilder(); $qb->select($qb->func()->count('*', 'count')) ->from($this->getTableName()) ->where($qb->expr()->gte('created_at', $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))) ->andWhere($qb->expr()->isNull('deleted_at')); $result = $qb->executeQuery(); $row = $result->fetch(); $result->closeCursor(); return (int)($row['count'] ?? 0); } /** * Mark a user as deleted */ public function markDeleted(string $userId): void { try { $user = $this->find($userId); $user->setDeletedAt(time()); $user->setUpdatedAt(time()); $this->update($user); } catch (DoesNotExistException $e) { // User has no record, create one marking them as deleted $user = new ForumUser(); $user->setUserId($userId); $user->setPostCount(0); $user->setThreadCount(0); $user->setDeletedAt(time()); $user->setCreatedAt(time()); $user->setUpdatedAt(time()); $this->insert($user); } } }