From f57d8e7f58a65b50f19c8e25c0ce991ad6d25a5e Mon Sep 17 00:00:00 2001 From: Richard Steinmetz Date: Mon, 13 Nov 2023 17:36:24 +0100 Subject: [PATCH] feat(dav): dispatch out-of-office started and ended events Signed-off-by: Richard Steinmetz --- .../composer/composer/autoload_classmap.php | 1 + .../dav/composer/composer/autoload_static.php | 1 + .../OutOfOfficeEventDispatcherJob.php | 105 ++++++++++++++++++ .../AvailabilitySettingsController.php | 15 +-- apps/dav/lib/Db/AbsenceMapper.php | 25 +++++ apps/dav/lib/Service/AbsenceService.php | 53 +++++---- lib/composer/composer/autoload_classmap.php | 2 + lib/composer/composer/autoload_static.php | 2 + .../User/Events/OutOfOfficeEndedEvent.php | 51 +++++++++ .../User/Events/OutOfOfficeStartedEvent.php | 51 +++++++++ 10 files changed, 278 insertions(+), 28 deletions(-) create mode 100644 apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php create mode 100644 lib/public/User/Events/OutOfOfficeEndedEvent.php create mode 100644 lib/public/User/Events/OutOfOfficeStartedEvent.php diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 0bec456d1f109..53d500b213af9 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -18,6 +18,7 @@ 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => $baseDir . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', 'OCA\\DAV\\BackgroundJob\\EventReminderJob' => $baseDir . '/../lib/BackgroundJob/EventReminderJob.php', 'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => $baseDir . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php', + 'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => $baseDir . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php', 'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => $baseDir . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php', 'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => $baseDir . '/../lib/BackgroundJob/RefreshWebcalJob.php', 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => $baseDir . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 3c1891cf192ee..4b28104323fa8 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -33,6 +33,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\BackgroundJob\\CleanupInvitationTokenJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/CleanupInvitationTokenJob.php', 'OCA\\DAV\\BackgroundJob\\EventReminderJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/EventReminderJob.php', 'OCA\\DAV\\BackgroundJob\\GenerateBirthdayCalendarBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/GenerateBirthdayCalendarBackgroundJob.php', + 'OCA\\DAV\\BackgroundJob\\OutOfOfficeEventDispatcherJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php', 'OCA\\DAV\\BackgroundJob\\PruneOutdatedSyncTokensJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/PruneOutdatedSyncTokensJob.php', 'OCA\\DAV\\BackgroundJob\\RefreshWebcalJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/RefreshWebcalJob.php', 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', diff --git a/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php new file mode 100644 index 0000000000000..e151a5b6552e6 --- /dev/null +++ b/apps/dav/lib/BackgroundJob/OutOfOfficeEventDispatcherJob.php @@ -0,0 +1,105 @@ + + * + * @author Richard Steinmetz + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\BackgroundJob; + +use OCA\DAV\Db\AbsenceMapper; +use OCA\Mail\Vendor\Psr\Log\LoggerInterface; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IUser; +use OCP\IUserManager; +use OCP\User\Events\OutOfOfficeEndedEvent; +use OCP\User\Events\OutOfOfficeStartedEvent; + +class OutOfOfficeEventDispatcherJob extends QueuedJob { + public const EVENT_START = 'start'; + public const EVENT_END = 'end'; + + /** @var array $users */ + private array $userCache = []; + + public function __construct( + ITimeFactory $time, + private AbsenceMapper $absenceMapper, + private LoggerInterface $logger, + private IEventDispatcher $eventDispatcher, + private IUserManager $userManager, + ) { + parent::__construct($time); + } + + protected function run($argument) { + $id = $argument['id']; + $event = $argument['event']; + + try { + $absence = $this->absenceMapper->findById($id); + } catch (DoesNotExistException | \OCP\DB\Exception $e) { + $this->logger->error('Failed to dispatch out-of-office event: ' . $e->getMessage(), [ + 'exception' => $e, + 'argument' => $argument, + ]); + return; + } + + $userId = $absence->getUserId(); + $user = $this->getUser($userId); + if ($user === null) { + $this->logger->error("Failed to dispatch out-of-office event: User $userId does not exist", [ + 'argument' => $argument, + ]); + return; + } + + $data = $absence->toOutOufOfficeData($user); + if ($event === self::EVENT_START) { + $this->eventDispatcher->dispatchTyped(new OutOfOfficeStartedEvent($data)); + } elseif ($event === self::EVENT_END) { + $this->eventDispatcher->dispatchTyped(new OutOfOfficeEndedEvent($data)); + } else { + $this->logger->error("Invalid out-of-office event: $event", [ + 'argument' => $argument, + ]); + } + } + + private function getUser(string $userId): ?IUser { + if (isset($this->userCache[$userId])) { + return $this->userCache[$userId]; + } + + $user = $this->userManager->get($userId); + if ($user === null) { + return null; + } + + $this->userCache[$userId] = $user; + return $user; + } +} diff --git a/apps/dav/lib/Controller/AvailabilitySettingsController.php b/apps/dav/lib/Controller/AvailabilitySettingsController.php index 3ff89fe87eb66..c32a3d8f7a4e0 100644 --- a/apps/dav/lib/Controller/AvailabilitySettingsController.php +++ b/apps/dav/lib/Controller/AvailabilitySettingsController.php @@ -35,11 +35,12 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\AppFramework\Http\Response; use OCP\IRequest; +use OCP\IUserSession; class AvailabilitySettingsController extends Controller { public function __construct( IRequest $request, - private ?string $userId, + private ?IUserSession $userSession, private AbsenceService $absenceService, ) { parent::__construct(Application::APP_ID, $request); @@ -56,8 +57,8 @@ public function updateAbsence( string $status, string $message, ): Response { - $userId = $this->userId; - if ($userId === null) { + $user = $this->userSession->getUser(); + if ($user === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } @@ -68,7 +69,7 @@ public function updateAbsence( } $absence = $this->absenceService->createOrUpdateAbsence( - $userId, + $user, $firstDay, $lastDay, $status, @@ -82,12 +83,12 @@ public function updateAbsence( */ #[NoAdminRequired] public function clearAbsence(): Response { - $userId = $this->userId; - if ($userId === null) { + $user = $this->userSession->getUser(); + if ($user === null) { return new JSONResponse([], Http::STATUS_FORBIDDEN); } - $this->absenceService->clearAbsence($userId); + $this->absenceService->clearAbsence($user); return new JSONResponse([]); } diff --git a/apps/dav/lib/Db/AbsenceMapper.php b/apps/dav/lib/Db/AbsenceMapper.php index 6e1133f779c33..deb9a43182648 100644 --- a/apps/dav/lib/Db/AbsenceMapper.php +++ b/apps/dav/lib/Db/AbsenceMapper.php @@ -40,6 +40,31 @@ public function __construct(IDBConnection $db) { parent::__construct($db, 'dav_absence', Absence::class); } + /** + * @throws DoesNotExistException + * @throws \OCP\DB\Exception + */ + public function findById(int $id): Absence { + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->getTableName()) + ->where($qb->expr()->eq( + 'id', + $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), + IQueryBuilder::PARAM_INT), + ); + try { + return $this->findEntity($qb); + } catch (MultipleObjectsReturnedException $e) { + // Won't happen as there is a unique index on user_id + throw new \RuntimeException( + 'The impossible has happened! The query returned multiple absence settings for one user.', + 0, + $e, + ); + } + } + /** * @throws DoesNotExistException * @throws \OCP\DB\Exception diff --git a/apps/dav/lib/Service/AbsenceService.php b/apps/dav/lib/Service/AbsenceService.php index 69dee1bd8cc36..d472a32438c23 100644 --- a/apps/dav/lib/Service/AbsenceService.php +++ b/apps/dav/lib/Service/AbsenceService.php @@ -27,11 +27,13 @@ namespace OCA\DAV\Service; use InvalidArgumentException; +use OCA\DAV\BackgroundJob\OutOfOfficeEventDispatcherJob; use OCA\DAV\Db\Absence; use OCA\DAV\Db\AbsenceMapper; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\BackgroundJob\IJobList; use OCP\EventDispatcher\IEventDispatcher; -use OCP\IUserManager; +use OCP\IUser; use OCP\User\Events\OutOfOfficeChangedEvent; use OCP\User\Events\OutOfOfficeClearedEvent; use OCP\User\Events\OutOfOfficeScheduledEvent; @@ -40,7 +42,7 @@ class AbsenceService { public function __construct( private AbsenceMapper $absenceMapper, private IEventDispatcher $eventDispatcher, - private IUserManager $userManager, + private IJobList $jobList, ) { } @@ -52,56 +54,65 @@ public function __construct( * @throws InvalidArgumentException If no user with the given user id exists. */ public function createOrUpdateAbsence( - string $userId, + IUser $user, string $firstDay, string $lastDay, string $status, string $message, ): Absence { try { - $absence = $this->absenceMapper->findByUserId($userId); + $absence = $this->absenceMapper->findByUserId($user->getUID()); } catch (DoesNotExistException) { $absence = new Absence(); } - $absence->setUserId($userId); + $absence->setUserId($user->getUID()); $absence->setFirstDay($firstDay); $absence->setLastDay($lastDay); $absence->setStatus($status); $absence->setMessage($message); - - // TODO: this method should probably just take a IUser instance - $user = $this->userManager->get($userId); - if ($user === null) { - throw new InvalidArgumentException("User $userId does not exist"); - } $eventData = $absence->toOutOufOfficeData($user); if ($absence->getId() === null) { + $absence = $this->absenceMapper->insert($absence); $this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent($eventData)); - return $this->absenceMapper->insert($absence); + } else { + $absence = $this->absenceMapper->update($absence); + $this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData)); } - $this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData)); - return $this->absenceMapper->update($absence); + $this->jobList->scheduleAfter( + OutOfOfficeEventDispatcherJob::class, + $eventData->getStartDate(), + [ + 'id' => $absence->getId(), + 'event' => OutOfOfficeEventDispatcherJob::EVENT_START, + ], + ); + $this->jobList->scheduleAfter( + OutOfOfficeEventDispatcherJob::class, + $eventData->getEndDate(), + [ + 'id' => $absence->getId(), + 'event' => OutOfOfficeEventDispatcherJob::EVENT_END, + ], + ); + + return $absence; } /** * @throws \OCP\DB\Exception */ - public function clearAbsence(string $userId): void { + public function clearAbsence(IUser $user): void { try { - $absence = $this->absenceMapper->findByUserId($userId); + $absence = $this->absenceMapper->findByUserId($user->getUID()); } catch (DoesNotExistException $e) { // Nothing to clear return; } $this->absenceMapper->delete($absence); - // TODO: this method should probably just take a IUser instance - $user = $this->userManager->get($userId); - if ($user === null) { - throw new InvalidArgumentException("User $userId does not exist"); - } + $this->jobList->remove(OutOfOfficeEventDispatcherJob::class); $eventData = $absence->toOutOufOfficeData($user); $this->eventDispatcher->dispatchTyped(new OutOfOfficeClearedEvent($eventData)); } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index bb1dd4b5df067..6f368058b205e 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -740,7 +740,9 @@ 'OCP\\User\\Events\\BeforeUserLoggedOutEvent' => $baseDir . '/lib/public/User/Events/BeforeUserLoggedOutEvent.php', 'OCP\\User\\Events\\OutOfOfficeChangedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeChangedEvent.php', 'OCP\\User\\Events\\OutOfOfficeClearedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeClearedEvent.php', + 'OCP\\User\\Events\\OutOfOfficeEndedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeEndedEvent.php', 'OCP\\User\\Events\\OutOfOfficeScheduledEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeScheduledEvent.php', + 'OCP\\User\\Events\\OutOfOfficeStartedEvent' => $baseDir . '/lib/public/User/Events/OutOfOfficeStartedEvent.php', 'OCP\\User\\Events\\PasswordUpdatedEvent' => $baseDir . '/lib/public/User/Events/PasswordUpdatedEvent.php', 'OCP\\User\\Events\\PostLoginEvent' => $baseDir . '/lib/public/User/Events/PostLoginEvent.php', 'OCP\\User\\Events\\UserChangedEvent' => $baseDir . '/lib/public/User/Events/UserChangedEvent.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 9667aa6853313..64224fb0bb2c4 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -773,7 +773,9 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\User\\Events\\BeforeUserLoggedOutEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/BeforeUserLoggedOutEvent.php', 'OCP\\User\\Events\\OutOfOfficeChangedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeChangedEvent.php', 'OCP\\User\\Events\\OutOfOfficeClearedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeClearedEvent.php', + 'OCP\\User\\Events\\OutOfOfficeEndedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeEndedEvent.php', 'OCP\\User\\Events\\OutOfOfficeScheduledEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeScheduledEvent.php', + 'OCP\\User\\Events\\OutOfOfficeStartedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/OutOfOfficeStartedEvent.php', 'OCP\\User\\Events\\PasswordUpdatedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/PasswordUpdatedEvent.php', 'OCP\\User\\Events\\PostLoginEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/PostLoginEvent.php', 'OCP\\User\\Events\\UserChangedEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserChangedEvent.php', diff --git a/lib/public/User/Events/OutOfOfficeEndedEvent.php b/lib/public/User/Events/OutOfOfficeEndedEvent.php new file mode 100644 index 0000000000000..43a6bf77e28bd --- /dev/null +++ b/lib/public/User/Events/OutOfOfficeEndedEvent.php @@ -0,0 +1,51 @@ + + * + * @author Richard Steinmetz + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\User\Events; + +use OCP\EventDispatcher\Event; +use OCP\User\IOutOfOfficeData; + +/** + * Emitted when a user's out-of-office period ended + * + * @since 28.0.0 + */ +class OutOfOfficeEndedEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct(private IOutOfOfficeData $data) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getData(): IOutOfOfficeData { + return $this->data; + } +} diff --git a/lib/public/User/Events/OutOfOfficeStartedEvent.php b/lib/public/User/Events/OutOfOfficeStartedEvent.php new file mode 100644 index 0000000000000..f7816c968ddee --- /dev/null +++ b/lib/public/User/Events/OutOfOfficeStartedEvent.php @@ -0,0 +1,51 @@ + + * + * @author Richard Steinmetz + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\User\Events; + +use OCP\EventDispatcher\Event; +use OCP\User\IOutOfOfficeData; + +/** + * Emitted when a user's out-of-office period started + * + * @since 28.0.0 + */ +class OutOfOfficeStartedEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct(private IOutOfOfficeData $data) { + parent::__construct(); + } + + /** + * @since 28.0.0 + */ + public function getData(): IOutOfOfficeData { + return $this->data; + } +}