Skip to content

Commit

Permalink
feat(federation): Send room modifications via OCM notifications to re…
Browse files Browse the repository at this point in the history
…motes

Signed-off-by: Joas Schilling <[email protected]>
  • Loading branch information
nickvergessen committed Oct 27, 2023
1 parent 4d54c28 commit ef3fe8f
Show file tree
Hide file tree
Showing 10 changed files with 217 additions and 12 deletions.
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
use OCA\Talk\Events\RoomModifiedEvent;
use OCA\Talk\Events\SystemMessageSentEvent;
use OCA\Talk\Federation\CloudFederationProviderTalk;
use OCA\Talk\Federation\Listener as FederationListener;
use OCA\Talk\Files\Listener as FilesListener;
use OCA\Talk\Files\TemplateLoader as FilesTemplateLoader;
use OCA\Talk\Flow\RegisterOperationsListener;
Expand Down Expand Up @@ -184,6 +185,9 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(TranscriptionSuccessfulEvent::class, RecordingListener::class);
$context->registerEventListener(TranscriptionFailedEvent::class, RecordingListener::class);

// Federation listeners
$context->registerEventListener(RoomModifiedEvent::class, FederationListener::class);

// Signaling listeners
$context->registerEventListener(RoomModifiedEvent::class, SignalingListener::class);

Expand Down
41 changes: 39 additions & 2 deletions lib/Federation/BackendNotifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
use OCP\IUser;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use SensitiveParameter;

class BackendNotifier {

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

public function sendRoomModifiedUpdate(
string $remoteServer,
int $localAttendeeId,
#[SensitiveParameter]
string $accessToken,
string $localToken,
string $changedProperty,
string|int|bool|null $newValue,
string|int|bool|null $oldValue,
): void {
$remote = $this->prepareRemoteUrl($remoteServer);

$notification = $this->cloudFederationFactory->getCloudFederationNotification();
$notification->setMessage(
FederationManager::NOTIFICATION_ROOM_MODIFIED,
FederationManager::TALK_ROOM_RESOURCE,
(string) $localAttendeeId,
[
'sharedSecret' => $accessToken,
'remoteToken' => $localToken,
'changedProperty' => $changedProperty,
'newValue' => $newValue,
'oldValue' => $oldValue,
],
);

$this->sendUpdateToRemote($remote, $notification);
}

/**
* @internal Used to send retries in background jobs
* @param string $remote
* @param array $data
* @param int $try
* @return void
*/
public function sendUpdateDataToRemote(string $remote, array $data = [], int $try = 0): void {
$notification = $this->cloudFederationFactory->getCloudFederationNotification();
$notification->setMessage(
Expand All @@ -203,7 +240,7 @@ public function sendUpdateDataToRemote(string $remote, array $data = [], int $tr
$this->sendUpdateToRemote($remote, $notification, $try);
}

public function sendUpdateToRemote(string $remote, ICloudFederationNotification $notification, int $try = 0): void {
protected function sendUpdateToRemote(string $remote, ICloudFederationNotification $notification, int $try = 0): void {
$response = $this->federationProviderManager->sendNotification($remote, $notification);
if (!is_array($response)) {
$this->jobList->add(RetryJob::class,
Expand All @@ -216,7 +253,7 @@ public function sendUpdateToRemote(string $remote, ICloudFederationNotification
}
}

private function prepareRemoteUrl(string $remote): string {
protected function prepareRemoteUrl(string $remote): string {
if (!$this->addressHandler->urlContainProtocol($remote)) {
return 'https://' . $remote;
}
Expand Down
60 changes: 55 additions & 5 deletions lib/Federation/CloudFederationProviderTalk.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,18 @@
use OCA\FederatedFileSharing\AddressHandler;
use OCA\Talk\AppInfo\Application;
use OCA\Talk\Config;
use OCA\Talk\Events\ARoomModifiedEvent;
use OCA\Talk\Events\AttendeesAddedEvent;
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\Participant;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RoomService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http;
use OCP\DB\Exception as DBException;
use OCP\EventDispatcher\IEventDispatcher;
Expand Down Expand Up @@ -64,7 +69,9 @@ public function __construct(
private INotificationManager $notificationManager,
private IURLGenerator $urlGenerator,
private ParticipantService $participantService,
private RoomService $roomService,
private AttendeeMapper $attendeeMapper,
private InvitationMapper $invitationMapper,
private Manager $manager,
private ISession $session,
private IEventDispatcher $dispatcher,
Expand Down Expand Up @@ -151,6 +158,8 @@ public function notificationReceived($notificationType, $providerId, array $noti
return $this->shareDeclined((int) $providerId, $notification);
case FederationManager::NOTIFICATION_SHARE_UNSHARED:
return $this->shareUnshared((int) $providerId, $notification);
case FederationManager::NOTIFICATION_ROOM_MODIFIED:
return $this->roomModified((int) $providerId, $notification);
}

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

/**
* @param int $remoteAttendeeId
* @param array{sharedSecret: string, remoteToken: string, changedProperty: string, newValue: string|int|bool|null, oldValue: string|int|bool|null} $notification
* @return array
* @throws ActionNotSupportedException
* @throws AuthenticationFailedException
* @throws ShareNotFound
* @throws \OCA\Talk\Exceptions\RoomNotFoundException
*/
private function roomModified(int $remoteAttendeeId, array $notification): array {
$attendee = $this->getRemoteAttendeeAndValidate($remoteAttendeeId, $notification['sharedSecret']);

$room = $this->manager->getRoomById($attendee->getRoomId());

// Sanity check to make sure the room is a remote room
if (!$room->isFederatedRemoteRoom()) {
throw new ShareNotFound();
}

if ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_AVATAR) {
$this->roomService->setAvatar($room, $notification['newValue']);
} elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_DESCRIPTION) {
$this->roomService->setDescription($room, $notification['newValue']);
} elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_NAME) {
$this->roomService->setName($room, $notification['newValue'], $notification['oldValue']);
} elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_READ_ONLY) {
$this->roomService->setReadOnly($room, $notification['newValue']);
} elseif ($notification['changedProperty'] === ARoomModifiedEvent::PROPERTY_TYPE) {
$this->roomService->setType($room, $notification['newValue']);
} else {
$this->logger->debug('Update of room property "' . $notification['changedProperty'] . '" is not handled and should not be send via federation');
}

return [];
}

/**
* @throws AuthenticationFailedException
* @throws ActionNotSupportedException
Expand Down Expand Up @@ -248,12 +293,12 @@ private function getAttendeeAndValidate(int $id, string $sharedSecret): Attendee
/**
* @param int $id
* @param string $sharedSecret
* @return Attendee
* @return Attendee|Invitation
* @throws ActionNotSupportedException
* @throws ShareNotFound
* @throws AuthenticationFailedException
*/
private function getRemoteAttendeeAndValidate(int $id, string $sharedSecret): Attendee {
private function getRemoteAttendeeAndValidate(int $id, string $sharedSecret): Attendee|Invitation {
if (!$this->federationManager->isEnabled()) {
throw new ActionNotSupportedException('Server does not support Talk federation');
}
Expand All @@ -263,11 +308,16 @@ private function getRemoteAttendeeAndValidate(int $id, string $sharedSecret): At
}

try {
$attendee = $this->attendeeMapper->getByRemoteIdAndToken($id, $sharedSecret);
} catch (Exception $ex) {
return $this->attendeeMapper->getByRemoteIdAndToken($id, $sharedSecret);
} catch (DoesNotExistException) {
try {
return $this->invitationMapper->getByRemoteIdAndToken($id, $sharedSecret);
} catch (DoesNotExistException $e) {
throw new ShareNotFound();
}
} catch (Exception) {
throw new ShareNotFound();
}
return $attendee;
}

private function notifyAboutNewShare(IUser $shareWith, string $shareId, string $sharedByFederatedId, string $sharedByName, string $roomName, string $roomToken, string $serverUrl): void {
Expand Down
1 change: 1 addition & 0 deletions lib/Federation/FederationManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class FederationManager {
public const NOTIFICATION_SHARE_ACCEPTED = 'SHARE_ACCEPTED';
public const NOTIFICATION_SHARE_DECLINED = 'SHARE_DECLINED';
public const NOTIFICATION_SHARE_UNSHARED = 'SHARE_UNSHARED';
public const NOTIFICATION_ROOM_MODIFIED = 'ROOM_MODIFIED';
public const TOKEN_LENGTH = 64;

public function __construct(
Expand Down
75 changes: 75 additions & 0 deletions lib/Federation/Listener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Joas Schilling <[email protected]>
*
* @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\Federation;

use OCA\Talk\Events\ARoomModifiedEvent;
use OCA\Talk\Events\RoomModifiedEvent;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Service\ParticipantService;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Federation\ICloudIdManager;

/**
* @template-implements IEventListener<Event>
*/
class Listener implements IEventListener {
public function __construct(
protected BackendNotifier $backendNotifier,
protected ParticipantService $participantService,
protected ICloudIdManager $cloudIdManager,
) {
}

public function handle(Event $event): void {
if (!$event instanceof RoomModifiedEvent) {
return;
}

if (!in_array($event->getProperty(), [
ARoomModifiedEvent::PROPERTY_AVATAR,
ARoomModifiedEvent::PROPERTY_DESCRIPTION,
ARoomModifiedEvent::PROPERTY_NAME,
ARoomModifiedEvent::PROPERTY_READ_ONLY,
ARoomModifiedEvent::PROPERTY_TYPE,
], true)) {
return;
}

$participants = $this->participantService->getParticipantsByActorType($event->getRoom(), Attendee::ACTOR_FEDERATED_USERS);
foreach ($participants as $participant) {
$cloudId = $this->cloudIdManager->resolveCloudId($participant->getAttendee()->getActorId());

$this->backendNotifier->sendRoomModifiedUpdate(
$cloudId->getRemote(),
$participant->getAttendee()->getId(),
$participant->getAttendee()->getAccessToken(),
$event->getRoom()->getToken(),
$event->getProperty(),
$event->getNewValue(),
$event->getOldValue(),
);
}
}
}
14 changes: 14 additions & 0 deletions lib/Model/InvitationMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,20 @@ public function getInvitationById(int $id): Invitation {
return $this->findEntity($qb);
}

/**
* @throws DoesNotExistException
*/
public function getByRemoteIdAndToken(int $remoteId, string $accessToken): Invitation {
$qb = $this->db->getQueryBuilder();

$qb->select('*')
->from($this->getTableName())
->where($qb->expr()->eq('remote_id', $qb->createNamedParameter($remoteId, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('access_token', $qb->createNamedParameter($accessToken)));

return $this->findEntity($qb);
}

/**
* @param Room $room
* @return Invitation[]
Expand Down
4 changes: 2 additions & 2 deletions lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@ protected function parseRemoteInvitationMessage(INotification $notification, IL1
if ($invite->getUserId() !== $notification->getUser()) {
throw new AlreadyProcessedException();
}
$this->manager->getRoomById($invite->getRoomId());
$room = $this->manager->getRoomById($invite->getRoomId());
} catch (RoomNotFoundException $e) {
// Room does not exist
throw new AlreadyProcessedException();
Expand All @@ -423,7 +423,7 @@ protected function parseRemoteInvitationMessage(INotification $notification, IL1
'roomName' => [
'type' => 'highlight',
'id' => $subjectParameters['serverUrl'] . '::' . $subjectParameters['roomToken'],
'name' => $subjectParameters['roomName'],
'name' => $room->getName(),
],
'remoteServer' => [
'type' => 'highlight',
Expand Down
7 changes: 4 additions & 3 deletions tests/integration/features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -380,16 +380,17 @@ private function assertRooms($rooms, TableNode $formData, bool $shouldOrder = fa
}

Assert::assertEquals($expected, array_map(function ($room, $expectedRoom) {
if (isset($room['remoteAccessToken'])) {
self::$remoteAuth[self::translateRemoteServer($room['remoteServer']) . '#' . self::$identifierToToken[$room['name']]] = $room['remoteAccessToken'];
}
if (!isset(self::$identifierToToken[$room['name']])) {
self::$identifierToToken[$room['name']] = $room['token'];
}
if (!isset(self::$tokenToIdentifier[$room['token']])) {
self::$tokenToIdentifier[$room['token']] = $room['name'];
}

if (isset($room['remoteAccessToken'])) {
self::$remoteAuth[self::translateRemoteServer($room['remoteServer']) . '#' . self::$identifierToToken[$room['name']]] = $room['remoteAccessToken'];
}

$data = [];
if (isset($expectedRoom['id'])) {
$data['id'] = self::$tokenToIdentifier[$room['token']];
Expand Down
19 changes: 19 additions & 0 deletions tests/integration/features/federation/invite.feature
Original file line number Diff line number Diff line change
Expand Up @@ -112,3 +112,22 @@ Feature: federation/invite
Then user "participant1" sees the following messages in room "room" with 200
| room | actorType | actorId | actorDisplayName | message | messageParameters | parentMessage |
| room |federated_users | participant2@http://localhost:8180 | participant2@http://localhost:8180 | Message 1 | [] | |

Scenario: Federate conversation meta data
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 remote "participant2" to room "room" with 200 (v4)
And user "participant2" has the following invitations (v1)
| remote_server | remote_token |
| LOCAL | room |
And user "participant2" accepts invite to room "room" of server "LOCAL" (v1)
Then user "participant2" is participant of the following rooms (v4)
| id | name | type |
| room | room | 2 |
And user "participant1" renames room "room" to "Federated room" with 200 (v4)
Then user "participant2" is participant of the following rooms (v4)
| id | name | type |
| room | Federated room | 2 |
4 changes: 4 additions & 0 deletions tests/php/Federation/FederationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@
use OCA\Talk\Manager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\AttendeeMapper;
use OCA\Talk\Model\InvitationMapper;
use OCA\Talk\Room;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\RoomService;
use OCP\BackgroundJob\IJobList;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Federation\ICloudFederationFactory;
Expand Down Expand Up @@ -110,7 +112,9 @@ public function setUp(): void {
$this->notificationManager,
$this->createMock(IURLGenerator::class),
$this->createMock(ParticipantService::class),
$this->createMock(RoomService::class),
$this->attendeeMapper,
$this->createMock(InvitationMapper::class),
$this->createMock(Manager::class),
$this->createMock(ISession::class),
$this->createMock(IEventDispatcher::class),
Expand Down

0 comments on commit ef3fe8f

Please sign in to comment.