From ea5e128fefba6d87c400cdaafa87cd566746c109 Mon Sep 17 00:00:00 2001 From: Christopher Ng Date: Mon, 31 Jul 2023 12:10:50 -0700 Subject: [PATCH] feat(files_reminders): add service and notifier Signed-off-by: Christopher Ng --- .../files_reminders/lib/Db/ReminderMapper.php | 18 ++- .../lib/Exception/NodeNotFoundException.php | 32 ++++ .../lib/Exception/UserNotFoundException.php | 32 ++++ .../lib/Notification/Notifier.php | 142 ++++++++++++++++++ .../lib/Service/ReminderService.php | 87 +++++++++++ 5 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 apps/files_reminders/lib/Exception/NodeNotFoundException.php create mode 100644 apps/files_reminders/lib/Exception/UserNotFoundException.php create mode 100644 apps/files_reminders/lib/Notification/Notifier.php create mode 100644 apps/files_reminders/lib/Service/ReminderService.php diff --git a/apps/files_reminders/lib/Db/ReminderMapper.php b/apps/files_reminders/lib/Db/ReminderMapper.php index 974416245fa..f383ee66511 100644 --- a/apps/files_reminders/lib/Db/ReminderMapper.php +++ b/apps/files_reminders/lib/Db/ReminderMapper.php @@ -60,7 +60,23 @@ class ReminderMapper extends QBMapper { $qb->select('user_id', 'file_id', 'remind_at') ->from($this->getTableName()) ->where($qb->expr()->lt('remind_at', $qb->createFunction('NOW()'))) - ->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))); + ->andWhere($qb->expr()->eq('notified', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))) + ->orderBy('remind_at', 'ASC'); + + return $this->findEntities($qb); + } + + /** + * @return Reminder[] + */ + public function findToDelete(?int $limit = null) { + $qb = $this->db->getQueryBuilder(); + + $qb->select('user_id', 'file_id', 'remind_at') + ->from($this->getTableName()) + ->where($qb->expr()->eq('notified', $qb->createNamedParameter(true, IQueryBuilder::PARAM_BOOL))) + ->orderBy('remind_at', 'ASC') + ->setMaxResults($limit); 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..5dfe784f5fc --- /dev/null +++ b/apps/files_reminders/lib/Exception/NodeNotFoundException.php @@ -0,0 +1,32 @@ + + * + * @author Christopher Ng + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\FilesReminders\Exception; + +use Exception; + +class NodeNotFoundException 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..3c57ea475e9 --- /dev/null +++ b/apps/files_reminders/lib/Exception/UserNotFoundException.php @@ -0,0 +1,32 @@ + + * + * @author Christopher Ng + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\FilesReminders\Exception; + +use Exception; + +class UserNotFoundException extends Exception { +} diff --git a/apps/files_reminders/lib/Notification/Notifier.php b/apps/files_reminders/lib/Notification/Notifier.php new file mode 100644 index 00000000000..b3c1a98484a --- /dev/null +++ b/apps/files_reminders/lib/Notification/Notifier.php @@ -0,0 +1,142 @@ + + * + * @author Christopher Ng + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\FilesReminders\Notification; + +use InvalidArgumentException; +use OCA\FilesReminders\AppInfo\Application; +use OCA\FilesReminders\Exception\NodeNotFoundException; +use OCP\Files\FileInfo; +use OCP\Files\IRootFolder; +use OCP\Files\Node; +use OCP\IURLGenerator; +use OCP\L10N\IFactory; +use OCP\Notification\IAction; +use OCP\Notification\INotification; +use OCP\Notification\INotifier; + +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 InvalidArgumentException + * @throws NodeNotFoundException + */ + public function prepare(INotification $notification, string $languageCode): INotification { + $l = $this->l10nFactory->get(Application::APP_ID, $languageCode); + + if ($notification->getApp() !== Application::APP_ID) { + throw new InvalidArgumentException(); + } + + switch ($notification->getSubject()) { + case 'reminder-due': + $fileId = $notification->getSubjectParameters()['fileId']; + $node = $this->getNode($fileId); + + $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()], + ); + + $subject = $l->t('Reminder for {filename}'); + $notification + ->setRichSubject( + $subject, + [ + 'filename' => [ + 'type' => 'file', + 'id' => $node->getId(), + 'name' => $node->getName(), + 'path' => $path, + 'link' => $link, + ], + ], + ) + ->setParsedSubject(str_replace( + ['{filename}'], + [$node->getName()], + $subject, + )) + ->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 InvalidArgumentException(); + break; + } + + 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); + } + + /** + * @throws NodeNotFoundException + */ + protected function getNode(int $fileId): Node { + $nodes = $this->root->getById($fileId); + if (empty($nodes)) { + throw new NodeNotFoundException(); + } + $node = reset($nodes); + return $node; + } +} diff --git a/apps/files_reminders/lib/Service/ReminderService.php b/apps/files_reminders/lib/Service/ReminderService.php new file mode 100644 index 00000000000..69cef367856 --- /dev/null +++ b/apps/files_reminders/lib/Service/ReminderService.php @@ -0,0 +1,87 @@ + + * + * @author Christopher Ng + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\FilesReminders\Service; + +use DateTime; +use InvalidArgumentException; +use OCA\FilesReminders\AppInfo\Application; +use OCA\FilesReminders\Db\Reminder; +use OCA\FilesReminders\Db\ReminderMapper; +use OCA\FilesReminders\Exception\UserNotFoundException; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\Notification\IManager as INotificationManager; +use Psr\Log\LoggerInterface; + +class ReminderService { + public function __construct( + protected IUserManager $userManager, + protected IURLGenerator $urlGenerator, + protected INotificationManager $notificationManager, + protected ReminderMapper $reminderMapper, + protected LoggerInterface $logger, + ) {} + + /** + * @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(DateTime::createFromFormat('U', (string)$reminder->getRemindAt())); + + try { + $this->notificationManager->notify($notification); + $this->reminderMapper->markNotified($reminder); + } catch (InvalidArgumentException $e) { + $this->logger->error('Failed to send reminder notification', $e->getTrace()); + } + } + + public function cleanUp(?int $limit = null): void { + $reminders = $this->reminderMapper->findToDelete($limit); + foreach ($reminders as $reminder) { + $this->reminderMapper->delete($reminder); + } + } +} -- 2.39.5