feat(hpb): support chat relay

Signed-off-by: Anna Larch <anna@nextcloud.com>
This commit is contained in:
Anna Larch 2025-09-28 15:33:40 +02:00
parent 6f32ca490b
commit fa44dd0b3e
6 changed files with 244 additions and 6 deletions

View file

@ -332,6 +332,8 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(ChatMessageSentEvent::class, SignalingListener::class);
$context->registerEventListener(SystemMessageSentEvent::class, SignalingListener::class);
$context->registerEventListener(SystemMessagesMultipleSentEvent::class, SignalingListener::class);
$context->registerEventListener(ReactionAddedEvent::class, SignalingListener::class);
$context->registerEventListener(ReactionRemovedEvent::class, SignalingListener::class);
// Signaling listeners (Both)
$context->registerEventListener(BeforeRoomDeletedEvent::class, SignalingListener::class);

View file

@ -8,9 +8,13 @@ declare(strict_types=1);
namespace OCA\Talk\Signaling;
use OCA\Talk\AppInfo\Application;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Config;
use OCA\Talk\Events\AMessageSentEvent;
use OCA\Talk\Events\AParticipantModifiedEvent;
use OCA\Talk\Events\AReactionEvent;
use OCA\Talk\Events\ARoomEvent;
use OCA\Talk\Events\ARoomModifiedEvent;
use OCA\Talk\Events\ASystemMessageSentEvent;
@ -27,6 +31,8 @@ use OCA\Talk\Events\GuestJoinedRoomEvent;
use OCA\Talk\Events\GuestsCleanedUpEvent;
use OCA\Talk\Events\LobbyModifiedEvent;
use OCA\Talk\Events\ParticipantModifiedEvent;
use OCA\Talk\Events\ReactionAddedEvent;
use OCA\Talk\Events\ReactionRemovedEvent;
use OCA\Talk\Events\RoomExtendedEvent;
use OCA\Talk\Events\RoomModifiedEvent;
use OCA\Talk\Events\RoomSyncedEvent;
@ -41,9 +47,12 @@ use OCA\Talk\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\SessionService;
use OCA\Talk\Service\ThreadService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\L10N\IFactory;
use OCP\Server;
/**
@ -75,6 +84,9 @@ class Listener implements IEventListener {
protected ParticipantService $participantService,
protected SessionService $sessionService,
protected ITimeFactory $timeFactory,
protected MessageParser $messageParser,
protected ThreadService $threadService,
protected IFactory $l10nFactory,
) {
}
@ -144,6 +156,8 @@ class Listener implements IEventListener {
ChatMessageSentEvent::class,
SystemMessageSentEvent::class,
SystemMessagesMultipleSentEvent::class => $this->notifyMessageSent($event),
ReactionAddedEvent::class,
ReactionRemovedEvent::class => $this->notifyReactionSent($event),
default => null, // Ignoring events subscribed by the internal signaling
};
}
@ -458,17 +472,116 @@ class Listener implements IEventListener {
}
protected function notifyMessageSent(AMessageSentEvent $event): void {
$comment = $event->getComment();
if ($event instanceof ASystemMessageSentEvent && $event->shouldSkipLastActivityUpdate()) {
return;
$messageDecoded = json_decode($comment->getMessage(), true);
$messageType = $messageDecoded['message'] ?? '';
if ($messageType !== 'message_deleted' && $messageType !== 'message_edited') {
return;
}
}
$room = $event->getRoom();
$message = [
$data = [
'type' => 'chat',
'chat' => [
'refresh' => true,
],
];
$this->externalSignaling->sendRoomMessage($room, $message);
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;
}
$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 ($event instanceof ASystemMessageSentEvent && $event->getParent() !== null) {
$parent = $event->getParent();
$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 notifyReactionSent(AReactionEvent $event): void {
$room = $event->getRoom();
$data = [
'type' => 'chat',
'chat' => [
'refresh' => true,
],
];
$comment = $event->getMessage();
$messageType = $event instanceof ReactionAddedEvent ? ChatManager::VERB_REACTION : 'reaction_revoked';
$threadId = (int)$comment->getTopmostParentId() ?: $comment->getId();
try {
$thread = $this->threadService->findByThreadId($room->getId(), (int)$threadId);
} catch (DoesNotExistException) {
$thread = null;
}
$reactions = $comment->getReactions();
if ($event instanceof ReactionRemovedEvent) {
if (array_key_exists($event->getReaction(), $reactions) && $reactions[$event->getReaction()] > 1) {
--$reactions[$event->getReaction()];
} else {
unset($reactions[$event->getReaction()]);
}
} elseif ($event instanceof ReactionAddedEvent) {
if (array_key_exists($event->getReaction(), $reactions)) {
++$reactions[$event->getReaction()];
} else {
$reactions[$event->getReaction()] = 1;
}
}
$comment->setReactions($reactions);
$l10n = $this->l10nFactory->get(Application::APP_ID, 'en');
$message = $this->messageParser->createMessage($event->getRoom(), null, $comment, $l10n);
$this->messageParser->parseMessage($message);
// Build reaction message data
$data['chat']['comment'] = [
'id' => null,
'token' => $event->getRoom()->getToken(),
'actorType' => $event->getActorType(),
'actorId' => $event->getActorId(),
'actorDisplayName' => $event->getActorDisplayName(),
'timestamp' => $this->timeFactory->getTime(),
'message' => $event->getReaction(),
'messageParameters' => [],
'systemMessage' => $messageType,
'messageType' => ChatManager::VERB_SYSTEM,
'isReplyable' => false,
'referenceId' => '',
'reactions' => [],
'markdown' => false ,
'expirationTimestamp' => $message->getExpirationDateTime()?->getTimestamp(), // base on parent post timestamp + room expiration
'threadId' => $threadId,
];
$data['chat']['comment']['parent'] = $message->toArray('json', $thread);
$this->externalSignaling->sendRoomMessage($room, $data);
}
}

View file

@ -188,6 +188,7 @@ class Manager {
return array_values(array_diff([
'dialout',
'join-features',
'chat-relay',
], $features));
}

View file

@ -1024,7 +1024,7 @@ Signaling.Standalone.prototype.sendHello = function() {
} else {
helloVersion = '1.0'
}
const features = []
const features = ['chat-relay']
Encryption.isSupported()
.then(() => {
features.push('encryption')

View file

@ -8,6 +8,8 @@ declare(strict_types=1);
namespace OCA\Talk\Tests\php\Signaling;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Config;
use OCA\Talk\Events\ARoomModifiedEvent;
use OCA\Talk\Events\BeforeRoomDeletedEvent;
@ -18,15 +20,20 @@ use OCA\Talk\Events\RoomModifiedEvent;
use OCA\Talk\Events\SystemMessageSentEvent;
use OCA\Talk\Events\SystemMessagesMultipleSentEvent;
use OCA\Talk\Manager;
use OCA\Talk\Model\Message;
use OCA\Talk\Model\Thread;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\SessionService;
use OCA\Talk\Service\ThreadService;
use OCA\Talk\Signaling\BackendNotifier;
use OCA\Talk\Signaling\Listener;
use OCA\Talk\Signaling\Messages;
use OCA\Talk\Webinary;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\IComment;
use OCP\IL10N;
use OCP\L10N\IFactory;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\MockObject;
@ -40,6 +47,9 @@ class ListenerTest extends TestCase {
protected SessionService&MockObject $sessionService;
protected ITimeFactory&MockObject $timeFactory;
protected ?Listener $listener;
protected MessageParser&MockObject $messageParser;
protected ThreadService&MockObject $threadService;
protected IFactory $l10nFactory;
public function setUp(): void {
parent::setUp();
@ -49,6 +59,9 @@ class ListenerTest extends TestCase {
$this->participantService = $this->createMock(ParticipantService::class);
$this->sessionService = $this->createMock(SessionService::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->messageParser = $this->createMock(MessageParser::class);
$this->threadService = $this->createMock(ThreadService::class);
$this->l10nFactory = $this->createMock(IFactory::class);
$this->listener = new Listener(
$this->createMock(Config::class),
@ -58,6 +71,9 @@ class ListenerTest extends TestCase {
$this->participantService,
$this->sessionService,
$this->timeFactory,
$this->messageParser,
$this->threadService,
$this->l10nFactory,
);
}
@ -228,15 +244,27 @@ class ListenerTest extends TestCase {
$this->listener->handle($event);
}
public function testChatMessageSentEvent(): void {
public function testChatMessageInvisibleSentEvent(): void {
$room = $this->createMock(Room::class);
$room->method('getId')->willReturn(1);
$comment = $this->createMock(IComment::class);
$comment->method('getTopmostParentId')->willReturn(null);
$comment->method('getVerb')->willReturn(ChatManager::VERB_MESSAGE);
$comment->method('getId')->willReturn(1);
$event = new ChatMessageSentEvent(
$room,
$comment,
);
$this->messageParser->expects($this->once())
->method('createMessage');
$this->messageParser->expects($this->once())
->method('parseMessage');
$this->l10nFactory->expects($this->once())
->method('get')
->willReturn($this->createMock(IL10N::class));
$this->backendNotifier->expects($this->once())
->method('sendRoomMessage')
->with($room, [
@ -249,9 +277,101 @@ class ListenerTest extends TestCase {
$this->listener->handle($event);
}
public function testChatMessageSentEvent(): void {
$room = $this->createMock(Room::class);
$room->method('getId')->willReturn(1);
$comment = $this->createMock(IComment::class);
$comment->method('getTopmostParentId')->willReturn(null);
$comment->method('getVerb')->willReturn(ChatManager::VERB_MESSAGE);
$comment->method('getId')->willReturn(1);
$message = $this->createConfiguredMock(Message::class, [
'getVisibility' => true,
'toArray' => [],
'getMessageId' => 123,
]);
$l10n = $this->createMock(IL10N::class);
$event = new ChatMessageSentEvent(
$room,
$comment,
);
$this->l10nFactory->expects($this->once())
->method('get')
->willReturn($l10n);
$this->messageParser->expects($this->once())
->method('createMessage')
->with($room, null, $comment, $l10n)
->willReturn($message);
$this->messageParser->expects($this->once())
->method('parseMessage')
->with($message);
$this->backendNotifier->expects($this->once())
->method('sendRoomMessage')
->with($room, [
'type' => 'chat',
'chat' => [
'refresh' => true,
'comment' => [],
],
]);
$this->listener->handle($event);
}
public function testChatMessageSentWithThreadEvent(): void {
$room = $this->createMock(Room::class);
$room->method('getId')->willReturn(1);
$comment = $this->createMock(IComment::class);
$comment->method('getTopmostParentId')->willReturn(null);
$comment->method('getVerb')->willReturn(ChatManager::VERB_MESSAGE);
$comment->method('getId')->willReturn(1);
$message = $this->createConfiguredMock(Message::class, [
'getVisibility' => true,
'toArray' => [],
'getMessageId' => 123,
]);
$l10n = $this->createMock(IL10N::class);
$thread = $this->createMock(Thread::class);
$event = new ChatMessageSentEvent(
$room,
$comment,
);
$this->l10nFactory->expects($this->once())
->method('get')
->willReturn($l10n);
$this->messageParser->expects($this->once())
->method('createMessage')
->with($room, null, $comment, $l10n)
->willReturn($message);
$this->messageParser->expects($this->once())
->method('parseMessage')
->with($message);
$this->threadService->expects($this->once())
->method('findByThreadId')
->willReturn($thread);
$this->backendNotifier->expects($this->once())
->method('sendRoomMessage')
->with($room, [
'type' => 'chat',
'chat' => [
'refresh' => true,
'comment' => [],
],
]);
$this->listener->handle($event);
}
public function testSystemMessageSentEvent(): void {
$room = $this->createMock(Room::class);
$comment = $this->createMock(IComment::class);
$comment->method('getVerb')->willReturn(ChatManager::VERB_SYSTEM);
$event = new SystemMessageSentEvent(
$room,
@ -274,6 +394,7 @@ class ListenerTest extends TestCase {
public function testSystemMessageSentEventSkippingUpdate(): void {
$room = $this->createMock(Room::class);
$comment = $this->createMock(IComment::class);
$comment->method('getMessage')->willReturn(json_encode(['message' => 'test']));
$event = new SystemMessageSentEvent(
$room,
@ -290,6 +411,7 @@ class ListenerTest extends TestCase {
public function testSystemMessagesMultipleSentEvent(): void {
$room = $this->createMock(Room::class);
$comment = $this->createMock(IComment::class);
$comment->method('getVerb')->willReturn(ChatManager::VERB_SYSTEM);
$event = new SystemMessagesMultipleSentEvent(
$room,

View file

@ -80,7 +80,7 @@ class Manager implements \OCP\Comments\ICommentsManager {
// TODO: Implement getNumberOfUnreadCommentsForFolder() method.
}
public function create($actorType, $actorId, $objectType, $objectId) {
public function create($actorType, $actorId, $objectType, $objectId) :IComment {
// TODO: Implement create() method.
}