Merge pull request #10729 from nextcloud/feat/noid/federate-room-modifications

feat(federation): Send room modifications via OCM notifications to re…
This commit is contained in:
Joas Schilling 2023-10-30 14:21:09 +01:00 committed by GitHub
commit 64cef3b1df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 217 additions and 12 deletions

View file

@ -65,6 +65,7 @@ use OCA\Talk\Events\RoomDeletedEvent;
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;
@ -218,6 +219,9 @@ class Application extends App implements IBootstrap {
$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);

View file

@ -40,6 +40,7 @@ use OCP\HintException;
use OCP\IUser;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use SensitiveParameter;
class BackendNotifier {
@ -192,6 +193,42 @@ class BackendNotifier {
$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(
@ -203,7 +240,7 @@ class BackendNotifier {
$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,
@ -216,7 +253,7 @@ class BackendNotifier {
}
}
private function prepareRemoteUrl(string $remote): string {
protected function prepareRemoteUrl(string $remote): string {
if (!$this->addressHandler->urlContainProtocol($remote)) {
return 'https://' . $remote;
}

View file

@ -29,13 +29,18 @@ use Exception;
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;
@ -64,7 +69,9 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
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,
@ -151,6 +158,8 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
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]);
@ -221,6 +230,42 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
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
@ -248,12 +293,12 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
/**
* @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');
}
@ -263,11 +308,16 @@ class CloudFederationProviderTalk implements ICloudFederationProvider {
}
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 {

View file

@ -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(

View file

@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
*
* @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(),
);
}
}
}

View file

@ -60,6 +60,20 @@ class InvitationMapper extends QBMapper {
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[]

View file

@ -403,7 +403,7 @@ class Notifier implements INotifier {
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();
@ -423,7 +423,7 @@ class Notifier implements INotifier {
'roomName' => [
'type' => 'highlight',
'id' => $subjectParameters['serverUrl'] . '::' . $subjectParameters['roomToken'],
'name' => $subjectParameters['roomName'],
'name' => $room->getName(),
],
'remoteServer' => [
'type' => 'highlight',

View file

@ -380,9 +380,6 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
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'];
}
@ -390,6 +387,10 @@ class FeatureContext implements Context, SnippetAcceptingContext {
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']];

View file

@ -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 |

View file

@ -31,8 +31,10 @@ use OCA\Talk\Federation\FederationManager;
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;
@ -110,7 +112,9 @@ class FederationTest extends TestCase {
$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),