diff --git a/appinfo/info.xml b/appinfo/info.xml
index 56a40f5..7d39139 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -56,11 +56,12 @@ The forum integrates seamlessly with your Nextcloud instance, using your existin
- OCA\Forum\Cron\RebuildUserStatsTask
+ OCA\Forum\Cron\RebuildStatsTask
OCA\Forum\Command\TestNotifier
OCA\Forum\Command\RebuildUserStats
+ OCA\Forum\Command\SetRole
diff --git a/lib/Command/RebuildUserStats.php b/lib/Command/RebuildUserStats.php
index 19b58f5..264dbdb 100644
--- a/lib/Command/RebuildUserStats.php
+++ b/lib/Command/RebuildUserStats.php
@@ -7,14 +7,14 @@ declare(strict_types=1);
namespace OCA\Forum\Command;
-use OCA\Forum\Service\UserStatsService;
+use OCA\Forum\Service\StatsService;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class RebuildUserStats extends Command {
public function __construct(
- private UserStatsService $userStatsService,
+ private StatsService $statsService,
) {
parent::__construct();
}
@@ -28,7 +28,7 @@ class RebuildUserStats extends Command {
protected function execute(InputInterface $input, OutputInterface $output): int {
$output->writeln('Rebuilding user statistics for all users...');
- $result = $this->userStatsService->createStatsForAllUsers();
+ $result = $this->statsService->rebuildAllUserStats();
$output->writeln(sprintf('Processed %d users', $result['users']));
$output->writeln(sprintf('Created %d new user stats', $result['created']));
diff --git a/lib/Cron/RebuildStatsTask.php b/lib/Cron/RebuildStatsTask.php
new file mode 100644
index 0000000..80cd98c
--- /dev/null
+++ b/lib/Cron/RebuildStatsTask.php
@@ -0,0 +1,54 @@
+
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+namespace OCA\Forum\Cron;
+
+use OCA\Forum\Service\StatsService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use Psr\Log\LoggerInterface;
+
+class RebuildStatsTask extends TimedJob {
+ public function __construct(
+ ITimeFactory $time,
+ private StatsService $statsService,
+ private LoggerInterface $logger,
+ ) {
+ parent::__construct($time);
+
+ // Run once a week (604800 seconds = 7 days)
+ $this->setInterval(604800);
+ }
+
+ protected function run($arguments): void {
+ $this->logger->info('Starting weekly stats rebuild');
+
+ // Rebuild user stats
+ $userResult = $this->statsService->rebuildAllUserStats();
+ $this->logger->info('User stats rebuild completed', [
+ 'users' => $userResult['users'],
+ 'created' => $userResult['created'],
+ 'updated' => $userResult['updated'],
+ ]);
+
+ // Rebuild category stats
+ $categoryResult = $this->statsService->rebuildAllCategoryStats();
+ $this->logger->info('Category stats rebuild completed', [
+ 'categories' => $categoryResult['categories'],
+ 'updated' => $categoryResult['updated'],
+ ]);
+
+ // Rebuild thread stats
+ $threadResult = $this->statsService->rebuildAllThreadStats();
+ $this->logger->info('Thread stats rebuild completed', [
+ 'threads' => $threadResult['threads'],
+ 'updated' => $threadResult['updated'],
+ ]);
+
+ $this->logger->info('Weekly stats rebuild completed');
+ }
+}
diff --git a/lib/Cron/RebuildUserStatsTask.php b/lib/Cron/RebuildUserStatsTask.php
deleted file mode 100644
index 5097a72..0000000
--- a/lib/Cron/RebuildUserStatsTask.php
+++ /dev/null
@@ -1,38 +0,0 @@
-
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-namespace OCA\Forum\Cron;
-
-use OCA\Forum\Service\UserStatsService;
-use OCP\AppFramework\Utility\ITimeFactory;
-use OCP\BackgroundJob\TimedJob;
-use Psr\Log\LoggerInterface;
-
-class RebuildUserStatsTask extends TimedJob {
- public function __construct(
- ITimeFactory $time,
- private UserStatsService $userStatsService,
- private LoggerInterface $logger,
- ) {
- parent::__construct($time);
-
- // Run once a week (604800 seconds = 7 days)
- $this->setInterval(604800);
- }
-
- protected function run($arguments): void {
- $this->logger->info('Starting weekly user stats rebuild for all users');
-
- $result = $this->userStatsService->createStatsForAllUsers();
-
- $this->logger->info('User stats rebuild completed', [
- 'users' => $result['users'],
- 'created' => $result['created'],
- 'updated' => $result['updated'],
- ]);
- }
-}
diff --git a/lib/Listener/UserEventListener.php b/lib/Listener/UserEventListener.php
index 3c77516..32f5a80 100644
--- a/lib/Listener/UserEventListener.php
+++ b/lib/Listener/UserEventListener.php
@@ -8,7 +8,7 @@ declare(strict_types=1);
namespace OCA\Forum\Listener;
use OCA\Forum\Db\UserStatsMapper;
-use OCA\Forum\Service\UserStatsService;
+use OCA\Forum\Service\StatsService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\User\Events\UserCreatedEvent;
@@ -21,7 +21,7 @@ use Psr\Log\LoggerInterface;
class UserEventListener implements IEventListener {
public function __construct(
private UserStatsMapper $userStatsMapper,
- private UserStatsService $userStatsService,
+ private StatsService $statsService,
private LoggerInterface $logger,
) {
}
@@ -40,7 +40,7 @@ class UserEventListener implements IEventListener {
try {
// Create user stats with zero counts for new user
- $this->userStatsService->rebuildUserStats($userId);
+ $this->statsService->rebuildUserStats($userId);
$this->logger->info("Created user stats for new Nextcloud user: {$userId}");
} catch (\Exception $ex) {
$this->logger->error("Failed to create user stats for new user: {$userId}", [
diff --git a/lib/Migration/Version2Date20251114222614.php b/lib/Migration/Version2Date20251114222614.php
index d136a01..af274a6 100644
--- a/lib/Migration/Version2Date20251114222614.php
+++ b/lib/Migration/Version2Date20251114222614.php
@@ -8,14 +8,14 @@ declare(strict_types=1);
namespace OCA\Forum\Migration;
use Closure;
-use OCA\Forum\Service\UserStatsService;
+use OCA\Forum\Service\StatsService;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2Date20251114222614 extends SimpleMigrationStep {
public function __construct(
- private UserStatsService $userStatsService,
+ private StatsService $statsService,
) {
}
@@ -108,7 +108,7 @@ class Version2Date20251114222614 extends SimpleMigrationStep {
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
$output->info('Creating user statistics for all users...');
- $result = $this->userStatsService->createStatsForAllUsers();
+ $result = $this->statsService->rebuildAllUserStats();
$output->info(sprintf('Processed %d users', $result['users']));
$output->info(sprintf('Created %d new user stats', $result['created']));
diff --git a/lib/Service/UserStatsService.php b/lib/Service/StatsService.php
similarity index 58%
rename from lib/Service/UserStatsService.php
rename to lib/Service/StatsService.php
index 2d4203f..fb03d16 100644
--- a/lib/Service/UserStatsService.php
+++ b/lib/Service/StatsService.php
@@ -11,7 +11,7 @@ use OCP\IDBConnection;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
-class UserStatsService {
+class StatsService {
public function __construct(
private IDBConnection $db,
private IUserManager $userManager,
@@ -24,7 +24,7 @@ class UserStatsService {
*
* @return array{users: int, updated: int, created: int} Statistics about the creation
*/
- public function createStatsForAllUsers(): array {
+ public function rebuildAllUserStats(): array {
// Get all user IDs from Nextcloud
$users = [];
$this->userManager->callForAllUsers(function ($user) use (&$users) {
@@ -50,16 +50,6 @@ class UserStatsService {
];
}
- /**
- * Rebuild user statistics from actual post and thread counts
- *
- * @return array{users: int, updated: int, created: int} Statistics about the rebuild
- */
- public function rebuildAllUserStats(): array {
- // Delegate to createStatsForAllUsers which processes all Nextcloud users
- return $this->createStatsForAllUsers();
- }
-
/**
* Rebuild statistics for a single user
*
@@ -170,4 +160,128 @@ class UserStatsService {
}
}
}
+
+ /**
+ * Rebuild thread and post counts for all categories
+ *
+ * @return array{categories: int, updated: int} Statistics about the rebuild
+ */
+ public function rebuildAllCategoryStats(): array {
+ // Get all category IDs
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('id')
+ ->from('forum_categories');
+ $result = $qb->executeQuery();
+ $categoryIds = [];
+ while ($row = $result->fetch()) {
+ $categoryIds[] = (int)$row['id'];
+ }
+ $result->closeCursor();
+
+ $updated = 0;
+ foreach ($categoryIds as $categoryId) {
+ $this->rebuildCategoryStats($categoryId);
+ $updated++;
+ }
+
+ return [
+ 'categories' => count($categoryIds),
+ 'updated' => $updated,
+ ];
+ }
+
+ /**
+ * Rebuild statistics for a single category
+ *
+ * @param int $categoryId The category ID to rebuild stats for
+ * @return void
+ */
+ public function rebuildCategoryStats(int $categoryId): void {
+ // Count non-deleted threads in this category
+ $threadQb = $this->db->getQueryBuilder();
+ $threadQb->select($threadQb->func()->count('*', 'count'))
+ ->from('forum_threads')
+ ->where($threadQb->expr()->eq('category_id', $threadQb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
+ ->andWhere($threadQb->expr()->isNull('deleted_at'));
+ $threadResult = $threadQb->executeQuery();
+ $threadCount = (int)($threadResult->fetchOne() ?? 0);
+ $threadResult->closeCursor();
+
+ // Count non-deleted posts in non-deleted threads in this category
+ $postQb = $this->db->getQueryBuilder();
+ $postQb->select($postQb->func()->count('*', 'count'))
+ ->from('forum_posts', 'p')
+ ->innerJoin('p', 'forum_threads', 't', $postQb->expr()->eq('p.thread_id', 't.id'))
+ ->where($postQb->expr()->eq('t.category_id', $postQb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
+ ->andWhere($postQb->expr()->isNull('p.deleted_at'))
+ ->andWhere($postQb->expr()->isNull('t.deleted_at'));
+ $postResult = $postQb->executeQuery();
+ $postCount = (int)($postResult->fetchOne() ?? 0);
+ $postResult->closeCursor();
+
+ // Update category stats
+ $updateQb = $this->db->getQueryBuilder();
+ $updateQb->update('forum_categories')
+ ->set('thread_count', $updateQb->createNamedParameter($threadCount, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
+ ->set('post_count', $updateQb->createNamedParameter($postCount, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
+ ->set('updated_at', $updateQb->createNamedParameter(time(), \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
+ ->where($updateQb->expr()->eq('id', $updateQb->createNamedParameter($categoryId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)));
+ $updateQb->executeStatement();
+ }
+
+ /**
+ * Rebuild post counts for all threads
+ *
+ * @return array{threads: int, updated: int} Statistics about the rebuild
+ */
+ public function rebuildAllThreadStats(): array {
+ // Get all non-deleted thread IDs
+ $qb = $this->db->getQueryBuilder();
+ $qb->select('id')
+ ->from('forum_threads')
+ ->where($qb->expr()->isNull('deleted_at'));
+ $result = $qb->executeQuery();
+ $threadIds = [];
+ while ($row = $result->fetch()) {
+ $threadIds[] = (int)$row['id'];
+ }
+ $result->closeCursor();
+
+ $updated = 0;
+ foreach ($threadIds as $threadId) {
+ $this->rebuildThreadStats($threadId);
+ $updated++;
+ }
+
+ return [
+ 'threads' => count($threadIds),
+ 'updated' => $updated,
+ ];
+ }
+
+ /**
+ * Rebuild statistics for a single thread
+ *
+ * @param int $threadId The thread ID to rebuild stats for
+ * @return void
+ */
+ public function rebuildThreadStats(int $threadId): void {
+ // Count non-deleted posts in this thread
+ $postQb = $this->db->getQueryBuilder();
+ $postQb->select($postQb->func()->count('*', 'count'))
+ ->from('forum_posts')
+ ->where($postQb->expr()->eq('thread_id', $postQb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)))
+ ->andWhere($postQb->expr()->isNull('deleted_at'));
+ $postResult = $postQb->executeQuery();
+ $postCount = (int)($postResult->fetchOne() ?? 0);
+ $postResult->closeCursor();
+
+ // Update thread stats
+ $updateQb = $this->db->getQueryBuilder();
+ $updateQb->update('forum_threads')
+ ->set('post_count', $updateQb->createNamedParameter($postCount, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
+ ->set('updated_at', $updateQb->createNamedParameter(time(), \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT))
+ ->where($updateQb->expr()->eq('id', $updateQb->createNamedParameter($threadId, \OCP\DB\QueryBuilder\IQueryBuilder::PARAM_INT)));
+ $updateQb->executeStatement();
+ }
}