diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 8c1bcf17516..b01ae68e43a 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -22,6 +22,7 @@ return array( 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => $baseDir . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', 'OCA\\DAV\\BackgroundJob\\UpdateCalendarResourcesRoomsBackgroundJob' => $baseDir . '/../lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php', 'OCA\\DAV\\BackgroundJob\\UploadCleanup' => $baseDir . '/../lib/BackgroundJob/UploadCleanup.php', + 'OCA\\DAV\\BackgroundJob\\UserStatusAutomation' => $baseDir . '/../lib/BackgroundJob/UserStatusAutomation.php', 'OCA\\DAV\\BulkUpload\\BulkUploadPlugin' => $baseDir . '/../lib/BulkUpload/BulkUploadPlugin.php', 'OCA\\DAV\\BulkUpload\\MultipartRequestParser' => $baseDir . '/../lib/BulkUpload/MultipartRequestParser.php', 'OCA\\DAV\\CalDAV\\Activity\\Backend' => $baseDir . '/../lib/CalDAV/Activity/Backend.php', @@ -247,6 +248,7 @@ return array( 'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => $baseDir . '/../lib/Listener/ClearPhotoCacheListener.php', 'OCA\\DAV\\Listener\\SubscriptionListener' => $baseDir . '/../lib/Listener/SubscriptionListener.php', 'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => $baseDir . '/../lib/Listener/TrustedServerRemovedListener.php', + 'OCA\\DAV\\Listener\\UserPreferenceListener' => $baseDir . '/../lib/Listener/UserPreferenceListener.php', 'OCA\\DAV\\Migration\\BuildCalendarSearchIndex' => $baseDir . '/../lib/Migration/BuildCalendarSearchIndex.php', 'OCA\\DAV\\Migration\\BuildCalendarSearchIndexBackgroundJob' => $baseDir . '/../lib/Migration/BuildCalendarSearchIndexBackgroundJob.php', 'OCA\\DAV\\Migration\\BuildSocialSearchIndex' => $baseDir . '/../lib/Migration/BuildSocialSearchIndex.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 29085a868de..4c9a1dcc793 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -37,6 +37,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\BackgroundJob\\RegisterRegenerateBirthdayCalendars' => __DIR__ . '/..' . '/../lib/BackgroundJob/RegisterRegenerateBirthdayCalendars.php', 'OCA\\DAV\\BackgroundJob\\UpdateCalendarResourcesRoomsBackgroundJob' => __DIR__ . '/..' . '/../lib/BackgroundJob/UpdateCalendarResourcesRoomsBackgroundJob.php', 'OCA\\DAV\\BackgroundJob\\UploadCleanup' => __DIR__ . '/..' . '/../lib/BackgroundJob/UploadCleanup.php', + 'OCA\\DAV\\BackgroundJob\\UserStatusAutomation' => __DIR__ . '/..' . '/../lib/BackgroundJob/UserStatusAutomation.php', 'OCA\\DAV\\BulkUpload\\BulkUploadPlugin' => __DIR__ . '/..' . '/../lib/BulkUpload/BulkUploadPlugin.php', 'OCA\\DAV\\BulkUpload\\MultipartRequestParser' => __DIR__ . '/..' . '/../lib/BulkUpload/MultipartRequestParser.php', 'OCA\\DAV\\CalDAV\\Activity\\Backend' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Backend.php', @@ -262,6 +263,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => __DIR__ . '/..' . '/../lib/Listener/ClearPhotoCacheListener.php', 'OCA\\DAV\\Listener\\SubscriptionListener' => __DIR__ . '/..' . '/../lib/Listener/SubscriptionListener.php', 'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => __DIR__ . '/..' . '/../lib/Listener/TrustedServerRemovedListener.php', + 'OCA\\DAV\\Listener\\UserPreferenceListener' => __DIR__ . '/..' . '/../lib/Listener/UserPreferenceListener.php', 'OCA\\DAV\\Migration\\BuildCalendarSearchIndex' => __DIR__ . '/..' . '/../lib/Migration/BuildCalendarSearchIndex.php', 'OCA\\DAV\\Migration\\BuildCalendarSearchIndexBackgroundJob' => __DIR__ . '/..' . '/../lib/Migration/BuildCalendarSearchIndexBackgroundJob.php', 'OCA\\DAV\\Migration\\BuildSocialSearchIndex' => __DIR__ . '/..' . '/../lib/Migration/BuildSocialSearchIndex.php', diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index fe8405e09e2..86749862626 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -85,6 +85,7 @@ use OCA\DAV\Listener\CardListener; use OCA\DAV\Listener\ClearPhotoCacheListener; use OCA\DAV\Listener\SubscriptionListener; use OCA\DAV\Listener\TrustedServerRemovedListener; +use OCA\DAV\Listener\UserPreferenceListener; use OCA\DAV\Search\ContactsSearchProvider; use OCA\DAV\Search\EventsSearchProvider; use OCA\DAV\Search\TasksSearchProvider; @@ -96,6 +97,8 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\IAppContainer; use OCP\Calendar\IManager as ICalendarManager; +use OCP\Config\BeforePreferenceDeletedEvent; +use OCP\Config\BeforePreferenceSetEvent; use OCP\Contacts\IManager as IContactsManager; use OCP\IServerContainer; use OCP\IUser; @@ -186,6 +189,9 @@ class Application extends App implements IBootstrap { $context->registerEventListener(CardUpdatedEvent::class, ClearPhotoCacheListener::class); $context->registerEventListener(TrustedServerRemovedEvent::class, TrustedServerRemovedListener::class); + $context->registerEventListener(BeforePreferenceDeletedEvent::class, UserPreferenceListener::class); + $context->registerEventListener(BeforePreferenceSetEvent::class, UserPreferenceListener::class); + $context->registerNotifierService(Notifier::class); $context->registerCalendarProvider(CalendarProvider::class); diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php new file mode 100644 index 00000000000..bbd92d903ee --- /dev/null +++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php @@ -0,0 +1,188 @@ + + * + * @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\DAV\BackgroundJob; + +use OCA\DAV\CalDAV\Schedule\Plugin; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\TimedJob; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\UserStatus\IManager; +use OCP\UserStatus\IUserStatus; +use Psr\Log\LoggerInterface; +use Sabre\VObject\Component\Available; +use Sabre\VObject\Component\VAvailability; +use Sabre\VObject\Reader; +use Sabre\VObject\Recur\RRuleIterator; + +class UserStatusAutomation extends TimedJob { + protected IDBConnection $connection; + protected IJobList $jobList; + protected LoggerInterface $logger; + protected IManager $manager; + protected IConfig $config; + + public function __construct(ITimeFactory $timeFactory, + IDBConnection $connection, + IJobList $jobList, + LoggerInterface $logger, + IManager $manager, + IConfig $config) { + parent::__construct($timeFactory); + $this->connection = $connection; + $this->jobList = $jobList; + $this->logger = $logger; + $this->manager = $manager; + $this->config = $config; + + // Interval 0 might look weird, but the last_checked is always moved + // to the next time we need this and then it's 0 seconds ago. + $this->setInterval(0); + } + + /** + * @inheritDoc + */ + protected function run($argument) { + if (!isset($argument['userId'])) { + $this->jobList->remove(self::class, $argument); + $this->logger->info('Removing invalid ' . self::class . ' background job'); + return; + } + + $userId = $argument['userId']; + $automationEnabled = $this->config->getUserValue($userId, 'dav', 'user_status_automation', 'no') === 'yes'; + if (!$automationEnabled) { + $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the setting is disabled'); + $this->jobList->remove(self::class, $argument); + return; + } + + $property = $this->getAvailabilityFromPropertiesTable($userId); + + if (!$property) { + $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no availability settings'); + $this->jobList->remove(self::class, $argument); + return; + } + + $isCurrentlyAvailable = false; + $nextPotentialToggles = []; + + $now = new \DateTime('now'); + $lastMidnight = (clone $now)->setTime(0, 0); + + $vObject = Reader::read($property); + foreach ($vObject->getComponents() as $component) { + if ($component->name !== 'VAVAILABILITY') { + continue; + } + /** @var VAvailability $component */ + $availables = $component->getComponents(); + foreach ($availables as $available) { + /** @var Available $available */ + if ($available->name === 'AVAILABLE') { + /** @var \DateTimeInterface $effectiveStart */ + /** @var \DateTimeInterface $effectiveEnd */ + [$effectiveStart, $effectiveEnd] = $available->getEffectiveStartEnd(); + + try { + $it = new RRuleIterator((string) $available->RRULE, $effectiveStart); + $it->fastForward($lastMidnight); + + $startToday = $it->current(); + if ($startToday && $startToday <= $now) { + $duration = $effectiveStart->diff($effectiveEnd); + $endToday = $startToday->add($duration); + if ($endToday > $now) { + // User is currently available + // Also queuing the end time as next status toggle + $isCurrentlyAvailable = true; + $nextPotentialToggles[] = $endToday->getTimestamp(); + } + + // Availability enabling already done for today, + // so jump to the next recurrence to find the next status toggle + $it->next(); + } + + if ($it->current()) { + $nextPotentialToggles[] = $it->current()->getTimestamp(); + } + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } + } + } + + $nextAutomaticToggle = min($nextPotentialToggles); + $this->setLastRunToNextToggleTime($userId, $nextAutomaticToggle - 1); + + if ($isCurrentlyAvailable) { + $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + } else { + // The DND status automation is more important than the "Away - In call" so we also restore that one if it exists. + $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); + $this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); + } + $this->logger->debug('User status automation ran'); + } + + protected function setLastRunToNextToggleTime(string $userId, int $timestamp): void { + $query = $this->connection->getQueryBuilder(); + + $query->update('jobs') + ->set('last_run', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + + $this->logger->debug('Updated user status automation last_run to ' . $timestamp . ' for user ' . $userId); + } + + /** + * @param string $userId + * @return false|string + */ + protected function getAvailabilityFromPropertiesTable(string $userId) { + $propertyPath = 'calendars/' . $userId . '/inbox'; + $propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability'; + + $query = $this->connection->getQueryBuilder(); + $query->select('propertyvalue') + ->from('properties') + ->where($query->expr()->eq('userid', $query->createNamedParameter($userId))) + ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($propertyPath))) + ->where($query->expr()->eq('propertyname', $query->createNamedParameter($propertyName))) + ->setMaxResults(1); + + $result = $query->executeQuery(); + $property = $result->fetchOne(); + $result->closeCursor(); + + return $property; + } +} diff --git a/apps/dav/lib/Listener/UserPreferenceListener.php b/apps/dav/lib/Listener/UserPreferenceListener.php new file mode 100644 index 00000000000..947f6d3fd01 --- /dev/null +++ b/apps/dav/lib/Listener/UserPreferenceListener.php @@ -0,0 +1,59 @@ + + * + * @author Joas Schilling + * + * @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\DAV\Listener; + +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCP\BackgroundJob\IJobList; +use OCP\Config\BeforePreferenceDeletedEvent; +use OCP\Config\BeforePreferenceSetEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +class UserPreferenceListener implements IEventListener { + + protected IJobList $jobList; + + public function __construct(IJobList $jobList) { + $this->jobList = $jobList; + } + + public function handle(Event $event): void { + if ($event instanceof BeforePreferenceSetEvent) { + if ($event->getAppId() === 'dav' && $event->getConfigKey() === 'user_status_automation' && $event->getConfigValue() === 'yes') { + $event->setValid(true); + + // Not the cleanest way, but we just add the job in the before event. + // If something ever turns wrong the first execution will remove the job again. + // We also first delete the current job, so the next run time is reset. + $this->jobList->remove(UserStatusAutomation::class, ['userId' => $event->getUserId()]); + $this->jobList->add(UserStatusAutomation::class, ['userId' => $event->getUserId()]); + } + } elseif ($event instanceof BeforePreferenceDeletedEvent) { + if ($event->getAppId() === 'dav' && $event->getConfigKey() === 'user_status_automation') { + $event->setValid(true); + } + } + } +} diff --git a/apps/dav/lib/Settings/AvailabilitySettings.php b/apps/dav/lib/Settings/AvailabilitySettings.php index 9a163e21edb..d2b75ba4866 100644 --- a/apps/dav/lib/Settings/AvailabilitySettings.php +++ b/apps/dav/lib/Settings/AvailabilitySettings.php @@ -27,10 +27,34 @@ namespace OCA\DAV\Settings; use OCA\DAV\AppInfo\Application; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IConfig; use OCP\Settings\ISettings; class AvailabilitySettings implements ISettings { + protected IConfig $config; + protected IInitialState $initialState; + protected ?string $userId; + + public function __construct(IConfig $config, + IInitialState $initialState, + ?string $userId) { + $this->config = $config; + $this->initialState = $initialState; + $this->userId = $userId; + } + public function getForm(): TemplateResponse { + $this->initialState->provideInitialState( + 'user_status_automation', + $this->config->getUserValue( + $this->userId, + 'dav', + 'user_status_automation', + 'no' + ) + ); + return new TemplateResponse(Application::APP_ID, 'settings-personal-availability'); } diff --git a/apps/dav/src/service/PreferenceService.js b/apps/dav/src/service/PreferenceService.js new file mode 100644 index 00000000000..6b8d29029b5 --- /dev/null +++ b/apps/dav/src/service/PreferenceService.js @@ -0,0 +1,50 @@ +/** + * @copyright 2022 Joas Schilling + * + * @author Joas Schilling + * + * @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 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 . + */ + +import axios from '@nextcloud/axios' +import { generateOcsUrl } from '@nextcloud/router' + +/** + * Enable user status automation based on availability + */ +export async function enableUserStatusAutomation() { + return await axios.post( + generateOcsUrl('/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', { + appId: 'dav', + configKey: 'user_status_automation', + }), + { + configValue: 'yes', + } + ) +} + +/** + * Disable user status automation based on availability + */ +export async function disableUserStatusAutomation() { + return await axios.delete( + generateOcsUrl('/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', { + appId: 'dav', + configKey: 'user_status_automation', + }) + ) +} diff --git a/apps/dav/src/views/Availability.vue b/apps/dav/src/views/Availability.vue index f3b3ec34bd2..21a0c3a5227 100644 --- a/apps/dav/src/views/Availability.vue +++ b/apps/dav/src/views/Availability.vue @@ -24,6 +24,10 @@ :l10n-saturday="$t('dav', 'Saturday')" :l10n-sunday="$t('dav', 'Sunday')" /> + + {{ $t('dav', 'Automatically set user status to "Do not distrub" outside of availability to mute all notifications.') }} + +