diff options
author | Joas Schilling <213943+nickvergessen@users.noreply.github.com> | 2022-09-29 19:27:22 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-09-29 19:27:22 +0300 |
commit | 76d376e941a1b51c5627d538a747bdc899a93865 (patch) | |
tree | 7ddef9a9e1f1865ff11cbd972b897fc893da7f29 /lib | |
parent | cdc3c5598ffb6eb3f476420c36dcc6646d5d75a1 (diff) | |
parent | 0c7599f9d2e076eae51c527ea76b2d005a7186e1 (diff) |
Merge pull request #8001 from nextcloud/feature/noid/call-url-reference-provider
🏷️ /call/ reference provider
Diffstat (limited to 'lib')
-rw-r--r-- | lib/AppInfo/Application.php | 5 | ||||
-rw-r--r-- | lib/Chat/ChatManager.php | 11 | ||||
-rw-r--r-- | lib/Collaboration/Reference/ReferenceInvalidationListener.php | 52 | ||||
-rw-r--r-- | lib/Collaboration/Reference/TalkReferenceProvider.php | 282 | ||||
-rw-r--r-- | lib/Room.php | 15 |
5 files changed, 361 insertions, 4 deletions
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 3e44c14c7..d50322263 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace OCA\Talk\AppInfo; +use OCA\Talk\Collaboration\Reference\ReferenceInvalidationListener; +use OCA\Talk\Collaboration\Reference\TalkReferenceProvider; use OCP\Util; use OCA\Circles\Events\AddingCircleMemberEvent; use OCA\Circles\Events\CircleDestroyedEvent; @@ -145,6 +147,8 @@ class Application extends App implements IBootstrap { $context->registerProfileLinkAction(TalkAction::class); + $context->registerReferenceProvider(TalkReferenceProvider::class); + $context->registerTalkBackend(TalkBackend::class); } @@ -172,6 +176,7 @@ class Application extends App implements IBootstrap { CommandListener::register($dispatcher); CollaboratorsListener::register($dispatcher); ResourceListener::register($dispatcher); + ReferenceInvalidationListener::register($dispatcher); // Register only when Talk Updates are not disabled if ($server->getConfig()->getAppValue('spreed', 'changelog', 'yes') === 'yes') { ChangelogListener::register($dispatcher); diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index d26cbeb40..e62e44b62 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -38,6 +38,7 @@ use OCA\Talk\Service\ParticipantService; use OCA\Talk\Service\PollService; use OCA\Talk\Share\RoomShareProvider; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Collaboration\Reference\IReferenceManager; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; use OCP\Comments\NotFoundException; @@ -86,7 +87,6 @@ class ChatManager { private IDBConnection $connection; private INotificationManager $notificationManager; private IManager $shareManager; - private Manager $manager; private RoomShareProvider $shareProvider; private ParticipantService $participantService; private PollService $pollService; @@ -95,26 +95,26 @@ class ChatManager { protected ICache $cache; protected ICache $unreadCountCache; protected AttachmentService $attachmentService; + protected IReferenceManager $referenceManager; public function __construct(CommentsManager $commentsManager, IEventDispatcher $dispatcher, IDBConnection $connection, INotificationManager $notificationManager, IManager $shareManager, - Manager $manager, RoomShareProvider $shareProvider, ParticipantService $participantService, PollService $pollService, Notifier $notifier, ICacheFactory $cacheFactory, ITimeFactory $timeFactory, - AttachmentService $attachmentService) { + AttachmentService $attachmentService, + IReferenceManager $referenceManager) { $this->commentsManager = $commentsManager; $this->dispatcher = $dispatcher; $this->connection = $connection; $this->notificationManager = $notificationManager; $this->shareManager = $shareManager; - $this->manager = $manager; $this->shareProvider = $shareProvider; $this->participantService = $participantService; $this->pollService = $pollService; @@ -123,6 +123,7 @@ class ChatManager { $this->unreadCountCache = $cacheFactory->createDistributed('talk/unreadcount'); $this->timeFactory = $timeFactory; $this->attachmentService = $attachmentService; + $this->referenceManager = $referenceManager; } /** @@ -378,6 +379,8 @@ class ChatManager { $this->attachmentService->deleteAttachmentByMessageId((int) $comment->getId()); + $this->referenceManager->invalidateCache($chat->getToken()); + return $this->addSystemMessage( $chat, $participant->getAttendee()->getActorType(), diff --git a/lib/Collaboration/Reference/ReferenceInvalidationListener.php b/lib/Collaboration/Reference/ReferenceInvalidationListener.php new file mode 100644 index 000000000..2db37c824 --- /dev/null +++ b/lib/Collaboration/Reference/ReferenceInvalidationListener.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 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\Talk\Collaboration\Reference; + +use OCA\Talk\Events\RoomEvent; +use OCA\Talk\Room; +use OCP\Collaboration\Reference\IReferenceManager; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Server; + +class ReferenceInvalidationListener { + public static function register(IEventDispatcher $dispatcher): void { + $listener = static function (RoomEvent $event): void { + $room = $event->getRoom(); + $referenceManager = Server::get(IReferenceManager::class); + + $referenceManager->invalidateCache($room->getToken()); + }; + + $dispatcher->addListener(Room::EVENT_AFTER_ROOM_DELETE, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_USERS_ADD, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_DESCRIPTION_SET, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_LISTABLE_SET, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_LOBBY_STATE_SET, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_NAME_SET, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_REMOVE, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, $listener); + $dispatcher->addListener(Room::EVENT_AFTER_SET_MESSAGE_EXPIRATION, $listener); + } +} diff --git a/lib/Collaboration/Reference/TalkReferenceProvider.php b/lib/Collaboration/Reference/TalkReferenceProvider.php new file mode 100644 index 000000000..f3b0b1ba2 --- /dev/null +++ b/lib/Collaboration/Reference/TalkReferenceProvider.php @@ -0,0 +1,282 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2022 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/>. + */ + +namespace OCA\Talk\Collaboration\Reference; + +use OCA\Talk\Chat\ChatManager; +use OCA\Talk\Chat\MessageParser; +use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCP\Collaboration\Reference\IReference; +use OCP\Collaboration\Reference\IReferenceProvider; +use OCP\Collaboration\Reference\Reference; +use OCP\IL10N; +use OCP\IURLGenerator; + +/** + * @psalm-type ReferenceMatch = array{token: string, message: int|null} + */ +class TalkReferenceProvider implements IReferenceProvider { + protected IURLGenerator $urlGenerator; + protected Manager $roomManager; + protected ChatManager $chatManager; + protected MessageParser $messageParser; + protected IL10N $l; + protected ?string $userId; + + public function __construct(IURLGenerator $urlGenerator, + Manager $manager, + ChatManager $chatManager, + MessageParser $messageParser, + IL10N $l, + ?string $userId) { + $this->urlGenerator = $urlGenerator; + $this->roomManager = $manager; + $this->chatManager = $chatManager; + $this->messageParser = $messageParser; + $this->l = $l; + $this->userId = $userId; + } + + + public function matchReference(string $referenceText): bool { + return $this->getTalkAppLinkToken($referenceText) !== null; + } + + /** + * @param string $referenceText + * @return array|null + * @psalm-return ReferenceMatch|null + */ + protected function getTalkAppLinkToken(string $referenceText): ?array { + $indexPhpUrl = $this->urlGenerator->getAbsoluteURL('/index.php/call/'); + $rewriteUrl = $this->urlGenerator->getAbsoluteURL('/call/'); + + if (str_starts_with($referenceText, $indexPhpUrl)) { + $urlOfInterest = substr($referenceText, strlen($indexPhpUrl)); + } elseif (str_starts_with($referenceText, $rewriteUrl)) { + $urlOfInterest = substr($referenceText, strlen($rewriteUrl)); + } else { + return null; + } + + $hashPosition = strpos($urlOfInterest, '#'); + $queryPosition = strpos($urlOfInterest, '?'); + + if ($hashPosition === false && $queryPosition === false) { + return [ + 'token' => $urlOfInterest, + 'message' => null, + ]; + } + + if ($hashPosition !== false && $queryPosition !== false) { + $cutPosition = min($hashPosition, $queryPosition); + } elseif ($hashPosition !== false) { + $cutPosition = $hashPosition; + } else { + $cutPosition = $queryPosition; + } + + $token = substr($urlOfInterest, 0, $cutPosition); + $messageId = null; + if ($hashPosition !== false) { + $afterHash = substr($urlOfInterest, $hashPosition + 1); + if (preg_match('/^message_(\d+)$/', $afterHash, $matches)) { + $messageId = (int) $matches[1]; + } + } + + return [ + 'token' => $token, + 'message' => $messageId, + ]; + } + + /** + * @inheritDoc + */ + public function resolveReference(string $referenceText): ?IReference { + if ($this->matchReference($referenceText)) { + $reference = new Reference($referenceText); + try { + $this->fetchReference($reference); + } catch (RoomNotFoundException|ParticipantNotFoundException $e) { + $reference->setRichObject('call', null); + $reference->setAccessible(false); + } + return $reference; + } + + return null; + } + + /** + * @throws RoomNotFoundException + */ + protected function fetchReference(Reference $reference): void { + if ($this->userId === null) { + throw new RoomNotFoundException(); + } + + $referenceMatch = $this->getTalkAppLinkToken($reference->getId()); + if ($referenceMatch === null) { + throw new RoomNotFoundException(); + } + + $room = $this->roomManager->getRoomForUserByToken($referenceMatch['token'], $this->userId); + try { + $participant = $room->getParticipant($this->userId); + } catch (ParticipantNotFoundException $e) { + $participant = null; + } + + /** + * Default handling: + * Title is the conversation name + * Description the conversation description + */ + $roomName = $room->getDisplayName($this->userId); + $title = $roomName; + $description = ''; + + if ($participant instanceof Participant + || $this->roomManager->isRoomListableByUser($room, $this->userId)) { + $description = $room->getDescription(); + } + + + /** + * If linking to a comment and the user is already a participant + * Title is "Message of {user} in {conversation}" + * Description is the plain text chat message + */ + if ($participant && !empty($referenceMatch['message'])) { + $comment = $this->chatManager->getComment($room, (string) $referenceMatch['message']); + $message = $this->messageParser->createMessage($room, $participant, $comment, $this->l); + $this->messageParser->parseMessage($message); + + $placeholders = $replacements = []; + foreach ($message->getMessageParameters() as $placeholder => $parameter) { + $placeholders[] = '{' . $placeholder . '}'; + if ($parameter['type'] === 'user' || $parameter['type'] === 'guest') { + $replacements[] = '@' . $parameter['name']; + } else { + $replacements[] = $parameter['name']; + } + } + $description = str_replace($placeholders, $replacements, $message->getMessage()); + + $titleLine = $this->l->t('Message of {user} in {conversation}'); + if ($room->getType() === Room::TYPE_ONE_TO_ONE) { + $titleLine = $this->l->t('Message of {user}'); + } + + $displayName = $message->getActorDisplayName(); + if ($message->getActorType() === Attendee::ACTOR_GUESTS) { + if ($displayName === '') { + $displayName = $this->l->t('Guest'); + } else { + $displayName = $this->l->t('%s (guest)', $displayName); + } + } elseif ($displayName === '') { + $titleLine = $this->l->t('Message of a deleted user in {conversation}'); + } + + $title = str_replace( + ['{user}', '{conversation}'], + [$displayName, $title], + $titleLine + ); + } + + $reference->setTitle($title); + $reference->setDescription($description); + $reference->setUrl($this->urlGenerator->linkToRouteAbsolute('spreed.Page.showCall', ['token' => $room->getToken()])); + $reference->setImageUrl($this->getRoomIconUrl($room, $this->userId)); + + $reference->setRichObject('call', [ + 'id' => $room->getToken(), + 'name' => $roomName, + 'link' => $reference->getUrl(), + 'call-type' => $this->getRoomType($room), + ]); + } + + /** + * @inheritDoc + */ + public function getCachePrefix(string $referenceId): string { + $referenceMatch = $this->getTalkAppLinkToken($referenceId); + if ($referenceMatch === null) { + return ''; + } + + return $referenceMatch['token']; + } + + /** + * @inheritDoc + */ + public function getCacheKey(string $referenceId): ?string { + $referenceMatch = $this->getTalkAppLinkToken($referenceId); + if ($referenceMatch === null) { + return ''; + } + + return ($this->userId ?? '') . '#' . ($referenceMatch['message'] ?? 0); + } + + protected function getRoomIconUrl(Room $room, string $userId): string { + if ($room->getType() === Room::TYPE_ONE_TO_ONE) { + return $this->urlGenerator->linkToRouteAbsolute( + 'core.avatar.getAvatar', + [ + 'userId' => $room->getSecondParticipant($userId), + 'size' => 64, + ] + ); + } + + return $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('spreed', 'changelog.svg')); + } + + protected function getRoomType(Room $room): string { + switch ($room->getType()) { + case Room::TYPE_ONE_TO_ONE: + return 'one2one'; + case Room::TYPE_GROUP: + return 'group'; + case Room::TYPE_PUBLIC: + return 'public'; + default: + return 'unknown'; + } + } +} diff --git a/lib/Room.php b/lib/Room.php index aee8ff8ce..032325008 100644 --- a/lib/Room.php +++ b/lib/Room.php @@ -369,6 +369,21 @@ class Room { return $this->name; } + public function getSecondParticipant(string $userId): string { + if ($this->getType() !== self::TYPE_ONE_TO_ONE) { + throw new \InvalidArgumentException('Not a one-to-one room'); + } + $participants = json_decode($this->getName(), true); + + foreach ($participants as $uid) { + if ($uid !== $userId) { + return $uid; + } + } + + return $this->getName(); + } + public function getDisplayName(string $userId): string { return $this->manager->resolveRoomDisplayName($this, $userId); } |