aboutsummaryrefslogtreecommitdiffstats
path: root/apps/user_status/lib/Service
diff options
context:
space:
mode:
Diffstat (limited to 'apps/user_status/lib/Service')
-rw-r--r--apps/user_status/lib/Service/JSDataService.php62
-rw-r--r--apps/user_status/lib/Service/PredefinedStatusService.php223
-rw-r--r--apps/user_status/lib/Service/StatusService.php599
3 files changed, 884 insertions, 0 deletions
diff --git a/apps/user_status/lib/Service/JSDataService.php b/apps/user_status/lib/Service/JSDataService.php
new file mode 100644
index 00000000000..a777e97fe57
--- /dev/null
+++ b/apps/user_status/lib/Service/JSDataService.php
@@ -0,0 +1,62 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\UserStatus\Service;
+
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\IUserSession;
+use OCP\UserStatus\IUserStatus;
+
+class JSDataService implements \JsonSerializable {
+
+ /**
+ * JSDataService constructor.
+ *
+ * @param IUserSession $userSession
+ * @param StatusService $statusService
+ */
+ public function __construct(
+ private IUserSession $userSession,
+ private StatusService $statusService,
+ ) {
+ }
+
+ public function jsonSerialize(): array {
+ $user = $this->userSession->getUser();
+
+ if ($user === null) {
+ return [];
+ }
+
+ try {
+ $status = $this->statusService->findByUserId($user->getUID());
+ } catch (DoesNotExistException $ex) {
+ return [
+ 'userId' => $user->getUID(),
+ 'message' => null,
+ 'messageId' => null,
+ 'messageIsPredefined' => false,
+ 'icon' => null,
+ 'clearAt' => null,
+ 'status' => IUserStatus::OFFLINE,
+ 'statusIsUserDefined' => false,
+ ];
+ }
+
+ return [
+ 'userId' => $status->getUserId(),
+ 'message' => $status->getCustomMessage(),
+ 'messageId' => $status->getMessageId(),
+ 'messageIsPredefined' => $status->getMessageId() !== null,
+ 'icon' => $status->getCustomIcon(),
+ 'clearAt' => $status->getClearAt(),
+ 'status' => $status->getStatus(),
+ 'statusIsUserDefined' => $status->getIsUserDefined(),
+ ];
+ }
+}
diff --git a/apps/user_status/lib/Service/PredefinedStatusService.php b/apps/user_status/lib/Service/PredefinedStatusService.php
new file mode 100644
index 00000000000..599d5b8b52f
--- /dev/null
+++ b/apps/user_status/lib/Service/PredefinedStatusService.php
@@ -0,0 +1,223 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\UserStatus\Service;
+
+use OCP\IL10N;
+use OCP\UserStatus\IUserStatus;
+
+/**
+ * Class DefaultStatusService
+ *
+ * We are offering a set of default statuses, so we can
+ * translate them into different languages.
+ *
+ * @package OCA\UserStatus\Service
+ */
+class PredefinedStatusService {
+ private const BE_RIGHT_BACK = 'be-right-back';
+ private const MEETING = 'meeting';
+ private const COMMUTING = 'commuting';
+ private const SICK_LEAVE = 'sick-leave';
+ private const VACATIONING = 'vacationing';
+ private const REMOTE_WORK = 'remote-work';
+ /**
+ * @deprecated See \OCP\UserStatus\IUserStatus::MESSAGE_CALL
+ */
+ public const CALL = 'call';
+ public const OUT_OF_OFFICE = 'out-of-office';
+
+ /**
+ * DefaultStatusService constructor.
+ *
+ * @param IL10N $l10n
+ */
+ public function __construct(
+ private IL10N $l10n,
+ ) {
+ }
+
+ /**
+ * @return array
+ */
+ public function getDefaultStatuses(): array {
+ return [
+ [
+ 'id' => self::MEETING,
+ 'icon' => '📅',
+ 'message' => $this->getTranslatedStatusForId(self::MEETING),
+ 'clearAt' => [
+ 'type' => 'period',
+ 'time' => 3600,
+ ],
+ ],
+ [
+ 'id' => self::COMMUTING,
+ 'icon' => '🚌',
+ 'message' => $this->getTranslatedStatusForId(self::COMMUTING),
+ 'clearAt' => [
+ 'type' => 'period',
+ 'time' => 1800,
+ ],
+ ],
+ [
+ 'id' => self::BE_RIGHT_BACK,
+ 'icon' => '⏳',
+ 'message' => $this->getTranslatedStatusForId(self::BE_RIGHT_BACK),
+ 'clearAt' => [
+ 'type' => 'period',
+ 'time' => 900,
+ ],
+ ],
+ [
+ 'id' => self::REMOTE_WORK,
+ 'icon' => '🏡',
+ 'message' => $this->getTranslatedStatusForId(self::REMOTE_WORK),
+ 'clearAt' => [
+ 'type' => 'end-of',
+ 'time' => 'day',
+ ],
+ ],
+ [
+ 'id' => self::SICK_LEAVE,
+ 'icon' => '🤒',
+ 'message' => $this->getTranslatedStatusForId(self::SICK_LEAVE),
+ 'clearAt' => [
+ 'type' => 'end-of',
+ 'time' => 'day',
+ ],
+ ],
+ [
+ 'id' => self::VACATIONING,
+ 'icon' => '🌴',
+ 'message' => $this->getTranslatedStatusForId(self::VACATIONING),
+ 'clearAt' => null,
+ ],
+ [
+ 'id' => self::CALL,
+ 'icon' => '💬',
+ 'message' => $this->getTranslatedStatusForId(self::CALL),
+ 'clearAt' => null,
+ 'visible' => false,
+ ],
+ [
+ 'id' => self::OUT_OF_OFFICE,
+ 'icon' => '🛑',
+ 'message' => $this->getTranslatedStatusForId(self::OUT_OF_OFFICE),
+ 'clearAt' => null,
+ 'visible' => false,
+ ],
+ ];
+ }
+
+ /**
+ * @param string $id
+ * @return array|null
+ */
+ public function getDefaultStatusById(string $id): ?array {
+ foreach ($this->getDefaultStatuses() as $status) {
+ if ($status['id'] === $id) {
+ return $status;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @param string $id
+ * @return string|null
+ */
+ public function getIconForId(string $id): ?string {
+ switch ($id) {
+ case self::MEETING:
+ return '📅';
+
+ case self::COMMUTING:
+ return '🚌';
+
+ case self::SICK_LEAVE:
+ return '🤒';
+
+ case self::VACATIONING:
+ return '🌴';
+
+ case self::OUT_OF_OFFICE:
+ return '🛑';
+
+ case self::REMOTE_WORK:
+ return '🏡';
+
+ case self::BE_RIGHT_BACK:
+ return '⏳';
+
+ case self::CALL:
+ return '💬';
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * @param string $lang
+ * @param string $id
+ * @return string|null
+ */
+ public function getTranslatedStatusForId(string $id): ?string {
+ switch ($id) {
+ case self::MEETING:
+ return $this->l10n->t('In a meeting');
+
+ case self::COMMUTING:
+ return $this->l10n->t('Commuting');
+
+ case self::SICK_LEAVE:
+ return $this->l10n->t('Out sick');
+
+ case self::VACATIONING:
+ return $this->l10n->t('Vacationing');
+
+ case self::OUT_OF_OFFICE:
+ return $this->l10n->t('Out of office');
+
+ case self::REMOTE_WORK:
+ return $this->l10n->t('Working remotely');
+
+ case self::CALL:
+ return $this->l10n->t('In a call');
+
+ case self::BE_RIGHT_BACK:
+ return $this->l10n->t('Be right back');
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * @param string $id
+ * @return bool
+ */
+ public function isValidId(string $id): bool {
+ return \in_array($id, [
+ self::MEETING,
+ self::COMMUTING,
+ self::SICK_LEAVE,
+ self::VACATIONING,
+ self::OUT_OF_OFFICE,
+ self::BE_RIGHT_BACK,
+ self::REMOTE_WORK,
+ IUserStatus::MESSAGE_CALL,
+ IUserStatus::MESSAGE_AVAILABILITY,
+ IUserStatus::MESSAGE_VACATION,
+ 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
new file mode 100644
index 00000000000..188eb26d1d7
--- /dev/null
+++ b/apps/user_status/lib/Service/StatusService.php
@@ -0,0 +1,599 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\UserStatus\Service;
+
+use OCA\UserStatus\Db\UserStatus;
+use OCA\UserStatus\Db\UserStatusMapper;
+use OCA\UserStatus\Exception\InvalidClearAtException;
+use OCA\UserStatus\Exception\InvalidMessageIdException;
+use OCA\UserStatus\Exception\InvalidStatusIconException;
+use OCA\UserStatus\Exception\InvalidStatusTypeException;
+use OCA\UserStatus\Exception\StatusMessageTooLongException;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\DB\Exception;
+use OCP\IConfig;
+use OCP\IEmojiHelper;
+use OCP\IUserManager;
+use OCP\UserStatus\IUserStatus;
+use Psr\Log\LoggerInterface;
+use function in_array;
+
+/**
+ * Class StatusService
+ *
+ * @package OCA\UserStatus\Service
+ */
+class StatusService {
+ private bool $shareeEnumeration;
+ private bool $shareeEnumerationInGroupOnly;
+ private bool $shareeEnumerationPhone;
+
+ /**
+ * List of priorities ordered by their priority
+ */
+ public const PRIORITY_ORDERED_STATUSES = [
+ IUserStatus::ONLINE,
+ IUserStatus::AWAY,
+ IUserStatus::DND,
+ IUserStatus::BUSY,
+ IUserStatus::INVISIBLE,
+ IUserStatus::OFFLINE,
+ ];
+
+ /**
+ * List of statuses that persist the clear-up
+ * or UserLiveStatusEvents
+ */
+ public const PERSISTENT_STATUSES = [
+ IUserStatus::AWAY,
+ IUserStatus::BUSY,
+ IUserStatus::DND,
+ IUserStatus::INVISIBLE,
+ ];
+
+ /** @var int */
+ public const INVALIDATE_STATUS_THRESHOLD = 15 /* minutes */ * 60 /* seconds */;
+
+ /** @var int */
+ public const MAXIMUM_MESSAGE_LENGTH = 80;
+
+ public function __construct(
+ private UserStatusMapper $mapper,
+ private ITimeFactory $timeFactory,
+ private PredefinedStatusService $predefinedStatusService,
+ private IEmojiHelper $emojiHelper,
+ private IConfig $config,
+ private IUserManager $userManager,
+ private LoggerInterface $logger,
+ ) {
+ $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';
+ }
+
+ /**
+ * @param int|null $limit
+ * @param int|null $offset
+ * @return UserStatus[]
+ */
+ public function findAll(?int $limit = null, ?int $offset = null): array {
+ // Return empty array if user enumeration is disabled or limited to groups
+ // TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
+ // groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
+ if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
+ return [];
+ }
+
+ return array_map(function ($status) {
+ return $this->processStatus($status);
+ }, $this->mapper->findAll($limit, $offset));
+ }
+
+ /**
+ * @param int|null $limit
+ * @param int|null $offset
+ * @return array
+ */
+ public function findAllRecentStatusChanges(?int $limit = null, ?int $offset = null): array {
+ // Return empty array if user enumeration is disabled or limited to groups
+ // TODO: find a solution that scales to get only users from common groups if user enumeration is limited to
+ // groups. See discussion at https://github.com/nextcloud/server/pull/27879#discussion_r729715936
+ if (!$this->shareeEnumeration || $this->shareeEnumerationInGroupOnly || $this->shareeEnumerationPhone) {
+ return [];
+ }
+
+ return array_map(function ($status) {
+ return $this->processStatus($status);
+ }, $this->mapper->findAllRecent($limit, $offset));
+ }
+
+ /**
+ * @param string $userId
+ * @return UserStatus
+ * @throws DoesNotExistException
+ */
+ public function findByUserId(string $userId): UserStatus {
+ return $this->processStatus($this->mapper->findByUserId($userId));
+ }
+
+ /**
+ * @param array $userIds
+ * @return UserStatus[]
+ */
+ public function findByUserIds(array $userIds):array {
+ return array_map(function ($status) {
+ return $this->processStatus($status);
+ }, $this->mapper->findByUserIds($userIds));
+ }
+
+ /**
+ * @param string $userId
+ * @param string $status
+ * @param int|null $statusTimestamp
+ * @param bool $isUserDefined
+ * @return UserStatus
+ * @throws InvalidStatusTypeException
+ */
+ public function setStatus(string $userId,
+ string $status,
+ ?int $statusTimestamp,
+ bool $isUserDefined): UserStatus {
+ try {
+ $userStatus = $this->mapper->findByUserId($userId);
+ } catch (DoesNotExistException $ex) {
+ $userStatus = new UserStatus();
+ $userStatus->setUserId($userId);
+ }
+
+ // Check if status-type is valid
+ 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();
+ }
+
+ $userStatus->setStatus($status);
+ $userStatus->setStatusTimestamp($statusTimestamp);
+ $userStatus->setIsUserDefined($isUserDefined);
+ $userStatus->setIsBackup(false);
+
+ if ($userStatus->getId() === null) {
+ return $this->insertWithoutThrowingUniqueConstrain($userStatus);
+ }
+
+ return $this->mapper->update($userStatus);
+ }
+
+ /**
+ * @param string $userId
+ * @param string $messageId
+ * @param int|null $clearAt
+ * @return UserStatus
+ * @throws InvalidMessageIdException
+ * @throws InvalidClearAtException
+ */
+ public function setPredefinedMessage(string $userId,
+ string $messageId,
+ ?int $clearAt): UserStatus {
+ try {
+ $userStatus = $this->mapper->findByUserId($userId);
+ } catch (DoesNotExistException $ex) {
+ $userStatus = new UserStatus();
+ $userStatus->setUserId($userId);
+ $userStatus->setStatus(IUserStatus::OFFLINE);
+ $userStatus->setStatusTimestamp(0);
+ $userStatus->setIsUserDefined(false);
+ $userStatus->setIsBackup(false);
+ }
+
+ if (!$this->predefinedStatusService->isValidId($messageId)) {
+ throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
+ }
+
+ // Check that clearAt is in the future
+ if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
+ throw new InvalidClearAtException('ClearAt is in the past');
+ }
+
+ $userStatus->setMessageId($messageId);
+ $userStatus->setCustomIcon(null);
+ $userStatus->setCustomMessage(null);
+ $userStatus->setClearAt($clearAt);
+ $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp());
+
+ if ($userStatus->getId() === null) {
+ return $this->insertWithoutThrowingUniqueConstrain($userStatus);
+ }
+
+ return $this->mapper->update($userStatus);
+ }
+
+ /**
+ * @param string $userId
+ * @param string $status
+ * @param string $messageId
+ * @param bool $createBackup
+ * @param string|null $customMessage
+ * @throws InvalidStatusTypeException
+ * @throws InvalidMessageIdException
+ */
+ public function setUserStatus(string $userId,
+ 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)) {
+ throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
+ }
+
+ if (!$this->predefinedStatusService->isValidId($messageId)) {
+ throw new InvalidMessageIdException('Message-Id "' . $messageId . '" is not supported');
+ }
+
+ try {
+ $userStatus = $this->mapper->findByUserId($userId);
+ } catch (DoesNotExistException $e) {
+ // We don't need to do anything
+ $userStatus = new UserStatus();
+ $userStatus->setUserId($userId);
+ }
+
+ $updateStatus = false;
+ if ($messageId === IUserStatus::MESSAGE_OUT_OF_OFFICE) {
+ // OUT_OF_OFFICE trumps AVAILABILITY, CALL and CALENDAR status
+ $updateStatus = $userStatus->getMessageId() === IUserStatus::MESSAGE_AVAILABILITY || $userStatus->getMessageId() === IUserStatus::MESSAGE_CALL || $userStatus->getMessageId() === IUserStatus::MESSAGE_CALENDAR_BUSY;
+ } elseif ($messageId === IUserStatus::MESSAGE_AVAILABILITY) {
+ // AVAILABILITY trumps CALL and CALENDAR status
+ $updateStatus = $userStatus->getMessageId() === IUserStatus::MESSAGE_CALL || $userStatus->getMessageId() === IUserStatus::MESSAGE_CALENDAR_BUSY;
+ } elseif ($messageId === IUserStatus::MESSAGE_CALL) {
+ // CALL trumps CALENDAR status
+ $updateStatus = $userStatus->getMessageId() === IUserStatus::MESSAGE_CALENDAR_BUSY;
+ }
+
+ if ($messageId === IUserStatus::MESSAGE_OUT_OF_OFFICE || $messageId === IUserStatus::MESSAGE_AVAILABILITY || $messageId === IUserStatus::MESSAGE_CALL || $messageId === IUserStatus::MESSAGE_CALENDAR_BUSY) {
+ if ($updateStatus) {
+ $this->logger->debug('User ' . $userId . ' is currently NOT available, overwriting status [status: ' . $userStatus->getStatus() . ', messageId: ' . json_encode($userStatus->getMessageId()) . ']', ['app' => 'dav']);
+ } else {
+ $this->logger->debug('User ' . $userId . ' is currently NOT available, but we are NOT overwriting status [status: ' . $userStatus->getStatus() . ', messageId: ' . json_encode($userStatus->getMessageId()) . ']', ['app' => 'dav']);
+ }
+ }
+
+ // There should be a backup already or none is needed. So we take a shortcut.
+ if ($updateStatus) {
+ $userStatus->setStatus($status);
+ $userStatus->setStatusTimestamp($this->timeFactory->getTime());
+ $userStatus->setIsUserDefined(true);
+ $userStatus->setIsBackup(false);
+ $userStatus->setMessageId($messageId);
+ $userStatus->setCustomIcon(null);
+ $userStatus->setCustomMessage($customMessage);
+ $userStatus->setClearAt(null);
+ $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp());
+ return $this->mapper->update($userStatus);
+ }
+
+ if ($createBackup) {
+ if ($this->backupCurrentStatus($userId) === false) {
+ return null; // Already a status set automatically => abort.
+ }
+
+ // If we just created the backup
+ // we need to create a new status to insert
+ // Unfortunately there's no way to unset the DB ID on an Entity
+ $userStatus = new UserStatus();
+ $userStatus->setUserId($userId);
+ }
+
+ $userStatus->setStatus($status);
+ $userStatus->setStatusTimestamp($this->timeFactory->getTime());
+ $userStatus->setIsUserDefined(true);
+ $userStatus->setIsBackup(false);
+ $userStatus->setMessageId($messageId);
+ $userStatus->setCustomIcon(null);
+ $userStatus->setCustomMessage($customMessage);
+ $userStatus->setClearAt(null);
+ if ($this->predefinedStatusService->getTranslatedStatusForId($messageId) !== null
+ || ($customMessage !== null && $customMessage !== '')) {
+ // Only track status message ID if there is one
+ $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp());
+ } else {
+ $userStatus->setStatusMessageTimestamp(0);
+ }
+
+ if ($userStatus->getId() !== null) {
+ return $this->mapper->update($userStatus);
+ }
+ return $this->insertWithoutThrowingUniqueConstrain($userStatus);
+ }
+
+ /**
+ * @param string $userId
+ * @param string|null $statusIcon
+ * @param string|null $message
+ * @param int|null $clearAt
+ * @return UserStatus
+ * @throws InvalidClearAtException
+ * @throws InvalidStatusIconException
+ * @throws StatusMessageTooLongException
+ */
+ public function setCustomMessage(string $userId,
+ ?string $statusIcon,
+ ?string $message,
+ ?int $clearAt): UserStatus {
+ try {
+ $userStatus = $this->mapper->findByUserId($userId);
+ } catch (DoesNotExistException $ex) {
+ $userStatus = new UserStatus();
+ $userStatus->setUserId($userId);
+ $userStatus->setStatus(IUserStatus::OFFLINE);
+ $userStatus->setStatusTimestamp(0);
+ $userStatus->setIsUserDefined(false);
+ }
+
+ // Check if statusIcon contains only one character
+ if ($statusIcon !== null && !$this->emojiHelper->isValidSingleEmoji($statusIcon)) {
+ throw new InvalidStatusIconException('Status-Icon is longer than one character');
+ }
+ // Check for maximum length of custom message
+ if ($message !== null && \mb_strlen($message) > self::MAXIMUM_MESSAGE_LENGTH) {
+ throw new StatusMessageTooLongException('Message is longer than supported length of ' . self::MAXIMUM_MESSAGE_LENGTH . ' characters');
+ }
+ // Check that clearAt is in the future
+ if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
+ throw new InvalidClearAtException('ClearAt is in the past');
+ }
+
+ $userStatus->setMessageId(null);
+ $userStatus->setCustomIcon($statusIcon);
+ $userStatus->setCustomMessage($message);
+ $userStatus->setClearAt($clearAt);
+ $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp());
+
+ if ($userStatus->getId() === null) {
+ return $this->insertWithoutThrowingUniqueConstrain($userStatus);
+ }
+
+ return $this->mapper->update($userStatus);
+ }
+
+ /**
+ * @param string $userId
+ * @return bool
+ */
+ public function clearStatus(string $userId): bool {
+ try {
+ $userStatus = $this->mapper->findByUserId($userId);
+ } catch (DoesNotExistException $ex) {
+ // if there is no status to remove, just return
+ return false;
+ }
+
+ $userStatus->setStatus(IUserStatus::OFFLINE);
+ $userStatus->setStatusTimestamp(0);
+ $userStatus->setIsUserDefined(false);
+
+ $this->mapper->update($userStatus);
+ return true;
+ }
+
+ /**
+ * @param string $userId
+ * @return bool
+ */
+ public function clearMessage(string $userId): bool {
+ try {
+ $userStatus = $this->mapper->findByUserId($userId);
+ } catch (DoesNotExistException $ex) {
+ // if there is no status to remove, just return
+ return false;
+ }
+
+ $userStatus->setMessageId(null);
+ $userStatus->setCustomMessage(null);
+ $userStatus->setCustomIcon(null);
+ $userStatus->setClearAt(null);
+ $userStatus->setStatusMessageTimestamp(0);
+
+ $this->mapper->update($userStatus);
+ return true;
+ }
+
+ /**
+ * @param string $userId
+ * @return bool
+ */
+ public function removeUserStatus(string $userId): bool {
+ try {
+ $userStatus = $this->mapper->findByUserId($userId, false);
+ } catch (DoesNotExistException $ex) {
+ // if there is no status to remove, just return
+ return false;
+ }
+
+ $this->mapper->delete($userStatus);
+ return true;
+ }
+
+ public function removeBackupUserStatus(string $userId): bool {
+ try {
+ $userStatus = $this->mapper->findByUserId($userId, true);
+ } catch (DoesNotExistException $ex) {
+ // if there is no status to remove, just return
+ return false;
+ }
+
+ $this->mapper->delete($userStatus);
+ return true;
+ }
+
+ /**
+ * Processes a status to check if custom message is still
+ * up to date and provides translated default status if needed
+ *
+ * @param UserStatus $status
+ * @return UserStatus
+ */
+ private function processStatus(UserStatus $status): UserStatus {
+ $clearAt = $status->getClearAt();
+
+ if ($status->getStatusTimestamp() < $this->timeFactory->getTime() - self::INVALIDATE_STATUS_THRESHOLD
+ && (!$status->getIsUserDefined() || $status->getStatus() === IUserStatus::ONLINE)) {
+ $this->cleanStatus($status);
+ }
+ if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) {
+ $this->cleanStatus($status);
+ $this->cleanStatusMessage($status);
+ }
+ if ($status->getMessageId() !== null) {
+ $this->addDefaultMessage($status);
+ }
+
+ return $status;
+ }
+
+ /**
+ * @param UserStatus $status
+ */
+ private function cleanStatus(UserStatus $status): void {
+ if ($status->getStatus() === IUserStatus::OFFLINE && !$status->getIsUserDefined()) {
+ return;
+ }
+
+ $status->setStatus(IUserStatus::OFFLINE);
+ $status->setStatusTimestamp($this->timeFactory->getTime());
+ $status->setIsUserDefined(false);
+
+ $this->mapper->update($status);
+ }
+
+ /**
+ * @param UserStatus $status
+ */
+ private function cleanStatusMessage(UserStatus $status): void {
+ $status->setMessageId(null);
+ $status->setCustomIcon(null);
+ $status->setCustomMessage(null);
+ $status->setClearAt(null);
+ $status->setStatusMessageTimestamp(0);
+
+ $this->mapper->update($status);
+ }
+
+ /**
+ * @param UserStatus $status
+ */
+ private function addDefaultMessage(UserStatus $status): void {
+ // If the message is predefined, insert the translated message and icon
+ $predefinedMessage = $this->predefinedStatusService->getDefaultStatusById($status->getMessageId());
+ if ($predefinedMessage === null) {
+ return;
+ }
+ // If there is a custom message, don't overwrite it
+ if (empty($status->getCustomMessage())) {
+ $status->setCustomMessage($predefinedMessage['message']);
+ }
+ if (empty($status->getCustomIcon())) {
+ $status->setCustomIcon($predefinedMessage['icon']);
+ }
+ }
+
+ /**
+ * @return bool false if there is already a backup. In this case abort the procedure.
+ */
+ public function backupCurrentStatus(string $userId): bool {
+ try {
+ $this->mapper->createBackupStatus($userId);
+ return true;
+ } catch (Exception $ex) {
+ if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ return false;
+ }
+ throw $ex;
+ }
+ }
+
+ public function revertUserStatus(string $userId, string $messageId, bool $revertedManually = false): ?UserStatus {
+ try {
+ /** @var UserStatus $userStatus */
+ $backupUserStatus = $this->mapper->findByUserId($userId, true);
+ } catch (DoesNotExistException $ex) {
+ // No user status to revert, do nothing
+ return null;
+ }
+
+ $deleted = $this->mapper->deleteCurrentStatusToRestoreBackup($userId, $messageId);
+ if (!$deleted) {
+ // Another status is set automatically or no status, do nothing
+ return null;
+ }
+
+ if ($revertedManually) {
+ if ($backupUserStatus->getStatus() === IUserStatus::OFFLINE) {
+ // When the user reverts the status manually they are online
+ $backupUserStatus->setStatus(IUserStatus::ONLINE);
+ }
+ $backupUserStatus->setStatusTimestamp($this->timeFactory->getTime());
+ }
+
+ $backupUserStatus->setIsBackup(false);
+ // Remove the underscore prefix added when creating the backup
+ $backupUserStatus->setUserId(substr($backupUserStatus->getUserId(), 1));
+ $this->mapper->update($backupUserStatus);
+
+ return $backupUserStatus;
+ }
+
+ public function revertMultipleUserStatus(array $userIds, string $messageId): void {
+ // Get all user statuses and the backups
+ $findById = $userIds;
+ foreach ($userIds as $userId) {
+ $findById[] = '_' . $userId;
+ }
+ $userStatuses = $this->mapper->findByUserIds($findById);
+
+ $backups = $restoreIds = $statuesToDelete = [];
+ foreach ($userStatuses as $userStatus) {
+ if (!$userStatus->getIsBackup()
+ && $userStatus->getMessageId() === $messageId) {
+ $statuesToDelete[$userStatus->getUserId()] = $userStatus->getId();
+ } elseif ($userStatus->getIsBackup()) {
+ $backups[$userStatus->getUserId()] = $userStatus->getId();
+ }
+ }
+
+ // For users with both (normal and backup) delete the status when matching
+ foreach ($statuesToDelete as $userId => $statusId) {
+ $backupUserId = '_' . $userId;
+ if (isset($backups[$backupUserId])) {
+ $restoreIds[] = $backups[$backupUserId];
+ }
+ }
+
+ $this->mapper->deleteByIds(array_values($statuesToDelete));
+
+ // For users that matched restore the previous status
+ $this->mapper->restoreBackupStatuses($restoreIds);
+ }
+
+ protected function insertWithoutThrowingUniqueConstrain(UserStatus $userStatus): UserStatus {
+ try {
+ return $this->mapper->insert($userStatus);
+ } catch (Exception $e) {
+ // Ignore if a parallel request already set the status
+ if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ throw $e;
+ }
+ }
+ return $userStatus;
+ }
+}