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 | |
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')
22 files changed, 2067 insertions, 0 deletions
diff --git a/apps/user_status/lib/AppInfo/Application.php b/apps/user_status/lib/AppInfo/Application.php new file mode 100644 index 00000000000..6de72e01839 --- /dev/null +++ b/apps/user_status/lib/AppInfo/Application.php @@ -0,0 +1,74 @@ +<?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\AppInfo; + +use OCA\UserStatus\Capabilities; +use OCA\UserStatus\Listener\BeforeTemplateRenderedListener; +use OCA\UserStatus\Listener\UserDeletedListener; +use OCA\UserStatus\Listener\UserLiveStatusListener; +use OCP\AppFramework\App; +use OCP\AppFramework\Bootstrap\IBootContext; +use OCP\AppFramework\Bootstrap\IBootstrap; +use OCP\AppFramework\Bootstrap\IRegistrationContext; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\User\Events\UserDeletedEvent; +use OCP\User\Events\UserLiveStatusEvent; + +/** + * Class Application + * + * @package OCA\UserStatus\AppInfo + */ +class Application extends App implements IBootstrap { + + /** @var string */ + public const APP_ID = 'user_status'; + + /** + * Application constructor. + * + * @param array $urlParams + */ + public function __construct(array $urlParams = []) { + parent::__construct(self::APP_ID, $urlParams); + } + + /** + * @inheritDoc + */ + public function register(IRegistrationContext $context): void { + // Register OCS Capabilities + $context->registerCapability(Capabilities::class); + + // Register Event Listeners + $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); + $context->registerEventListener(UserLiveStatusEvent::class, UserLiveStatusListener::class); + $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); + } + + public function boot(IBootContext $context): void { + } +} diff --git a/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php b/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php new file mode 100644 index 00000000000..40639048843 --- /dev/null +++ b/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php @@ -0,0 +1,63 @@ +<?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\BackgroundJob; + +use OCA\UserStatus\Db\UserStatusMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; + +/** + * Class ClearOldStatusesBackgroundJob + * + * @package OCA\UserStatus\BackgroundJob + */ +class ClearOldStatusesBackgroundJob extends TimedJob { + + /** @var UserStatusMapper */ + private $mapper; + + /** + * ClearOldStatusesBackgroundJob constructor. + * + * @param ITimeFactory $time + * @param UserStatusMapper $mapper + */ + public function __construct(ITimeFactory $time, + UserStatusMapper $mapper) { + parent::__construct($time); + $this->mapper = $mapper; + + // Run every time the cron is run + $this->setInterval(60); + } + + /** + * @inheritDoc + */ + protected function run($argument) { + $this->mapper->clearOlderThan($this->time->getTime()); + } +} diff --git a/apps/user_status/lib/Capabilities.php b/apps/user_status/lib/Capabilities.php new file mode 100644 index 00000000000..ada65402a30 --- /dev/null +++ b/apps/user_status/lib/Capabilities.php @@ -0,0 +1,60 @@ +<?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; + +use OCA\UserStatus\Service\EmojiService; +use OCP\Capabilities\ICapability; + +/** + * Class Capabilities + * + * @package OCA\UserStatus + */ +class Capabilities implements ICapability { + + /** @var EmojiService */ + private $emojiService; + + /** + * Capabilities constructor. + * + * @param EmojiService $emojiService + */ + public function __construct(EmojiService $emojiService) { + $this->emojiService = $emojiService; + } + + /** + * @inheritDoc + */ + public function getCapabilities() { + return [ + 'user_status' => [ + 'enabled' => true, + 'supports_emoji' => $this->emojiService->doesPlatformSupportEmoji(), + ], + ]; + } +} diff --git a/apps/user_status/lib/Controller/HeartbeatController.php b/apps/user_status/lib/Controller/HeartbeatController.php new file mode 100644 index 00000000000..fb8259a2ad7 --- /dev/null +++ b/apps/user_status/lib/Controller/HeartbeatController.php @@ -0,0 +1,92 @@ +<?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\Controller; + +use OCP\AppFramework\Controller; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\IRequest; +use OCP\IUserSession; +use OCP\User\Events\UserLiveStatusEvent; + +class HeartbeatController extends Controller { + + /** @var IEventDispatcher */ + private $eventDispatcher; + + /** @var IUserSession */ + private $userSession; + + /** @var ITimeFactory */ + private $timeFactory; + + /** + * HeartbeatController constructor. + * + * @param string $appName + * @param IRequest $request + * @param IEventDispatcher $eventDispatcher + */ + public function __construct(string $appName, + IRequest $request, + IEventDispatcher $eventDispatcher, + IUserSession $userSession, + ITimeFactory $timeFactory) { + parent::__construct($appName, $request); + $this->eventDispatcher = $eventDispatcher; + $this->userSession = $userSession; + $this->timeFactory = $timeFactory; + } + + /** + * @NoAdminRequired + * + * @param string $status + * @return JSONResponse + */ + public function heartbeat(string $status): JSONResponse { + if (!\in_array($status, ['online', 'away'])) { + return new JSONResponse([], Http::STATUS_BAD_REQUEST); + } + + $user = $this->userSession->getUser(); + if ($user === null) { + return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + } + + $this->eventDispatcher->dispatchTyped( + new UserLiveStatusEvent( + $user, + $status, + $this->timeFactory->getTime() + ) + ); + + return new JSONResponse([], Http::STATUS_NO_CONTENT); + } +} diff --git a/apps/user_status/lib/Controller/PredefinedStatusController.php b/apps/user_status/lib/Controller/PredefinedStatusController.php new file mode 100644 index 00000000000..4c3530624f3 --- /dev/null +++ b/apps/user_status/lib/Controller/PredefinedStatusController.php @@ -0,0 +1,65 @@ +<?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\Controller; + +use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +/** + * Class DefaultStatusController + * + * @package OCA\UserStatus\Controller + */ +class PredefinedStatusController extends OCSController { + + /** @var PredefinedStatusService */ + private $predefinedStatusService; + + /** + * AStatusController constructor. + * + * @param string $appName + * @param IRequest $request + * @param PredefinedStatusService $predefinedStatusService + */ + public function __construct(string $appName, + IRequest $request, + PredefinedStatusService $predefinedStatusService) { + parent::__construct($appName, $request); + $this->predefinedStatusService = $predefinedStatusService; + } + + /** + * @NoAdminRequired + * + * @return DataResponse + */ + public function findAll():DataResponse { + return new DataResponse($this->predefinedStatusService->getDefaultStatuses()); + } +} diff --git a/apps/user_status/lib/Controller/StatusesController.php b/apps/user_status/lib/Controller/StatusesController.php new file mode 100644 index 00000000000..b707708f46a --- /dev/null +++ b/apps/user_status/lib/Controller/StatusesController.php @@ -0,0 +1,107 @@ +<?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\Controller; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\IRequest; + +class StatusesController extends OCSController { + + /** @var StatusService */ + private $service; + + /** + * StatusesController constructor. + * + * @param string $appName + * @param IRequest $request + * @param StatusService $service + */ + public function __construct(string $appName, + IRequest $request, + StatusService $service) { + parent::__construct($appName, $request); + $this->service = $service; + } + + /** + * @NoAdminRequired + * + * @param int|null $limit + * @param int|null $offset + * @return DataResponse + */ + public function findAll(?int $limit=null, ?int $offset=null): DataResponse { + $allStatuses = $this->service->findAll($limit, $offset); + + return new DataResponse(array_map(function ($userStatus) { + return $this->formatStatus($userStatus); + }, $allStatuses)); + } + + /** + * @NoAdminRequired + * + * @param string $userId + * @return DataResponse + * @throws OCSNotFoundException + */ + public function find(string $userId): DataResponse { + try { + $userStatus = $this->service->findByUserId($userId); + } catch (DoesNotExistException $ex) { + throw new OCSNotFoundException('No status for the requested userId'); + } + + return new DataResponse($this->formatStatus($userStatus)); + } + + /** + * @NoAdminRequired + * + * @param UserStatus $status + * @return array + */ + private function formatStatus(UserStatus $status): array { + $visibleStatus = $status->getStatus(); + if ($visibleStatus === 'invisible') { + $visibleStatus = 'offline'; + } + + return [ + 'userId' => $status->getUserId(), + 'message' => $status->getCustomMessage(), + 'icon' => $status->getCustomIcon(), + 'clearAt' => $status->getClearAt(), + 'status' => $visibleStatus, + ]; + } +} diff --git a/apps/user_status/lib/Controller/UserStatusController.php b/apps/user_status/lib/Controller/UserStatusController.php new file mode 100644 index 00000000000..ffbe1e753ef --- /dev/null +++ b/apps/user_status/lib/Controller/UserStatusController.php @@ -0,0 +1,191 @@ +<?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\Controller; + +use OCA\UserStatus\Db\UserStatus; +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 OCA\UserStatus\Service\StatusService; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCS\OCSBadRequestException; +use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCSController; +use OCP\ILogger; +use OCP\IRequest; + +class UserStatusController extends OCSController { + + /** @var string */ + private $userId; + + /** @var ILogger */ + private $logger; + + /** @var StatusService */ + private $service; + + /** + * StatusesController constructor. + * + * @param string $appName + * @param IRequest $request + * @param string $userId + * @param ILogger $logger; + * @param StatusService $service + */ + public function __construct(string $appName, + IRequest $request, + string $userId, + ILogger $logger, + StatusService $service) { + parent::__construct($appName, $request); + $this->userId = $userId; + $this->logger = $logger; + $this->service = $service; + } + + /** + * @NoAdminRequired + * + * @return DataResponse + * @throws OCSNotFoundException + */ + public function getStatus(): DataResponse { + try { + $userStatus = $this->service->findByUserId($this->userId); + } catch (DoesNotExistException $ex) { + throw new OCSNotFoundException('No status for the current user'); + } + + return new DataResponse($this->formatStatus($userStatus)); + } + + /** + * @NoAdminRequired + * + * @param string $statusType + * @return DataResponse + * @throws OCSBadRequestException + */ + public function setStatus(string $statusType): DataResponse { + try { + $status = $this->service->setStatus($this->userId, $statusType, null, true); + return new DataResponse($this->formatStatus($status)); + } catch (InvalidStatusTypeException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid status type "' . $statusType . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } + } + + /** + * @NoAdminRequired + * + * @param string $messageId + * @param int|null $clearAt + * @return DataResponse + * @throws OCSBadRequestException + */ + public function setPredefinedMessage(string $messageId, + ?int $clearAt): DataResponse { + try { + $status = $this->service->setPredefinedMessage($this->userId, $messageId, $clearAt); + return new DataResponse($this->formatStatus($status)); + } catch (InvalidClearAtException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid clearAt value "' . $clearAt . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } catch (InvalidMessageIdException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid message-id "' . $messageId . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } + } + + /** + * @NoAdminRequired + * + * @param string|null $statusIcon + * @param string $message + * @param int|null $clearAt + * @return DataResponse + * @throws OCSBadRequestException + */ + public function setCustomMessage(?string $statusIcon, + string $message, + ?int $clearAt): DataResponse { + try { + $status = $this->service->setCustomMessage($this->userId, $statusIcon, $message, $clearAt); + return new DataResponse($this->formatStatus($status)); + } catch (InvalidClearAtException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid clearAt value "' . $clearAt . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } catch (InvalidStatusIconException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to an invalid icon value "' . $statusIcon . '"'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } catch (StatusMessageTooLongException $ex) { + $this->logger->debug('New user-status for "' . $this->userId . '" was rejected due to a too long status message.'); + throw new OCSBadRequestException($ex->getMessage(), $ex); + } + } + + /** + * @NoAdminRequired + * + * @return DataResponse + */ + public function clearStatus(): DataResponse { + $this->service->clearStatus($this->userId); + return new DataResponse([]); + } + + /** + * @NoAdminRequired + * + * @return DataResponse + */ + public function clearMessage(): DataResponse { + $this->service->clearMessage($this->userId); + return new DataResponse([]); + } + + /** + * @param UserStatus $status + * @return array + */ + private function formatStatus(UserStatus $status): array { + 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/Db/UserStatus.php b/apps/user_status/lib/Db/UserStatus.php new file mode 100644 index 00000000000..6faea6e0ecd --- /dev/null +++ b/apps/user_status/lib/Db/UserStatus.php @@ -0,0 +1,90 @@ +<?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\Db; + +use OCP\AppFramework\Db\Entity; + +/** + * Class UserStatus + * + * @package OCA\UserStatus\Db + * + * @method int getId() + * @method void setId(int $id) + * @method string getUserId() + * @method void setUserId(string $userId) + * @method string getStatus() + * @method void setStatus(string $status) + * @method int getStatusTimestamp() + * @method void setStatusTimestamp(int $statusTimestamp) + * @method bool getIsUserDefined() + * @method void setIsUserDefined(bool $isUserDefined) + * @method string getMessageId() + * @method void setMessageId(string|null $messageId) + * @method string getCustomIcon() + * @method void setCustomIcon(string|null $customIcon) + * @method string getCustomMessage() + * @method void setCustomMessage(string|null $customMessage) + * @method int getClearAt() + * @method void setClearAt(int|null $clearAt) + */ +class UserStatus extends Entity { + + /** @var string */ + public $userId; + + /** @var string */ + public $status; + + /** @var int */ + public $statusTimestamp; + + /** @var boolean */ + public $isUserDefined; + + /** @var string|null */ + public $messageId; + + /** @var string|null */ + public $customIcon; + + /** @var string|null */ + public $customMessage; + + /** @var int|null */ + public $clearAt; + + public function __construct() { + $this->addType('userId', 'string'); + $this->addType('status', 'string'); + $this->addType('statusTimestamp', 'int'); + $this->addType('isUserDefined', 'boolean'); + $this->addType('messageId', 'string'); + $this->addType('customIcon', 'string'); + $this->addType('customMessage', 'string'); + $this->addType('clearAt', 'int'); + } +} diff --git a/apps/user_status/lib/Db/UserStatusMapper.php b/apps/user_status/lib/Db/UserStatusMapper.php new file mode 100644 index 00000000000..4e78ef11e05 --- /dev/null +++ b/apps/user_status/lib/Db/UserStatusMapper.php @@ -0,0 +1,104 @@ +<?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\Db; + +use OCP\AppFramework\Db\QBMapper; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * Class UserStatusMapper + * + * @package OCA\UserStatus\Db + * + * @method UserStatus insert(UserStatus $entity) + * @method UserStatus update(UserStatus $entity) + * @method UserStatus insertOrUpdate(UserStatus $entity) + * @method UserStatus delete(UserStatus $entity) + */ +class UserStatusMapper extends QBMapper { + + /** + * @param IDBConnection $db + */ + public function __construct(IDBConnection $db) { + parent::__construct($db, 'user_status'); + } + + /** + * @param int|null $limit + * @param int|null $offset + * @return UserStatus[] + */ + public function findAll(?int $limit = null, ?int $offset = null):array { + $qb = $this->db->getQueryBuilder(); + $qb + ->select('*') + ->from($this->tableName); + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->findEntities($qb); + } + + /** + * @param string $userId + * @return UserStatus + * @throws \OCP\AppFramework\Db\DoesNotExistException + */ + public function findByUserId(string $userId):UserStatus { + $qb = $this->db->getQueryBuilder(); + $qb + ->select('*') + ->from($this->tableName) + ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))); + + return $this->findEntity($qb); + } + + /** + * Clear all statuses older than a given timestamp + * + * @param int $timestamp + */ + public function clearOlderThan(int $timestamp): void { + $qb = $this->db->getQueryBuilder(); + $qb->update($this->tableName) + ->set('message_id', $qb->createNamedParameter(null)) + ->set('custom_icon', $qb->createNamedParameter(null)) + ->set('custom_message', $qb->createNamedParameter(null)) + ->set('clear_at', $qb->createNamedParameter(null)) + ->where($qb->expr()->isNotNull('clear_at')) + ->andWhere($qb->expr()->lte('clear_at', $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))); + + $qb->execute(); + } +} diff --git a/apps/user_status/lib/Exception/InvalidClearAtException.php b/apps/user_status/lib/Exception/InvalidClearAtException.php new file mode 100644 index 00000000000..da49a8ee672 --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidClearAtException.php @@ -0,0 +1,29 @@ +<?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\Exception; + +class InvalidClearAtException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/InvalidMessageIdException.php b/apps/user_status/lib/Exception/InvalidMessageIdException.php new file mode 100644 index 00000000000..e08045aa8d2 --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidMessageIdException.php @@ -0,0 +1,29 @@ +<?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\Exception; + +class InvalidMessageIdException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/InvalidStatusIconException.php b/apps/user_status/lib/Exception/InvalidStatusIconException.php new file mode 100644 index 00000000000..e8dc089327f --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidStatusIconException.php @@ -0,0 +1,29 @@ +<?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\Exception; + +class InvalidStatusIconException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/InvalidStatusTypeException.php b/apps/user_status/lib/Exception/InvalidStatusTypeException.php new file mode 100644 index 00000000000..f11b899e7bd --- /dev/null +++ b/apps/user_status/lib/Exception/InvalidStatusTypeException.php @@ -0,0 +1,29 @@ +<?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\Exception; + +class InvalidStatusTypeException extends \Exception { +} diff --git a/apps/user_status/lib/Exception/StatusMessageTooLongException.php b/apps/user_status/lib/Exception/StatusMessageTooLongException.php new file mode 100644 index 00000000000..675d7417b06 --- /dev/null +++ b/apps/user_status/lib/Exception/StatusMessageTooLongException.php @@ -0,0 +1,29 @@ +<?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\Exception; + +class StatusMessageTooLongException extends \Exception { +} diff --git a/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php new file mode 100644 index 00000000000..fa3d8ce9e68 --- /dev/null +++ b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php @@ -0,0 +1,75 @@ +<?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\Listener; + +use OCA\UserStatus\AppInfo\Application; +use OCA\UserStatus\Service\JSDataService; +use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\IInitialStateService; + +class BeforeTemplateRenderedListener implements IEventListener { + + /** @var IInitialStateService */ + private $initialState; + + /** @var JSDataService */ + private $jsDataService; + + /** + * BeforeTemplateRenderedListener constructor. + * + * @param IInitialStateService $initialState + * @param JSDataService $jsDataService + */ + public function __construct(IInitialStateService $initialState, + JSDataService $jsDataService) { + $this->initialState = $initialState; + $this->jsDataService = $jsDataService; + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if (!($event instanceof BeforeTemplateRenderedEvent)) { + // Unrelated + return; + } + + if (!$event->isLoggedIn()) { + return; + } + + $this->initialState->provideLazyInitialState(Application::APP_ID, 'status', function () { + return $this->jsDataService; + }); + + \OCP\Util::addScript('user_status', 'user-status-menu'); + \OCP\Util::addStyle('user_status', 'user-status-menu'); + } +} diff --git a/apps/user_status/lib/Listener/UserDeletedListener.php b/apps/user_status/lib/Listener/UserDeletedListener.php new file mode 100644 index 00000000000..b376be00949 --- /dev/null +++ b/apps/user_status/lib/Listener/UserDeletedListener.php @@ -0,0 +1,65 @@ +<?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\Listener; + +use OCA\UserStatus\Service\StatusService; +use OCP\EventDispatcher\IEventListener; +use OCP\EventDispatcher\Event; +use OCP\User\Events\UserDeletedEvent; + +/** + * Class UserDeletedListener + * + * @package OCA\UserStatus\Listener + */ +class UserDeletedListener implements IEventListener { + + /** @var StatusService */ + private $service; + + /** + * UserDeletedListener constructor. + * + * @param StatusService $service + */ + public function __construct(StatusService $service) { + $this->service = $service; + } + + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if (!($event instanceof UserDeletedEvent)) { + // Unrelated + return; + } + + $user = $event->getUser(); + $this->service->removeUserStatus($user->getUID()); + } +} diff --git a/apps/user_status/lib/Listener/UserLiveStatusListener.php b/apps/user_status/lib/Listener/UserLiveStatusListener.php new file mode 100644 index 00000000000..ce97841d9ad --- /dev/null +++ b/apps/user_status/lib/Listener/UserLiveStatusListener.php @@ -0,0 +1,133 @@ +<?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\Listener; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Db\UserStatusMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\IEventListener; +use OCP\EventDispatcher\Event; +use OCP\User\Events\UserLiveStatusEvent; + +/** + * Class UserDeletedListener + * + * @package OCA\UserStatus\Listener + */ +class UserLiveStatusListener implements IEventListener { + + /** @var UserStatusMapper */ + private $mapper; + + /** @var ITimeFactory */ + private $timeFactory; + + /** @var string[] */ + private $priorityOrderedStatuses = [ + 'online', + 'away', + 'dnd', + 'invisible', + 'offline' + ]; + + /** @var string[] */ + private $persistentUserStatuses = [ + 'away', + 'dnd', + 'invisible', + ]; + + /** @var int */ + private $offlineThreshold = 300; + + /** + * UserLiveStatusListener constructor. + * + * @param UserStatusMapper $mapper + * @param ITimeFactory $timeFactory + */ + public function __construct(UserStatusMapper $mapper, + ITimeFactory $timeFactory) { + $this->mapper = $mapper; + $this->timeFactory = $timeFactory; + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if (!($event instanceof UserLiveStatusEvent)) { + // Unrelated + return; + } + + $user = $event->getUser(); + try { + $userStatus = $this->mapper->findByUserId($user->getUID()); + } catch (DoesNotExistException $ex) { + $userStatus = new UserStatus(); + $userStatus->setUserId($user->getUID()); + $userStatus->setStatus('offline'); + $userStatus->setStatusTimestamp(0); + $userStatus->setIsUserDefined(false); + } + + // If the status is user-defined and one of the persistent statuses, we + // will not override it. + if ($userStatus->getIsUserDefined() && + \in_array($userStatus->getStatus(), $this->persistentUserStatuses, true)) { + return; + } + + $needsUpdate = false; + + // If the current status is older than 5 minutes, + // treat it as outdated and update + if ($userStatus->getStatusTimestamp() < ($this->timeFactory->getTime() - $this->offlineThreshold)) { + $needsUpdate = true; + } + + // If the emitted status is more important than the current status + // treat it as outdated and update + if (array_search($event->getStatus(), $this->priorityOrderedStatuses) < array_search($userStatus->getStatus(), $this->priorityOrderedStatuses)) { + $needsUpdate = true; + } + + if ($needsUpdate) { + $userStatus->setStatus($event->getStatus()); + $userStatus->setStatusTimestamp($event->getTimestamp()); + $userStatus->setIsUserDefined(false); + + if ($userStatus->getId() === null) { + $this->mapper->insert($userStatus); + } else { + $this->mapper->update($userStatus); + } + } + } +} diff --git a/apps/user_status/lib/Migration/Version0001Date20200602134824.php b/apps/user_status/lib/Migration/Version0001Date20200602134824.php new file mode 100644 index 00000000000..82b33f815b7 --- /dev/null +++ b/apps/user_status/lib/Migration/Version0001Date20200602134824.php @@ -0,0 +1,97 @@ +<?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\Migration; + +use Doctrine\DBAL\Types\Types; +use OCP\DB\ISchemaWrapper; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Class Version0001Date20200602134824 + * + * @package OCA\UserStatus\Migration + */ +class Version0001Date20200602134824 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + * @since 20.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $statusTable = $schema->createTable('user_status'); + $statusTable->addColumn('id', Types::BIGINT, [ + 'autoincrement' => true, + 'notnull' => true, + 'length' => 20, + 'unsigned' => true, + ]); + $statusTable->addColumn('user_id', Types::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $statusTable->addColumn('status', Types::STRING, [ + 'notnull' => true, + 'length' => 255, + ]); + $statusTable->addColumn('status_timestamp', Types::INTEGER, [ + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + ]); + $statusTable->addColumn('is_user_defined', Types::BOOLEAN, [ + 'notnull' => true, + ]); + $statusTable->addColumn('message_id', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + ]); + $statusTable->addColumn('custom_icon', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + ]); + $statusTable->addColumn('custom_message', Types::TEXT, [ + 'notnull' => false, + ]); + $statusTable->addColumn('clear_at', Types::INTEGER, [ + 'notnull' => false, + 'length' => 11, + 'unsigned' => true, + ]); + + $statusTable->setPrimaryKey(['id']); + $statusTable->addUniqueIndex(['user_id'], 'user_status_uid_ix'); + $statusTable->addIndex(['clear_at'], 'user_status_clr_ix'); + + return $schema; + } +} 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']); + } + } +} |