aboutsummaryrefslogtreecommitdiffstats
path: root/apps/user_status/lib
diff options
context:
space:
mode:
authorAnna Larch <anna@nextcloud.com>2023-09-25 14:47:02 +0200
committerAnna Larch <anna@nextcloud.com>2023-11-09 16:20:19 +0100
commitf14a4f8fd73c71e76a9747ac51e657030f5bb835 (patch)
tree6d5ec8e5365a72b83bfd270ce56bcbaa06703b8a /apps/user_status/lib
parent1aa24c024e207b54df3867f5f7ccd67625ac0492 (diff)
downloadnextcloud-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')
-rw-r--r--apps/user_status/lib/Db/UserStatusMapper.php20
-rw-r--r--apps/user_status/lib/Listener/UserLiveStatusListener.php2
-rw-r--r--apps/user_status/lib/Service/PredefinedStatusService.php2
-rw-r--r--apps/user_status/lib/Service/StatusService.php140
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);
+ }
}