mirror of
https://github.com/nextcloud/spreed.git
synced 2025-12-18 05:20:50 +01:00
feat(federation): Implement notifications for mentions, reply and full
Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
parent
578b1d5873
commit
cdc988c7ff
17 changed files with 365 additions and 66 deletions
|
|
@ -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(), []);
|
||||
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 === '') {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
140
lib/Notification/FederationChatNotifier.php
Normal file
140
lib/Notification/FederationChatNotifier.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue