aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_reminders/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_reminders/lib')
-rw-r--r--apps/files_reminders/lib/AppInfo/Application.php49
-rw-r--r--apps/files_reminders/lib/BackgroundJob/CleanUpReminders.php33
-rw-r--r--apps/files_reminders/lib/BackgroundJob/ScheduledNotifications.php44
-rw-r--r--apps/files_reminders/lib/Command/ListCommand.php102
-rw-r--r--apps/files_reminders/lib/Controller/ApiController.php131
-rw-r--r--apps/files_reminders/lib/Dav/PropFindPlugin.php82
-rw-r--r--apps/files_reminders/lib/Db/Reminder.php50
-rw-r--r--apps/files_reminders/lib/Db/ReminderMapper.php151
-rw-r--r--apps/files_reminders/lib/Exception/NodeNotFoundException.php15
-rw-r--r--apps/files_reminders/lib/Exception/ReminderNotFoundException.php15
-rw-r--r--apps/files_reminders/lib/Exception/UserNotFoundException.php15
-rw-r--r--apps/files_reminders/lib/Listener/LoadAdditionalScriptsListener.php41
-rw-r--r--apps/files_reminders/lib/Listener/NodeDeletedListener.php32
-rw-r--r--apps/files_reminders/lib/Listener/SabrePluginAddListener.php34
-rw-r--r--apps/files_reminders/lib/Listener/UserDeletedListener.php32
-rw-r--r--apps/files_reminders/lib/Migration/Version10000Date20230725162149.php65
-rw-r--r--apps/files_reminders/lib/Model/RichReminder.php57
-rw-r--r--apps/files_reminders/lib/Notification/Notifier.php110
-rw-r--r--apps/files_reminders/lib/Service/ReminderService.php217
-rw-r--r--apps/files_reminders/lib/SetupChecks/NeedNotificationsApp.php39
20 files changed, 1314 insertions, 0 deletions
diff --git a/apps/files_reminders/lib/AppInfo/Application.php b/apps/files_reminders/lib/AppInfo/Application.php
new file mode 100644
index 00000000000..2776e9db0b1
--- /dev/null
+++ b/apps/files_reminders/lib/AppInfo/Application.php
@@ -0,0 +1,49 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\AppInfo;
+
+use OCA\DAV\Events\SabrePluginAddEvent;
+use OCA\Files\Event\LoadAdditionalScriptsEvent;
+use OCA\FilesReminders\Listener\LoadAdditionalScriptsListener;
+use OCA\FilesReminders\Listener\NodeDeletedListener;
+use OCA\FilesReminders\Listener\SabrePluginAddListener;
+use OCA\FilesReminders\Listener\UserDeletedListener;
+use OCA\FilesReminders\Notification\Notifier;
+use OCA\FilesReminders\SetupChecks\NeedNotificationsApp;
+use OCP\AppFramework\App;
+use OCP\AppFramework\Bootstrap\IBootContext;
+use OCP\AppFramework\Bootstrap\IBootstrap;
+use OCP\AppFramework\Bootstrap\IRegistrationContext;
+use OCP\Files\Events\Node\NodeDeletedEvent;
+use OCP\User\Events\UserDeletedEvent;
+
+class Application extends App implements IBootstrap {
+ public const APP_ID = 'files_reminders';
+
+ public function __construct() {
+ parent::__construct(static::APP_ID);
+ }
+
+ public function boot(IBootContext $context): void {
+ }
+
+ public function register(IRegistrationContext $context): void {
+ $context->registerNotifierService(Notifier::class);
+
+ $context->registerEventListener(SabrePluginAddEvent::class, SabrePluginAddListener::class);
+
+ $context->registerEventListener(NodeDeletedEvent::class, NodeDeletedListener::class);
+ $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
+
+ $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalScriptsListener::class);
+
+ $context->registerSetupCheck(NeedNotificationsApp::class);
+ }
+}
diff --git a/apps/files_reminders/lib/BackgroundJob/CleanUpReminders.php b/apps/files_reminders/lib/BackgroundJob/CleanUpReminders.php
new file mode 100644
index 00000000000..35b72b190e8
--- /dev/null
+++ b/apps/files_reminders/lib/BackgroundJob/CleanUpReminders.php
@@ -0,0 +1,33 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\BackgroundJob;
+
+use OCA\FilesReminders\Service\ReminderService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+
+class CleanUpReminders extends TimedJob {
+ public function __construct(
+ ITimeFactory $time,
+ private ReminderService $reminderService,
+ ) {
+ parent::__construct($time);
+
+ $this->setInterval(24 * 60 * 60); // 1 day
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ protected function run($argument) {
+ $this->reminderService->cleanUp(500);
+ }
+}
diff --git a/apps/files_reminders/lib/BackgroundJob/ScheduledNotifications.php b/apps/files_reminders/lib/BackgroundJob/ScheduledNotifications.php
new file mode 100644
index 00000000000..ab8c762d674
--- /dev/null
+++ b/apps/files_reminders/lib/BackgroundJob/ScheduledNotifications.php
@@ -0,0 +1,44 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\BackgroundJob;
+
+use OCA\FilesReminders\Db\ReminderMapper;
+use OCA\FilesReminders\Service\ReminderService;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use Psr\Log\LoggerInterface;
+
+class ScheduledNotifications extends TimedJob {
+ public function __construct(
+ ITimeFactory $time,
+ protected ReminderMapper $reminderMapper,
+ protected ReminderService $reminderService,
+ protected LoggerInterface $logger,
+ ) {
+ parent::__construct($time);
+
+ $this->setInterval(60);
+ }
+
+ /**
+ * @SuppressWarnings(PHPMD.UnusedFormalParameter)
+ */
+ public function run($argument) {
+ $reminders = $this->reminderMapper->findOverdue();
+ foreach ($reminders as $reminder) {
+ try {
+ $this->reminderService->send($reminder);
+ } catch (DoesNotExistException $e) {
+ $this->logger->debug('Could not send notification for reminder with id ' . $reminder->getId());
+ }
+ }
+ }
+}
diff --git a/apps/files_reminders/lib/Command/ListCommand.php b/apps/files_reminders/lib/Command/ListCommand.php
new file mode 100644
index 00000000000..118d00c45d3
--- /dev/null
+++ b/apps/files_reminders/lib/Command/ListCommand.php
@@ -0,0 +1,102 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Command;
+
+use DateTimeInterface;
+use OC\Core\Command\Base;
+use OCA\FilesReminders\Model\RichReminder;
+use OCA\FilesReminders\Service\ReminderService;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+
+class ListCommand extends Base {
+ public function __construct(
+ private ReminderService $reminderService,
+ private IUserManager $userManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files:reminders')
+ ->setDescription('List file reminders')
+ ->addArgument(
+ 'user',
+ InputArgument::OPTIONAL,
+ 'list reminders for user',
+ )
+ ->addOption(
+ 'output',
+ null,
+ InputOption::VALUE_OPTIONAL,
+ 'Output format (plain, json or json_pretty, default is plain)',
+ $this->defaultOutputFormat,
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $io = new SymfonyStyle($input, $output);
+
+ $uid = $input->getArgument('user');
+ if ($uid !== null) {
+ /** @var string $uid */
+ $user = $this->userManager->get($uid);
+ if ($user === null) {
+ $io->error("Unknown user <$uid>");
+ return 1;
+ }
+ }
+
+ $reminders = $this->reminderService->getAll($user ?? null);
+
+ $outputOption = $input->getOption('output');
+ switch ($outputOption) {
+ case static::OUTPUT_FORMAT_JSON:
+ case static::OUTPUT_FORMAT_JSON_PRETTY:
+ $this->writeArrayInOutputFormat(
+ $input,
+ $io,
+ array_map(
+ fn (RichReminder $reminder) => $reminder->jsonSerialize(),
+ $reminders,
+ ),
+ '',
+ );
+ return 0;
+ default:
+ if (empty($reminders)) {
+ $io->text('No reminders');
+ return 0;
+ }
+
+ $io->table(
+ ['User Id', 'File Id', 'Path', 'Due Date', 'Updated At', 'Created At', 'Notified'],
+ array_map(
+ fn (RichReminder $reminder) => [
+ $reminder->getUserId(),
+ $reminder->getFileId(),
+ $reminder->getNode()->getPath(),
+ $reminder->getDueDate()->format(DateTimeInterface::ATOM), // ISO 8601
+ $reminder->getUpdatedAt()->format(DateTimeInterface::ATOM), // ISO 8601
+ $reminder->getCreatedAt()->format(DateTimeInterface::ATOM), // ISO 8601
+ $reminder->getNotified() ? 'true' : 'false',
+ ],
+ $reminders,
+ ),
+ );
+ return 0;
+ }
+ }
+}
diff --git a/apps/files_reminders/lib/Controller/ApiController.php b/apps/files_reminders/lib/Controller/ApiController.php
new file mode 100644
index 00000000000..c95a74a04f4
--- /dev/null
+++ b/apps/files_reminders/lib/Controller/ApiController.php
@@ -0,0 +1,131 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Controller;
+
+use DateTime;
+use DateTimeInterface;
+use DateTimeZone;
+use Exception;
+use OCA\FilesReminders\Exception\NodeNotFoundException;
+use OCA\FilesReminders\Exception\ReminderNotFoundException;
+use OCA\FilesReminders\Service\ReminderService;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\IRequest;
+use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
+
+class ApiController extends OCSController {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ protected ReminderService $reminderService,
+ protected IUserSession $userSession,
+ protected LoggerInterface $logger,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * Get a reminder
+ *
+ * @param int $fileId ID of the file
+ * @return DataResponse<Http::STATUS_OK, array{dueDate: ?string}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, list<empty>, array{}>
+ *
+ * 200: Reminder returned
+ * 401: Account not found
+ */
+ #[NoAdminRequired]
+ public function get(int $fileId): DataResponse {
+ $user = $this->userSession->getUser();
+ if ($user === null) {
+ return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ }
+
+ try {
+ $reminder = $this->reminderService->getDueForUser($user, $fileId);
+ if ($reminder === null) {
+ return new DataResponse(['dueDate' => null], Http::STATUS_OK);
+ }
+ return new DataResponse([
+ 'dueDate' => $reminder->getDueDate()->format(DateTimeInterface::ATOM), // ISO 8601
+ ], Http::STATUS_OK);
+ } catch (NodeNotFoundException $e) {
+ return new DataResponse(['dueDate' => null], Http::STATUS_OK);
+ }
+ }
+
+ /**
+ * Set a reminder
+ *
+ * @param int $fileId ID of the file
+ * @param string $dueDate ISO 8601 formatted date time string
+ *
+ * @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED|Http::STATUS_BAD_REQUEST|Http::STATUS_UNAUTHORIZED|Http::STATUS_NOT_FOUND, list<empty>, array{}>
+ *
+ * 200: Reminder updated
+ * 201: Reminder created successfully
+ * 400: Creating reminder is not possible
+ * 401: Account not found
+ * 404: File not found
+ */
+ #[NoAdminRequired]
+ public function set(int $fileId, string $dueDate): DataResponse {
+ try {
+ $dueDate = (new DateTime($dueDate))->setTimezone(new DateTimeZone('UTC'));
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
+ }
+
+ $user = $this->userSession->getUser();
+ if ($user === null) {
+ return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ }
+
+ try {
+ $created = $this->reminderService->createOrUpdate($user, $fileId, $dueDate);
+ if ($created) {
+ return new DataResponse([], Http::STATUS_CREATED);
+ }
+ return new DataResponse([], Http::STATUS_OK);
+ } catch (NodeNotFoundException $e) {
+ return new DataResponse([], Http::STATUS_NOT_FOUND);
+ }
+ }
+
+ /**
+ * Remove a reminder
+ *
+ * @param int $fileId ID of the file
+ *
+ * @return DataResponse<Http::STATUS_OK|Http::STATUS_UNAUTHORIZED|Http::STATUS_NOT_FOUND, list<empty>, array{}>
+ *
+ * 200: Reminder deleted successfully
+ * 401: Account not found
+ * 404: Reminder not found
+ */
+ #[NoAdminRequired]
+ public function remove(int $fileId): DataResponse {
+ $user = $this->userSession->getUser();
+ if ($user === null) {
+ return new DataResponse([], Http::STATUS_UNAUTHORIZED);
+ }
+
+ try {
+ $this->reminderService->remove($user, $fileId);
+ return new DataResponse([], Http::STATUS_OK);
+ } catch (NodeNotFoundException|ReminderNotFoundException $e) {
+ return new DataResponse([], Http::STATUS_NOT_FOUND);
+ }
+ }
+}
diff --git a/apps/files_reminders/lib/Dav/PropFindPlugin.php b/apps/files_reminders/lib/Dav/PropFindPlugin.php
new file mode 100644
index 00000000000..014e636eb2d
--- /dev/null
+++ b/apps/files_reminders/lib/Dav/PropFindPlugin.php
@@ -0,0 +1,82 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Dav;
+
+use DateTimeInterface;
+use OCA\DAV\Connector\Sabre\Directory;
+use OCA\DAV\Connector\Sabre\Node;
+use OCA\FilesReminders\Service\ReminderService;
+use OCP\Files\Folder;
+use OCP\IUser;
+use OCP\IUserSession;
+use Sabre\DAV\INode;
+use Sabre\DAV\PropFind;
+use Sabre\DAV\Server;
+use Sabre\DAV\ServerPlugin;
+
+class PropFindPlugin extends ServerPlugin {
+
+ public const REMINDER_DUE_DATE_PROPERTY = '{http://nextcloud.org/ns}reminder-due-date';
+
+ public function __construct(
+ private ReminderService $reminderService,
+ private IUserSession $userSession,
+ ) {
+ }
+
+ public function initialize(Server $server): void {
+ $server->on('propFind', [$this, 'propFind']);
+ }
+
+ public function propFind(PropFind $propFind, INode $node) {
+ if (!in_array(static::REMINDER_DUE_DATE_PROPERTY, $propFind->getRequestedProperties())) {
+ return;
+ }
+
+ if (!($node instanceof Node)) {
+ return;
+ }
+
+ if (
+ $node instanceof Directory
+ && $propFind->getDepth() > 0
+ && $propFind->getStatus(static::REMINDER_DUE_DATE_PROPERTY) !== null
+ ) {
+ $folder = $node->getNode();
+ $this->cacheFolder($folder);
+ }
+
+ $propFind->handle(
+ static::REMINDER_DUE_DATE_PROPERTY,
+ function () use ($node) {
+ $user = $this->userSession->getUser();
+ if (!($user instanceof IUser)) {
+ return '';
+ }
+
+ $fileId = $node->getId();
+ $reminder = $this->reminderService->getDueForUser($user, $fileId, false);
+ if ($reminder === null) {
+ return '';
+ }
+
+ return $reminder->getDueDate()->format(DateTimeInterface::ATOM); // ISO 8601
+ },
+ );
+ }
+
+ private function cacheFolder(Folder $folder): void {
+ $user = $this->userSession->getUser();
+ if (!($user instanceof IUser)) {
+ return;
+ }
+ $this->reminderService->cacheFolder($user, $folder);
+ }
+}
diff --git a/apps/files_reminders/lib/Db/Reminder.php b/apps/files_reminders/lib/Db/Reminder.php
new file mode 100644
index 00000000000..1a8ba15063e
--- /dev/null
+++ b/apps/files_reminders/lib/Db/Reminder.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Db;
+
+use DateTime;
+use OCP\AppFramework\Db\Entity;
+
+/**
+ * @method void setUserId(string $userId)
+ * @method string getUserId()
+ *
+ * @method void setFileId(int $fileId)
+ * @method int getFileId()
+ *
+ * @method void setDueDate(DateTime $dueDate)
+ * @method DateTime getDueDate()
+ *
+ * @method void setUpdatedAt(DateTime $updatedAt)
+ * @method DateTime getUpdatedAt()
+ *
+ * @method void setCreatedAt(DateTime $createdAt)
+ * @method DateTime getCreatedAt()
+ *
+ * @method void setNotified(bool $notified)
+ * @method bool getNotified()
+ */
+class Reminder extends Entity {
+ protected $userId;
+ protected $fileId;
+ protected $dueDate;
+ protected $updatedAt;
+ protected $createdAt;
+ protected $notified = false;
+
+ public function __construct() {
+ $this->addType('userId', 'string');
+ $this->addType('fileId', 'integer');
+ $this->addType('dueDate', 'datetime');
+ $this->addType('updatedAt', 'datetime');
+ $this->addType('createdAt', 'datetime');
+ $this->addType('notified', 'boolean');
+ }
+}
diff --git a/apps/files_reminders/lib/Db/ReminderMapper.php b/apps/files_reminders/lib/Db/ReminderMapper.php
new file mode 100644
index 00000000000..63cba437d07
--- /dev/null
+++ b/apps/files_reminders/lib/Db/ReminderMapper.php
@@ -0,0 +1,151 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Db;
+
+use DateTime;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\QBMapper;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Files\Folder;
+use OCP\Files\Node;
+use OCP\Files\NotFoundException;
+use OCP\IDBConnection;
+use OCP\IUser;
+
+/**
+ * @template-extends QBMapper<Reminder>
+ */
+class ReminderMapper extends QBMapper {
+ public const TABLE_NAME = 'files_reminders';
+
+ public function __construct(IDBConnection $db) {
+ parent::__construct(
+ $db,
+ static::TABLE_NAME,
+ Reminder::class,
+ );
+ }
+
+ public function markNotified(Reminder $reminder): Reminder {
+ $reminderUpdate = new Reminder();
+ $reminderUpdate->setId($reminder->getId());
+ $reminderUpdate->setNotified(true);
+ return $this->update($reminderUpdate);
+ }
+
+ /**
+ * @throws DoesNotExistException
+ */
+ public function findDueForUser(IUser $user, int $fileId): Reminder {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)))
+ ->andWhere($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId, IQueryBuilder::PARAM_INT)))
+ ->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)));
+
+ return $this->findEntity($qb);
+ }
+
+ /**
+ * @return Reminder[]
+ */
+ public function findAll() {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
+ ->from($this->getTableName())
+ ->orderBy('due_date', 'ASC');
+
+ return $this->findEntities($qb);
+ }
+
+ /**
+ * @return Reminder[]
+ */
+ public function findAllForUser(IUser $user) {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)))
+ ->orderBy('due_date', 'ASC');
+
+ return $this->findEntities($qb);
+ }
+
+ /**
+ * @return Reminder[]
+ */
+ public function findAllForNode(Node $node) {
+ try {
+ $nodeId = $node->getId();
+ } catch (NotFoundException $e) {
+ return [];
+ }
+
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($nodeId, IQueryBuilder::PARAM_INT)))
+ ->orderBy('due_date', 'ASC');
+
+ return $this->findEntities($qb);
+ }
+
+ /**
+ * @return Reminder[]
+ */
+ public function findOverdue() {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
+ ->from($this->getTableName())
+ ->where($qb->expr()->lt('due_date', $qb->createFunction('NOW()')))
+ ->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
+ ->orderBy('due_date', 'ASC');
+
+ return $this->findEntities($qb);
+ }
+
+ /**
+ * @return Reminder[]
+ */
+ public function findNotified(DateTime $buffer, ?int $limit = null) {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('id', 'user_id', 'file_id', 'due_date', 'updated_at', 'created_at', 'notified')
+ ->from($this->getTableName())
+ ->where($qb->expr()->eq('notified', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL)))
+ ->andWhere($qb->expr()->lt('due_date', $qb->createNamedParameter($buffer, IQueryBuilder::PARAM_DATETIME_MUTABLE)))
+ ->orderBy('due_date', 'ASC')
+ ->setMaxResults($limit);
+
+ return $this->findEntities($qb);
+ }
+
+ /**
+ * @return Reminder[]
+ */
+ public function findAllInFolder(IUser $user, Folder $folder) {
+ $qb = $this->db->getQueryBuilder();
+
+ $qb->select('r.id', 'r.user_id', 'r.file_id', 'r.due_date', 'r.updated_at', 'r.created_at', 'r.notified')
+ ->from($this->getTableName(), 'r')
+ ->innerJoin('r', 'filecache', 'f', $qb->expr()->eq('r.file_id', 'f.fileid'))
+ ->where($qb->expr()->eq('r.user_id', $qb->createNamedParameter($user->getUID(), IQueryBuilder::PARAM_STR)))
+ ->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($folder->getId(), IQueryBuilder::PARAM_INT)))
+ ->orderBy('r.due_date', 'ASC');
+
+ return $this->findEntities($qb);
+ }
+}
diff --git a/apps/files_reminders/lib/Exception/NodeNotFoundException.php b/apps/files_reminders/lib/Exception/NodeNotFoundException.php
new file mode 100644
index 00000000000..65e1b28fe1e
--- /dev/null
+++ b/apps/files_reminders/lib/Exception/NodeNotFoundException.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Exception;
+
+use Exception;
+
+class NodeNotFoundException extends Exception {
+}
diff --git a/apps/files_reminders/lib/Exception/ReminderNotFoundException.php b/apps/files_reminders/lib/Exception/ReminderNotFoundException.php
new file mode 100644
index 00000000000..fd7031a834f
--- /dev/null
+++ b/apps/files_reminders/lib/Exception/ReminderNotFoundException.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Exception;
+
+use Exception;
+
+class ReminderNotFoundException extends Exception {
+}
diff --git a/apps/files_reminders/lib/Exception/UserNotFoundException.php b/apps/files_reminders/lib/Exception/UserNotFoundException.php
new file mode 100644
index 00000000000..d1ddf9148cb
--- /dev/null
+++ b/apps/files_reminders/lib/Exception/UserNotFoundException.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Exception;
+
+use Exception;
+
+class UserNotFoundException extends Exception {
+}
diff --git a/apps/files_reminders/lib/Listener/LoadAdditionalScriptsListener.php b/apps/files_reminders/lib/Listener/LoadAdditionalScriptsListener.php
new file mode 100644
index 00000000000..765bf1e3ce2
--- /dev/null
+++ b/apps/files_reminders/lib/Listener/LoadAdditionalScriptsListener.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Listener;
+
+use OCA\Files\Event\LoadAdditionalScriptsEvent;
+use OCA\FilesReminders\AppInfo\Application;
+use OCP\App\IAppManager;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Util;
+use Psr\Log\LoggerInterface;
+
+/** @template-implements IEventListener<LoadAdditionalScriptsEvent> */
+class LoadAdditionalScriptsListener implements IEventListener {
+ public function __construct(
+ private IAppManager $appManager,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof LoadAdditionalScriptsEvent)) {
+ return;
+ }
+
+ if (!$this->appManager->isEnabledForUser(Application::APP_ID)
+ || !$this->appManager->isEnabledForUser('notifications')
+ ) {
+ return;
+ }
+
+ Util::addInitScript(Application::APP_ID, 'init');
+ }
+}
diff --git a/apps/files_reminders/lib/Listener/NodeDeletedListener.php b/apps/files_reminders/lib/Listener/NodeDeletedListener.php
new file mode 100644
index 00000000000..06a4733e6cd
--- /dev/null
+++ b/apps/files_reminders/lib/Listener/NodeDeletedListener.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Listener;
+
+use OCA\FilesReminders\Service\ReminderService;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Files\Events\Node\NodeDeletedEvent;
+
+/** @template-implements IEventListener<NodeDeletedEvent> */
+class NodeDeletedListener implements IEventListener {
+ public function __construct(
+ private ReminderService $reminderService,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof NodeDeletedEvent)) {
+ return;
+ }
+
+ $node = $event->getNode();
+ $this->reminderService->removeAllForNode($node);
+ }
+}
diff --git a/apps/files_reminders/lib/Listener/SabrePluginAddListener.php b/apps/files_reminders/lib/Listener/SabrePluginAddListener.php
new file mode 100644
index 00000000000..b2c4501f9af
--- /dev/null
+++ b/apps/files_reminders/lib/Listener/SabrePluginAddListener.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Listener;
+
+use OCA\DAV\Events\SabrePluginAddEvent;
+use OCA\FilesReminders\Dav\PropFindPlugin;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use Psr\Container\ContainerInterface;
+
+/** @template-implements IEventListener<SabrePluginAddEvent> */
+class SabrePluginAddListener implements IEventListener {
+ public function __construct(
+ private ContainerInterface $container,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof SabrePluginAddEvent)) {
+ return;
+ }
+
+ $server = $event->getServer();
+ $plugin = $this->container->get(PropFindPlugin::class);
+ $server->addPlugin($plugin);
+ }
+}
diff --git a/apps/files_reminders/lib/Listener/UserDeletedListener.php b/apps/files_reminders/lib/Listener/UserDeletedListener.php
new file mode 100644
index 00000000000..366a5e60420
--- /dev/null
+++ b/apps/files_reminders/lib/Listener/UserDeletedListener.php
@@ -0,0 +1,32 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Listener;
+
+use OCA\FilesReminders\Service\ReminderService;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\User\Events\UserDeletedEvent;
+
+/** @template-implements IEventListener<UserDeletedEvent> */
+class UserDeletedListener implements IEventListener {
+ public function __construct(
+ private ReminderService $reminderService,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof UserDeletedEvent)) {
+ return;
+ }
+
+ $user = $event->getUser();
+ $this->reminderService->removeAllForUser($user);
+ }
+}
diff --git a/apps/files_reminders/lib/Migration/Version10000Date20230725162149.php b/apps/files_reminders/lib/Migration/Version10000Date20230725162149.php
new file mode 100644
index 00000000000..74614c6515e
--- /dev/null
+++ b/apps/files_reminders/lib/Migration/Version10000Date20230725162149.php
@@ -0,0 +1,65 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Migration;
+
+use Closure;
+use OCA\FilesReminders\Db\ReminderMapper;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version10000Date20230725162149 extends SimpleMigrationStep {
+ /**
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if ($schema->hasTable(ReminderMapper::TABLE_NAME)) {
+ return null;
+ }
+
+ $table = $schema->createTable(ReminderMapper::TABLE_NAME);
+ $table->addColumn('id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 20,
+ 'unsigned' => true,
+ ]);
+ $table->addColumn('user_id', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('file_id', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 20,
+ 'unsigned' => true,
+ ]);
+ $table->addColumn('due_date', Types::DATETIME, [
+ 'notnull' => true,
+ ]);
+ $table->addColumn('updated_at', Types::DATETIME, [
+ 'notnull' => true,
+ ]);
+ $table->addColumn('created_at', Types::DATETIME, [
+ 'notnull' => true,
+ ]);
+ $table->addColumn('notified', Types::BOOLEAN, [
+ 'notnull' => false,
+ 'default' => false,
+ ]);
+ $table->setPrimaryKey(['id']);
+ $table->addUniqueIndex(['user_id', 'file_id', 'due_date'], 'reminders_uniq_idx');
+
+ return $schema;
+ }
+}
diff --git a/apps/files_reminders/lib/Model/RichReminder.php b/apps/files_reminders/lib/Model/RichReminder.php
new file mode 100644
index 00000000000..4f221252717
--- /dev/null
+++ b/apps/files_reminders/lib/Model/RichReminder.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Model;
+
+use DateTimeInterface;
+use JsonSerializable;
+use OCA\FilesReminders\Db\Reminder;
+use OCA\FilesReminders\Exception\NodeNotFoundException;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+
+class RichReminder extends Reminder implements JsonSerializable {
+ public function __construct(
+ private Reminder $reminder,
+ private IRootFolder $root,
+ ) {
+ parent::__construct();
+ }
+
+ /**
+ * @throws NodeNotFoundException
+ */
+ public function getNode(): Node {
+ $node = $this->root->getUserFolder($this->getUserId())->getFirstNodeById($this->getFileId());
+ if (!$node) {
+ throw new NodeNotFoundException();
+ }
+ return $node;
+ }
+
+ protected function getter(string $name): mixed {
+ return $this->reminder->getter($name);
+ }
+
+ public function __call(string $methodName, array $args) {
+ return $this->reminder->__call($methodName, $args);
+ }
+
+ public function jsonSerialize(): array {
+ return [
+ 'userId' => $this->getUserId(),
+ 'fileId' => $this->getFileId(),
+ 'path' => $this->getNode()->getPath(),
+ 'dueDate' => $this->getDueDate()->format(DateTimeInterface::ATOM), // ISO 8601
+ 'updatedAt' => $this->getUpdatedAt()->format(DateTimeInterface::ATOM), // ISO 8601
+ 'createdAt' => $this->getCreatedAt()->format(DateTimeInterface::ATOM), // ISO 8601
+ 'notified' => $this->getNotified(),
+ ];
+ }
+}
diff --git a/apps/files_reminders/lib/Notification/Notifier.php b/apps/files_reminders/lib/Notification/Notifier.php
new file mode 100644
index 00000000000..337ef04c814
--- /dev/null
+++ b/apps/files_reminders/lib/Notification/Notifier.php
@@ -0,0 +1,110 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Notification;
+
+use OCA\FilesReminders\AppInfo\Application;
+use OCP\Files\FileInfo;
+use OCP\Files\IRootFolder;
+use OCP\IURLGenerator;
+use OCP\L10N\IFactory;
+use OCP\Notification\AlreadyProcessedException;
+use OCP\Notification\IAction;
+use OCP\Notification\INotification;
+use OCP\Notification\INotifier;
+use OCP\Notification\UnknownNotificationException;
+
+class Notifier implements INotifier {
+ public function __construct(
+ protected IFactory $l10nFactory,
+ protected IURLGenerator $urlGenerator,
+ protected IRootFolder $root,
+ ) {
+ }
+
+ public function getID(): string {
+ return Application::APP_ID;
+ }
+
+ public function getName(): string {
+ return $this->l10nFactory->get(Application::APP_ID)->t('File reminders');
+ }
+
+ /**
+ * @throws UnknownNotificationException
+ */
+ public function prepare(INotification $notification, string $languageCode): INotification {
+ $l = $this->l10nFactory->get(Application::APP_ID, $languageCode);
+
+ if ($notification->getApp() !== Application::APP_ID) {
+ throw new UnknownNotificationException();
+ }
+
+ switch ($notification->getSubject()) {
+ case 'reminder-due':
+ $params = $notification->getSubjectParameters();
+ $fileId = $params['fileId'];
+
+ $node = $this->root->getUserFolder($notification->getUser())->getFirstNodeById($fileId);
+ if ($node === null) {
+ throw new AlreadyProcessedException();
+ }
+
+ $path = rtrim($node->getPath(), '/');
+ if (strpos($path, '/' . $notification->getUser() . '/files/') === 0) {
+ // Remove /user/files/...
+ $fullPath = $path;
+ [,,, $path] = explode('/', $fullPath, 4);
+ }
+
+ $link = $this->urlGenerator->linkToRouteAbsolute(
+ 'files.viewcontroller.showFile',
+ ['fileid' => $node->getId()],
+ );
+
+ // TRANSLATORS The name placeholder is for a file or folder name
+ $subject = $l->t('Reminder for {name}');
+ $notification
+ ->setRichSubject(
+ $subject,
+ [
+ 'name' => [
+ 'type' => 'highlight',
+ 'id' => (string)$node->getId(),
+ 'name' => $node->getName(),
+ ],
+ ],
+ )
+ ->setLink($link);
+
+ $label = match ($node->getType()) {
+ FileInfo::TYPE_FILE => $l->t('View file'),
+ FileInfo::TYPE_FOLDER => $l->t('View folder'),
+ };
+
+ $this->addActionButton($notification, $label);
+ break;
+ default:
+ throw new UnknownNotificationException();
+ }
+
+ return $notification;
+ }
+
+ protected function addActionButton(INotification $notification, string $label): void {
+ $action = $notification->createAction();
+
+ $action->setLabel($label)
+ ->setParsedLabel($label)
+ ->setLink($notification->getLink(), IAction::TYPE_WEB)
+ ->setPrimary(true);
+
+ $notification->addParsedAction($action);
+ }
+}
diff --git a/apps/files_reminders/lib/Service/ReminderService.php b/apps/files_reminders/lib/Service/ReminderService.php
new file mode 100644
index 00000000000..6ee39562076
--- /dev/null
+++ b/apps/files_reminders/lib/Service/ReminderService.php
@@ -0,0 +1,217 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\Service;
+
+use DateTime;
+use DateTimeZone;
+use OCA\FilesReminders\AppInfo\Application;
+use OCA\FilesReminders\Db\Reminder;
+use OCA\FilesReminders\Db\ReminderMapper;
+use OCA\FilesReminders\Exception\NodeNotFoundException;
+use OCA\FilesReminders\Exception\ReminderNotFoundException;
+use OCA\FilesReminders\Exception\UserNotFoundException;
+use OCA\FilesReminders\Model\RichReminder;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\Node;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\IURLGenerator;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Notification\IManager as INotificationManager;
+use Psr\Log\LoggerInterface;
+use Throwable;
+
+class ReminderService {
+
+ private ICache $cache;
+
+ public function __construct(
+ protected IUserManager $userManager,
+ protected IURLGenerator $urlGenerator,
+ protected INotificationManager $notificationManager,
+ protected ReminderMapper $reminderMapper,
+ protected IRootFolder $root,
+ protected LoggerInterface $logger,
+ protected ICacheFactory $cacheFactory,
+ ) {
+ $this->cache = $this->cacheFactory->createInMemory();
+ }
+
+ public function cacheFolder(IUser $user, Folder $folder): void {
+ $reminders = $this->reminderMapper->findAllInFolder($user, $folder);
+ $reminderMap = [];
+ foreach ($reminders as $reminder) {
+ $reminderMap[$reminder->getFileId()] = $reminder;
+ }
+
+ $nodes = $folder->getDirectoryListing();
+ foreach ($nodes as $node) {
+ $reminder = $reminderMap[$node->getId()] ?? false;
+ $this->cache->set("{$user->getUID()}-{$node->getId()}", $reminder);
+ }
+ }
+
+ /**
+ * @throws NodeNotFoundException
+ */
+ public function getDueForUser(IUser $user, int $fileId, bool $checkNode = true): ?RichReminder {
+ if ($checkNode) {
+ $this->checkNode($user, $fileId);
+ }
+ /** @var null|false|Reminder $cachedReminder */
+ $cachedReminder = $this->cache->get("{$user->getUID()}-$fileId");
+ if ($cachedReminder === false) {
+ return null;
+ }
+ if ($cachedReminder instanceof Reminder) {
+ return new RichReminder($cachedReminder, $this->root);
+ }
+
+ try {
+ $reminder = $this->reminderMapper->findDueForUser($user, $fileId);
+ $this->cache->set("{$user->getUID()}-$fileId", $reminder);
+ return new RichReminder($reminder, $this->root);
+ } catch (DoesNotExistException $e) {
+ $this->cache->set("{$user->getUID()}-$fileId", false);
+ return null;
+ }
+ }
+
+ /**
+ * @return RichReminder[]
+ */
+ public function getAll(?IUser $user = null) {
+ $reminders = ($user !== null)
+ ? $this->reminderMapper->findAllForUser($user)
+ : $this->reminderMapper->findAll();
+ return array_map(
+ fn (Reminder $reminder) => new RichReminder($reminder, $this->root),
+ $reminders,
+ );
+ }
+
+ /**
+ * @return bool true if created, false if updated
+ *
+ * @throws NodeNotFoundException
+ */
+ public function createOrUpdate(IUser $user, int $fileId, DateTime $dueDate): bool {
+ $now = new DateTime('now', new DateTimeZone('UTC'));
+ $this->checkNode($user, $fileId);
+ $reminder = $this->getDueForUser($user, $fileId);
+ if ($reminder === null) {
+ $reminder = new Reminder();
+ $reminder->setUserId($user->getUID());
+ $reminder->setFileId($fileId);
+ $reminder->setDueDate($dueDate);
+ $reminder->setUpdatedAt($now);
+ $reminder->setCreatedAt($now);
+ $this->reminderMapper->insert($reminder);
+ $this->cache->set("{$user->getUID()}-$fileId", $reminder);
+ return true;
+ }
+ $reminder->setDueDate($dueDate);
+ $reminder->setUpdatedAt($now);
+ $this->reminderMapper->update($reminder);
+ $this->cache->set("{$user->getUID()}-$fileId", $reminder);
+ return false;
+ }
+
+ /**
+ * @throws NodeNotFoundException
+ * @throws ReminderNotFoundException
+ */
+ public function remove(IUser $user, int $fileId): void {
+ $this->checkNode($user, $fileId);
+ $reminder = $this->getDueForUser($user, $fileId);
+ if ($reminder === null) {
+ throw new ReminderNotFoundException();
+ }
+ $this->deleteReminder($reminder);
+ }
+
+ public function removeAllForNode(Node $node): void {
+ $reminders = $this->reminderMapper->findAllForNode($node);
+ foreach ($reminders as $reminder) {
+ $this->deleteReminder($reminder);
+ }
+ }
+
+ public function removeAllForUser(IUser $user): void {
+ $reminders = $this->reminderMapper->findAllForUser($user);
+ foreach ($reminders as $reminder) {
+ $this->deleteReminder($reminder);
+ }
+ }
+
+ /**
+ * @throws DoesNotExistException
+ * @throws UserNotFoundException
+ */
+ public function send(Reminder $reminder): void {
+ if ($reminder->getNotified()) {
+ return;
+ }
+
+ $user = $this->userManager->get($reminder->getUserId());
+ if ($user === null) {
+ throw new UserNotFoundException();
+ }
+
+ $notification = $this->notificationManager->createNotification();
+ $notification
+ ->setApp(Application::APP_ID)
+ ->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('files', 'folder.svg')))
+ ->setUser($user->getUID())
+ ->setObject('reminder', (string)$reminder->getId())
+ ->setSubject('reminder-due', [
+ 'fileId' => $reminder->getFileId(),
+ ])
+ ->setDateTime($reminder->getDueDate());
+
+ try {
+ $this->notificationManager->notify($notification);
+ $this->reminderMapper->markNotified($reminder);
+ $this->cache->set("{$user->getUID()}-{$reminder->getFileId()}", $reminder);
+ } catch (Throwable $th) {
+ $this->logger->error($th->getMessage(), $th->getTrace());
+ }
+ }
+
+ public function cleanUp(?int $limit = null): void {
+ $buffer = (new DateTime())
+ ->setTimezone(new DateTimeZone('UTC'))
+ ->modify('-1 day');
+ $reminders = $this->reminderMapper->findNotified($buffer, $limit);
+ foreach ($reminders as $reminder) {
+ $this->deleteReminder($reminder);
+ }
+ }
+
+ private function deleteReminder(Reminder $reminder): void {
+ $this->reminderMapper->delete($reminder);
+ $this->cache->set("{$reminder->getUserId()}-{$reminder->getFileId()}", false);
+ }
+
+
+ /**
+ * @throws NodeNotFoundException
+ */
+ private function checkNode(IUser $user, int $fileId): void {
+ $userFolder = $this->root->getUserFolder($user->getUID());
+ $node = $userFolder->getFirstNodeById($fileId);
+ if ($node === null) {
+ throw new NodeNotFoundException();
+ }
+ }
+}
diff --git a/apps/files_reminders/lib/SetupChecks/NeedNotificationsApp.php b/apps/files_reminders/lib/SetupChecks/NeedNotificationsApp.php
new file mode 100644
index 00000000000..e5890567181
--- /dev/null
+++ b/apps/files_reminders/lib/SetupChecks/NeedNotificationsApp.php
@@ -0,0 +1,39 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\FilesReminders\SetupChecks;
+
+use OCP\App\IAppManager;
+use OCP\IL10N;
+use OCP\SetupCheck\ISetupCheck;
+use OCP\SetupCheck\SetupResult;
+
+class NeedNotificationsApp implements ISetupCheck {
+ public function __construct(
+ private IAppManager $appManager,
+ private IL10N $l10n,
+ ) {
+ }
+
+ public function getName(): string {
+ return $this->l10n->t('Files reminder');
+ }
+
+ public function getCategory(): string {
+ return 'system';
+ }
+
+ public function run(): SetupResult {
+ if ($this->appManager->isEnabledForAnyone('notifications')) {
+ return SetupResult::success($this->l10n->t('The "files_reminders" app can work properly.'));
+ } else {
+ return SetupResult::warning($this->l10n->t('The "files_reminders" app needs the notification app to work properly. You should either enable notifications or disable files_reminder.'));
+ }
+ }
+}