diff options
author | Anna Larch <anna@nextcloud.com> | 2023-09-25 14:47:02 +0200 |
---|---|---|
committer | Anna Larch <anna@nextcloud.com> | 2023-11-09 16:20:19 +0100 |
commit | f14a4f8fd73c71e76a9747ac51e657030f5bb835 (patch) | |
tree | 6d5ec8e5365a72b83bfd270ce56bcbaa06703b8a /apps/user_status/lib | |
parent | 1aa24c024e207b54df3867f5f7ccd67625ac0492 (diff) | |
download | nextcloud-server-f14a4f8fd73c71e76a9747ac51e657030f5bb835.tar.gz nextcloud-server-f14a4f8fd73c71e76a9747ac51e657030f5bb835.zip |
feat(user status): automate user status for events
and automatically set a user status to free or busy depending on their calendar
transparency, event status and availability settings
Signed-off-by: Anna Larch <anna@nextcloud.com>
Diffstat (limited to 'apps/user_status/lib')
4 files changed, 119 insertions, 45 deletions
diff --git a/apps/user_status/lib/Db/UserStatusMapper.php b/apps/user_status/lib/Db/UserStatusMapper.php index 3621c1bfa96..c8ffcca5ad9 100644 --- a/apps/user_status/lib/Db/UserStatusMapper.php +++ b/apps/user_status/lib/Db/UserStatusMapper.php @@ -26,6 +26,7 @@ declare(strict_types=1); namespace OCA\UserStatus\Db; +use Sabre\CalDAV\Schedule\Plugin; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -210,4 +211,23 @@ class UserStatusMapper extends QBMapper { $qb->executeStatement(); } + + public function getAvailabilityFromPropertiesTable(string $userId): ?string { + $propertyPath = 'calendars/' . $userId . '/inbox'; + $propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability'; + + $query = $this->db->getQueryBuilder(); + $query->select('propertyvalue') + ->from('properties') + ->where($query->expr()->eq('userid', $query->createNamedParameter($userId))) + ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($propertyPath))) + ->andWhere($query->expr()->eq('propertyname', $query->createNamedParameter($propertyName))) + ->setMaxResults(1); + + $result = $query->executeQuery(); + $property = $result->fetchOne(); + $result->closeCursor(); + + return ($property === false ? null : $property); + } } diff --git a/apps/user_status/lib/Listener/UserLiveStatusListener.php b/apps/user_status/lib/Listener/UserLiveStatusListener.php index 3e05efa7118..687e01fc3a7 100644 --- a/apps/user_status/lib/Listener/UserLiveStatusListener.php +++ b/apps/user_status/lib/Listener/UserLiveStatusListener.php @@ -74,7 +74,7 @@ class UserLiveStatusListener implements IEventListener { $userStatus->setIsUserDefined(false); } - // If the status is user-defined and one of the persistent statuses, we + // If the status is user-defined and one of the persistent status, we // will not override it. if ($userStatus->getIsUserDefined() && \in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) { diff --git a/apps/user_status/lib/Service/PredefinedStatusService.php b/apps/user_status/lib/Service/PredefinedStatusService.php index 7d2f985c168..516680ba683 100644 --- a/apps/user_status/lib/Service/PredefinedStatusService.php +++ b/apps/user_status/lib/Service/PredefinedStatusService.php @@ -202,6 +202,8 @@ class PredefinedStatusService { self::REMOTE_WORK, IUserStatus::MESSAGE_CALL, IUserStatus::MESSAGE_AVAILABILITY, + IUserStatus::MESSAGE_CALENDAR_BUSY, + IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE, ], true); } } diff --git a/apps/user_status/lib/Service/StatusService.php b/apps/user_status/lib/Service/StatusService.php index f9ae769a5a9..003c9aa849a 100644 --- a/apps/user_status/lib/Service/StatusService.php +++ b/apps/user_status/lib/Service/StatusService.php @@ -7,6 +7,7 @@ declare(strict_types=1); * * @author Georg Ehrke <oc.list@georgehrke.com> * @author Joas Schilling <coding@schilljs.com> + * @author Anna Larch <anna.larch@gmx.net> * * @license GNU AGPL version 3 or any later version * @@ -26,6 +27,8 @@ declare(strict_types=1); */ namespace OCA\UserStatus\Service; +use OCA\DAV\CalDAV\Status\Status as CalendarStatus; +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; use OCA\UserStatus\Db\UserStatus; use OCA\UserStatus\Db\UserStatusMapper; use OCA\UserStatus\Exception\InvalidClearAtException; @@ -35,10 +38,13 @@ use OCA\UserStatus\Exception\InvalidStatusTypeException; use OCA\UserStatus\Exception\StatusMessageTooLongException; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\ISchedulingInformation; use OCP\DB\Exception; use OCP\IConfig; use OCP\IEmojiHelper; +use OCP\IUserManager; use OCP\UserStatus\IUserStatus; +use function in_array; /** * Class StatusService @@ -46,26 +52,9 @@ use OCP\UserStatus\IUserStatus; * @package OCA\UserStatus\Service */ class StatusService { - - /** @var UserStatusMapper */ - private $mapper; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var PredefinedStatusService */ - private $predefinedStatusService; - - private IEmojiHelper $emojiHelper; - - /** @var bool */ - private $shareeEnumeration; - - /** @var bool */ - private $shareeEnumerationInGroupOnly; - - /** @var bool */ - private $shareeEnumerationPhone; + private bool $shareeEnumeration; + private bool $shareeEnumerationInGroupOnly; + private bool $shareeEnumerationPhone; /** * List of priorities ordered by their priority @@ -74,6 +63,7 @@ class StatusService { IUserStatus::ONLINE, IUserStatus::AWAY, IUserStatus::DND, + IUserStatus::BUSY, IUserStatus::INVISIBLE, IUserStatus::OFFLINE, ]; @@ -84,6 +74,7 @@ class StatusService { */ public const PERSISTENT_STATUSES = [ IUserStatus::AWAY, + IUserStatus::BUSY, IUserStatus::DND, IUserStatus::INVISIBLE, ]; @@ -94,18 +85,16 @@ class StatusService { /** @var int */ public const MAXIMUM_MESSAGE_LENGTH = 80; - public function __construct(UserStatusMapper $mapper, - ITimeFactory $timeFactory, - PredefinedStatusService $defaultStatusService, - IEmojiHelper $emojiHelper, - IConfig $config) { - $this->mapper = $mapper; - $this->timeFactory = $timeFactory; - $this->predefinedStatusService = $defaultStatusService; - $this->emojiHelper = $emojiHelper; - $this->shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; - $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; - $this->shareeEnumerationPhone = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + public function __construct(private UserStatusMapper $mapper, + private ITimeFactory $timeFactory, + private PredefinedStatusService $predefinedStatusService, + private IEmojiHelper $emojiHelper, + private IConfig $config, + private IUserManager $userManager, + private CalendarStatusService $calendarStatusService) { + $this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; + $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; + $this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; } /** @@ -149,8 +138,37 @@ class StatusService { * @return UserStatus * @throws DoesNotExistException */ - public function findByUserId(string $userId):UserStatus { - return $this->processStatus($this->mapper->findByUserId($userId)); + public function findByUserId(string $userId): UserStatus { + $userStatus = $this->mapper->findByUserId($userId); + // If the status is user-defined and one of the persistent status, we + // will not override it. + if ($userStatus->getIsUserDefined() && \in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) { + return $this->processStatus($userStatus); + } + + $calendarStatus = $this->getCalendarStatus($userId); + // We found no status from the calendar, proceed with the existing status + if($calendarStatus === null) { + return $this->processStatus($userStatus); + } + + // if we have the same status result for the calendar and the current status, + // and a custom message to boot, we leave the existing status alone + // as to not overwrite a custom message / emoji + if($userStatus->getIsUserDefined() + && $calendarStatus->getStatus() === $userStatus->getStatus() + && !empty($userStatus->getCustomMessage())) { + return $this->processStatus($userStatus); + } + + // If the new status is null, there's already an identical status in place + $newUserStatus = $this->setUserStatus($userId, + $calendarStatus->getStatus(), + $calendarStatus->getMessage() ?? IUserStatus::MESSAGE_AVAILABILITY, + true, + $calendarStatus->getCustomMessage() ?? ''); + + return $newUserStatus === null ? $this->processStatus($userStatus) : $this->processStatus($newUserStatus); } /** @@ -183,9 +201,12 @@ class StatusService { } // Check if status-type is valid - if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { + if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported'); } + + + if ($statusTimestamp === null) { $statusTimestamp = $this->timeFactory->getTime(); } @@ -255,11 +276,12 @@ class StatusService { * @throws InvalidMessageIdException */ public function setUserStatus(string $userId, - string $status, - string $messageId, - bool $createBackup): void { + string $status, + string $messageId, + bool $createBackup, + string $customMessage = null): ?UserStatus { // Check if status-type is valid - if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { + if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported'); } @@ -269,7 +291,7 @@ class StatusService { if ($createBackup) { if ($this->backupCurrentStatus($userId) === false) { - return; // Already a status set automatically => abort. + return null; // Already a status set automatically => abort. } // If we just created the backup @@ -290,15 +312,14 @@ class StatusService { $userStatus->setIsBackup(false); $userStatus->setMessageId($messageId); $userStatus->setCustomIcon(null); - $userStatus->setCustomMessage(null); + $userStatus->setCustomMessage($customMessage); $userStatus->setClearAt(null); $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); if ($userStatus->getId() !== null) { - $this->mapper->update($userStatus); - return; + return $this->mapper->update($userStatus); } - $this->mapper->insert($userStatus); + return $this->mapper->insert($userStatus); } /** @@ -561,4 +582,35 @@ class StatusService { // For users that matched restore the previous status $this->mapper->restoreBackupStatuses($restoreIds); } + + /** + * Calculate a users' status according to their availabilit settings and their calendar + * events + * + * There are 4 predefined types of FBTYPE - 'FREE', 'BUSY', 'BUSY-UNAVAILABLE', 'BUSY-TENTATIVE', + * but 'X-' properties are possible + * + * @link https://icalendar.org/iCalendar-RFC-5545/3-2-9-free-busy-time-type.html + * + * The status will be changed for types + * - 'BUSY' + * - 'BUSY-UNAVAILABLE' (ex.: when a VAVILABILITY setting is in effect) + * - 'BUSY-TENTATIVE' (ex.: an event has been accepted tentatively) + * and all FREEBUSY components without a type (implicitly a 'BUSY' status) + * + * 'X-' properties are not handled for now + * + * @param string $userId + * @return CalendarStatus|null + */ + public function getCalendarStatus(string $userId): ?CalendarStatus { + $user = $this->userManager->get($userId); + if ($user === null) { + return null; + } + + $availability = $this->mapper->getAvailabilityFromPropertiesTable($userId); + + return $this->calendarStatusService->processCalendarAvailability($user, $availability); + } } |