diff options
Diffstat (limited to 'apps/user_status/lib')
33 files changed, 869 insertions, 1043 deletions
diff --git a/apps/user_status/lib/AppInfo/Application.php b/apps/user_status/lib/AppInfo/Application.php index 7a47fc45c95..5199c3fdbf0 100644 --- a/apps/user_status/lib/AppInfo/Application.php +++ b/apps/user_status/lib/AppInfo/Application.php @@ -3,40 +3,29 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\AppInfo; use OCA\UserStatus\Capabilities; use OCA\UserStatus\Connector\UserStatusProvider; +use OCA\UserStatus\Dashboard\UserStatusWidget; use OCA\UserStatus\Listener\BeforeTemplateRenderedListener; +use OCA\UserStatus\Listener\OutOfOfficeStatusListener; use OCA\UserStatus\Listener\UserDeletedListener; use OCA\UserStatus\Listener\UserLiveStatusListener; -use OCA\UserStatus\Dashboard\UserStatusWidget; 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\IConfig; +use OCP\User\Events\OutOfOfficeChangedEvent; +use OCP\User\Events\OutOfOfficeClearedEvent; +use OCP\User\Events\OutOfOfficeEndedEvent; +use OCP\User\Events\OutOfOfficeScheduledEvent; +use OCP\User\Events\OutOfOfficeStartedEvent; use OCP\User\Events\UserDeletedEvent; use OCP\User\Events\UserLiveStatusEvent; use OCP\UserStatus\IManager; @@ -71,6 +60,11 @@ class Application extends App implements IBootstrap { $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(UserLiveStatusEvent::class, UserLiveStatusListener::class); $context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class); + $context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeStartedEvent::class, OutOfOfficeStatusListener::class); + $context->registerEventListener(OutOfOfficeEndedEvent::class, OutOfOfficeStatusListener::class); $config = $this->getContainer()->query(IConfig::class); $shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; diff --git a/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php b/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php index 0d8f5ac431b..51a9c623a03 100644 --- a/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php +++ b/apps/user_status/lib/BackgroundJob/ClearOldStatusesBackgroundJob.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\BackgroundJob; @@ -37,21 +20,18 @@ use OCP\BackgroundJob\TimedJob; */ class ClearOldStatusesBackgroundJob extends TimedJob { - /** @var UserStatusMapper */ - private $mapper; - /** * ClearOldStatusesBackgroundJob constructor. * * @param ITimeFactory $time * @param UserStatusMapper $mapper */ - public function __construct(ITimeFactory $time, - UserStatusMapper $mapper) { + public function __construct( + ITimeFactory $time, + private UserStatusMapper $mapper, + ) { parent::__construct($time); - $this->mapper = $mapper; - // Run every time the cron is run $this->setInterval(60); } @@ -61,7 +41,7 @@ class ClearOldStatusesBackgroundJob extends TimedJob { protected function run($argument) { $now = $this->time->getTime(); - $this->mapper->clearMessagesOlderThan($now); + $this->mapper->clearOlderThanClearAt($now); $this->mapper->clearStatusesOlderThan($now - StatusService::INVALIDATE_STATUS_THRESHOLD, $now); } } diff --git a/apps/user_status/lib/Capabilities.php b/apps/user_status/lib/Capabilities.php index f5b35896a56..c3edbc032d6 100644 --- a/apps/user_status/lib/Capabilities.php +++ b/apps/user_status/lib/Capabilities.php @@ -3,30 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus; -use OCA\UserStatus\Service\EmojiService; use OCP\Capabilities\ICapability; +use OCP\IEmojiHelper; /** * Class Capabilities @@ -34,27 +17,21 @@ use OCP\Capabilities\ICapability; * @package OCA\UserStatus */ class Capabilities implements ICapability { - - /** @var EmojiService */ - private $emojiService; - - /** - * Capabilities constructor. - * - * @param EmojiService $emojiService - */ - public function __construct(EmojiService $emojiService) { - $this->emojiService = $emojiService; + public function __construct( + private IEmojiHelper $emojiHelper, + ) { } /** - * @inheritDoc + * @return array{user_status: array{enabled: bool, restore: bool, supports_emoji: bool, supports_busy: bool}} */ public function getCapabilities() { return [ 'user_status' => [ 'enabled' => true, - 'supports_emoji' => $this->emojiService->doesPlatformSupportEmoji(), + 'restore' => true, + 'supports_emoji' => $this->emojiHelper->doesPlatformSupportEmoji(), + 'supports_busy' => true, ], ]; } diff --git a/apps/user_status/lib/Connector/UserStatus.php b/apps/user_status/lib/Connector/UserStatus.php index ff05ded9e2b..04467a99e5e 100644 --- a/apps/user_status/lib/Connector/UserStatus.php +++ b/apps/user_status/lib/Connector/UserStatus.php @@ -3,31 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Connector; use DateTimeImmutable; -use OCP\UserStatus\IUserStatus; use OCA\UserStatus\Db; +use OCP\UserStatus\IUserStatus; class UserStatus implements IUserStatus { @@ -46,21 +29,19 @@ class UserStatus implements IUserStatus { /** @var DateTimeImmutable|null */ private $clearAt; - /** @var Db\UserStatus */ - private $internalStatus; - - public function __construct(Db\UserStatus $status) { - $this->internalStatus = $status; - $this->userId = $status->getUserId(); - $this->status = $status->getStatus(); - $this->message = $status->getCustomMessage(); - $this->icon = $status->getCustomIcon(); + public function __construct( + private Db\UserStatus $internalStatus, + ) { + $this->userId = $this->internalStatus->getUserId(); + $this->status = $this->internalStatus->getStatus(); + $this->message = $this->internalStatus->getCustomMessage(); + $this->icon = $this->internalStatus->getCustomIcon(); - if ($status->getStatus() === IUserStatus::INVISIBLE) { + if ($this->internalStatus->getStatus() === IUserStatus::INVISIBLE) { $this->status = IUserStatus::OFFLINE; } - if ($status->getClearAt() !== null) { - $this->clearAt = DateTimeImmutable::createFromFormat('U', (string)$status->getClearAt()); + if ($this->internalStatus->getClearAt() !== null) { + $this->clearAt = DateTimeImmutable::createFromFormat('U', (string)$this->internalStatus->getClearAt()); } } diff --git a/apps/user_status/lib/Connector/UserStatusProvider.php b/apps/user_status/lib/Connector/UserStatusProvider.php index 46b89739f7c..e84d69d1eb2 100644 --- a/apps/user_status/lib/Connector/UserStatusProvider.php +++ b/apps/user_status/lib/Connector/UserStatusProvider.php @@ -3,44 +3,25 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Connector; +use OC\UserStatus\ISettableProvider; use OCA\UserStatus\Service\StatusService; use OCP\UserStatus\IProvider; -use OC\UserStatus\ISettableProvider; class UserStatusProvider implements IProvider, ISettableProvider { - /** @var StatusService */ - private $service; - /** * UserStatusProvider constructor. * * @param StatusService $service */ - public function __construct(StatusService $service) { - $this->service = $service; + public function __construct( + private StatusService $service, + ) { } /** @@ -57,15 +38,15 @@ class UserStatusProvider implements IProvider, ISettableProvider { return $userStatuses; } - public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup): void { - $this->service->setUserStatus($userId, $status, $messageId, $createBackup); + public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup, ?string $customMessage = null): void { + $this->service->setUserStatus($userId, $status, $messageId, $createBackup, $customMessage); } public function revertUserStatus(string $userId, string $messageId, string $status): void { - $this->service->revertUserStatus($userId, $messageId, $status); + $this->service->revertUserStatus($userId, $messageId); } public function revertMultipleUserStatus(array $userIds, string $messageId, string $status): void { - $this->service->revertMultipleUserStatus($userIds, $messageId, $status); + $this->service->revertMultipleUserStatus($userIds, $messageId); } } diff --git a/apps/user_status/lib/ContactsMenu/StatusProvider.php b/apps/user_status/lib/ContactsMenu/StatusProvider.php new file mode 100644 index 00000000000..6a6949b46ba --- /dev/null +++ b/apps/user_status/lib/ContactsMenu/StatusProvider.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus\ContactsMenu; + +use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\Service\StatusService; +use OCP\Contacts\ContactsMenu\IBulkProvider; +use OCP\Contacts\ContactsMenu\IEntry; +use function array_combine; +use function array_filter; +use function array_map; + +class StatusProvider implements IBulkProvider { + + public function __construct( + private StatusService $statusService, + ) { + } + + public function process(array $entries): void { + $uids = array_filter( + array_map(fn (IEntry $entry): ?string => $entry->getProperty('UID'), $entries) + ); + + $statuses = $this->statusService->findByUserIds($uids); + /** @var array<string, UserStatus> $indexed */ + $indexed = array_combine( + array_map(fn (UserStatus $status) => $status->getUserId(), $statuses), + $statuses + ); + + foreach ($entries as $entry) { + $uid = $entry->getProperty('UID'); + if ($uid !== null && isset($indexed[$uid])) { + $status = $indexed[$uid]; + $entry->setStatus( + $status->getStatus(), + $status->getCustomMessage(), + $status->getStatusMessageTimestamp(), + $status->getCustomIcon(), + ); + } + } + } + +} diff --git a/apps/user_status/lib/Controller/HeartbeatController.php b/apps/user_status/lib/Controller/HeartbeatController.php index c11a63b4420..30f4af6572a 100644 --- a/apps/user_status/lib/Controller/HeartbeatController.php +++ b/apps/user_status/lib/Controller/HeartbeatController.php @@ -3,34 +3,19 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Controller; use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\ResponseDefinitions; use OCA\UserStatus\Service\StatusService; -use OCP\AppFramework\Controller; -use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; -use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; @@ -38,47 +23,43 @@ use OCP\IUserSession; use OCP\User\Events\UserLiveStatusEvent; use OCP\UserStatus\IUserStatus; -class HeartbeatController extends Controller { - - /** @var IEventDispatcher */ - private $eventDispatcher; - - /** @var IUserSession */ - private $userSession; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var StatusService */ - private $service; - - public function __construct(string $appName, - IRequest $request, - IEventDispatcher $eventDispatcher, - IUserSession $userSession, - ITimeFactory $timeFactory, - StatusService $service) { +/** + * @psalm-import-type UserStatusPrivate from ResponseDefinitions + */ +class HeartbeatController extends OCSController { + + public function __construct( + string $appName, + IRequest $request, + private IEventDispatcher $eventDispatcher, + private IUserSession $userSession, + private ITimeFactory $timeFactory, + private StatusService $service, + ) { parent::__construct($appName, $request); - $this->eventDispatcher = $eventDispatcher; - $this->userSession = $userSession; - $this->timeFactory = $timeFactory; - $this->service = $service; } /** - * @NoAdminRequired + * Keep the status alive + * + * @param string $status Only online, away + * + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NO_CONTENT, list<empty>, array{}> * - * @param string $status - * @return JSONResponse + * 200: Status successfully updated + * 204: User has no status to keep alive + * 400: Invalid status to update */ - public function heartbeat(string $status): JSONResponse { + #[NoAdminRequired] + #[ApiRoute(verb: 'PUT', url: '/api/v1/heartbeat')] + public function heartbeat(string $status): DataResponse { if (!\in_array($status, [IUserStatus::ONLINE, IUserStatus::AWAY], true)) { - return new JSONResponse([], Http::STATUS_BAD_REQUEST); + return new DataResponse([], Http::STATUS_BAD_REQUEST); } $user = $this->userSession->getUser(); if ($user === null) { - return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); + return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR); } $event = new UserLiveStatusEvent( @@ -91,11 +72,11 @@ class HeartbeatController extends Controller { $userStatus = $event->getUserStatus(); if (!$userStatus) { - return new JSONResponse([], Http::STATUS_NO_CONTENT); + return new DataResponse([], Http::STATUS_NO_CONTENT); } /** @psalm-suppress UndefinedInterfaceMethod */ - return new JSONResponse($this->formatStatus($userStatus->getInternal())); + return new DataResponse($this->formatStatus($userStatus->getInternal())); } private function formatStatus(UserStatus $status): array { diff --git a/apps/user_status/lib/Controller/PredefinedStatusController.php b/apps/user_status/lib/Controller/PredefinedStatusController.php index ea1ff5209b8..70262d1108a 100644 --- a/apps/user_status/lib/Controller/PredefinedStatusController.php +++ b/apps/user_status/lib/Controller/PredefinedStatusController.php @@ -3,43 +3,27 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Controller; +use OCA\UserStatus\ResponseDefinitions; use OCA\UserStatus\Service\PredefinedStatusService; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IRequest; /** - * Class DefaultStatusController - * * @package OCA\UserStatus\Controller + * + * @psalm-import-type UserStatusPredefined from ResponseDefinitions */ class PredefinedStatusController extends OCSController { - /** @var PredefinedStatusService */ - private $predefinedStatusService; - /** * AStatusController constructor. * @@ -47,22 +31,27 @@ class PredefinedStatusController extends OCSController { * @param IRequest $request * @param PredefinedStatusService $predefinedStatusService */ - public function __construct(string $appName, - IRequest $request, - PredefinedStatusService $predefinedStatusService) { + public function __construct( + string $appName, + IRequest $request, + private PredefinedStatusService $predefinedStatusService, + ) { parent::__construct($appName, $request); - $this->predefinedStatusService = $predefinedStatusService; } /** - * @NoAdminRequired + * Get all predefined messages + * + * @return DataResponse<Http::STATUS_OK, list<UserStatusPredefined>, array{}> * - * @return DataResponse + * 200: Predefined statuses returned */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/predefined_statuses/')] public function findAll():DataResponse { // Filtering out the invisible one, that should only be set by API - return new DataResponse(array_filter($this->predefinedStatusService->getDefaultStatuses(), function (array $status) { + return new DataResponse(array_values(array_filter($this->predefinedStatusService->getDefaultStatuses(), function (array $status) { return !array_key_exists('visible', $status) || $status['visible'] === true; - })); + }))); } } diff --git a/apps/user_status/lib/Controller/StatusesController.php b/apps/user_status/lib/Controller/StatusesController.php index d30389e1716..44688c39023 100644 --- a/apps/user_status/lib/Controller/StatusesController.php +++ b/apps/user_status/lib/Controller/StatusesController.php @@ -3,43 +3,30 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Controller; use OCA\UserStatus\Db\UserStatus; +use OCA\UserStatus\ResponseDefinitions; use OCA\UserStatus\Service\StatusService; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSNotFoundException; use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\UserStatus\IUserStatus; +/** + * @psalm-import-type UserStatusType from ResponseDefinitions + * @psalm-import-type UserStatusPublic from ResponseDefinitions + */ class StatusesController extends OCSController { - /** @var StatusService */ - private $service; - /** * StatusesController constructor. * @@ -47,35 +34,44 @@ class StatusesController extends OCSController { * @param IRequest $request * @param StatusService $service */ - public function __construct(string $appName, - IRequest $request, - StatusService $service) { + public function __construct( + string $appName, + IRequest $request, + private StatusService $service, + ) { parent::__construct($appName, $request); - $this->service = $service; } /** - * @NoAdminRequired + * Find statuses of users * - * @param int|null $limit - * @param int|null $offset - * @return DataResponse + * @param int|null $limit Maximum number of statuses to find + * @param non-negative-int|null $offset Offset for finding statuses + * @return DataResponse<Http::STATUS_OK, list<UserStatusPublic>, array{}> + * + * 200: Statuses returned */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/statuses')] public function findAll(?int $limit = null, ?int $offset = null): DataResponse { $allStatuses = $this->service->findAll($limit, $offset); - return new DataResponse(array_map(function ($userStatus) { + return new DataResponse(array_values(array_map(function ($userStatus) { return $this->formatStatus($userStatus); - }, $allStatuses)); + }, $allStatuses))); } /** - * @NoAdminRequired + * Find the status of a user + * + * @param string $userId ID of the user + * @return DataResponse<Http::STATUS_OK, UserStatusPublic, array{}> + * @throws OCSNotFoundException The user was not found * - * @param string $userId - * @return DataResponse - * @throws OCSNotFoundException + * 200: Status returned */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/statuses/{userId}')] public function find(string $userId): DataResponse { try { $userStatus = $this->service->findByUserId($userId); @@ -88,9 +84,10 @@ class StatusesController extends OCSController { /** * @param UserStatus $status - * @return array{userId: string, message: string, icon: string, clearAt: int, status: string} + * @return UserStatusPublic */ private function formatStatus(UserStatus $status): array { + /** @var UserStatusType $visibleStatus */ $visibleStatus = $status->getStatus(); if ($visibleStatus === IUserStatus::INVISIBLE) { $visibleStatus = IUserStatus::OFFLINE; diff --git a/apps/user_status/lib/Controller/UserStatusController.php b/apps/user_status/lib/Controller/UserStatusController.php index 8708a7c2aac..9b3807ce86e 100644 --- a/apps/user_status/lib/Controller/UserStatusController.php +++ b/apps/user_status/lib/Controller/UserStatusController.php @@ -3,84 +3,60 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Simon Spannagel <simonspa@kth.se> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Controller; +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; 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\ResponseDefinitions; use OCA\UserStatus\Service\StatusService; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\ApiRoute; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; 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; +use Psr\Log\LoggerInterface; +/** + * @psalm-import-type UserStatusType from ResponseDefinitions + * @psalm-import-type UserStatusPrivate from ResponseDefinitions + */ 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) { + public function __construct( + string $appName, + IRequest $request, + private ?string $userId, + private LoggerInterface $logger, + private StatusService $service, + private CalendarStatusService $calendarStatusService, + ) { parent::__construct($appName, $request); - $this->userId = $userId; - $this->logger = $logger; - $this->service = $service; } /** - * @NoAdminRequired + * Get the status of the current user * - * @return DataResponse - * @throws OCSNotFoundException + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}> + * @throws OCSNotFoundException The user was not found + * + * 200: The status was found successfully */ + #[NoAdminRequired] + #[ApiRoute(verb: 'GET', url: '/api/v1/user_status')] public function getStatus(): DataResponse { try { + $this->calendarStatusService->processCalendarStatus($this->userId); $userStatus = $this->service->findByUserId($this->userId); } catch (DoesNotExistException $ex) { throw new OCSNotFoundException('No status for the current user'); @@ -90,12 +66,16 @@ class UserStatusController extends OCSController { } /** - * @NoAdminRequired + * Update the status type of the current user + * + * @param string $statusType The new status type + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}> + * @throws OCSBadRequestException The status type is invalid * - * @param string $statusType - * @return DataResponse - * @throws OCSBadRequestException + * 200: The status was updated successfully */ + #[NoAdminRequired] + #[ApiRoute(verb: 'PUT', url: '/api/v1/user_status/status')] public function setStatus(string $statusType): DataResponse { try { $status = $this->service->setStatus($this->userId, $statusType, null, true); @@ -109,15 +89,19 @@ class UserStatusController extends OCSController { } /** - * @NoAdminRequired + * Set the message to a predefined message for the current user + * + * @param string $messageId ID of the predefined message + * @param int|null $clearAt When the message should be cleared + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}> + * @throws OCSBadRequestException The clearAt or message-id is invalid * - * @param string $messageId - * @param int|null $clearAt - * @return DataResponse - * @throws OCSBadRequestException + * 200: The message was updated successfully */ + #[NoAdminRequired] + #[ApiRoute(verb: 'PUT', url: '/api/v1/user_status/message/predefined')] public function setPredefinedMessage(string $messageId, - ?int $clearAt): DataResponse { + ?int $clearAt): DataResponse { try { $status = $this->service->setPredefinedMessage($this->userId, $messageId, $clearAt); $this->service->removeBackupUserStatus($this->userId); @@ -132,19 +116,24 @@ class UserStatusController extends OCSController { } /** - * @NoAdminRequired + * Set the message to a custom message for the current user * - * @param string|null $statusIcon - * @param string $message - * @param int|null $clearAt - * @return DataResponse - * @throws OCSBadRequestException + * @param string|null $statusIcon Icon of the status + * @param string|null $message Message of the status + * @param int|null $clearAt When the message should be cleared + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate, array{}> + * @throws OCSBadRequestException The clearAt or icon is invalid or the message is too long + * @throws OCSNotFoundException No status for the current user + * + * 200: The message was updated successfully */ + #[NoAdminRequired] + #[ApiRoute(verb: 'PUT', url: '/api/v1/user_status/message/custom')] public function setCustomMessage(?string $statusIcon, - ?string $message, - ?int $clearAt): DataResponse { + ?string $message, + ?int $clearAt): DataResponse { try { - if ($message !== null && $message !== '') { + if (($statusIcon !== null && $statusIcon !== '') || ($message !== null && $message !== '') || ($clearAt !== null && $clearAt !== 0)) { $status = $this->service->setCustomMessage($this->userId, $statusIcon, $message, $clearAt); } else { $this->service->clearMessage($this->userId); @@ -161,34 +150,51 @@ class UserStatusController extends OCSController { } 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); + } catch (DoesNotExistException $ex) { + throw new OCSNotFoundException('No status for the current user'); } } /** - * @NoAdminRequired + * Clear the message of the current user * - * @return DataResponse + * @return DataResponse<Http::STATUS_OK, list<empty>, array{}> + * + * 200: Message cleared successfully */ - public function clearStatus(): DataResponse { - $this->service->clearStatus($this->userId); + #[NoAdminRequired] + #[ApiRoute(verb: 'DELETE', url: '/api/v1/user_status/message')] + public function clearMessage(): DataResponse { + $this->service->clearMessage($this->userId); return new DataResponse([]); } /** - * @NoAdminRequired + * Revert the status to the previous status * - * @return DataResponse + * @param string $messageId ID of the message to delete + * + * @return DataResponse<Http::STATUS_OK, UserStatusPrivate|list<empty>, array{}> + * + * 200: Status reverted */ - public function clearMessage(): DataResponse { - $this->service->clearMessage($this->userId); + #[NoAdminRequired] + #[ApiRoute(verb: 'DELETE', url: '/api/v1/user_status/revert/{messageId}')] + public function revertStatus(string $messageId): DataResponse { + $backupStatus = $this->service->revertUserStatus($this->userId, $messageId, true); + if ($backupStatus) { + return new DataResponse($this->formatStatus($backupStatus)); + } return new DataResponse([]); } /** * @param UserStatus $status - * @return array + * @return UserStatusPrivate */ private function formatStatus(UserStatus $status): array { + /** @var UserStatusType $visibleStatus */ + $visibleStatus = $status->getStatus(); return [ 'userId' => $status->getUserId(), 'message' => $status->getCustomMessage(), @@ -196,7 +202,7 @@ class UserStatusController extends OCSController { 'messageIsPredefined' => $status->getMessageId() !== null, 'icon' => $status->getCustomIcon(), 'clearAt' => $status->getClearAt(), - 'status' => $status->getStatus(), + 'status' => $visibleStatus, 'statusIsUserDefined' => $status->getIsUserDefined(), ]; } diff --git a/apps/user_status/lib/Dashboard/UserStatusWidget.php b/apps/user_status/lib/Dashboard/UserStatusWidget.php index 10411dc7f9d..2870a2c1907 100644 --- a/apps/user_status/lib/Dashboard/UserStatusWidget.php +++ b/apps/user_status/lib/Dashboard/UserStatusWidget.php @@ -3,34 +3,25 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Dashboard; use OCA\UserStatus\AppInfo\Application; use OCA\UserStatus\Db\UserStatus; use OCA\UserStatus\Service\StatusService; -use OCP\Dashboard\IWidget; -use OCP\IInitialStateService; +use OCP\AppFramework\Services\IInitialState; +use OCP\Dashboard\IAPIWidget; +use OCP\Dashboard\IAPIWidgetV2; +use OCP\Dashboard\IIconWidget; +use OCP\Dashboard\IOptionWidget; +use OCP\Dashboard\Model\WidgetItem; +use OCP\Dashboard\Model\WidgetItems; +use OCP\Dashboard\Model\WidgetOptions; +use OCP\IDateTimeFormatter; use OCP\IL10N; +use OCP\IURLGenerator; use OCP\IUserManager; use OCP\IUserSession; use OCP\UserStatus\IUserStatus; @@ -40,42 +31,27 @@ use OCP\UserStatus\IUserStatus; * * @package OCA\UserStatus */ -class UserStatusWidget implements IWidget { - - /** @var IL10N */ - private $l10n; - - /** @var IInitialStateService */ - private $initialStateService; - - /** @var IUserManager */ - private $userManager; - - /** @var IUserSession */ - private $userSession; - - /** @var StatusService */ - private $service; - +class UserStatusWidget implements IAPIWidget, IAPIWidgetV2, IIconWidget, IOptionWidget { /** * UserStatusWidget constructor * * @param IL10N $l10n - * @param IInitialStateService $initialStateService + * @param IDateTimeFormatter $dateTimeFormatter + * @param IURLGenerator $urlGenerator + * @param IInitialState $initialStateService * @param IUserManager $userManager * @param IUserSession $userSession * @param StatusService $service */ - public function __construct(IL10N $l10n, - IInitialStateService $initialStateService, - IUserManager $userManager, - IUserSession $userSession, - StatusService $service) { - $this->l10n = $l10n; - $this->initialStateService = $initialStateService; - $this->userManager = $userManager; - $this->userSession = $userSession; - $this->service = $service; + public function __construct( + private IL10N $l10n, + private IDateTimeFormatter $dateTimeFormatter, + private IURLGenerator $urlGenerator, + private IInitialState $initialStateService, + private IUserManager $userManager, + private IUserSession $userSession, + private StatusService $service, + ) { } /** @@ -103,7 +79,16 @@ class UserStatusWidget implements IWidget { * @inheritDoc */ public function getIconClass(): string { - return 'icon-user-status'; + return 'icon-user-status-dark'; + } + + /** + * @inheritDoc + */ + public function getIconUrl(): string { + return $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->imagePath(Application::APP_ID, 'app-dark.svg') + ); } /** @@ -117,28 +102,22 @@ class UserStatusWidget implements IWidget { * @inheritDoc */ public function load(): void { - \OCP\Util::addScript(Application::APP_ID, 'dashboard'); - - $currentUser = $this->userSession->getUser(); - if ($currentUser === null) { - $this->initialStateService->provideInitialState(Application::APP_ID, 'dashboard_data', []); - return; - } - $currentUserId = $currentUser->getUID(); + } + private function getWidgetData(string $userId, ?string $since = null, int $limit = 7): array { // Fetch status updates and filter current user $recentStatusUpdates = array_slice( array_filter( - $this->service->findAllRecentStatusChanges(8, 0), - static function (UserStatus $status) use ($currentUserId): bool { - return $status->getUserId() !== $currentUserId; + $this->service->findAllRecentStatusChanges($limit + 1, 0), + static function (UserStatus $status) use ($userId, $since): bool { + return $status->getUserId() !== $userId + && ($since === null || $status->getStatusTimestamp() > (int)$since); } ), 0, - 7 + $limit ); - - $this->initialStateService->provideInitialState(Application::APP_ID, 'dashboard_data', array_map(function (UserStatus $status): array { + return array_map(function (UserStatus $status): array { $user = $this->userManager->get($status->getUserId()); $displayName = $status->getUserId(); if ($user !== null) { @@ -153,8 +132,46 @@ class UserStatusWidget implements IWidget { : $status->getStatus(), 'icon' => $status->getCustomIcon(), 'message' => $status->getCustomMessage(), - 'timestamp' => $status->getStatusTimestamp(), + 'timestamp' => $status->getStatusMessageTimestamp(), ]; - }, $recentStatusUpdates)); + }, $recentStatusUpdates); + } + + /** + * @inheritDoc + */ + public function getItems(string $userId, ?string $since = null, int $limit = 7): array { + $widgetItemsData = $this->getWidgetData($userId, $since, $limit); + + return array_values(array_map(function (array $widgetData) { + $formattedDate = $this->dateTimeFormatter->formatTimeSpan($widgetData['timestamp']); + return new WidgetItem( + $widgetData['displayName'], + $widgetData['icon'] . ($widgetData['icon'] ? ' ' : '') . $widgetData['message'] . ', ' . $formattedDate, + // https://nextcloud.local/index.php/u/julien + $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute('profile.ProfilePage.index', ['targetUserId' => $widgetData['userId']]) + ), + $this->urlGenerator->getAbsoluteURL( + $this->urlGenerator->linkToRoute('core.avatar.getAvatar', ['userId' => $widgetData['userId'], 'size' => 44]) + ), + (string)$widgetData['timestamp'] + ); + }, $widgetItemsData)); + } + + /** + * @inheritDoc + */ + public function getItemsV2(string $userId, ?string $since = null, int $limit = 7): WidgetItems { + $items = $this->getItems($userId, $since, $limit); + return new WidgetItems( + $items, + count($items) === 0 ? $this->l10n->t('No recent status changes') : '', + ); + } + + public function getWidgetOptions(): WidgetOptions { + return new WidgetOptions(true); } } diff --git a/apps/user_status/lib/Db/UserStatus.php b/apps/user_status/lib/Db/UserStatus.php index 8907c4a2c1b..b2da4a9e07a 100644 --- a/apps/user_status/lib/Db/UserStatus.php +++ b/apps/user_status/lib/Db/UserStatus.php @@ -3,29 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Db; use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; /** * Class UserStatus @@ -42,16 +26,18 @@ use OCP\AppFramework\Db\Entity; * @method void setStatusTimestamp(int $statusTimestamp) * @method bool getIsUserDefined() * @method void setIsUserDefined(bool $isUserDefined) - * @method string getMessageId() + * @method string|null getMessageId() * @method void setMessageId(string|null $messageId) - * @method string getCustomIcon() + * @method string|null getCustomIcon() * @method void setCustomIcon(string|null $customIcon) - * @method string getCustomMessage() + * @method string|null getCustomMessage() * @method void setCustomMessage(string|null $customMessage) - * @method int getClearAt() + * @method int|null getClearAt() * @method void setClearAt(int|null $clearAt) - * @method setIsBackup(bool $true): void + * @method setIsBackup(bool $isBackup): void * @method getIsBackup(): bool + * @method int getStatusMessageTimestamp() + * @method void setStatusMessageTimestamp(int $statusTimestamp) */ class UserStatus extends Entity { @@ -82,15 +68,19 @@ class UserStatus extends Entity { /** @var bool $isBackup */ public $isBackup; + /** @var int */ + protected $statusMessageTimestamp = 0; + public function __construct() { $this->addType('userId', 'string'); $this->addType('status', 'string'); - $this->addType('statusTimestamp', 'int'); + $this->addType('statusTimestamp', Types::INTEGER); $this->addType('isUserDefined', 'boolean'); $this->addType('messageId', 'string'); $this->addType('customIcon', 'string'); $this->addType('customMessage', 'string'); - $this->addType('clearAt', 'int'); + $this->addType('clearAt', Types::INTEGER); $this->addType('isBackup', 'boolean'); + $this->addType('statusMessageTimestamp', Types::INTEGER); } } diff --git a/apps/user_status/lib/Db/UserStatusMapper.php b/apps/user_status/lib/Db/UserStatusMapper.php index f67cfcd472d..15982d44fd8 100644 --- a/apps/user_status/lib/Db/UserStatusMapper.php +++ b/apps/user_status/lib/Db/UserStatusMapper.php @@ -3,29 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Db; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\QBMapper; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; @@ -75,11 +59,17 @@ class UserStatusMapper extends QBMapper { $qb ->select('*') ->from($this->tableName) - ->orderBy('status_timestamp', 'DESC') - ->where($qb->expr()->notIn('status', $qb->createNamedParameter([IUserStatus::ONLINE, IUserStatus::AWAY, IUserStatus::OFFLINE], IQueryBuilder::PARAM_STR_ARRAY))) - ->orWhere($qb->expr()->isNotNull('message_id')) - ->orWhere($qb->expr()->isNotNull('custom_icon')) - ->orWhere($qb->expr()->isNotNull('custom_message')); + ->orderBy('status_message_timestamp', 'DESC') + ->where($qb->expr()->andX( + $qb->expr()->neq('status_message_timestamp', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT), + $qb->expr()->orX( + $qb->expr()->notIn('status', $qb->createNamedParameter([IUserStatus::ONLINE, IUserStatus::AWAY, IUserStatus::OFFLINE], IQueryBuilder::PARAM_STR_ARRAY)), + $qb->expr()->isNotNull('message_id'), + $qb->expr()->isNotNull('custom_icon'), + $qb->expr()->isNotNull('custom_message'), + ), + $qb->expr()->notLike('user_id', $qb->createNamedParameter($this->db->escapeLikeParameter('_') . '%')) + )); if ($limit !== null) { $qb->setMaxResults($limit); @@ -94,9 +84,9 @@ class UserStatusMapper extends QBMapper { /** * @param string $userId * @return UserStatus - * @throws \OCP\AppFramework\Db\DoesNotExistException + * @throws DoesNotExistException */ - public function findByUserId(string $userId, bool $isBackup = false):UserStatus { + public function findByUserId(string $userId, bool $isBackup = false): UserStatus { $qb = $this->db->getQueryBuilder(); $qb ->select('*') @@ -137,7 +127,7 @@ class UserStatusMapper extends QBMapper { $qb->expr()->eq('status', $qb->createNamedParameter(IUserStatus::ONLINE)) )); - $qb->execute(); + $qb->executeStatement(); } /** @@ -145,17 +135,13 @@ class UserStatusMapper extends QBMapper { * * @param int $timestamp */ - public function clearMessagesOlderThan(int $timestamp): void { + public function clearOlderThanClearAt(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)) + $qb->delete($this->tableName) ->where($qb->expr()->isNotNull('clear_at')) ->andWhere($qb->expr()->lte('clear_at', $qb->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))); - $qb->execute(); + $qb->executeStatement(); } @@ -164,15 +150,13 @@ class UserStatusMapper extends QBMapper { * * @param string $userId * @param string $messageId - * @param string $status * @return bool True if an entry was deleted */ - public function deleteCurrentStatusToRestoreBackup(string $userId, string $messageId, string $status): bool { + public function deleteCurrentStatusToRestoreBackup(string $userId, string $messageId): bool { $qb = $this->db->getQueryBuilder(); $qb->delete($this->tableName) ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))) ->andWhere($qb->expr()->eq('message_id', $qb->createNamedParameter($messageId))) - ->andWhere($qb->expr()->eq('status', $qb->createNamedParameter($status))) ->andWhere($qb->expr()->eq('is_backup', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL))); return $qb->executeStatement() > 0; } diff --git a/apps/user_status/lib/Exception/InvalidClearAtException.php b/apps/user_status/lib/Exception/InvalidClearAtException.php index a0ca08ade76..a3bd4dfa0d0 100644 --- a/apps/user_status/lib/Exception/InvalidClearAtException.php +++ b/apps/user_status/lib/Exception/InvalidClearAtException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Exception; diff --git a/apps/user_status/lib/Exception/InvalidMessageIdException.php b/apps/user_status/lib/Exception/InvalidMessageIdException.php index dbf8d83d970..1feb36a916a 100644 --- a/apps/user_status/lib/Exception/InvalidMessageIdException.php +++ b/apps/user_status/lib/Exception/InvalidMessageIdException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Exception; diff --git a/apps/user_status/lib/Exception/InvalidStatusIconException.php b/apps/user_status/lib/Exception/InvalidStatusIconException.php index 7a7c7947d5b..80dff2a7666 100644 --- a/apps/user_status/lib/Exception/InvalidStatusIconException.php +++ b/apps/user_status/lib/Exception/InvalidStatusIconException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Exception; diff --git a/apps/user_status/lib/Exception/InvalidStatusTypeException.php b/apps/user_status/lib/Exception/InvalidStatusTypeException.php index 12115054b2c..a09284be40e 100644 --- a/apps/user_status/lib/Exception/InvalidStatusTypeException.php +++ b/apps/user_status/lib/Exception/InvalidStatusTypeException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Exception; diff --git a/apps/user_status/lib/Exception/StatusMessageTooLongException.php b/apps/user_status/lib/Exception/StatusMessageTooLongException.php index c52f6079874..03d578abf46 100644 --- a/apps/user_status/lib/Exception/StatusMessageTooLongException.php +++ b/apps/user_status/lib/Exception/StatusMessageTooLongException.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Exception; diff --git a/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php index daf7265d988..ab3a1e62beb 100644 --- a/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php +++ b/apps/user_status/lib/Listener/BeforeTemplateRenderedListener.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Julius Härtl <jus@bitgrid.net> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Listener; @@ -36,21 +18,14 @@ use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\IInitialStateService; use OCP\IUserSession; +use OCP\Util; +/** @template-implements IEventListener<BeforeTemplateRenderedEvent> */ class BeforeTemplateRenderedListener implements IEventListener { /** @var ProfileManager */ private $profileManager; - /** @var IUserSession */ - private $userSession; - - /** @var IInitialStateService */ - private $initialState; - - /** @var JSDataService */ - private $jsDataService; - /** * BeforeTemplateRenderedListener constructor. * @@ -61,14 +36,11 @@ class BeforeTemplateRenderedListener implements IEventListener { */ public function __construct( ProfileManager $profileManager, - IUserSession $userSession, - IInitialStateService $initialState, - JSDataService $jsDataService + private IUserSession $userSession, + private IInitialStateService $initialState, + private JSDataService $jsDataService, ) { $this->profileManager = $profileManager; - $this->userSession = $userSession; - $this->initialState = $initialState; - $this->jsDataService = $jsDataService; } /** @@ -97,7 +69,7 @@ class BeforeTemplateRenderedListener implements IEventListener { return ['profileEnabled' => $this->profileManager->isProfileEnabled($user)]; }); - \OCP\Util::addScript('user_status', 'menu'); - \OCP\Util::addStyle('user_status', 'user-status-menu'); + Util::addScript('user_status', 'menu'); + Util::addStyle('user_status', 'user-status-menu'); } } diff --git a/apps/user_status/lib/Listener/OutOfOfficeStatusListener.php b/apps/user_status/lib/Listener/OutOfOfficeStatusListener.php new file mode 100644 index 00000000000..6337d637896 --- /dev/null +++ b/apps/user_status/lib/Listener/OutOfOfficeStatusListener.php @@ -0,0 +1,57 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Listener; + +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\User\Events\OutOfOfficeChangedEvent; +use OCP\User\Events\OutOfOfficeClearedEvent; +use OCP\User\Events\OutOfOfficeEndedEvent; +use OCP\User\Events\OutOfOfficeScheduledEvent; +use OCP\User\Events\OutOfOfficeStartedEvent; +use OCP\UserStatus\IManager; +use OCP\UserStatus\IUserStatus; + +/** + * Class UserDeletedListener + * + * @template-implements IEventListener<OutOfOfficeScheduledEvent|OutOfOfficeChangedEvent|OutOfOfficeClearedEvent|OutOfOfficeStartedEvent|OutOfOfficeEndedEvent> + * + */ +class OutOfOfficeStatusListener implements IEventListener { + public function __construct( + private IJobList $jobsList, + private ITimeFactory $time, + private IManager $manager, + ) { + } + + /** + * @inheritDoc + */ + public function handle(Event $event): void { + if ($event instanceof OutOfOfficeClearedEvent) { + $this->manager->revertUserStatus($event->getData()->getUser()->getUID(), IUserStatus::MESSAGE_OUT_OF_OFFICE, IUserStatus::DND); + $this->jobsList->scheduleAfter(UserStatusAutomation::class, $this->time->getTime(), ['userId' => $event->getData()->getUser()->getUID()]); + return; + } + + if ($event instanceof OutOfOfficeScheduledEvent + || $event instanceof OutOfOfficeChangedEvent + || $event instanceof OutOfOfficeStartedEvent + || $event instanceof OutOfOfficeEndedEvent + ) { + // This might be overwritten by the office hours automation, but that is ok. This is just in case no office hours are set + $this->jobsList->scheduleAfter(UserStatusAutomation::class, $this->time->getTime(), ['userId' => $event->getData()->getUser()->getUID()]); + } + } +} diff --git a/apps/user_status/lib/Listener/UserDeletedListener.php b/apps/user_status/lib/Listener/UserDeletedListener.php index 0cacc89971a..bf021635156 100644 --- a/apps/user_status/lib/Listener/UserDeletedListener.php +++ b/apps/user_status/lib/Listener/UserDeletedListener.php @@ -3,50 +3,32 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Listener; use OCA\UserStatus\Service\StatusService; -use OCP\EventDispatcher\IEventListener; use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; use OCP\User\Events\UserDeletedEvent; /** * Class UserDeletedListener * * @package OCA\UserStatus\Listener + * @template-implements IEventListener<UserDeletedEvent> */ class UserDeletedListener implements IEventListener { - /** @var StatusService */ - private $service; - /** * UserDeletedListener constructor. * * @param StatusService $service */ - public function __construct(StatusService $service) { - $this->service = $service; + public function __construct( + private StatusService $service, + ) { } diff --git a/apps/user_status/lib/Listener/UserLiveStatusListener.php b/apps/user_status/lib/Listener/UserLiveStatusListener.php index c015e684142..2db999d3712 100644 --- a/apps/user_status/lib/Listener/UserLiveStatusListener.php +++ b/apps/user_status/lib/Listener/UserLiveStatusListener.php @@ -3,52 +3,39 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Listener; -use OCA\UserStatus\Db\UserStatus; +use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService; use OCA\UserStatus\Connector\UserStatus as ConnectorUserStatus; +use OCA\UserStatus\Db\UserStatus; use OCA\UserStatus\Db\UserStatusMapper; use OCA\UserStatus\Service\StatusService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; -use OCP\EventDispatcher\IEventListener; +use OCP\DB\Exception; use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; use OCP\User\Events\UserLiveStatusEvent; use OCP\UserStatus\IUserStatus; +use Psr\Log\LoggerInterface; /** * Class UserDeletedListener * * @package OCA\UserStatus\Listener + * @template-implements IEventListener<UserLiveStatusEvent> */ class UserLiveStatusListener implements IEventListener { - private UserStatusMapper $mapper; - private ITimeFactory $timeFactory; - - public function __construct(UserStatusMapper $mapper, - ITimeFactory $timeFactory) { - $this->mapper = $mapper; - $this->timeFactory = $timeFactory; + public function __construct( + private UserStatusMapper $mapper, + private StatusService $statusService, + private ITimeFactory $timeFactory, + private CalendarStatusService $calendarStatusService, + private LoggerInterface $logger, + ) { } /** @@ -62,7 +49,8 @@ class UserLiveStatusListener implements IEventListener { $user = $event->getUser(); try { - $userStatus = $this->mapper->findByUserId($user->getUID()); + $this->calendarStatusService->processCalendarStatus($user->getUID()); + $userStatus = $this->statusService->findByUserId($user->getUID()); } catch (DoesNotExistException $ex) { $userStatus = new UserStatus(); $userStatus->setUserId($user->getUID()); @@ -71,10 +59,16 @@ class UserLiveStatusListener implements IEventListener { $userStatus->setIsUserDefined(false); } - // If the status is user-defined and one of the persistent statuses, we + // If the status is user-defined and one of the persistent status, we // will not override it. - if ($userStatus->getIsUserDefined() && - \in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) { + if ($userStatus->getIsUserDefined() + && \in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) { + return; + } + + // Don't overwrite the "away" calendar status if it's set + if ($userStatus->getMessageId() === IUserStatus::MESSAGE_CALENDAR_BUSY) { + $event->setUserStatus(new ConnectorUserStatus($userStatus)); return; } @@ -98,7 +92,19 @@ class UserLiveStatusListener implements IEventListener { $userStatus->setIsUserDefined(false); if ($userStatus->getId() === null) { - $this->mapper->insert($userStatus); + try { + $this->mapper->insert($userStatus); + } catch (Exception $e) { + if ($e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { + // A different process might have written another status + // update to the DB while we're processing our stuff. + // We can safely ignore it as we're only changing between AWAY and ONLINE + // and not doing anything with the message or icon. + $this->logger->debug('Unique constraint violation for live user status', ['exception' => $e]); + return; + } + throw $e; + } } else { $this->mapper->update($userStatus); } diff --git a/apps/user_status/lib/Migration/Version0001Date20200602134824.php b/apps/user_status/lib/Migration/Version0001Date20200602134824.php index 64490cc4207..678c2ec245a 100644 --- a/apps/user_status/lib/Migration/Version0001Date20200602134824.php +++ b/apps/user_status/lib/Migration/Version0001Date20200602134824.php @@ -3,32 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Migration; -use OCP\DB\Types; use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; use OCP\Migration\IOutput; use OCP\Migration\SimpleMigrationStep; diff --git a/apps/user_status/lib/Migration/Version0002Date20200902144824.php b/apps/user_status/lib/Migration/Version0002Date20200902144824.php index 0c222eff8cd..199d2a4cc6b 100644 --- a/apps/user_status/lib/Migration/Version0002Date20200902144824.php +++ b/apps/user_status/lib/Migration/Version0002Date20200902144824.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Migration; diff --git a/apps/user_status/lib/Migration/Version1000Date20201111130204.php b/apps/user_status/lib/Migration/Version1000Date20201111130204.php index 8b20ed8306f..b0789684da0 100644 --- a/apps/user_status/lib/Migration/Version1000Date20201111130204.php +++ b/apps/user_status/lib/Migration/Version1000Date20201111130204.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020 Joas Schilling <coding@schilljs.com> - * - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Migration; diff --git a/apps/user_status/lib/Migration/Version1003Date20210809144824.php b/apps/user_status/lib/Migration/Version1003Date20210809144824.php new file mode 100644 index 00000000000..7c6cf76adbe --- /dev/null +++ b/apps/user_status/lib/Migration/Version1003Date20210809144824.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\UserStatus\Migration; + +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * @package OCA\UserStatus\Migration + */ +class Version1003Date20210809144824 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + * @since 23.0.0 + */ + public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $statusTable = $schema->getTable('user_status'); + + if (!$statusTable->hasColumn('is_backup')) { + $statusTable->addColumn('is_backup', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + ]); + } + + return $schema; + } +} diff --git a/apps/user_status/lib/Migration/Version1008Date20230921144701.php b/apps/user_status/lib/Migration/Version1008Date20230921144701.php new file mode 100644 index 00000000000..30ebbf37b0e --- /dev/null +++ b/apps/user_status/lib/Migration/Version1008Date20230921144701.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version1008Date20230921144701 extends SimpleMigrationStep { + + public function __construct( + private IDBConnection $connection, + ) { + } + + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $statusTable = $schema->getTable('user_status'); + if (!($statusTable->hasColumn('status_message_timestamp'))) { + $statusTable->addColumn('status_message_timestamp', Types::INTEGER, [ + 'notnull' => true, + 'length' => 11, + 'unsigned' => true, + 'default' => 0, + ]); + } + if (!$statusTable->hasIndex('user_status_mtstmp_ix')) { + $statusTable->addIndex(['status_message_timestamp'], 'user_status_mtstmp_ix'); + } + + return $schema; + } + + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + $qb = $this->connection->getQueryBuilder(); + + $update = $qb->update('user_status') + ->set('status_message_timestamp', 'status_timestamp'); + + $update->executeStatement(); + } +} diff --git a/apps/user_status/lib/Migration/Version2301Date20210809144824.php b/apps/user_status/lib/Migration/Version2301Date20210809144824.php deleted file mode 100644 index 947378484c6..00000000000 --- a/apps/user_status/lib/Migration/Version2301Date20210809144824.php +++ /dev/null @@ -1,58 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2021 Carl Schwan <carl@carlschwan.eu> - * - * @author Carl Schwan <carl@carlschwan.eu> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * - */ -namespace OCA\UserStatus\Migration; - -use OCP\DB\ISchemaWrapper; -use OCP\DB\Types; -use OCP\Migration\IOutput; -use OCP\Migration\SimpleMigrationStep; - -/** - * @package OCA\UserStatus\Migration - */ -class Version2301Date20210809144824 extends SimpleMigrationStep { - - /** - * @param IOutput $output - * @param \Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` - * @param array $options - * @return null|ISchemaWrapper - * @since 23.0.0 - */ - public function changeSchema(IOutput $output, \Closure $schemaClosure, array $options) { - /** @var ISchemaWrapper $schema */ - $schema = $schemaClosure(); - - $statusTable = $schema->getTable('user_status'); - - $statusTable->addColumn('is_backup', Types::BOOLEAN, [ - 'notnull' => false, - 'default' => false, - ]); - - return $schema; - } -} diff --git a/apps/user_status/lib/ResponseDefinitions.php b/apps/user_status/lib/ResponseDefinitions.php new file mode 100644 index 00000000000..82f606dd301 --- /dev/null +++ b/apps/user_status/lib/ResponseDefinitions.php @@ -0,0 +1,44 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\UserStatus; + +/** + * @psalm-type UserStatusClearAtTimeType = "day"|"week" + * + * @psalm-type UserStatusClearAt = array{ + * type: "period"|"end-of", + * time: int|UserStatusClearAtTimeType, + * } + * + * @psalm-type UserStatusPredefined = array{ + * id: string, + * icon: string, + * message: string, + * clearAt: ?UserStatusClearAt, + * } + * + * @psalm-type UserStatusType = "online"|"away"|"dnd"|"busy"|"offline"|"invisible" + * + * @psalm-type UserStatusPublic = array{ + * userId: string, + * message: ?string, + * icon: ?string, + * clearAt: ?int, + * status: UserStatusType, + * } + * + * @psalm-type UserStatusPrivate = UserStatusPublic&array{ + * messageId: ?string, + * messageIsPredefined: bool, + * statusIsUserDefined: bool, + * } + */ +class ResponseDefinitions { +} diff --git a/apps/user_status/lib/Service/EmojiService.php b/apps/user_status/lib/Service/EmojiService.php deleted file mode 100644 index 0f197933872..00000000000 --- a/apps/user_status/lib/Service/EmojiService.php +++ /dev/null @@ -1,102 +0,0 @@ -<?php - -declare(strict_types=1); - -/** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * 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 || - $codePointType === \IntlChar::CHAR_CATEGORY_GENERAL_OTHER_TYPES) { - 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 index c08643ec64f..a777e97fe57 100644 --- a/apps/user_status/lib/Service/JSDataService.php +++ b/apps/user_status/lib/Service/JSDataService.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Service; @@ -31,22 +14,16 @@ use OCP\UserStatus\IUserStatus; 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 __construct( + private IUserSession $userSession, + private StatusService $statusService, + ) { } public function jsonSerialize(): array { diff --git a/apps/user_status/lib/Service/PredefinedStatusService.php b/apps/user_status/lib/Service/PredefinedStatusService.php index 354e0f16b32..599d5b8b52f 100644 --- a/apps/user_status/lib/Service/PredefinedStatusService.php +++ b/apps/user_status/lib/Service/PredefinedStatusService.php @@ -3,29 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * 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 @@ -36,23 +20,26 @@ use OCP\IL10N; * @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'; - - /** @var IL10N */ - private $l10n; + public const OUT_OF_OFFICE = 'out-of-office'; /** * DefaultStatusService constructor. * * @param IL10N $l10n */ - public function __construct(IL10N $l10n) { - $this->l10n = $l10n; + public function __construct( + private IL10N $l10n, + ) { } /** @@ -79,6 +66,15 @@ class PredefinedStatusService { ], ], [ + '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), @@ -109,6 +105,13 @@ class PredefinedStatusService { 'clearAt' => null, 'visible' => false, ], + [ + 'id' => self::OUT_OF_OFFICE, + 'icon' => '🛑', + 'message' => $this->getTranslatedStatusForId(self::OUT_OF_OFFICE), + 'clearAt' => null, + 'visible' => false, + ], ]; } @@ -144,9 +147,15 @@ class PredefinedStatusService { 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 '💬'; @@ -174,12 +183,18 @@ class PredefinedStatusService { 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; } @@ -195,8 +210,14 @@ class PredefinedStatusService { self::COMMUTING, self::SICK_LEAVE, self::VACATIONING, + self::OUT_OF_OFFICE, + self::BE_RIGHT_BACK, self::REMOTE_WORK, - self::CALL, + 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 index 5dd70e4ea5e..188eb26d1d7 100644 --- a/apps/user_status/lib/Service/StatusService.php +++ b/apps/user_status/lib/Service/StatusService.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2020, Georg Ehrke - * - * @author Georg Ehrke <oc.list@georgehrke.com> - * @author Joas Schilling <coding@schilljs.com> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\UserStatus\Service; @@ -37,8 +19,11 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Utility\ITimeFactory; use OCP\DB\Exception; use OCP\IConfig; -use OCP\IUser; +use OCP\IEmojiHelper; +use OCP\IUserManager; use OCP\UserStatus\IUserStatus; +use Psr\Log\LoggerInterface; +use function in_array; /** * Class StatusService @@ -46,27 +31,9 @@ use OCP\UserStatus\IUserStatus; * @package OCA\UserStatus\Service */ class StatusService { - - /** @var UserStatusMapper */ - private $mapper; - - /** @var ITimeFactory */ - private $timeFactory; - - /** @var PredefinedStatusService */ - private $predefinedStatusService; - - /** @var EmojiService */ - private $emojiService; - - /** @var bool */ - private $shareeEnumeration; - - /** @var bool */ - private $shareeEnumerationInGroupOnly; - - /** @var bool */ - private $shareeEnumerationPhone; + private bool $shareeEnumeration; + private bool $shareeEnumerationInGroupOnly; + private bool $shareeEnumerationPhone; /** * List of priorities ordered by their priority @@ -75,6 +42,7 @@ class StatusService { IUserStatus::ONLINE, IUserStatus::AWAY, IUserStatus::DND, + IUserStatus::BUSY, IUserStatus::INVISIBLE, IUserStatus::OFFLINE, ]; @@ -85,6 +53,7 @@ class StatusService { */ public const PERSISTENT_STATUSES = [ IUserStatus::AWAY, + IUserStatus::BUSY, IUserStatus::DND, IUserStatus::INVISIBLE, ]; @@ -95,27 +64,18 @@ class StatusService { /** @var int */ public const MAXIMUM_MESSAGE_LENGTH = 80; - /** - * StatusService constructor. - * - * @param UserStatusMapper $mapper - * @param ITimeFactory $timeFactory - * @param PredefinedStatusService $defaultStatusService - * @param EmojiService $emojiService - * @param IConfig $config - */ - public function __construct(UserStatusMapper $mapper, - ITimeFactory $timeFactory, - PredefinedStatusService $defaultStatusService, - EmojiService $emojiService, - IConfig $config) { - $this->mapper = $mapper; - $this->timeFactory = $timeFactory; - $this->predefinedStatusService = $defaultStatusService; - $this->emojiService = $emojiService; - $this->shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes'; - $this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes'; - $this->shareeEnumerationPhone = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes'; + public function __construct( + private UserStatusMapper $mapper, + private ITimeFactory $timeFactory, + private PredefinedStatusService $predefinedStatusService, + private IEmojiHelper $emojiHelper, + private IConfig $config, + private IUserManager $userManager, + private 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'; } /** @@ -159,7 +119,7 @@ class StatusService { * @return UserStatus * @throws DoesNotExistException */ - public function findByUserId(string $userId):UserStatus { + public function findByUserId(string $userId): UserStatus { return $this->processStatus($this->mapper->findByUserId($userId)); } @@ -182,9 +142,9 @@ class StatusService { * @throws InvalidStatusTypeException */ public function setStatus(string $userId, - string $status, - ?int $statusTimestamp, - bool $isUserDefined): UserStatus { + string $status, + ?int $statusTimestamp, + bool $isUserDefined): UserStatus { try { $userStatus = $this->mapper->findByUserId($userId); } catch (DoesNotExistException $ex) { @@ -193,9 +153,10 @@ class StatusService { } // Check if status-type is valid - if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { + if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported'); } + if ($statusTimestamp === null) { $statusTimestamp = $this->timeFactory->getTime(); } @@ -206,7 +167,7 @@ class StatusService { $userStatus->setIsBackup(false); if ($userStatus->getId() === null) { - return $this->mapper->insert($userStatus); + return $this->insertWithoutThrowingUniqueConstrain($userStatus); } return $this->mapper->update($userStatus); @@ -221,8 +182,8 @@ class StatusService { * @throws InvalidClearAtException */ public function setPredefinedMessage(string $userId, - string $messageId, - ?int $clearAt): UserStatus { + string $messageId, + ?int $clearAt): UserStatus { try { $userStatus = $this->mapper->findByUserId($userId); } catch (DoesNotExistException $ex) { @@ -247,9 +208,10 @@ class StatusService { $userStatus->setCustomIcon(null); $userStatus->setCustomMessage(null); $userStatus->setClearAt($clearAt); + $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); if ($userStatus->getId() === null) { - return $this->mapper->insert($userStatus); + return $this->insertWithoutThrowingUniqueConstrain($userStatus); } return $this->mapper->update($userStatus); @@ -260,15 +222,17 @@ class StatusService { * @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): void { + string $status, + string $messageId, + bool $createBackup, + ?string $customMessage = null): ?UserStatus { // Check if status-type is valid - if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { + if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) { throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported'); } @@ -276,21 +240,58 @@ class StatusService { 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; // Already a status set automatically => abort. + 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); - } else { - try { - $userStatus = $this->mapper->findByUserId($userId); - } catch (DoesNotExistException $ex) { - $userStatus = new UserStatus(); - $userStatus->setUserId($userId); - } } $userStatus->setStatus($status); @@ -299,20 +300,26 @@ class StatusService { $userStatus->setIsBackup(false); $userStatus->setMessageId($messageId); $userStatus->setCustomIcon(null); - $userStatus->setCustomMessage(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) { - $this->mapper->update($userStatus); - return; + return $this->mapper->update($userStatus); } - $this->mapper->insert($userStatus); + return $this->insertWithoutThrowingUniqueConstrain($userStatus); } /** * @param string $userId * @param string|null $statusIcon - * @param string $message + * @param string|null $message * @param int|null $clearAt * @return UserStatus * @throws InvalidClearAtException @@ -320,9 +327,9 @@ class StatusService { * @throws StatusMessageTooLongException */ public function setCustomMessage(string $userId, - ?string $statusIcon, - string $message, - ?int $clearAt): UserStatus { + ?string $statusIcon, + ?string $message, + ?int $clearAt): UserStatus { try { $userStatus = $this->mapper->findByUserId($userId); } catch (DoesNotExistException $ex) { @@ -334,11 +341,11 @@ class StatusService { } // Check if statusIcon contains only one character - if ($statusIcon !== null && !$this->emojiService->isValidEmoji($statusIcon)) { + 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 (\mb_strlen($message) > self::MAXIMUM_MESSAGE_LENGTH) { + 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 @@ -350,9 +357,10 @@ class StatusService { $userStatus->setCustomIcon($statusIcon); $userStatus->setCustomMessage($message); $userStatus->setClearAt($clearAt); + $userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp()); if ($userStatus->getId() === null) { - return $this->mapper->insert($userStatus); + return $this->insertWithoutThrowingUniqueConstrain($userStatus); } return $this->mapper->update($userStatus); @@ -394,6 +402,7 @@ class StatusService { $userStatus->setCustomMessage(null); $userStatus->setCustomIcon(null); $userStatus->setClearAt(null); + $userStatus->setStatusMessageTimestamp(0); $this->mapper->update($userStatus); return true; @@ -442,6 +451,7 @@ class StatusService { $this->cleanStatus($status); } if ($clearAt !== null && $clearAt < $this->timeFactory->getTime()) { + $this->cleanStatus($status); $this->cleanStatusMessage($status); } if ($status->getMessageId() !== null) { @@ -474,6 +484,7 @@ class StatusService { $status->setCustomIcon(null); $status->setCustomMessage(null); $status->setClearAt(null); + $status->setStatusMessageTimestamp(0); $this->mapper->update($status); } @@ -484,8 +495,14 @@ class StatusService { 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) { + 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']); } } @@ -505,28 +522,38 @@ class StatusService { } } - public function revertUserStatus(string $userId, string $messageId, string $status): void { + 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; + return null; } - $deleted = $this->mapper->deleteCurrentStatusToRestoreBackup($userId, $messageId, $status); + $deleted = $this->mapper->deleteCurrentStatusToRestoreBackup($userId, $messageId); if (!$deleted) { // Another status is set automatically or no status, do nothing - return; + 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, string $status): void { + public function revertMultipleUserStatus(array $userIds, string $messageId): void { // Get all user statuses and the backups $findById = $userIds; foreach ($userIds as $userId) { @@ -537,10 +564,9 @@ class StatusService { $backups = $restoreIds = $statuesToDelete = []; foreach ($userStatuses as $userStatus) { if (!$userStatus->getIsBackup() - && $userStatus->getMessageId() === $messageId - && $userStatus->getStatus() === $status) { + && $userStatus->getMessageId() === $messageId) { $statuesToDelete[$userStatus->getUserId()] = $userStatus->getId(); - } else if ($userStatus->getIsBackup()) { + } elseif ($userStatus->getIsBackup()) { $backups[$userStatus->getUserId()] = $userStatus->getId(); } } @@ -558,4 +584,16 @@ class StatusService { // 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; + } } |