From 4156e18340a07a57885a47e1658019f75a800975 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 29 Feb 2024 17:13:31 +0100 Subject: [PATCH] feat(federation): Notifications Signed-off-by: Joas Schilling --- lib/Chat/MessageParser.php | 20 ++++++ .../CloudFederationProviderTalk.php | 43 +++++++++++++ lib/Model/Message.php | 13 +++- lib/Model/ProxyCacheMessages.php | 10 ++- lib/Model/ProxyCacheMessagesMapper.php | 12 ++++ lib/Notification/Notifier.php | 64 +++++++++++-------- 6 files changed, 133 insertions(+), 29 deletions(-) diff --git a/lib/Chat/MessageParser.php b/lib/Chat/MessageParser.php index 77e42316a9ba..f3d4da9cba22 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\ProxyCacheMessages; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\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, ProxyCacheMessages $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/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index adf439b65c22..0599d6ab7b45 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -64,6 +64,7 @@ use OCP\IUser; use OCP\IUserManager; use OCP\Notification\IManager as INotificationManager; +use OCP\Notification\INotification; use OCP\Share\Exceptions\ShareNotFound; use Psr\Log\LoggerInterface; use SensitiveParameter; @@ -374,6 +375,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->proxyCacheMessagesMapper->findByRemote( + $notification['remoteServerUrl'], + $notification['remoteToken'], + $notification['messageData']['remoteMessageId'], + ); } try { @@ -390,9 +397,45 @@ private function messagePosted(int $remoteAttendeeId, array $notification): arra $notification['unreadInfo']['unreadMentionDirect'], ); + // 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)) { + // FIXME Check if actually mentioned OR reply + $notification = $this->createNotification($room, $message, 'mention'); + $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); + } + return []; } + /** + * Creates a notification for the given proxy message and mentioned users + */ + private function createNotification(Room $chat, ProxyCacheMessages $message, string $subject, array $subjectData = [], ?IComment $reaction = null): INotification { + $subjectData['userType'] = $reaction ? $reaction->getActorType() : $message->getActorType(); + $subjectData['userId'] = $reaction ? $reaction->getActorId() : $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($reaction ? $reaction->getCreationDateTime() : new \DateTime()); + + return $notification; + } + /** * @throws AuthenticationFailedException * @throws ActionNotSupportedException diff --git a/lib/Model/Message.php b/lib/Model/Message.php index fe11403476c5..1225cb2e9cb9 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -76,8 +76,9 @@ class Message { public function __construct( protected Room $room, protected ?Participant $participant, - protected IComment $comment, + protected ?IComment $comment, protected IL10N $l, + protected ?ProxyCacheMessages $proxy = null, ) { } @@ -89,7 +90,7 @@ public function getRoom(): Room { return $this->room; } - public function getComment(): IComment { + public function getComment(): ?IComment { return $this->comment; } @@ -105,6 +106,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/ProxyCacheMessages.php index 9803f3e2b700..f15738016615 100644 --- a/lib/Model/ProxyCacheMessages.php +++ b/lib/Model/ProxyCacheMessages.php @@ -85,6 +85,14 @@ public function __construct() { $this->addType('expirationDatetime', 'datetime'); $this->addType('message', 'string'); $this->addType('messageParameters', 'string'); + // Reply author + // Silent + // Creation date + // Verb?! + } + + public function getParsedMessageParameters(): array { + return json_decode($this->getMessageParameters() ?? '[]', true); } /** @@ -104,7 +112,7 @@ public function jsonSerialize(): array { '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/ProxyCacheMessagesMapper.php index 7d6855fa143e..b9c41afdf5b1 100644 --- a/lib/Model/ProxyCacheMessagesMapper.php +++ b/lib/Model/ProxyCacheMessagesMapper.php @@ -46,6 +46,18 @@ public function __construct( parent::__construct($db, 'talk_proxy_messages', ProxyCacheMessages::class); } + /** + * @throws DoesNotExistException + */ + public function findById(int $proxyId): ProxyCacheMessages { + $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 */ diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index e29f7b7d6ff3..f634bec57389 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\ProxyCacheMessagesMapper; 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 ProxyCacheMessagesMapper $proxyCacheMessagesMapper, 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->proxyCacheMessagesMapper->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 = [