diff options
author | Georg Ehrke <developer@georgehrke.com> | 2020-06-02 12:48:37 +0200 |
---|---|---|
committer | Georg Ehrke <developer@georgehrke.com> | 2020-07-31 16:45:27 +0200 |
commit | 0fad921840eb801492522af6ef795231163cff20 (patch) | |
tree | ddab0d1567d81eeb8d956ec98196180ad296cabd /apps/user_status/lib/Service | |
parent | fce6df06e2bd1d68ee5614621ae7f92c6f7fa53d (diff) | |
download | nextcloud-server-0fad921840eb801492522af6ef795231163cff20.tar.gz nextcloud-server-0fad921840eb801492522af6ef795231163cff20.zip |
Add user-status app
Signed-off-by: Georg Ehrke <developer@georgehrke.com>
Diffstat (limited to 'apps/user_status/lib/Service')
-rw-r--r-- | apps/user_status/lib/Service/EmojiService.php | 100 | ||||
-rw-r--r-- | apps/user_status/lib/Service/JSDataService.php | 84 | ||||
-rw-r--r-- | apps/user_status/lib/Service/PredefinedStatusService.php | 187 | ||||
-rw-r--r-- | apps/user_status/lib/Service/StatusService.php | 335 |
4 files changed, 706 insertions, 0 deletions
diff --git a/apps/user_status/lib/Service/EmojiService.php b/apps/user_status/lib/Service/EmojiService.php new file mode 100644 index 00000000000..bb0506d242f --- /dev/null +++ b/apps/user_status/lib/Service/EmojiService.php @@ -0,0 +1,100 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\UserStatus\Service; + +use OCP\IDBConnection; + +/** + * Class EmojiService + * + * @package OCA\UserStatus\Service + */ +class EmojiService { + + /** @var IDBConnection */ + private $db; + + /** + * EmojiService constructor. + * + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + $this->db = $db; + } + + /** + * @return bool + */ + public function doesPlatformSupportEmoji(): bool { + return $this->db->supports4ByteText() && + \class_exists(\IntlBreakIterator::class); + } + + /** + * @param string $emoji + * @return bool + */ + public function isValidEmoji(string $emoji): bool { + $intlBreakIterator = \IntlBreakIterator::createCharacterInstance(); + $intlBreakIterator->setText($emoji); + + $characterCount = 0; + while ($intlBreakIterator->next() !== \IntlBreakIterator::DONE) { + $characterCount++; + } + + if ($characterCount !== 1) { + return false; + } + + $codePointIterator = \IntlBreakIterator::createCodePointInstance(); + $codePointIterator->setText($emoji); + + foreach ($codePointIterator->getPartsIterator() as $codePoint) { + $codePointType = \IntlChar::charType($codePoint); + + // If the current code-point is an emoji or a modifier (like a skin-tone) + // just continue and check the next character + if ($codePointType === \IntlChar::CHAR_CATEGORY_MODIFIER_SYMBOL || + $codePointType === \IntlChar::CHAR_CATEGORY_MODIFIER_LETTER || + $codePointType === \IntlChar::CHAR_CATEGORY_OTHER_SYMBOL) { + continue; + } + + // If it's neither a modifier nor an emoji, we only allow + // a zero-width-joiner or a variation selector 16 + $codePointValue = \IntlChar::ord($codePoint); + if ($codePointValue === 8205 || $codePointValue === 65039) { + continue; + } + + return false; + } + + return true; + } +} diff --git a/apps/user_status/lib/Service/JSDataService.php b/apps/user_status/lib/Service/JSDataService.php new file mode 100644 index 00000000000..ebe801cd57a --- /dev/null +++ b/apps/user_status/lib/Service/JSDataService.php @@ -0,0 +1,84 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\UserStatus\Service; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\IUserSession; + +class JSDataService implements \JsonSerializable { + + /** @var IUserSession */ + private $userSession; + + /** @var StatusService */ + private $statusService; + + /** + * JSDataService constructor. + * + * @param IUserSession $userSession + * @param StatusService $statusService + */ + public function __construct(IUserSession $userSession, + StatusService $statusService) { + $this->userSession = $userSession; + $this->statusService = $statusService; + } + + public function jsonSerialize() { + $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' => '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..e8a82014b7b --- /dev/null +++ b/apps/user_status/lib/Service/PredefinedStatusService.php @@ -0,0 +1,187 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\UserStatus\Service; + +use OCP\IL10N; + +/** + * 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 MEETING = 'meeting'; + private const COMMUTING = 'commuting'; + private const SICK_LEAVE = 'sick-leave'; + private const VACATIONING = 'vacationing'; + private const REMOTE_WORK = 'remote-work'; + + /** @var IL10N */ + private $l10n; + + /** + * DefaultStatusService constructor. + * + * @param IL10N $l10n + */ + public function __construct(IL10N $l10n) { + $this->l10n = $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::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, + ], + ]; + } + + /** + * @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::REMOTE_WORK: + 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::REMOTE_WORK: + return $this->l10n->t('Working remotely'); + + 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::REMOTE_WORK, + ], 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..83fcd0a8f02 --- /dev/null +++ b/apps/user_status/lib/Service/StatusService.php @@ -0,0 +1,335 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +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; + +/** + * Class StatusService + * + * @package OCA\UserStatus\Service + */ +class StatusService { + + /** @var UserStatusMapper */ + private $mapper; + + /** @var ITimeFactory */ + private $timeFactory; + + /** @var PredefinedStatusService */ + private $predefinedStatusService; + + /** @var EmojiService */ + private $emojiService; + + /** @var string[] */ + private $allowedStatusTypes = [ + 'online', + 'away', + 'dnd', + 'invisible', + 'offline' + ]; + + /** @var int */ + private $maximumMessageLength = 80; + + /** + * StatusService constructor. + * + * @param UserStatusMapper $mapper + * @param ITimeFactory $timeFactory + * @param PredefinedStatusService $defaultStatusService, + * @param EmojiService $emojiService + */ + public function __construct(UserStatusMapper $mapper, + ITimeFactory $timeFactory, + PredefinedStatusService $defaultStatusService, + EmojiService $emojiService) { + $this->mapper = $mapper; + $this->timeFactory = $timeFactory; + $this->predefinedStatusService = $defaultStatusService; + $this->emojiService = $emojiService; + } + + /** + * @param int|null $limit + * @param int|null $offset + * @return UserStatus[] + */ + public function findAll(?int $limit = null, ?int $offset = null): array { + return array_map(function ($status) { + return $this->processStatus($status); + }, $this->mapper->findAll($limit, $offset)); + } + + /** + * @param string $userId + * @return UserStatus + * @throws DoesNotExistException + */ + public function findByUserId(string $userId):UserStatus { + return $this->processStatus($this->mapper->findByUserId($userId)); + } + + /** + * @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, $this->allowedStatusTypes, 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); + + if ($userStatus->getId() === null) { + return $this->mapper->insert($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('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(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); + + if ($userStatus->getId() === null) { + return $this->mapper->insert($userStatus); + } + + return $this->mapper->update($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('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + } + + // Check if statusIcon contains only one character + if ($statusIcon !== null && !$this->emojiService->isValidEmoji($statusIcon)) { + throw new InvalidStatusIconException('Status-Icon is longer than one character'); + } + // Check for maximum length of custom message + if (\mb_strlen($message) > $this->maximumMessageLength) { + throw new StatusMessageTooLongException('Message is longer than supported length of ' . $this->maximumMessageLength . ' 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); + + if ($userStatus->getId() === null) { + return $this->mapper->insert($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('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); + + $this->mapper->update($userStatus); + return true; + } + + /** + * @param string $userId + * @return bool + */ + public function removeUserStatus(string $userId): bool { + try { + $userStatus = $this->mapper->findByUserId($userId); + } 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 + * @returns UserStatus + */ + private function processStatus(UserStatus $status): UserStatus { + $clearAt = $status->getClearAt(); + if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) { + $this->cleanStatus($status); + } + if ($status->getMessageId() !== null) { + $this->addDefaultMessage($status); + } + + return $status; + } + + /** + * @param UserStatus $status + */ + private function cleanStatus(UserStatus $status): void { + $status->setMessageId(null); + $status->setCustomIcon(null); + $status->setCustomMessage(null); + $status->setClearAt(null); + + $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) { + $status->setCustomMessage($predefinedMessage['message']); + $status->setCustomIcon($predefinedMessage['icon']); + } + } +} |