From f495741e621665f27b9ac456532d92461f8c484f Mon Sep 17 00:00:00 2001 From: Joas Schilling <coding@schilljs.com> Date: Tue, 5 Mar 2024 13:05:05 +0100 Subject: [PATCH 1/5] feat(federation): Add columns to save the creation datetime and meta data Signed-off-by: Joas Schilling <coding@schilljs.com> --- appinfo/info.xml | 2 +- .../Version19000Date20240305115243.php | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 lib/Migration/Version19000Date20240305115243.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 8af180d8da9..7eaad44889f 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m ]]></description> - <version>19.0.0-dev.3</version> + <version>19.0.0-dev.4</version> <licence>agpl</licence> <author>Daniel Calviño Sánchez</author> diff --git a/lib/Migration/Version19000Date20240305115243.php b/lib/Migration/Version19000Date20240305115243.php new file mode 100644 index 00000000000..58545a26b74 --- /dev/null +++ b/lib/Migration/Version19000Date20240305115243.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2024 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\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Add creation datetime and meta-data columns to the proxy cache + */ +class Version19000Date20240305115243 extends SimpleMigrationStep { + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('talk_proxy_messages'); + if (!$table->hasColumn('creation_datetime')) { + $table->addColumn('creation_datetime', Types::DATETIME, [ + 'notnull' => false, + ]); + $table->addColumn('meta_data', Types::TEXT, [ + 'notnull' => false, + ]); + + return $schema; + } + + return null; + } +} From d4c11cc68708324df6746db40d37f6a5ebdee8f0 Mon Sep 17 00:00:00 2001 From: Joas Schilling <coding@schilljs.com> Date: Tue, 5 Mar 2024 14:40:47 +0100 Subject: [PATCH 2/5] fix(events): Expose the parent comment in events Signed-off-by: Joas Schilling <coding@schilljs.com> --- docs/events.md | 2 ++ lib/Chat/ChatManager.php | 8 ++++---- lib/Events/AMessageSentEvent.php | 5 +++++ lib/Events/ASystemMessageSentEvent.php | 4 +++- 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/events.md b/docs/events.md index ad21c67ff42..ea6bd421ec4 100644 --- a/docs/events.md +++ b/docs/events.md @@ -134,6 +134,7 @@ Used to parse mentions, replace parameters in messages with rich objects, transf * Before event: `OCA\Talk\Events\BeforeChatMessageSentEvent` * After event: `OCA\Talk\Events\ChatMessageSentEvent` * Since: 18.0.0 +* Since: 19.0.0 - Method `getParent()` was added ### Duplicate share sent @@ -153,6 +154,7 @@ listen to the `OCA\Talk\Events\SystemMessagesMultipleSentEvent` event instead. * After event: `OCA\Talk\Events\SystemMessageSentEvent` * Final event: `OCA\Talk\Events\SystemMessagesMultipleSentEvent` - Only sent once as per above explanation * Since: 18.0.0 +* Since: 19.0.0 - Method `getParent()` was added ### Deprecated events diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index 7a731213da3..e51f12a0ce3 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -173,7 +173,7 @@ public function addSystemMessage( $shouldFlush = $this->notificationManager->defer(); - $event = new BeforeSystemMessageSentEvent($chat, $comment, silent: $silent, skipLastActivityUpdate: $shouldSkipLastMessageUpdate); + $event = new BeforeSystemMessageSentEvent($chat, $comment, silent: $silent, skipLastActivityUpdate: $shouldSkipLastMessageUpdate, parent: $replyTo); $this->dispatcher->dispatchTyped($event); try { $this->commentsManager->save($comment); @@ -228,7 +228,7 @@ public function addSystemMessage( } } - $event = new SystemMessageSentEvent($chat, $comment, silent: $silent, skipLastActivityUpdate: $shouldSkipLastMessageUpdate); + $event = new SystemMessageSentEvent($chat, $comment, silent: $silent, skipLastActivityUpdate: $shouldSkipLastMessageUpdate, parent: $replyTo); $this->dispatcher->dispatchTyped($event); } catch (NotFoundException $e) { } @@ -326,7 +326,7 @@ public function sendMessage(Room $chat, ?Participant $participant, string $actor ]); } - $event = new BeforeChatMessageSentEvent($chat, $comment, $participant, $silent); + $event = new BeforeChatMessageSentEvent($chat, $comment, $participant, $silent, $replyTo); $this->dispatcher->dispatchTyped($event); $shouldFlush = $this->notificationManager->defer(); @@ -371,7 +371,7 @@ public function sendMessage(Room $chat, ?Participant $participant, string $actor // User was not mentioned, send a normal notification $this->notifier->notifyOtherParticipant($chat, $comment, $alreadyNotifiedUsers, $silent); - $event = new ChatMessageSentEvent($chat, $comment, $participant, $silent); + $event = new ChatMessageSentEvent($chat, $comment, $participant, $silent, $replyTo); $this->dispatcher->dispatchTyped($event); } catch (NotFoundException $e) { } diff --git a/lib/Events/AMessageSentEvent.php b/lib/Events/AMessageSentEvent.php index 8f3654c3121..9c8a5d7f47b 100644 --- a/lib/Events/AMessageSentEvent.php +++ b/lib/Events/AMessageSentEvent.php @@ -33,6 +33,7 @@ public function __construct( protected IComment $comment, protected ?Participant $participant = null, protected bool $silent = false, + protected ?IComment $parent = null, ) { parent::__construct( $room, @@ -50,4 +51,8 @@ public function getParticipant(): ?Participant { public function isSilentMessage(): bool { return $this->silent; } + + public function getParent(): ?IComment { + return $this->parent; + } } diff --git a/lib/Events/ASystemMessageSentEvent.php b/lib/Events/ASystemMessageSentEvent.php index a540edc5aa9..4dd55374b9d 100644 --- a/lib/Events/ASystemMessageSentEvent.php +++ b/lib/Events/ASystemMessageSentEvent.php @@ -33,13 +33,15 @@ public function __construct( IComment $comment, ?Participant $participant = null, bool $silent = false, + ?IComment $parent = null, protected bool $skipLastActivityUpdate = false, ) { parent::__construct( $room, $comment, $participant, - $silent + $silent, + $parent, ); } From 1a26108cdd7ae6680f22ee51b8763f1dea0850ed Mon Sep 17 00:00:00 2001 From: Joas Schilling <coding@schilljs.com> Date: Tue, 5 Mar 2024 14:42:42 +0100 Subject: [PATCH 3/5] fix(API): Add constants for known comments metadata Signed-off-by: Joas Schilling <coding@schilljs.com> --- lib/Chat/ChatManager.php | 21 ++++++++++--------- lib/Chat/Parser/SystemMessage.php | 2 +- lib/Chat/SystemMessage/Listener.php | 5 +++-- lib/Model/Message.php | 8 +++++-- .../features/bootstrap/FeatureContext.php | 4 ++-- 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index e51f12a0ce3..6c0d3b4786d 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -34,6 +34,7 @@ use OCA\Talk\Exceptions\MessagingNotAllowedException; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Message; use OCA\Talk\Model\Poll; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -165,7 +166,7 @@ public function addSystemMessage( if ($silent) { $comment->setMetaData([ - 'silent' => true, + Message::METADATA_SILENT => true, ]); } @@ -322,7 +323,7 @@ public function sendMessage(Room $chat, ?Participant $participant, string $actor if ($silent) { $comment->setMetaData([ - 'silent' => true, + Message::METADATA_SILENT => true, ]); } @@ -500,11 +501,11 @@ public function deleteMessage(Room $chat, IComment $comment, Participant $partic $comment->setVerb(self::VERB_MESSAGE_DELETED); $metaData = $comment->getMetaData() ?? []; - if (isset($metaData['last_edited_by_type'])) { + if (isset($metaData[Message::METADATA_LAST_EDITED_BY_TYPE])) { unset( - $metaData['last_edited_by_type'], - $metaData['last_edited_by_id'], - $metaData['last_edited_time'] + $metaData[Message::METADATA_LAST_EDITED_BY_TYPE], + $metaData[Message::METADATA_LAST_EDITED_BY_ID], + $metaData[Message::METADATA_LAST_EDITED_TIME], ); $comment->setMetaData($metaData); } @@ -557,12 +558,12 @@ public function editMessage(Room $chat, IComment $comment, Participant $particip } $metaData = $comment->getMetaData() ?? []; - $metaData['last_edited_by_type'] = $participant->getAttendee()->getActorType(); - $metaData['last_edited_by_id'] = $participant->getAttendee()->getActorId(); - $metaData['last_edited_time'] = $editTime->getTimestamp(); + $metaData[Message::METADATA_LAST_EDITED_BY_TYPE] = $participant->getAttendee()->getActorType(); + $metaData[Message::METADATA_LAST_EDITED_BY_ID] = $participant->getAttendee()->getActorId(); + $metaData[Message::METADATA_LAST_EDITED_TIME] = $editTime->getTimestamp(); $comment->setMetaData($metaData); - $wasSilent = $metaData['silent'] ?? false; + $wasSilent = $metaData[Message::METADATA_SILENT] ?? false; if (!$wasSilent) { $mentionsBefore = $comment->getMentions(); diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php index d7f48e61106..645e0e14765 100644 --- a/lib/Chat/Parser/SystemMessage.php +++ b/lib/Chat/Parser/SystemMessage.php @@ -207,7 +207,7 @@ protected function parseMessage(Message $chatMessage): void { } } elseif ($message === 'call_started') { $metaData = $comment->getMetaData() ?? []; - $silentCall = $metaData['silent'] ?? false; + $silentCall = $metaData[Message::METADATA_SILENT] ?? false; if ($silentCall) { if ($currentUserIsActor) { $parsedMessage = $this->l->t('You started a silent call'); diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index ddd680ab22d..8f0ff6839ad 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -43,6 +43,7 @@ use OCA\Talk\Manager; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\BreakoutRoom; +use OCA\Talk\Model\Message; use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -425,8 +426,8 @@ protected function fixMimeTypeOfVoiceMessage(ShareCreatedEvent|BeforeDuplicateSh } } - if (isset($metaData['silent'])) { - $silent = (bool) $metaData['silent']; + if (isset($metaData[Message::METADATA_SILENT])) { + $silent = (bool) $metaData[Message::METADATA_SILENT]; } else { $silent = false; } diff --git a/lib/Model/Message.php b/lib/Model/Message.php index d279f583db7..49ae97d4f5d 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -36,6 +36,10 @@ * @psalm-import-type TalkChatMessage from ResponseDefinitions */ class Message { + public const METADATA_LAST_EDITED_BY_TYPE = 'last_edited_by_type'; + public const METADATA_LAST_EDITED_BY_ID = 'last_edited_by_id'; + public const METADATA_LAST_EDITED_TIME = 'last_edited_time'; + public const METADATA_SILENT = 'silent'; /** @var bool */ protected $visible = true; @@ -226,8 +230,8 @@ public function toArray(string $format): array { } $metaData = $this->comment->getMetaData() ?? []; - if (!empty($metaData['silent'])) { - $data['silent'] = true; + if (!empty($metaData[self::METADATA_SILENT])) { + $data[self::METADATA_SILENT] = true; } return $data; diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index a462025eccb..d9fc9f60b22 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -2728,16 +2728,16 @@ protected function compareDataResponse(TableNode $formData = null) { $data['reactionsSelf'] = null; } } + if ($includeLastEdit) { $data['lastEditActorType'] = $message['lastEditActorType'] ?? ''; $data['lastEditActorDisplayName'] = $message['lastEditActorDisplayName'] ?? ''; $data['lastEditActorId'] = $message['lastEditActorId'] ?? ''; - if ($message['lastEditActorType'] === 'guests') { + if (($message['lastEditActorType'] ?? '') === 'guests') { $data['lastEditActorId'] = self::$sessionIdToUser[$message['lastEditActorId']]; } } - return $data; }, $messages, $expected)); } From 578b1d5873784490daed3d47540ae30781581c8d Mon Sep 17 00:00:00 2001 From: Joas Schilling <coding@schilljs.com> Date: Wed, 6 Mar 2024 12:31:22 +0100 Subject: [PATCH 4/5] feat(federation): Allow to convert a single type and id Signed-off-by: Joas Schilling <coding@schilljs.com> --- lib/Federation/Proxy/TalkV1/UserConverter.php | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/Federation/Proxy/TalkV1/UserConverter.php b/lib/Federation/Proxy/TalkV1/UserConverter.php index debc018839a..d58aa8e4004 100644 --- a/lib/Federation/Proxy/TalkV1/UserConverter.php +++ b/lib/Federation/Proxy/TalkV1/UserConverter.php @@ -45,6 +45,26 @@ public function __construct( ) { } + /** + * @return array{type: string, id: string} + */ + public function convertTypeAndId(Room $room, string $type, string $id): array { + if ($type === Attendee::ACTOR_USERS) { + $type = Attendee::ACTOR_FEDERATED_USERS; + $id .= '@' . $room->getRemoteServer(); + } elseif ($type === Attendee::ACTOR_FEDERATED_USERS) { + $localParticipants = $this->getLocalParticipants($room); + if (isset($localParticipants[$id])) { + $local = $localParticipants[$id]; + + $type = Attendee::ACTOR_USERS; + $id = $local['userId']; + } + } + + return ['type' => $type, 'id' => $id]; + } + public function convertAttendee(Room $room, array $entry, string $typeField, string $idField, string $displayNameField): array { if (!isset($entry[$typeField])) { return $entry; @@ -89,7 +109,7 @@ protected function convertMessageParameter(Room $room, array $parameter): array return $parameter; } - protected function convertMessageParameters(Room $room, array $message): array { + public function convertMessageParameters(Room $room, array $message): array { $message['messageParameters'] = array_map( fn (array $message): array => $this->convertMessageParameter($room, $message), $message['messageParameters'] From cdc988c7ffc28f212f6df847a02f8136ada0d9ea Mon Sep 17 00:00:00 2001 From: Joas Schilling <coding@schilljs.com> Date: Thu, 29 Feb 2024 17:13:31 +0100 Subject: [PATCH 5/5] feat(federation): Implement notifications for mentions, reply and full Signed-off-by: Joas Schilling <coding@schilljs.com> --- lib/Chat/MessageParser.php | 26 +++- lib/Chat/Parser/UserMention.php | 2 +- lib/Controller/ChatController.php | 2 +- lib/Federation/BackendNotifier.php | 1 + .../CloudFederationProviderTalk.php | 39 ++++- .../TalkV1/Notifier/MessageSentListener.php | 14 +- lib/Manager.php | 10 +- lib/Model/Message.php | 13 +- ...acheMessages.php => ProxyCacheMessage.php} | 32 +++- ...Mapper.php => ProxyCacheMessageMapper.php} | 26 +++- lib/Notification/FederationChatNotifier.php | 140 ++++++++++++++++++ lib/Notification/Notifier.php | 64 ++++---- lib/Service/RoomFormatter.php | 6 +- .../features/bootstrap/FeatureContext.php | 9 +- .../features/federation/chat.feature | 29 ++++ tests/php/Federation/FederationTest.php | 14 +- tests/php/Notification/NotifierTest.php | 4 + 17 files changed, 365 insertions(+), 66 deletions(-) rename lib/Model/{ProxyCacheMessages.php => ProxyCacheMessage.php} (75%) rename lib/Model/{ProxyCacheMessagesMapper.php => ProxyCacheMessageMapper.php} (72%) create mode 100644 lib/Notification/FederationChatNotifier.php diff --git a/lib/Chat/MessageParser.php b/lib/Chat/MessageParser.php index 77e42316a9b..de88ab506bc 100644 --- a/lib/Chat/MessageParser.php +++ b/lib/Chat/MessageParser.php @@ -29,6 +29,7 @@ use OCA\Talk\MatterbridgeManager; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\Message; +use OCA\Talk\Model\ProxyCacheMessage; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\BotService; @@ -51,10 +52,10 @@ class MessageParser { protected array $botNames = []; public function __construct( - protected IEventDispatcher $dispatcher, - protected IUserManager $userManager, + protected IEventDispatcher $dispatcher, + protected IUserManager $userManager, protected ParticipantService $participantService, - protected BotService $botService, + protected BotService $botService, ) { } @@ -62,6 +63,25 @@ public function createMessage(Room $room, ?Participant $participant, IComment $c return new Message($room, $participant, $comment, $l); } + public function createMessageFromProxyCache(Room $room, ?Participant $participant, ProxyCacheMessage $proxy, IL10N $l): Message { + $message = new Message($room, $participant, null, $l, $proxy); + + $message->setActor( + $proxy->getActorType(), + $proxy->getActorId(), + $proxy->getActorDisplayName(), + ); + + $message->setMessageType($proxy->getMessageType()); + + $message->setMessage( + $proxy->getMessage(), + $proxy->getParsedMessageParameters() + ); + + return $message; + } + public function parseMessage(Message $message): void { $message->setMessage($message->getComment()->getMessage(), []); diff --git a/lib/Chat/Parser/UserMention.php b/lib/Chat/Parser/UserMention.php index 257ae64eeaa..1b241088ea9 100644 --- a/lib/Chat/Parser/UserMention.php +++ b/lib/Chat/Parser/UserMention.php @@ -162,7 +162,7 @@ protected function parseMessage(Message $chatMessage): void { $messageParameters[$mentionParameterId] = [ 'type' => $mention['type'], 'id' => $chatMessage->getRoom()->getToken(), - 'name' => $chatMessage->getRoom()->getDisplayName($userId), + 'name' => $chatMessage->getRoom()->getDisplayName($userId, true), 'call-type' => $this->getRoomType($chatMessage->getRoom()), 'icon-url' => $this->avatarService->getAvatarUrl($chatMessage->getRoom()), ]; diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 5b6a9804902..bad6ee7ea7e 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -530,7 +530,7 @@ protected function prepareCommentsAsDataResponse(array $comments, int $lastCommo $message = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l); $this->messageParser->parseMessage($message); - $expireDate = $message->getComment()->getExpireDate(); + $expireDate = $message->getExpirationDateTime(); if ($expireDate instanceof \DateTime && $expireDate < $now) { $commentIdToIndex[$id] = null; continue; diff --git a/lib/Federation/BackendNotifier.php b/lib/Federation/BackendNotifier.php index 1b13b260ee7..ccb422b23bd 100644 --- a/lib/Federation/BackendNotifier.php +++ b/lib/Federation/BackendNotifier.php @@ -313,6 +313,7 @@ public function sendRoomModifiedUpdate( * Send information to remote participants that a message was posted * Sent from Host server to Remote participant server * + * @param array{remoteMessageId: int, actorType: string, actorId: string, actorDisplayName: string, messageType: string, systemMessage: string, expirationDatetime: string, message: string, messageParameter: string, creationDatetime: string, metaData: string} $messageData * @param array{unreadMessages: int, unreadMention: bool, unreadMentionDirect: bool} $unreadInfo */ public function sendMessageUpdate( diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index adf439b65c2..abe0dccd632 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -34,13 +34,15 @@ use OCA\Talk\Events\AttendeesAddedEvent; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Federation\Proxy\TalkV1\UserConverter; use OCA\Talk\Manager; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\AttendeeMapper; use OCA\Talk\Model\Invitation; use OCA\Talk\Model\InvitationMapper; -use OCA\Talk\Model\ProxyCacheMessages; -use OCA\Talk\Model\ProxyCacheMessagesMapper; +use OCA\Talk\Model\ProxyCacheMessage; +use OCA\Talk\Model\ProxyCacheMessageMapper; +use OCA\Talk\Notification\FederationChatNotifier; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; @@ -87,7 +89,9 @@ public function __construct( private ISession $session, private IEventDispatcher $dispatcher, private LoggerInterface $logger, - private ProxyCacheMessagesMapper $proxyCacheMessagesMapper, + private ProxyCacheMessageMapper $proxyCacheMessageMapper, + private FederationChatNotifier $federationChatNotifier, + private UserConverter $userConverter, ICacheFactory $cacheFactory, ) { $this->proxyCacheMessages = $cacheFactory->isAvailable() ? $cacheFactory->createDistributed('talk/pcm/') : null; @@ -316,7 +320,7 @@ private function roomModified(int $remoteAttendeeId, array $notification): array /** * @param int $remoteAttendeeId - * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, messageData: array{remoteMessageId: int, actorType: string, actorId: string, actorDisplayName: string, messageType: string, systemMessage: string, expirationDatetime: string, message: string, messageParameter: string}, unreadInfo: array{unreadMessages: int, unreadMention: bool, unreadMentionDirect: bool}} $notification + * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, messageData: array{remoteMessageId: int, actorType: string, actorId: string, actorDisplayName: string, messageType: string, systemMessage: string, expirationDatetime: string, message: string, messageParameter: string, creationDatetime: string, metaData: string}, unreadInfo: array{unreadMessages: int, unreadMention: bool, unreadMentionDirect: bool}} $notification * @return array * @throws ActionNotSupportedException * @throws AuthenticationFailedException @@ -335,7 +339,7 @@ private function messagePosted(int $remoteAttendeeId, array $notification): arra throw new ShareNotFound(); } - $message = new ProxyCacheMessages(); + $message = new ProxyCacheMessage(); $message->setLocalToken($room->getToken()); $message->setRemoteServerUrl($notification['remoteServerUrl']); $message->setRemoteToken($notification['remoteToken']); @@ -346,12 +350,25 @@ private function messagePosted(int $remoteAttendeeId, array $notification): arra $message->setMessageType($notification['messageData']['messageType']); $message->setSystemMessage($notification['messageData']['systemMessage']); if ($notification['messageData']['expirationDatetime']) { - $message->setExpirationDatetime(new \DateTimeImmutable($notification['messageData']['expirationDatetime'])); + $message->setExpirationDatetime(new \DateTime($notification['messageData']['expirationDatetime'])); } + + // We transform the parameters when storing in the PCM, so we only have + // to do it once for each message. + $convertedParameters = $this->userConverter->convertMessageParameters($room, [ + 'message' => $notification['messageData']['message'], + 'messageParameters' => json_decode($notification['messageData']['messageParameter'], true, flags: JSON_THROW_ON_ERROR), + ]); + $notification['messageData']['message'] = $convertedParameters['message']; + $notification['messageData']['messageParameter'] = json_encode($convertedParameters['messageParameters'], JSON_THROW_ON_ERROR); + $message->setMessage($notification['messageData']['message']); $message->setMessageParameters($notification['messageData']['messageParameter']); + $message->setCreationDatetime(new \DateTime($notification['messageData']['creationDatetime'])); + $message->setMetaData($notification['messageData']['metaData']); + try { - $this->proxyCacheMessagesMapper->insert($message); + $this->proxyCacheMessageMapper->insert($message); $lastMessageId = $room->getLastMessageId(); if ($notification['messageData']['remoteMessageId'] > $lastMessageId) { @@ -374,6 +391,12 @@ private function messagePosted(int $remoteAttendeeId, array $notification): arra $this->logger->error('Error saving proxy cache message failed: ' . $e->getMessage(), ['exception' => $e]); throw $e; } + + $message = $this->proxyCacheMessageMapper->findByRemote( + $notification['remoteServerUrl'], + $notification['remoteToken'], + $notification['messageData']['remoteMessageId'], + ); } try { @@ -390,6 +413,8 @@ private function messagePosted(int $remoteAttendeeId, array $notification): arra $notification['unreadInfo']['unreadMentionDirect'], ); + $this->federationChatNotifier->handleChatMessage($room, $participant, $message, $notification); + return []; } diff --git a/lib/Federation/Proxy/TalkV1/Notifier/MessageSentListener.php b/lib/Federation/Proxy/TalkV1/Notifier/MessageSentListener.php index e6920cc0654..fb795b80425 100644 --- a/lib/Federation/Proxy/TalkV1/Notifier/MessageSentListener.php +++ b/lib/Federation/Proxy/TalkV1/Notifier/MessageSentListener.php @@ -32,7 +32,9 @@ use OCA\Talk\Events\SystemMessagesMultipleSentEvent; use OCA\Talk\Federation\BackendNotifier; use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\ProxyCacheMessage; use OCA\Talk\Service\ParticipantService; +use OCP\Comments\IComment; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Federation\ICloudIdManager; @@ -78,6 +80,14 @@ public function handle(Event $event): void { } $expireDate = $event->getComment()->getExpireDate(); + $creationDate = $event->getComment()->getCreationDateTime(); + + $metaData = $event->getComment()->getMetaData() ?? []; + $parent = $event->getParent(); + if ($parent instanceof IComment) { + $metaData[ProxyCacheMessage::METADATA_REPLYTO_TYPE] = $parent->getActorType(); + $metaData[ProxyCacheMessage::METADATA_REPLYTO_ID] = $parent->getActorId(); + } $messageData = [ 'remoteMessageId' => (int) $event->getComment()->getId(), @@ -88,7 +98,9 @@ public function handle(Event $event): void { 'systemMessage' => $chatMessage->getMessageType() === ChatManager::VERB_SYSTEM ? $chatMessage->getMessageRaw() : '', 'expirationDatetime' => $expireDate ? $expireDate->format(\DateTime::ATOM) : '', 'message' => $chatMessage->getMessage(), - 'messageParameter' => json_encode($chatMessage->getMessageParameters()), + 'messageParameter' => json_encode($chatMessage->getMessageParameters(), JSON_THROW_ON_ERROR), + 'creationDatetime' => $creationDate->format(\DateTime::ATOM), + 'metaData' => json_encode($metaData, JSON_THROW_ON_ERROR), ]; $participants = $this->participantService->getParticipantsByActorType($event->getRoom(), Attendee::ACTOR_FEDERATED_USERS); diff --git a/lib/Manager.php b/lib/Manager.php index a3ff4435722..2761fb8726d 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -1096,10 +1096,6 @@ public function resolveRoomDisplayName(Room $room, string $userId, bool $forceNa return $this->l->t('Talk updates ✅'); } - if ($forceName) { - return $room->getName(); - } - if ($this->federationAuthenticator->isFederationRequest()) { try { $authenticatedRoom = $this->federationAuthenticator->getRoom(); @@ -1110,7 +1106,7 @@ public function resolveRoomDisplayName(Room $room, string $userId, bool $forceNa } } - if ($userId === '' && $room->getType() !== Room::TYPE_PUBLIC) { + if (!$forceName && $userId === '' && $room->getType() !== Room::TYPE_PUBLIC) { return $this->l->t('Private conversation'); } @@ -1151,6 +1147,10 @@ public function resolveRoomDisplayName(Room $room, string $userId, bool $forceNa return $otherParticipant; } + if ($forceName) { + return $room->getName(); + } + if (!$this->isRoomListableByUser($room, $userId)) { try { if ($userId === '') { diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 49ae97d4f5d..6f2eb993301 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -80,8 +80,9 @@ class Message { public function __construct( protected Room $room, protected ?Participant $participant, - protected IComment $comment, + protected ?IComment $comment, protected IL10N $l, + protected ?ProxyCacheMessage $proxy = null, ) { } @@ -93,7 +94,7 @@ public function getRoom(): Room { return $this->room; } - public function getComment(): IComment { + public function getComment(): ?IComment { return $this->comment; } @@ -109,6 +110,14 @@ public function getParticipant(): ?Participant { * Parsed message information */ + public function getMessageId(): int { + return $this->comment ? (int) $this->comment->getId() : $this->proxy->getRemoteMessageId(); + } + + public function getExpirationDateTime(): ?\DateTimeInterface { + return $this->comment ? $this->comment->getExpireDate() : $this->proxy->getExpirationDatetime(); + } + public function setVisibility(bool $visible): void { $this->visible = $visible; } diff --git a/lib/Model/ProxyCacheMessages.php b/lib/Model/ProxyCacheMessage.php similarity index 75% rename from lib/Model/ProxyCacheMessages.php rename to lib/Model/ProxyCacheMessage.php index 9803f3e2b70..e08afbb3914 100644 --- a/lib/Model/ProxyCacheMessages.php +++ b/lib/Model/ProxyCacheMessage.php @@ -3,7 +3,7 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> + * @copyright Copyright (c) 2024 Joas Schilling <coding@schilljs.com> * * @author Joas Schilling <coding@schilljs.com> * @@ -48,16 +48,23 @@ * @method string getMessageType() * @method void setSystemMessage(?string $systemMessage) * @method string|null getSystemMessage() - * @method void setExpirationDatetime(?\DateTimeImmutable $expirationDatetime) - * @method \DateTimeImmutable|null getExpirationDatetime() + * @method void setExpirationDatetime(?\DateTime $expirationDatetime) + * @method \DateTime|null getExpirationDatetime() * @method void setMessage(?string $message) * @method string|null getMessage() * @method void setMessageParameters(?string $messageParameters) * @method string|null getMessageParameters() + * @method void setCreationDatetime(?\DateTime $creationDatetime) + * @method \DateTime|null getCreationDatetime() + * @method void setMetaData(?string $metaData) + * @method string|null getMetaData() * * @psalm-import-type TalkRoomProxyMessage from ResponseDefinitions */ -class ProxyCacheMessages extends Entity implements \JsonSerializable { +class ProxyCacheMessage extends Entity implements \JsonSerializable { + public const METADATA_REPLYTO_TYPE = 'replyToActorType'; + public const METADATA_REPLYTO_ID = 'replyToActorId'; + protected string $localToken = ''; protected string $remoteServerUrl = ''; @@ -68,9 +75,11 @@ class ProxyCacheMessages extends Entity implements \JsonSerializable { protected ?string $actorDisplayName = null; protected ?string $messageType = null; protected ?string $systemMessage = null; - protected ?\DateTimeImmutable $expirationDatetime = null; + protected ?\DateTime $expirationDatetime = null; protected ?string $message = null; protected ?string $messageParameters = null; + protected ?\DateTime $creationDatetime = null; + protected ?string $metaData = null; public function __construct() { $this->addType('localToken', 'string'); @@ -85,6 +94,16 @@ public function __construct() { $this->addType('expirationDatetime', 'datetime'); $this->addType('message', 'string'); $this->addType('messageParameters', 'string'); + $this->addType('creationDatetime', 'datetime'); + $this->addType('metaData', 'string'); + } + + public function getParsedMessageParameters(): array { + return json_decode($this->getMessageParameters() ?? '[]', true); + } + + public function getParsedMetaData(): array { + return json_decode($this->getMetaData() ?? '[]', true); } /** @@ -100,11 +119,12 @@ public function jsonSerialize(): array { 'actorType' => $this->getActorType(), 'actorId' => $this->getActorId(), 'actorDisplayName' => $this->getActorDisplayName(), + 'timestamp' => $this->getCreationDatetime()->getTimestamp(), 'expirationTimestamp' => $expirationTimestamp, 'messageType' => $this->getMessageType(), 'systemMessage' => $this->getSystemMessage() ?? '', 'message' => $this->getMessage() ?? '', - 'messageParameters' => json_decode($this->getMessageParameters() ?? '[]', true), + 'messageParameters' => $this->getParsedMessageParameters(), ]; } } diff --git a/lib/Model/ProxyCacheMessagesMapper.php b/lib/Model/ProxyCacheMessageMapper.php similarity index 72% rename from lib/Model/ProxyCacheMessagesMapper.php rename to lib/Model/ProxyCacheMessageMapper.php index 7d6855fa143..2f1664949c8 100644 --- a/lib/Model/ProxyCacheMessagesMapper.php +++ b/lib/Model/ProxyCacheMessageMapper.php @@ -32,24 +32,36 @@ use OCP\IDBConnection; /** - * @method ProxyCacheMessages mapRowToEntity(array $row) - * @method ProxyCacheMessages findEntity(IQueryBuilder $query) - * @method ProxyCacheMessages[] findEntities(IQueryBuilder $query) - * @template-extends QBMapper<ProxyCacheMessages> + * @method ProxyCacheMessage mapRowToEntity(array $row) + * @method ProxyCacheMessage findEntity(IQueryBuilder $query) + * @method ProxyCacheMessage[] findEntities(IQueryBuilder $query) + * @template-extends QBMapper<ProxyCacheMessage> */ -class ProxyCacheMessagesMapper extends QBMapper { +class ProxyCacheMessageMapper extends QBMapper { use TTransactional; public function __construct( IDBConnection $db, ) { - parent::__construct($db, 'talk_proxy_messages', ProxyCacheMessages::class); + parent::__construct($db, 'talk_proxy_messages', ProxyCacheMessage::class); } /** * @throws DoesNotExistException */ - public function findByRemote(string $remoteServerUrl, string $remoteToken, int $remoteMessageId): ProxyCacheMessages { + public function findById(int $proxyId): ProxyCacheMessage { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('id', $query->createNamedParameter($proxyId, IQueryBuilder::PARAM_INT))); + + return $this->findEntity($query); + } + + /** + * @throws DoesNotExistException + */ + public function findByRemote(string $remoteServerUrl, string $remoteToken, int $remoteMessageId): ProxyCacheMessage { $query = $this->db->getQueryBuilder(); $query->select('*') ->from($this->getTableName()) diff --git a/lib/Notification/FederationChatNotifier.php b/lib/Notification/FederationChatNotifier.php new file mode 100644 index 00000000000..8a630c541b0 --- /dev/null +++ b/lib/Notification/FederationChatNotifier.php @@ -0,0 +1,140 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2024 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\Notification; + +use OCA\Talk\Federation\Proxy\TalkV1\UserConverter; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Message; +use OCA\Talk\Model\ProxyCacheMessage; +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCP\AppFramework\Services\IAppConfig; +use OCP\Notification\IManager; +use OCP\Notification\INotification; + +class FederationChatNotifier { + public function __construct( + protected IAppConfig $appConfig, + protected IManager $notificationManager, + protected UserConverter $userConverter, + ) { + } + + /** + * @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, messageData: array{remoteMessageId: int, actorType: string, actorId: string, actorDisplayName: string, messageType: string, systemMessage: string, expirationDatetime: string, message: string, messageParameter: string, creationDatetime: string, metaData: string}, unreadInfo: array{unreadMessages: int, unreadMention: bool, unreadMentionDirect: bool}} $inboundNotification + */ + public function handleChatMessage(Room $room, Participant $participant, ProxyCacheMessage $message, array $inboundNotification): void { + $metaData = json_decode($inboundNotification['messageData']['metaData'] ?? '', true, flags: JSON_THROW_ON_ERROR); + + if (isset($metaData[Message::METADATA_SILENT])) { + // Silent message, skip notification handling + return; + } + + // Also notify default participants in one-to-one chats or when the admin default is "always" + $defaultLevel = $this->appConfig->getAppValueInt('default_group_notification', Participant::NOTIFY_MENTION); + if ($participant->getAttendee()->getNotificationLevel() === Participant::NOTIFY_MENTION + || ($defaultLevel !== Participant::NOTIFY_NEVER && $participant->getAttendee()->getNotificationLevel() === Participant::NOTIFY_DEFAULT)) { + if ($this->isRepliedTo($room, $participant, $metaData)) { + $notification = $this->createNotification($room, $message, 'reply'); + $notification->setUser($participant->getAttendee()->getActorId()); + $this->notificationManager->notify($notification); + } elseif ($this->isMentioned($participant, $message)) { + $notification = $this->createNotification($room, $message, 'mention'); + $notification->setUser($participant->getAttendee()->getActorId()); + $this->notificationManager->notify($notification); + } elseif ($this->isMentionedAll($room, $message)) { + $notification = $this->createNotification($room, $message, 'mention_all'); + $notification->setUser($participant->getAttendee()->getActorId()); + $this->notificationManager->notify($notification); + } + } elseif ($participant->getAttendee()->getNotificationLevel() === Participant::NOTIFY_ALWAYS + || ($defaultLevel === Participant::NOTIFY_ALWAYS && $participant->getAttendee()->getNotificationLevel() === Participant::NOTIFY_DEFAULT)) { + $notification = $this->createNotification($room, $message, 'chat'); + $notification->setUser($participant->getAttendee()->getActorId()); + $this->notificationManager->notify($notification); + } + } + + protected function isRepliedTo(Room $room, Participant $participant, array $metaData): bool { + if ($metaData[ProxyCacheMessage::METADATA_REPLYTO_TYPE] !== Attendee::ACTOR_FEDERATED_USERS) { + return false; + } + + $repliedTo = $this->userConverter->convertTypeAndId($room, $metaData[ProxyCacheMessage::METADATA_REPLYTO_TYPE], $metaData[ProxyCacheMessage::METADATA_REPLYTO_ID]); + return $repliedTo['type'] === $participant->getAttendee()->getActorType() + && $repliedTo['id'] === $participant->getAttendee()->getActorId(); + } + + protected function isMentioned(Participant $participant, ProxyCacheMessage $message): bool { + if ($participant->getAttendee()->getActorType() !== Attendee::ACTOR_USERS) { + return false; + } + + foreach ($message->getParsedMessageParameters() as $parameter) { + if ($parameter['type'] === 'user' // RichObjectDefinition, not Attendee::ACTOR_USERS + && $parameter['id'] === $participant->getAttendee()->getActorId() + && empty($parameter['server'])) { + return true; + } + } + + return false; + } + + protected function isMentionedAll(Room $room, ProxyCacheMessage $message): bool { + foreach ($message->getParsedMessageParameters() as $parameter) { + if ($parameter['type'] === 'call' // RichObjectDefinition + && $parameter['id'] === $room->getRemoteToken()) { + return true; + } + } + + return false; + } + + /** + * Creates a notification for the given proxy message and mentioned users + */ + protected function createNotification(Room $chat, ProxyCacheMessage $message, string $subject, array $subjectData = []): INotification { + $subjectData['userType'] = $message->getActorType(); + $subjectData['userId'] = $message->getActorId(); + + $notification = $this->notificationManager->createNotification(); + $notification + ->setApp('spreed') + ->setObject('chat', $chat->getToken()) + ->setSubject($subject, $subjectData) + ->setMessage($message->getMessageType(), [ + 'proxyId' => $message->getId(), + // FIXME Store more info to allow querying remote? + ]) + ->setDateTime($message->getCreationDatetime()); + + return $notification; + } +} diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index e29f7b7d6ff..939c7b8da90 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -36,6 +36,8 @@ use OCA\Talk\Manager; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\BotServerMapper; +use OCA\Talk\Model\Message; +use OCA\Talk\Model\ProxyCacheMessageMapper; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\AvatarService; @@ -83,6 +85,7 @@ public function __construct( protected AvatarService $avatarService, protected INotificationManager $notificationManager, CommentsManager $commentManager, + protected ProxyCacheMessageMapper $proxyCacheMessageMapper, protected MessageParser $messageParser, protected IRootFolder $rootFolder, protected ITimeFactory $timeFactory, @@ -517,42 +520,51 @@ protected function parseChatMessage(INotification $notification, Room $room, Par ]; $messageParameters = $notification->getMessageParameters(); - if (!isset($messageParameters['commentId'])) { + if (!isset($messageParameters['commentId']) && !isset($messageParameters['proxyId'])) { throw new AlreadyProcessedException(); } - if (!$this->notificationManager->isPreparingPushNotification() - && $notification->getObjectType() === 'chat' - /** - * Notification only contains the message id of the target comment - * not the one of the reaction, so we can't determine if it was read. - * @see Listener::markReactionNotificationsRead() - */ - && $notification->getSubject() !== 'reaction' - && ((int) $messageParameters['commentId']) <= $participant->getAttendee()->getLastReadMessage()) { - // Mark notifications of messages that are read as processed - throw new AlreadyProcessedException(); - } + if (isset($messageParameters['commentId'])) { + if (!$this->notificationManager->isPreparingPushNotification() + && $notification->getObjectType() === 'chat' + /** + * Notification only contains the message id of the target comment + * not the one of the reaction, so we can't determine if it was read. + * @see Listener::markReactionNotificationsRead() + */ + && $notification->getSubject() !== 'reaction' + && ((int) $messageParameters['commentId']) <= $participant->getAttendee()->getLastReadMessage()) { + // Mark notifications of messages that are read as processed + throw new AlreadyProcessedException(); + } - try { - $comment = $this->commentManager->get($messageParameters['commentId']); - } catch (NotFoundException $e) { - throw new AlreadyProcessedException(); - } + try { + $comment = $this->commentManager->get($messageParameters['commentId']); + } catch (NotFoundException $e) { + throw new AlreadyProcessedException(); + } - $message = $this->messageParser->createMessage($room, $participant, $comment, $l); - $this->messageParser->parseMessage($message); + $message = $this->messageParser->createMessage($room, $participant, $comment, $l); + $this->messageParser->parseMessage($message); - if (!$message->getVisibility()) { - throw new AlreadyProcessedException(); + if (!$message->getVisibility()) { + throw new AlreadyProcessedException(); + } + } else { + try { + $proxy = $this->proxyCacheMessageMapper->findById($messageParameters['proxyId']); + $message = $this->messageParser->createMessageFromProxyCache($room, $participant, $proxy, $l); + } catch (DoesNotExistException) { + throw new AlreadyProcessedException(); + } } // Set the link to the specific message - $notification->setLink($this->url->linkToRouteAbsolute('spreed.Page.showCall', ['token' => $room->getToken()]) . '#message_' . $comment->getId()); + $notification->setLink($this->url->linkToRouteAbsolute('spreed.Page.showCall', ['token' => $room->getToken()]) . '#message_' . $message->getMessageId()); $now = $this->timeFactory->getDateTime(); - $expireDate = $message->getComment()->getExpireDate(); - if ($expireDate instanceof \DateTime && $expireDate < $now) { + $expireDate = $message->getExpirationDateTime(); + if ($expireDate instanceof \DateTimeInterface && $expireDate < $now) { throw new AlreadyProcessedException(); } @@ -576,7 +588,7 @@ protected function parseChatMessage(INotification $notification, Room $room, Par $notification->setRichMessage($message->getMessage(), $message->getMessageParameters()); // Forward the message ID as well to the clients, so they can quote the message on replies - $notification->setObject($notification->getObjectType(), $notification->getObjectId() . '/' . $comment->getId()); + $notification->setObject($notification->getObjectType(), $notification->getObjectId() . '/' . $message->getMessageId()); } $richSubjectParameters = [ diff --git a/lib/Service/RoomFormatter.php b/lib/Service/RoomFormatter.php index 1cd8d9fb5f3..9f2455433c0 100644 --- a/lib/Service/RoomFormatter.php +++ b/lib/Service/RoomFormatter.php @@ -31,7 +31,7 @@ use OCA\Talk\Federation\Proxy\TalkV1\UserConverter; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\BreakoutRoom; -use OCA\Talk\Model\ProxyCacheMessagesMapper; +use OCA\Talk\Model\ProxyCacheMessageMapper; use OCA\Talk\Model\Session; use OCA\Talk\Participant; use OCA\Talk\ResponseDefinitions; @@ -64,7 +64,7 @@ public function __construct( protected IAppManager $appManager, protected IManager $userStatusManager, protected IUserManager $userManager, - protected ProxyCacheMessagesMapper $proxyCacheMessagesMapper, + protected ProxyCacheMessageMapper $proxyCacheMessageMapper, protected UserConverter $userConverter, protected IL10N $l10n, protected ?string $userId, @@ -390,7 +390,7 @@ public function formatRoomV4( ); } elseif ($room->getRemoteServer() !== '') { try { - $cachedMessage = $this->proxyCacheMessagesMapper->findByRemote( + $cachedMessage = $this->proxyCacheMessageMapper->findByRemote( $room->getRemoteServer(), $room->getRemoteToken(), $room->getLastMessageId(), diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index d9fc9f60b22..98209188dd2 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -1946,6 +1946,8 @@ public function userSeesPeersInCall(string $user, int $numPeers, string $identif public function userSendsMessageToRoom(string $user, string $sendingMode, string $message, string $identifier, string $statusCode, string $apiVersion = 'v1') { $message = substr($message, 1, -1); $message = str_replace('\n', "\n", $message); + $message = str_replace('{$BASE_URL}', $this->baseUrl, $message); + $message = str_replace('{$REMOTE_URL}', $this->baseRemoteUrl, $message); if ($message === '413 Payload Too Large') { $message .= "\n" . str_repeat('1', 32000); @@ -3285,7 +3287,12 @@ private function assertNotifications($notifications, TableNode $formData) { if (isset($expectedNotification['object_id'])) { if (strpos($notification['object_id'], '/') !== false) { [$roomToken, $message] = explode('/', $notification['object_id']); - $data['object_id'] = self::$tokenToIdentifier[$roomToken] . '/' . self::$messageIdToText[$message] ?? 'UNKNOWN_MESSAGE'; + $messageText = self::$messageIdToText[$message] ?? 'UNKNOWN_MESSAGE'; + + $messageText = str_replace($this->baseUrl, '{$BASE_URL}', $messageText); + $messageText = str_replace($this->baseRemoteUrl, '{$REMOTE_URL}', $messageText); + + $data['object_id'] = self::$tokenToIdentifier[$roomToken] . '/' . $messageText; } elseif (strpos($expectedNotification['object_id'], 'INVITE_ID') !== false) { $data['object_id'] = 'INVITE_ID(' . self::$inviteIdToRemote[$notification['object_id']] . ')'; } else { diff --git a/tests/integration/features/federation/chat.feature b/tests/integration/features/federation/chat.feature index a797d15fc24..c56f18ff243 100644 --- a/tests/integration/features/federation/chat.feature +++ b/tests/integration/features/federation/chat.feature @@ -150,3 +150,32 @@ Feature: federation/chat | id | type | | room | 2 | And user "participant2" sends message "413 Payload Too Large" to room "LOCAL::room" with 413 + + Scenario: Mentioning a federated user triggers a notification for them + Given the following "spreed" app config is set + | federation_enabled | yes | + Given user "participant1" creates room "room" (v4) + | roomType | 2 | + | roomName | room | + And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4) + And user "participant2" has the following invitations (v1) + | remoteServerUrl | remoteToken | state | inviterCloudId | inviterDisplayName | + | LOCAL | room | 0 | participant1@http://localhost:8080 | participant1-displayname | + And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1) + | id | name | type | remoteServer | remoteToken | + | room | room | 2 | LOCAL | room | + Then user "participant2" is participant of the following rooms (v4) + | id | type | + | room | 2 | + # Join and leave to clear the invite notification + Given user "participant2" joins room "LOCAL::room" with 200 (v4) + Given user "participant2" leaves room "LOCAL::room" with 200 (v4) + And user "participant2" sends message "Message 1" to room "LOCAL::room" with 201 + When user "participant1" sends reply "Message 1-1" on message "Message 1" to room "room" with 201 + And user "participant1" sends message 'Hi @"federated_user/participant2@{$REMOTE_URL}" bye' to room "room" with 201 + And user "participant1" sends message 'Hi @all bye' to room "room" with 201 + Then user "participant2" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | chat | room/Hi @all bye | participant1-displayname mentioned everyone in conversation room | Hi room bye | + | spreed | chat | room/Hi @"federated_user/participant2@{$REMOTE_URL}" bye | participant1-displayname mentioned you in conversation room | Hi @participant2-displayname bye | + | spreed | chat | room/Message 1-1 | participant1-displayname replied to your message in conversation room | Message 1-1 | diff --git a/tests/php/Federation/FederationTest.php b/tests/php/Federation/FederationTest.php index f53fa64144e..2d94caca280 100644 --- a/tests/php/Federation/FederationTest.php +++ b/tests/php/Federation/FederationTest.php @@ -28,12 +28,14 @@ use OCA\Talk\Federation\BackendNotifier; use OCA\Talk\Federation\CloudFederationProviderTalk; use OCA\Talk\Federation\FederationManager; +use OCA\Talk\Federation\Proxy\TalkV1\UserConverter; use OCA\Talk\Manager; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\AttendeeMapper; use OCA\Talk\Model\Invitation; use OCA\Talk\Model\InvitationMapper; -use OCA\Talk\Model\ProxyCacheMessagesMapper; +use OCA\Talk\Model\ProxyCacheMessageMapper; +use OCA\Talk\Notification\FederationChatNotifier; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; use OCA\Talk\Service\RoomService; @@ -95,7 +97,9 @@ class FederationTest extends TestCase { /** @var AttendeeMapper|MockObject */ protected $attendeeMapper; - protected ProxyCacheMessagesMapper|MockObject $proxyCacheMessageMapper; + protected ProxyCacheMessageMapper|MockObject $proxyCacheMessageMapper; + protected FederationChatNotifier|MockObject $federationChatNotifier; + protected UserConverter|MockObject $userConverter; protected ICacheFactory|MockObject $cacheFactory; public function setUp(): void { @@ -112,7 +116,7 @@ public function setUp(): void { $this->appManager = $this->createMock(IAppManager::class); $this->logger = $this->createMock(LoggerInterface::class); $this->url = $this->createMock(IURLGenerator::class); - $this->proxyCacheMessageMapper = $this->createMock(ProxyCacheMessagesMapper::class); + $this->proxyCacheMessageMapper = $this->createMock(ProxyCacheMessageMapper::class); $this->cacheFactory = $this->createMock(ICacheFactory::class); $this->backendNotifier = new BackendNotifier( @@ -130,6 +134,8 @@ public function setUp(): void { $this->federationManager = $this->createMock(FederationManager::class); $this->notificationManager = $this->createMock(INotificationManager::class); + $this->federationChatNotifier = $this->createMock(FederationChatNotifier::class); + $this->userConverter = $this->createMock(UserConverter::class); $this->cloudFederationProvider = new CloudFederationProviderTalk( $this->cloudIdManager, @@ -148,6 +154,8 @@ public function setUp(): void { $this->createMock(IEventDispatcher::class), $this->logger, $this->proxyCacheMessageMapper, + $this->federationChatNotifier, + $this->userConverter, $this->cacheFactory, ); } diff --git a/tests/php/Notification/NotifierTest.php b/tests/php/Notification/NotifierTest.php index 7f2b362d411..db22eff66f1 100644 --- a/tests/php/Notification/NotifierTest.php +++ b/tests/php/Notification/NotifierTest.php @@ -33,6 +33,7 @@ use OCA\Talk\Model\Attendee; use OCA\Talk\Model\BotServerMapper; use OCA\Talk\Model\Message; +use OCA\Talk\Model\ProxyCacheMessageMapper; use OCA\Talk\Notification\Notifier; use OCA\Talk\Participant; use OCA\Talk\Room; @@ -81,6 +82,7 @@ class NotifierTest extends TestCase { protected $notificationManager; /** @var CommentsManager|MockObject */ protected $commentsManager; + protected ProxyCacheMessageMapper|MockObject $proxyCacheMessageMapper; /** @var MessageParser|MockObject */ protected $messageParser; /** @var IRootFolder|MockObject */ @@ -112,6 +114,7 @@ public function setUp(): void { $this->avatarService = $this->createMock(AvatarService::class); $this->notificationManager = $this->createMock(INotificationManager::class); $this->commentsManager = $this->createMock(CommentsManager::class); + $this->proxyCacheMessageMapper = $this->createMock(ProxyCacheMessageMapper::class); $this->messageParser = $this->createMock(MessageParser::class); $this->rootFolder = $this->createMock(IRootFolder::class); $this->timeFactory = $this->createMock(ITimeFactory::class); @@ -133,6 +136,7 @@ public function setUp(): void { $this->avatarService, $this->notificationManager, $this->commentsManager, + $this->proxyCacheMessageMapper, $this->messageParser, $this->rootFolder, $this->timeFactory,