Skip to content

Commit 7475422

Browse files
committed
feat(federation): Send room modifications via OCM notifications to remotes
Signed-off-by: Joas Schilling <[email protected]>
1 parent 5ac7406 commit 7475422

File tree

10 files changed

+215
-12
lines changed

10 files changed

+215
-12
lines changed

lib/AppInfo/Application.php

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
use OCA\Talk\Events\RoomModifiedEvent;
5454
use OCA\Talk\Events\SendCallNotificationEvent;
5555
use OCA\Talk\Federation\CloudFederationProviderTalk;
56+
use OCA\Talk\Federation\Listener as FederationListener;
5657
use OCA\Talk\Files\Listener as FilesListener;
5758
use OCA\Talk\Files\TemplateLoader as FilesTemplateLoader;
5859
use OCA\Talk\Flow\RegisterOperationsListener;
@@ -153,6 +154,7 @@ public function register(IRegistrationContext $context): void {
153154

154155
// Talk internal listeners
155156
$context->registerEventListener(RoomModifiedEvent::class, SignalingListener::class);
157+
$context->registerEventListener(RoomModifiedEvent::class, FederationListener::class);
156158

157159
$context->registerEventListener(CircleDestroyedEvent::class, CircleDeletedListener::class);
158160
$context->registerEventListener(AddingCircleMemberEvent::class, CircleMembershipListener::class);

lib/Federation/BackendNotifier.php

+39-2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use OCP\IUser;
4141
use OCP\IUserManager;
4242
use Psr\Log\LoggerInterface;
43+
use SensitiveParameter;
4344

4445
class BackendNotifier {
4546

@@ -192,6 +193,42 @@ public function sendRemoteUnShare(string $remote, string $id, string $token): vo
192193
$this->sendUpdateToRemote($remote, $notification);
193194
}
194195

196+
public function sendRoomModifiedUpdate(
197+
string $remoteServer,
198+
int $localAttendeeId,
199+
#[SensitiveParameter]
200+
string $accessToken,
201+
string $localToken,
202+
string $changedProperty,
203+
string|int|bool|null $newValue,
204+
string|int|bool|null $oldValue,
205+
): void {
206+
$remote = $this->prepareRemoteUrl($remoteServer);
207+
208+
$notification = $this->cloudFederationFactory->getCloudFederationNotification();
209+
$notification->setMessage(
210+
FederationManager::NOTIFICATION_ROOM_MODIFIED,
211+
FederationManager::TALK_ROOM_RESOURCE,
212+
(string) $localAttendeeId,
213+
[
214+
'sharedSecret' => $accessToken,
215+
'remoteToken' => $localToken,
216+
'changedProperty' => $changedProperty,
217+
'newValue' => $newValue,
218+
'oldValue' => $oldValue,
219+
],
220+
);
221+
222+
$this->sendUpdateToRemote($remote, $notification);
223+
}
224+
225+
/**
226+
* @internal Used to send retries in background jobs
227+
* @param string $remote
228+
* @param array $data
229+
* @param int $try
230+
* @return void
231+
*/
195232
public function sendUpdateDataToRemote(string $remote, array $data = [], int $try = 0): void {
196233
$notification = $this->cloudFederationFactory->getCloudFederationNotification();
197234
$notification->setMessage(
@@ -203,7 +240,7 @@ public function sendUpdateDataToRemote(string $remote, array $data = [], int $tr
203240
$this->sendUpdateToRemote($remote, $notification, $try);
204241
}
205242

206-
public function sendUpdateToRemote(string $remote, ICloudFederationNotification $notification, int $try = 0): void {
243+
protected function sendUpdateToRemote(string $remote, ICloudFederationNotification $notification, int $try = 0): void {
207244
$response = $this->federationProviderManager->sendNotification($remote, $notification);
208245
if (!is_array($response)) {
209246
$this->jobList->add(RetryJob::class,
@@ -216,7 +253,7 @@ public function sendUpdateToRemote(string $remote, ICloudFederationNotification
216253
}
217254
}
218255

219-
private function prepareRemoteUrl(string $remote): string {
256+
protected function prepareRemoteUrl(string $remote): string {
220257
if (!$this->addressHandler->urlContainProtocol($remote)) {
221258
return 'https://' . $remote;
222259
}

lib/Federation/CloudFederationProviderTalk.php

+55-5
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,18 @@
2929
use OCA\FederatedFileSharing\AddressHandler;
3030
use OCA\Talk\AppInfo\Application;
3131
use OCA\Talk\Config;
32+
use OCA\Talk\Events\ARoomModifiedEvent;
3233
use OCA\Talk\Events\AttendeesAddedEvent;
3334
use OCA\Talk\Manager;
3435
use OCA\Talk\Model\Attendee;
3536
use OCA\Talk\Model\AttendeeMapper;
37+
use OCA\Talk\Model\Invitation;
38+
use OCA\Talk\Model\InvitationMapper;
3639
use OCA\Talk\Participant;
3740
use OCA\Talk\Room;
3841
use OCA\Talk\Service\ParticipantService;
42+
use OCA\Talk\Service\RoomService;
43+
use OCP\AppFramework\Db\DoesNotExistException;
3944
use OCP\AppFramework\Http;
4045
use OCP\DB\Exception as DBException;
4146
use OCP\EventDispatcher\IEventDispatcher;
@@ -64,7 +69,9 @@ public function __construct(
6469
private INotificationManager $notificationManager,
6570
private IURLGenerator $urlGenerator,
6671
private ParticipantService $participantService,
72+
private RoomService $roomService,
6773
private AttendeeMapper $attendeeMapper,
74+
private InvitationMapper $invitationMapper,
6875
private Manager $manager,
6976
private ISession $session,
7077
private IEventDispatcher $dispatcher,
@@ -151,6 +158,8 @@ public function notificationReceived($notificationType, $providerId, array $noti
151158
return $this->shareDeclined((int) $providerId, $notification);
152159
case FederationManager::NOTIFICATION_SHARE_UNSHARED:
153160
return $this->shareUnshared((int) $providerId, $notification);
161+
case FederationManager::NOTIFICATION_ROOM_MODIFIED:
162+
return $this->roomModified((int) $providerId, $notification);
154163
}
155164

156165
throw new BadRequestException([$notificationType]);
@@ -221,6 +230,42 @@ private function shareUnshared(int $id, array $notification): array {
221230
return [];
222231
}
223232

233+
/**
234+
* @param int $remoteAttendeeId
235+
* @param array{sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int|bool|null, oldValue: string|int|bool|null} $notification
236+
* @return array
237+
* @throws ActionNotSupportedException
238+
* @throws AuthenticationFailedException
239+
* @throws ShareNotFound
240+
* @throws \OCA\Talk\Exceptions\RoomNotFoundException
241+
*/
242+
private function roomModified(int $remoteAttendeeId, array $notification): array {
243+
$attendee = $this->getRemoteAttendeeAndValidate($remoteAttendeeId, $notification['sharedSecret']);
244+
245+
$room = $this->manager->getRoomById($attendee->getRoomId());
246+
247+
// Sanity check to make sure the room is a remote room
248+
if (!$room->isFederatedRemoteRoom()) {
249+
throw new ShareNotFound();
250+
}
251+
252+
if ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_AVATAR) {
253+
$this->roomService->setAvatar($room, $notification['newValue']);
254+
} elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_DESCRIPTION) {
255+
$this->roomService->setDescription($room, $notification['newValue']);
256+
} elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_NAME) {
257+
$this->roomService->setName($room, $notification['newValue'], $notification['oldValue']);
258+
} elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_READ_ONLY) {
259+
$this->roomService->setReadOnly($room, $notification['newValue']);
260+
} elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_TYPE) {
261+
$this->roomService->setType($room, $notification['newValue']);
262+
} else {
263+
$this->logger->debug('Update of room property "' . $notification['changedProperty'] . '" is not handled and should not be send via federation');
264+
}
265+
266+
return [];
267+
}
268+
224269
/**
225270
* @throws AuthenticationFailedException
226271
* @throws ActionNotSupportedException
@@ -248,12 +293,12 @@ private function getAttendeeAndValidate(int $id, string $sharedSecret): Attendee
248293
/**
249294
* @param int $id
250295
* @param string $sharedSecret
251-
* @return Attendee
296+
* @return Attendee|Invitation
252297
* @throws ActionNotSupportedException
253298
* @throws ShareNotFound
254299
* @throws AuthenticationFailedException
255300
*/
256-
private function getRemoteAttendeeAndValidate(int $id, string $sharedSecret): Attendee {
301+
private function getRemoteAttendeeAndValidate(int $id, string $sharedSecret): Attendee|Invitation {
257302
if (!$this->federationManager->isEnabled()) {
258303
throw new ActionNotSupportedException('Server does not support Talk federation');
259304
}
@@ -263,11 +308,16 @@ private function getRemoteAttendeeAndValidate(int $id, string $sharedSecret): At
263308
}
264309

265310
try {
266-
$attendee = $this->attendeeMapper->getByRemoteIdAndToken($id, $sharedSecret);
267-
} catch (Exception $ex) {
311+
return $this->attendeeMapper->getByRemoteIdAndToken($id, $sharedSecret);
312+
} catch (DoesNotExistException) {
313+
try {
314+
return $this->invitationMapper->getByRemoteIdAndToken($id, $sharedSecret);
315+
} catch (DoesNotExistException $e) {
316+
throw new ShareNotFound();
317+
}
318+
} catch (Exception) {
268319
throw new ShareNotFound();
269320
}
270-
return $attendee;
271321
}
272322

273323
private function notifyAboutNewShare(IUser $shareWith, string $shareId, string $sharedByFederatedId, string $sharedByName, string $roomName, string $roomToken, string $serverUrl): void {

lib/Federation/FederationManager.php

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class FederationManager {
5353
public const NOTIFICATION_SHARE_ACCEPTED = 'SHARE_ACCEPTED';
5454
public const NOTIFICATION_SHARE_DECLINED = 'SHARE_DECLINED';
5555
public const NOTIFICATION_SHARE_UNSHARED = 'SHARE_UNSHARED';
56+
public const NOTIFICATION_ROOM_MODIFIED = 'ROOM_MODIFIED';
5657
public const TOKEN_LENGTH = 64;
5758

5859
public function __construct(

lib/Federation/Listener.php

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* @copyright Copyright (c) 2023 Joas Schilling <[email protected]>
6+
*
7+
* @license GNU AGPL version 3 or any later version
8+
*
9+
* This program is free software: you can redistribute it and/or modify
10+
* it under the terms of the GNU Affero General Public License as
11+
* published by the Free Software Foundation, either version 3 of the
12+
* License, or (at your option) any later version.
13+
*
14+
* This program is distributed in the hope that it will be useful,
15+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
* GNU Affero General Public License for more details.
18+
*
19+
* You should have received a copy of the GNU Affero General Public License
20+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21+
*
22+
*/
23+
24+
namespace OCA\Talk\Federation;
25+
26+
use OCA\Talk\Events\ARoomModifiedEvent;
27+
use OCA\Talk\Events\RoomModifiedEvent;
28+
use OCA\Talk\Model\Attendee;
29+
use OCA\Talk\Service\ParticipantService;
30+
use OCP\EventDispatcher\Event;
31+
use OCP\EventDispatcher\IEventListener;
32+
use OCP\Federation\ICloudIdManager;
33+
34+
/**
35+
* @template-implements IEventListener<Event>
36+
*/
37+
class Listener implements IEventListener {
38+
public function __construct(
39+
protected BackendNotifier $backendNotifier,
40+
protected ParticipantService $participantService,
41+
protected ICloudIdManager $cloudIdManager,
42+
) {
43+
}
44+
45+
public function handle(Event $event): void {
46+
if (!$event instanceof RoomModifiedEvent) {
47+
return;
48+
}
49+
50+
if (!in_array($event->getProperty(), [
51+
ARoomModifiedEvent::PROPERTY_AVATAR,
52+
ARoomModifiedEvent::PROPERTY_DESCRIPTION,
53+
ARoomModifiedEvent::PROPERTY_NAME,
54+
ARoomModifiedEvent::PROPERTY_READ_ONLY,
55+
ARoomModifiedEvent::PROPERTY_TYPE,
56+
], true)) {
57+
return;
58+
}
59+
60+
$participants = $this->participantService->getParticipantsByActorType($event->getRoom(), Attendee::ACTOR_FEDERATED_USERS);
61+
foreach ($participants as $participant) {
62+
$cloudId = $this->cloudIdManager->resolveCloudId($participant->getAttendee()->getActorId());
63+
64+
$this->backendNotifier->sendRoomModifiedUpdate(
65+
$cloudId->getRemote(),
66+
$participant->getAttendee()->getId(),
67+
$participant->getAttendee()->getAccessToken(),
68+
$event->getRoom()->getToken(),
69+
$event->getProperty(),
70+
$event->getNewValue(),
71+
$event->getOldValue(),
72+
);
73+
}
74+
}
75+
}

lib/Model/InvitationMapper.php

+14
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,20 @@ public function getInvitationById(int $id): Invitation {
6060
return $this->findEntity($qb);
6161
}
6262

63+
/**
64+
* @throws DoesNotExistException
65+
*/
66+
public function getByRemoteIdAndToken(int $remoteId, string $accessToken): Invitation {
67+
$qb = $this->db->getQueryBuilder();
68+
69+
$qb->select('*')
70+
->from($this->getTableName())
71+
->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($remoteId, IQueryBuilder::PARAM_INT)))
72+
->andWhere($qb->expr()->eq('access_token', $qb->createNamedParameter($accessToken)));
73+
74+
return $this->findEntity($qb);
75+
}
76+
6377
/**
6478
* @param Room $room
6579
* @return Invitation[]

lib/Notification/Notifier.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,7 @@ protected function parseRemoteInvitationMessage(INotification $notification, IL1
403403
if ($invite->getUserId() !== $notification->getUser()) {
404404
throw new AlreadyProcessedException();
405405
}
406-
$this->manager->getRoomById($invite->getRoomId());
406+
$room = $this->manager->getRoomById($invite->getRoomId());
407407
} catch (RoomNotFoundException $e) {
408408
// Room does not exist
409409
throw new AlreadyProcessedException();
@@ -423,7 +423,7 @@ protected function parseRemoteInvitationMessage(INotification $notification, IL1
423423
'roomName' => [
424424
'type' => 'highlight',
425425
'id' => $subjectParameters['serverUrl'] . '::' . $subjectParameters['roomToken'],
426-
'name' => $subjectParameters['roomName'],
426+
'name' => $room->getName(),
427427
],
428428
'remoteServer' => [
429429
'type' => 'highlight',

tests/integration/features/bootstrap/FeatureContext.php

+4-3
Original file line numberDiff line numberDiff line change
@@ -369,16 +369,17 @@ private function assertRooms($rooms, TableNode $formData, bool $shouldOrder = fa
369369
}
370370

371371
Assert::assertEquals($expected, array_map(function ($room, $expectedRoom) {
372-
if (isset($room['remoteAccessToken'])) {
373-
self::$remoteAuth[self::translateRemoteServer($room['remoteServer']) . '#' . self::$identifierToToken[$room['name']]] = $room['remoteAccessToken'];
374-
}
375372
if (!isset(self::$identifierToToken[$room['name']])) {
376373
self::$identifierToToken[$room['name']] = $room['token'];
377374
}
378375
if (!isset(self::$tokenToIdentifier[$room['token']])) {
379376
self::$tokenToIdentifier[$room['token']] = $room['name'];
380377
}
381378

379+
if (isset($room['remoteAccessToken'])) {
380+
self::$remoteAuth[self::translateRemoteServer($room['remoteServer']) . '#' . self::$identifierToToken[$room['name']]] = $room['remoteAccessToken'];
381+
}
382+
382383
$data = [];
383384
if (isset($expectedRoom['id'])) {
384385
$data['id'] = self::$tokenToIdentifier[$room['token']];

tests/integration/features/federation/invite.feature

+19
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,22 @@ Feature: federation/invite
112112
Then user "participant1" sees the following messages in room "room" with 200
113113
| room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
114114
| room |federated_users | participant2@http://localhost:8180 | participant2@http://localhost:8180 | Message 1 | [] | |
115+
116+
Scenario: Federate conversation meta data
117+
Given the following "spreed" app config is set
118+
| federation_enabled | yes |
119+
Given user "participant1" creates room "room" (v4)
120+
| roomType | 2 |
121+
| roomName | room |
122+
And user "participant1" adds remote "participant2" to room "room" with 200 (v4)
123+
And user "participant2" has the following invitations (v1)
124+
| remote_server | remote_token |
125+
| LOCAL | room |
126+
And user "participant2" accepts invite to room "room" of server "LOCAL" (v1)
127+
Then user "participant2" is participant of the following rooms (v4)
128+
| id | name | type |
129+
| room | room | 2 |
130+
And user "participant1" renames room "room" to "Federated room" with 200 (v4)
131+
Then user "participant2" is participant of the following rooms (v4)
132+
| id | name | type |
133+
| room | Federated room | 2 |

tests/php/Federation/FederationTest.php

+4
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,10 @@
3131
use OCA\Talk\Manager;
3232
use OCA\Talk\Model\Attendee;
3333
use OCA\Talk\Model\AttendeeMapper;
34+
use OCA\Talk\Model\InvitationMapper;
3435
use OCA\Talk\Room;
3536
use OCA\Talk\Service\ParticipantService;
37+
use OCA\Talk\Service\RoomService;
3638
use OCP\BackgroundJob\IJobList;
3739
use OCP\EventDispatcher\IEventDispatcher;
3840
use OCP\Federation\ICloudFederationFactory;
@@ -110,7 +112,9 @@ public function setUp(): void {
110112
$this->notificationManager,
111113
$this->createMock(IURLGenerator::class),
112114
$this->createMock(ParticipantService::class),
115+
$this->createMock(RoomService::class),
113116
$this->attendeeMapper,
117+
$this->createMock(InvitationMapper::class),
114118
$this->createMock(Manager::class),
115119
$this->createMock(ISession::class),
116120
$this->createMock(IEventDispatcher::class),

0 commit comments

Comments
 (0)