feat(federation): Implement notifications for mentions, reply and full

Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
Joas Schilling 2024-02-29 17:13:31 +01:00
parent 578b1d5873
commit cdc988c7ff
No known key found for this signature in database
GPG key ID: 74434EFE0D2E2205
17 changed files with 365 additions and 66 deletions

View file

@ -29,6 +29,7 @@ use OCA\Talk\Exceptions\ParticipantNotFoundException;
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 @@ class MessageParser {
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(), []);

View file

@ -162,7 +162,7 @@ class UserMention implements IEventListener {
$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()),
];

View file

@ -530,7 +530,7 @@ class ChatController extends AEnvironmentAwareController {
$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;

View file

@ -313,6 +313,7 @@ class BackendNotifier {
* 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(

View file

@ -34,13 +34,15 @@ use OCA\Talk\Events\ARoomModifiedEvent;
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 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
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 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
/**
* @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 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
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 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
$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 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
$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 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
$notification['unreadInfo']['unreadMentionDirect'],
);
$this->federationChatNotifier->handleChatMessage($room, $participant, $message, $notification);
return [];
}

View file

@ -32,7 +32,9 @@ use OCA\Talk\Events\SystemMessageSentEvent;
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 @@ class MessageSentListener implements IEventListener {
}
$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 @@ class MessageSentListener implements IEventListener {
'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);

View file

@ -1096,10 +1096,6 @@ class Manager {
return $this->l->t('Talk updates ✅');
}
if ($forceName) {
return $room->getName();
}
if ($this->federationAuthenticator->isFederationRequest()) {
try {
$authenticatedRoom = $this->federationAuthenticator->getRoom();
@ -1110,7 +1106,7 @@ class Manager {
}
}
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 @@ class Manager {
return $otherParticipant;
}
if ($forceName) {
return $room->getName();
}
if (!$this->isRoomListableByUser($room, $userId)) {
try {
if ($userId === '') {

View file

@ -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 @@ class Message {
return $this->room;
}
public function getComment(): IComment {
public function getComment(): ?IComment {
return $this->comment;
}
@ -109,6 +110,14 @@ class Message {
* 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;
}

View file

@ -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 @@ use OCP\AppFramework\Db\Entity;
* @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 @@ class ProxyCacheMessages extends Entity implements \JsonSerializable {
$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 @@ class ProxyCacheMessages extends Entity implements \JsonSerializable {
'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(),
];
}
}

View file

@ -32,24 +32,36 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
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())

View file

@ -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;
}
}

View file

@ -36,6 +36,8 @@ use OCA\Talk\GuestManager;
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 @@ class Notifier implements INotifier {
protected AvatarService $avatarService,
protected INotificationManager $notificationManager,
CommentsManager $commentManager,
protected ProxyCacheMessageMapper $proxyCacheMessageMapper,
protected MessageParser $messageParser,
protected IRootFolder $rootFolder,
protected ITimeFactory $timeFactory,
@ -517,42 +520,51 @@ class Notifier implements INotifier {
];
$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 @@ class Notifier implements INotifier {
$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 = [

View file

@ -31,7 +31,7 @@ use OCA\Talk\Config;
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 @@ class RoomFormatter {
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 @@ class RoomFormatter {
);
} elseif ($room->getRemoteServer() !== '') {
try {
$cachedMessage = $this->proxyCacheMessagesMapper->findByRemote(
$cachedMessage = $this->proxyCacheMessageMapper->findByRemote(
$room->getRemoteServer(),
$room->getRemoteToken(),
$room->getLastMessageId(),

View file

@ -1946,6 +1946,8 @@ class FeatureContext implements Context, SnippetAcceptingContext {
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 @@ class FeatureContext implements Context, SnippetAcceptingContext {
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 {

View file

@ -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 |

View file

@ -28,12 +28,14 @@ use OCA\Talk\Config;
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 @@ class FederationTest extends TestCase {
$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 @@ class FederationTest extends TestCase {
$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 @@ class FederationTest extends TestCase {
$this->createMock(IEventDispatcher::class),
$this->logger,
$this->proxyCacheMessageMapper,
$this->federationChatNotifier,
$this->userConverter,
$this->cacheFactory,
);
}

View file

@ -33,6 +33,7 @@ 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\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 @@ class NotifierTest extends TestCase {
$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 @@ class NotifierTest extends TestCase {
$this->avatarService,
$this->notificationManager,
$this->commentsManager,
$this->proxyCacheMessageMapper,
$this->messageParser,
$this->rootFolder,
$this->timeFactory,