Merge pull request #16355 from nextcloud/fix/noid/chat-relay-all-system-messages

fix(chatrelay): use chat relay for system messages
This commit is contained in:
Anna 2025-11-21 14:28:53 +01:00 committed by GitHub
commit 6abf123814
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 141 additions and 22 deletions

View file

@ -42,10 +42,13 @@ use OCA\Talk\Events\SystemMessagesMultipleSentEvent;
use OCA\Talk\Events\UserJoinedRoomEvent;
use OCA\Talk\Manager;
use OCA\Talk\Model\BreakoutRoom;
use OCA\Talk\Model\Poll;
use OCA\Talk\Model\Session;
use OCA\Talk\Model\Vote;
use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\PollService;
use OCA\Talk\Service\SessionService;
use OCA\Talk\Service\ThreadService;
use OCP\AppFramework\Db\DoesNotExistException;
@ -74,6 +77,29 @@ class Listener implements IEventListener {
ARoomModifiedEvent::PROPERTY_TYPE,
];
public const SYSTEM_MESSAGE_TYPE_RELAY = [
'call_started',
'call_joined',
'call_left',
'call_ended',
'call_ended_everyone',
'thread_created',
'thread_renamed',
'message_deleted',
'message_edited',
'moderator_promoted',
'moderator_demoted',
'guest_moderator_promoted',
'guest_moderator_demoted',
'file_shared',
'object_shared',
'history_cleared',
'poll_voted',
'poll_closed',
'recording_started',
'recording_stopped',
];
protected bool $pauseRoomModifiedListener = false;
public function __construct(
@ -87,6 +113,7 @@ class Listener implements IEventListener {
protected MessageParser $messageParser,
protected ThreadService $threadService,
protected IFactory $l10nFactory,
protected PollService $pollService,
) {
}
@ -153,9 +180,9 @@ class Listener implements IEventListener {
AttendeesRemovedEvent::class => $this->notifyAttendeesRemoved($event),
ParticipantModifiedEvent::class => $this->notifyParticipantModified($event),
SessionLeftRoomEvent::class => $this->notifySessionLeftRoom($event),
ChatMessageSentEvent::class,
ChatMessageSentEvent::class => $this->notifyMessageSent($event),
SystemMessageSentEvent::class,
SystemMessagesMultipleSentEvent::class => $this->notifyMessageSent($event),
SystemMessagesMultipleSentEvent::class => $this->notifySystemMessageSent($event),
ReactionAddedEvent::class,
ReactionRemovedEvent::class => $this->notifyReactionSent($event),
default => null, // Ignoring events subscribed by the internal signaling
@ -472,13 +499,57 @@ class Listener implements IEventListener {
}
protected function notifyMessageSent(AMessageSentEvent $event): void {
if ($event instanceof ASystemMessageSentEvent) {
$this->notifySystemMessageSent($event);
return;
}
$comment = $event->getComment();
if ($event instanceof ASystemMessageSentEvent && $event->shouldSkipLastActivityUpdate()) {
$messageDecoded = json_decode($comment->getMessage(), true);
$messageType = $messageDecoded['message'] ?? '';
if ($messageType !== 'message_deleted' && $messageType !== 'message_edited') {
return;
}
$room = $event->getRoom();
$data = [
'type' => 'chat',
'chat' => [
'refresh' => true,
],
];
$l10n = $this->l10nFactory->get(Application::APP_ID, 'en');
$message = $this->messageParser->createMessage($event->getRoom(), null, $comment, $l10n);
$this->messageParser->parseMessage($message);
if ($message->getVisibility() === false) {
$this->externalSignaling->sendRoomMessage($room, $data);
return;
}
$threadId = (int)$comment->getTopmostParentId() ?: $comment->getId();
try {
$thread = $this->threadService->findByThreadId($room->getId(), (int)$threadId);
} catch (DoesNotExistException) {
$thread = null;
}
$data['chat']['comment'] = $message->toArray('json', $thread);
$parent = $event->getParent();
if ($parent !== null) {
$parentMessage = $this->messageParser->createMessage($event->getRoom(), null, $parent, $l10n);
$this->messageParser->parseMessage($parentMessage);
$data['chat']['comment']['parent'] = $parentMessage->toArray('json', $thread);
}
$this->externalSignaling->sendRoomMessage($room, $data);
}
protected function notifySystemMessageSent(ASystemMessageSentEvent $event): void {
$comment = $event->getComment();
$messageDecoded = json_decode($comment->getMessage(), true);
$params = $messageDecoded['parameters'] ?? [];
$messageType = $messageDecoded['message'] ?? '';
if ($event->shouldSkipLastActivityUpdate() === true
&& !in_array($messageType, ['message_deleted', 'message_edited', 'thread_created', 'thread_renamed'], true)
) {
return;
}
$room = $event->getRoom();
@ -489,11 +560,20 @@ class Listener implements IEventListener {
],
];
if ($event instanceof ASystemMessageSentEvent && $comment->getVerb() === ChatManager::VERB_SYSTEM && $event->shouldSkipLastActivityUpdate() === false) {
if (!in_array($messageType, self::SYSTEM_MESSAGE_TYPE_RELAY, true)) {
$this->externalSignaling->sendRoomMessage($room, $data);
return;
}
$thread = null;
if ($messageType === 'thread_created' || $messageType === 'thread_renamed') {
$threadId = (int)$comment->getTopmostParentId() ?: $comment->getId();
try {
$thread = $this->threadService->findByThreadId($room->getId(), (int)$threadId);
} catch (DoesNotExistException) {
}
}
$l10n = $this->l10nFactory->get(Application::APP_ID, 'en');
$message = $this->messageParser->createMessage($event->getRoom(), null, $comment, $l10n);
$this->messageParser->parseMessage($message);
@ -502,19 +582,45 @@ class Listener implements IEventListener {
return;
}
$thread = null;
if (!isset($messageType)) {
$threadId = (int)$comment->getTopmostParentId() ?: $comment->getId();
try {
$thread = $this->threadService->findByThreadId($room->getId(), (int)$threadId);
} catch (DoesNotExistException) {
$data['chat']['comment'] = $message->toArray('json', $thread);
if ($messageType === 'object_shared' && isset($params['objectType']) && $params['objectType'] === 'talk-poll') {
$poll = $this->pollService->getPoll($event->getRoom()->getId(), $params['objectId']);
$data['chat']['comment']['poll'] = $poll->renderAsPoll();
}
if ($messageType === 'poll_voted' && isset($params['poll']['id'])) {
$poll = $this->pollService->getPoll($event->getRoom()->getId(), $params['poll']['id']);
$data['chat']['comment']['poll'] = $poll->renderAsPoll();
}
if ($messageType === 'poll_closed' && isset($params['poll']['id'])) {
$poll = $this->pollService->getPoll($event->getRoom()->getId(), $params['poll']['id']);
$data['chat']['comment']['poll'] = $poll->renderAsPoll();
if ($poll->getResultMode() !== Poll::MODE_HIDDEN) {
$votes = $this->pollService->getVotes($poll);
$data['chat']['comment']['poll']['votes'] = array_map(fn (Vote $vote) => $vote->asArray(), $votes);
}
}
$data['chat']['comment'] = $message->toArray('json', $thread);
if ($messageType === 'thread_created' || $messageType === 'thread_renamed') {
$data['chat']['comment']['threadInfo']['thread'] = [
'id' => $thread->getId(),
'roomToken' => $room->getToken(),
'title' => $thread->getName(),
'lastMessageId' => $thread->getLastMessageId(),
'lastActivity' => $thread->getLastActivity()->getTimestamp(),
'numReplies' => $thread->getNumReplies(),
];
$data['chat']['comment']['threadInfo']['attendee'] = ['notificationLevel' => 0];
$data['chat']['comment']['threadInfo']['first'] = $thread->toArray($room);
$data['chat']['comment']['threadInfo']['last'] = null;
$this->externalSignaling->sendRoomMessage($room, $data);
return;
}
if ($event instanceof ASystemMessageSentEvent && $event->getParent() !== null) {
$parent = $event->getParent();
$parent = $event->getParent();
if ($parent !== null) {
$parentMessage = $this->messageParser->createMessage($event->getRoom(), null, $parent, $l10n);
$this->messageParser->parseMessage($parentMessage);
$data['chat']['comment']['parent'] = $parentMessage->toArray('json', $thread);
@ -577,7 +683,7 @@ class Listener implements IEventListener {
'referenceId' => '',
'reactions' => [],
'markdown' => false ,
'expirationTimestamp' => $message->getExpirationDateTime()?->getTimestamp(), // base on parent post timestamp + room expiration
'expirationTimestamp' => $message->getExpirationDateTime()?->getTimestamp(),
'threadId' => $threadId,
];

View file

@ -317,6 +317,10 @@ trait RecordingTrait {
usort($actualDataJson['participants']['users'], static fn (array $u1, array $u2) => $u1['userId'] <=> $u2['userId']);
$write = true;
}
if (isset($actualDataJson['message']['data']['chat']['comment'])) {
$actualDataJson['message']['data']['chat']['comment'] = [];
$write = true;
}
if ($write) {
$actual['data'] = json_encode($actualDataJson);

View file

@ -46,7 +46,7 @@ Feature: callapi/recording
And recording server sent stopped request for recording in room "room1" as "participant1" with 200
Then signaling server received the following requests
| token | data |
| room1 | {"type":"message","message":{"data":{"type":"chat","chat":{"refresh":true}}}} |
| room1 | {"type":"message","message":{"data":{"type":"chat","chat":{"refresh":true,"comment":[]}}}} |
| room1 | {"type":"message","message":{"data":{"type":"recording","recording":{"status":0}}}} |
| room1 | {"type":"update","update":{"userids":["participant1"],"properties":{"name":"Private conversation","type":2,"lobby-state":0,"lobby-timer":null,"read-only":0,"listable":0,"active-since":{"date":"ACTIVE_SINCE()","timezone_type":3,"timezone":"UTC"},"sip-enabled":0,"description":""}}} |
Then user "participant1" sees the following system messages in room "room1" with 200 (v1)

View file

@ -25,7 +25,7 @@ Feature: callapi/update-call-flags
And user "owner" joins call "public room" with 200 (v4)
Then signaling server received the following requests
| token | data |
| public room | {"type":"message","message":{"data":{"type":"chat","chat":{"refresh":true}}}} |
| public room | {"type":"message","message":{"data":{"type":"chat","chat":{"refresh":true,"comment":[]}}}} |
| public room | {"type":"incall","incall":{"incall":7,"changed":[{"inCall":7,"lastPing":LAST_PING(),"sessionId":"SESSION(owner)","nextcloudSessionId":"SESSION(owner)","participantType":1,"participantPermissions":254,"actorType":"users","actorId":"owner","userId":"owner"}],"users":[{"inCall":7,"lastPing":LAST_PING(),"sessionId":"SESSION(owner)","nextcloudSessionId":"SESSION(owner)","participantType":1,"participantPermissions":254,"actorType":"users","actorId":"owner","userId":"owner"}]}} |
And reset signaling server requests
When user "owner" updates call flags in room "public room" to "1" with 200 (v4)

View file

@ -18,7 +18,7 @@ Feature: conversation-2/promotion-demotion
When user "participant1" promotes "participant2" in room "room" with 200 (v4)
Then signaling server received the following requests
| token | data |
| room | {"type":"message","message":{"data":{"type":"chat","chat":{"refresh":true}}}} |
| room | {"type":"message","message":{"data":{"type":"chat","chat":{"refresh":true,"comment":[]}}}} |
# TODO remove handler with "roomModified" in favour of handler with
# "participantsModified" once the clients no longer expect a
# "roomModified" message for participant type changes.

View file

@ -24,6 +24,7 @@ use OCA\Talk\Model\Message;
use OCA\Talk\Model\Thread;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\PollService;
use OCA\Talk\Service\SessionService;
use OCA\Talk\Service\ThreadService;
use OCA\Talk\Signaling\BackendNotifier;
@ -50,6 +51,9 @@ class ListenerTest extends TestCase {
protected MessageParser&MockObject $messageParser;
protected ThreadService&MockObject $threadService;
protected IFactory $l10nFactory;
protected MockObject&PollService $pollService;
public function setUp(): void {
parent::setUp();
@ -62,6 +66,7 @@ class ListenerTest extends TestCase {
$this->messageParser = $this->createMock(MessageParser::class);
$this->threadService = $this->createMock(ThreadService::class);
$this->l10nFactory = $this->createMock(IFactory::class);
$this->pollService = $this->createMock(PollService::class);
$this->listener = new Listener(
$this->createMock(Config::class),
@ -74,6 +79,7 @@ class ListenerTest extends TestCase {
$this->messageParser,
$this->threadService,
$this->l10nFactory,
$this->pollService,
);
}
@ -372,6 +378,7 @@ class ListenerTest extends TestCase {
$room = $this->createMock(Room::class);
$comment = $this->createMock(IComment::class);
$comment->method('getVerb')->willReturn(ChatManager::VERB_SYSTEM);
$comment->method('getMessage')->willReturn(json_encode(['message' => 'test']));
$event = new SystemMessageSentEvent(
$room,
@ -391,6 +398,7 @@ class ListenerTest extends TestCase {
$this->listener->handle($event);
}
public function testSystemMessageSentEventSkippingUpdate(): void {
$room = $this->createMock(Room::class);
$comment = $this->createMock(IComment::class);
@ -412,6 +420,7 @@ class ListenerTest extends TestCase {
$room = $this->createMock(Room::class);
$comment = $this->createMock(IComment::class);
$comment->method('getVerb')->willReturn(ChatManager::VERB_SYSTEM);
$comment->method('getMessage')->willReturn(json_encode(['message' => 'test']));
$event = new SystemMessagesMultipleSentEvent(
$room,