From a62f2c60840af54f92c2497639355624582d54df Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Wed, 19 Nov 2025 16:44:38 +0100 Subject: [PATCH 1/2] fix(chatrelay): use chat relay for system messages ``` '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', ``` are all sent via signaling messages. Polls will only contain specific votes when the poll is not anonymous, otherwise the sum of all votes will be shown. - Split notify methods into separate calls for chat and system messages. Signed-off-by: Anna Larch --- lib/Signaling/Listener.php | 139 +++++++++++++++++++++++---- tests/php/Signaling/ListenerTest.php | 2 + 2 files changed, 122 insertions(+), 19 deletions(-) diff --git a/lib/Signaling/Listener.php b/lib/Signaling/Listener.php index f08b57f3ab3..0c804ab6ffd 100644 --- a/lib/Signaling/Listener.php +++ b/lib/Signaling/Listener.php @@ -42,10 +42,13 @@ 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 @@ public function __construct( protected MessageParser $messageParser, protected ThreadService $threadService, protected IFactory $l10nFactory, + protected PollService $pollService, ) { } @@ -153,9 +180,9 @@ protected function handleExternalSignaling(Event $event): void { 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 @@ -489,14 +516,6 @@ protected function notifyMessageSent(AMessageSentEvent $event): void { } $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', @@ -505,21 +524,60 @@ protected function notifyMessageSent(AMessageSentEvent $event): void { ], ]; - if ($event instanceof ASystemMessageSentEvent && $comment->getVerb() === ChatManager::VERB_SYSTEM && $event->shouldSkipLastActivityUpdate() === false) { - $this->externalSignaling->sendRoomMessage($room, $data); - return; - } - $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(); + $data = [ + 'type' => 'chat', + 'chat' => [ + 'refresh' => true, + ], + ]; + + if (!in_array($messageType, self::SYSTEM_MESSAGE_TYPE_RELAY, true)) { + $this->externalSignaling->sendRoomMessage($room, $data); + return; + } + $thread = null; - if (!isset($messageType)) { + if ($messageType === 'thread_created' || $messageType === 'thread_renamed') { $threadId = (int)$comment->getTopmostParentId() ?: $comment->getId(); try { $thread = $this->threadService->findByThreadId($room->getId(), (int)$threadId); @@ -527,10 +585,53 @@ protected function notifyMessageSent(AMessageSentEvent $event): void { } } + $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; + } + $data['chat']['comment'] = $message->toArray('json', $thread); - if ($event instanceof ASystemMessageSentEvent && $event->getParent() !== null) { - $parent = $event->getParent(); + 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); + } + } + + 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; + } + + $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); @@ -597,7 +698,7 @@ protected function notifyReactionSent(AReactionEvent $event): void { 'referenceId' => '', 'reactions' => [], 'markdown' => false , - 'expirationTimestamp' => $message->getExpirationDateTime()?->getTimestamp(), // base on parent post timestamp + room expiration + 'expirationTimestamp' => $message->getExpirationDateTime()?->getTimestamp(), 'threadId' => $threadId, ]; diff --git a/tests/php/Signaling/ListenerTest.php b/tests/php/Signaling/ListenerTest.php index 0914749572b..915c3328374 100644 --- a/tests/php/Signaling/ListenerTest.php +++ b/tests/php/Signaling/ListenerTest.php @@ -383,6 +383,7 @@ public function testSystemMessageSentEvent(): void { $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, @@ -423,6 +424,7 @@ public function testSystemMessagesMultipleSentEvent(): void { $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, From de5e753f875050e1e31e3b08bd75b81825673fa9 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Thu, 20 Nov 2025 16:59:58 +0100 Subject: [PATCH 2/2] test: deal with new comment reponse field Signed-off-by: Anna Larch [skip ci] --- tests/integration/features/bootstrap/RecordingTrait.php | 4 ++++ tests/integration/features/callapi/recording.feature | 2 +- .../integration/features/callapi/update-call-flags.feature | 2 +- .../features/conversation-4/promotion-demotion.feature | 2 +- tests/php/Signaling/ListenerTest.php | 6 ++++++ 5 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/integration/features/bootstrap/RecordingTrait.php b/tests/integration/features/bootstrap/RecordingTrait.php index 51e7d6197c2..6d840bb96e6 100644 --- a/tests/integration/features/bootstrap/RecordingTrait.php +++ b/tests/integration/features/bootstrap/RecordingTrait.php @@ -317,6 +317,10 @@ public function fakeServerReceivedTheFollowingRequests(string $server, ?TableNod 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); diff --git a/tests/integration/features/callapi/recording.feature b/tests/integration/features/callapi/recording.feature index f3081cca87f..52a76c153ec 100644 --- a/tests/integration/features/callapi/recording.feature +++ b/tests/integration/features/callapi/recording.feature @@ -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) diff --git a/tests/integration/features/callapi/update-call-flags.feature b/tests/integration/features/callapi/update-call-flags.feature index cc033300027..bfcfdc4869c 100644 --- a/tests/integration/features/callapi/update-call-flags.feature +++ b/tests/integration/features/callapi/update-call-flags.feature @@ -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) diff --git a/tests/integration/features/conversation-4/promotion-demotion.feature b/tests/integration/features/conversation-4/promotion-demotion.feature index b2b04a7d267..13701315c70 100644 --- a/tests/integration/features/conversation-4/promotion-demotion.feature +++ b/tests/integration/features/conversation-4/promotion-demotion.feature @@ -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. diff --git a/tests/php/Signaling/ListenerTest.php b/tests/php/Signaling/ListenerTest.php index 915c3328374..27e91eb8caf 100644 --- a/tests/php/Signaling/ListenerTest.php +++ b/tests/php/Signaling/ListenerTest.php @@ -24,6 +24,7 @@ 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; @@ -51,6 +52,9 @@ class ListenerTest extends TestCase { protected ThreadService&MockObject $threadService; protected Config&MockObject $config; protected IFactory $l10nFactory; + protected MockObject&PollService $pollService; + + public function setUp(): void { parent::setUp(); @@ -76,6 +80,7 @@ public function setUp(): void { $this->messageParser, $this->threadService, $this->l10nFactory, + $this->pollService, ); } @@ -403,6 +408,7 @@ public function testSystemMessageSentEvent(): void { $this->listener->handle($event); } + public function testSystemMessageSentEventSkippingUpdate(): void { $room = $this->createMock(Room::class); $comment = $this->createMock(IComment::class);