diff options
author | Joas Schilling <213943+nickvergessen@users.noreply.github.com> | 2021-10-04 18:26:17 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-10-04 18:26:17 +0300 |
commit | 820342ee28a919da20442561e64eee2b559f1650 (patch) | |
tree | 970dea8ff65cff59a9bde7a21ee784b24459c2e7 /lib | |
parent | d9903b759e081602cda7ee8a23c01f48dca2b357 (diff) | |
parent | 257d026b6857f428fe7c00513213570b909546c0 (diff) |
Merge pull request #6105 from nextcloud/enh/5723/sending-shares
Add sending room shares support
Diffstat (limited to 'lib')
-rw-r--r-- | lib/BackgroundJob/RetryJob.php | 101 | ||||
-rw-r--r-- | lib/Chat/Notifier.php | 2 | ||||
-rw-r--r-- | lib/Controller/RoomController.php | 41 | ||||
-rw-r--r-- | lib/Exceptions/CannotReachRemoteException.php | 28 | ||||
-rw-r--r-- | lib/Exceptions/RoomHasNoModeratorException.php | 28 | ||||
-rw-r--r-- | lib/Federation/CloudFederationProviderTalk.php | 64 | ||||
-rw-r--r-- | lib/Federation/FederationManager.php | 35 | ||||
-rw-r--r-- | lib/Federation/Notifications.php | 244 | ||||
-rw-r--r-- | lib/Manager.php | 2 | ||||
-rw-r--r-- | lib/Model/AttendeeMapper.php | 39 | ||||
-rw-r--r-- | lib/Service/ParticipantService.php | 54 | ||||
-rw-r--r-- | lib/Service/RoomService.php | 4 |
12 files changed, 616 insertions, 26 deletions
diff --git a/lib/BackgroundJob/RetryJob.php b/lib/BackgroundJob/RetryJob.php new file mode 100644 index 000000000..f5b1fc417 --- /dev/null +++ b/lib/BackgroundJob/RetryJob.php @@ -0,0 +1,101 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @copyright Copyright (c) 2021 Gary Kim <gary@garykim.dev> + * + * @author Bjoern Schiessle <bjoern@schiessle.org> + * @author Björn Schießle <bjoern@schiessle.org> + * @author Joas Schilling <coding@schilljs.com> + * @author Lukas Reschke <lukas@statuscode.ch> + * @author Morris Jobke <hey@morrisjobke.de> + * @author Roeland Jago Douma <roeland@famdouma.nl> + * @author Gary Kim <gary@garykim.dev> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\Talk\BackgroundJob; + +use OCA\Talk\Federation\Notifications; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\Job; +use OCP\ILogger; + +/** + * Class RetryJob + * + * Background job to re-send update of federated re-shares to the remote server in + * case the server was not available on the first try + * + * @package OCA\Talk\BackgroundJob + */ +class RetryJob extends Job { + /** @var Notifications */ + private $notifications; + + /** @var int max number of attempts to send the request */ + private $maxTry = 20; + + + public function __construct(Notifications $notifications, + ITimeFactory $timeFactory) { + parent::__construct($timeFactory); + $this->notifications = $notifications; + } + + /** + * run the job, then remove it from the jobList + * + * @param IJobList $jobList + * @param ILogger|null $logger + */ + public function execute(IJobList $jobList, ?ILogger $logger = null) { + if (((int)$this->argument['try']) > $this->maxTry) { + $jobList->remove($this, $this->argument); + return; + } + if ($this->shouldRun($this->argument)) { + parent::execute($jobList, $logger); + $jobList->remove($this, $this->argument); + } + } + + protected function run($argument): void { + $remote = $argument['remote']; + $data = json_decode($argument['data'], true); + $try = (int)$argument['try'] + 1; + + $this->notifications->sendUpdateDataToRemote($remote, $data, $try); + } + + /** + * test if it is time for the next run + * + * @param array $argument + * @return bool + */ + protected function shouldRun(array $argument): bool { + $lastRun = (int)$argument['lastRun']; + $try = (int)$argument['try']; + return (($this->time->getTime() - $lastRun) > $this->nextRunBreak($try)); + } + + protected function nextRunBreak(int $try): int { + return min(($try + 1) * 300, 3600); + } +} diff --git a/lib/Chat/Notifier.php b/lib/Chat/Notifier.php index 277305a35..deb8eb5f5 100644 --- a/lib/Chat/Notifier.php +++ b/lib/Chat/Notifier.php @@ -36,9 +36,9 @@ use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\IComment; use OCP\IConfig; +use OCP\IUserManager; use OCP\Notification\IManager as INotificationManager; use OCP\Notification\INotification; -use OCP\IUserManager; /** * Helper class for notifications related to user mentions in chat messages. diff --git a/lib/Controller/RoomController.php b/lib/Controller/RoomController.php index d5063d167..34d68b573 100644 --- a/lib/Controller/RoomController.php +++ b/lib/Controller/RoomController.php @@ -36,6 +36,7 @@ use OCA\Talk\Exceptions\InvalidPasswordException; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\Federation\FederationManager; use OCA\Talk\GuestManager; use OCA\Talk\Manager; use OCA\Talk\MatterbridgeManager; @@ -54,13 +55,14 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\IComment; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Federation\ICloudIdManager; +use OCP\IConfig; +use OCP\IGroup; +use OCP\IGroupManager; use OCP\IL10N; use OCP\IRequest; use OCP\IUser; use OCP\IUserManager; -use OCP\IGroup; -use OCP\IGroupManager; -use OCP\IConfig; use OCP\User\Events\UserLiveStatusEvent; use OCP\UserStatus\IManager as IUserStatusManager; use OCP\UserStatus\IUserStatus; @@ -80,6 +82,8 @@ class RoomController extends AEnvironmentAwareController { protected $groupManager; /** @var Manager */ protected $manager; + /** @var ICloudIdManager */ + protected $cloudIdManager; /** @var RoomService */ protected $roomService; /** @var ParticipantService */ @@ -104,6 +108,8 @@ class RoomController extends AEnvironmentAwareController { protected $config; /** @var Config */ protected $talkConfig; + /** @var FederationManager */ + protected $federationManager; /** @var array */ protected $commonReadMessages = []; @@ -127,7 +133,8 @@ class RoomController extends AEnvironmentAwareController { ITimeFactory $timeFactory, IL10N $l10n, IConfig $config, - Config $talkConfig) { + Config $talkConfig, + ICloudIdManager $cloudIdManager) { parent::__construct($appName, $request); $this->session = $session; $this->appManager = $appManager; @@ -147,6 +154,7 @@ class RoomController extends AEnvironmentAwareController { $this->l10n = $l10n; $this->config = $config; $this->talkConfig = $talkConfig; + $this->cloudIdManager = $cloudIdManager; } protected function getTalkHashHeader(): array { @@ -1029,9 +1037,12 @@ class RoomController extends AEnvironmentAwareController { $participants = $this->participantService->getParticipantsForRoom($this->room); $participantsByUserId = []; + $remoteParticipantsByFederatedId = []; foreach ($participants as $participant) { if ($participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS) { $participantsByUserId[$participant->getAttendee()->getActorId()] = $participant; + } elseif ($participant->getAttendee()->getAccessToken() === Attendee::ACTOR_FEDERATED_USERS) { + $remoteParticipantsByFederatedId[$participant->getAttendee()->getActorId()] = $participant; } } @@ -1084,6 +1095,21 @@ class RoomController extends AEnvironmentAwareController { $this->guestManager->sendEmailInvitation($this->room, $participant); return new DataResponse($data); + } elseif ($source === 'remote') { + if (!$this->federationManager->isEnabled()) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + try { + $newUser = $this->cloudIdManager->resolveCloudId($newParticipant); + } catch (\InvalidArgumentException $e) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + $participantsToAdd[] = [ + 'actorType' => Attendee::ACTOR_FEDERATED_USERS, + 'actorId' => $newUser->getId(), + 'displayName' => $newUser->getDisplayId(), + ]; } else { return new DataResponse([], Http::STATUS_BAD_REQUEST); } @@ -1092,6 +1118,9 @@ class RoomController extends AEnvironmentAwareController { // existing users with USER_SELF_JOINED will get converted to regular USER participants foreach ($participantsToAdd as $index => $participantToAdd) { $existingParticipant = $participantsByUserId[$participantToAdd['actorId']] ?? null; + if ($participantToAdd['actorType'] === Attendee::ACTOR_FEDERATED_USERS) { + $existingParticipant = $remoteParticipantsByFederatedId[$participantToAdd['actorId']] ?? null; + } if ($existingParticipant !== null) { unset($participantsToAdd[$index]); @@ -1103,8 +1132,10 @@ class RoomController extends AEnvironmentAwareController { } } + $addedBy = $this->userManager->get($this->userId); + // add the remaining users in batch - $this->participantService->addUsers($this->room, $participantsToAdd); + $this->participantService->addUsers($this->room, $participantsToAdd, $addedBy); return new DataResponse([]); } diff --git a/lib/Exceptions/CannotReachRemoteException.php b/lib/Exceptions/CannotReachRemoteException.php new file mode 100644 index 000000000..81f65934f --- /dev/null +++ b/lib/Exceptions/CannotReachRemoteException.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Gary Kim <gary@garykim.dev> + * + * @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\Exceptions; + +class CannotReachRemoteException extends \Exception { +} diff --git a/lib/Exceptions/RoomHasNoModeratorException.php b/lib/Exceptions/RoomHasNoModeratorException.php new file mode 100644 index 000000000..7b9087dbe --- /dev/null +++ b/lib/Exceptions/RoomHasNoModeratorException.php @@ -0,0 +1,28 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Gary Kim <gary@garykim.dev> + * + * @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\Exceptions; + +class RoomHasNoModeratorException extends \Exception { +} diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php index 3e89bda07..daf85efad 100644 --- a/lib/Federation/CloudFederationProviderTalk.php +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -116,8 +116,9 @@ class CloudFederationProviderTalk implements ICloudFederationProvider { // TODO: Implement group shares } - if (!is_numeric($share->getShareType())) { - throw new ProviderCouldNotAddShareException('shareType is not a number', '', Http::STATUS_BAD_REQUEST); + $roomType = $share->getProtocol()['roomType']; + if (!is_numeric($roomType) || !in_array((int) $roomType, $this->validSharedRoomTypes(), true)) { + throw new ProviderCouldNotAddShareException('roomType is not a valid number', '', Http::STATUS_BAD_REQUEST); } $shareSecret = $share->getShareSecret(); @@ -125,7 +126,7 @@ class CloudFederationProviderTalk implements ICloudFederationProvider { $remoteId = $share->getProviderId(); $roomToken = $share->getResourceName(); $roomName = $share->getProtocol()['roomName']; - $roomType = (int) $share->getShareType(); + $roomType = (int) $roomType; $sharedBy = $share->getSharedByDisplayName(); $sharedByFederatedId = $share->getSharedBy(); $owner = $share->getOwnerDisplayName(); @@ -166,7 +167,7 @@ class CloudFederationProviderTalk implements ICloudFederationProvider { case 'SHARE_DECLINED': return $this->shareDeclined((int) $providerId, $notification); case 'SHARE_UNSHARED': - return []; // TODO: Implement + return $this->shareUnshared((int) $providerId, $notification); case 'REQUEST_RESHARE': return []; // TODO: Implement case 'RESHARE_UNDO': @@ -206,6 +207,26 @@ class CloudFederationProviderTalk implements ICloudFederationProvider { } /** + * @throws ActionNotSupportedException + * @throws ShareNotFound + * @throws AuthenticationFailedException + */ + private function shareUnshared(int $id, array $notification): array { + $attendee = $this->getRemoteAttendeeAndValidate($id, $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(); + } + + $participant = new Participant($room, $attendee, null); + $this->participantService->removeAttendee($room, $participant, Room::PARTICIPANT_REMOVED); + return []; + } + + /** * @throws AuthenticationFailedException * @throws ActionNotSupportedException * @throws ShareNotFound @@ -229,6 +250,31 @@ class CloudFederationProviderTalk implements ICloudFederationProvider { return $attendee; } + /** + * @param int $id + * @param string $sharedSecret + * @return Attendee + * @throws ActionNotSupportedException + * @throws ShareNotFound + * @throws AuthenticationFailedException + */ + private function getRemoteAttendeeAndValidate(int $id, string $sharedSecret): Attendee { + if (!$this->federationManager->isEnabled()) { + throw new ActionNotSupportedException('Server does not support Talk federation'); + } + + if (!$sharedSecret) { + throw new AuthenticationFailedException(); + } + + try { + $attendee = $this->attendeeMapper->getByRemoteIdAndToken($id, $sharedSecret); + } catch (Exception $ex) { + throw new ShareNotFound(); + } + return $attendee; + } + private function notifyAboutNewShare(IUser $shareWith, string $shareId, string $sharedByFederatedId, string $sharedByName, string $roomName, string $roomToken, string $serverUrl) { $notification = $this->notificationManager->createNotification(); $notification->setApp(Application::APP_ID) @@ -256,10 +302,18 @@ class CloudFederationProviderTalk implements ICloudFederationProvider { $this->notificationManager->notify($notification); } + private function validSharedRoomTypes(): array { + return [ + Room::TYPE_ONE_TO_ONE, + Room::TYPE_GROUP, + Room::TYPE_PUBLIC, + ]; + } + /** * @inheritDoc */ - public function getSupportedShareTypes() { + public function getSupportedShareTypes(): array { return ['user']; } } diff --git a/lib/Federation/FederationManager.php b/lib/Federation/FederationManager.php index 691d83a6c..c3fd88c10 100644 --- a/lib/Federation/FederationManager.php +++ b/lib/Federation/FederationManager.php @@ -26,6 +26,7 @@ declare(strict_types=1); namespace OCA\Talk\Federation; use OCA\Talk\AppInfo\Application; +use OCA\Talk\Exceptions\CannotReachRemoteException; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Exceptions\UnauthorizedException; use OCA\Talk\Manager; @@ -34,6 +35,7 @@ use OCA\Talk\Model\Invitation; use OCA\Talk\Model\InvitationMapper; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\DB\Exception as DBException; use OCP\IConfig; @@ -47,6 +49,10 @@ use OCP\IUser; * FederationManager handles incoming federated rooms */ class FederationManager { + public const TALK_ROOM_RESOURCE = 'talk-room'; + public const TALK_PROTOCOL_NAME = 'nctalk'; + public const TOKEN_LENGTH = 15; + /** @var IConfig */ private $config; @@ -59,16 +65,21 @@ class FederationManager { /** @var InvitationMapper */ private $invitationMapper; + /** @var Notifications */ + private $notifications; + public function __construct( IConfig $config, Manager $manager, ParticipantService $participantService, - InvitationMapper $invitationMapper + InvitationMapper $invitationMapper, + Notifications $notifications ) { $this->config = $config; $this->manager = $manager; $this->participantService = $participantService; $this->invitationMapper = $invitationMapper; + $this->notifications = $notifications; } /** @@ -77,11 +88,12 @@ class FederationManager { */ public function isEnabled(): bool { // TODO: Set to default true once implementation is complete - return $this->config->getAppValue(Application::APP_ID, 'federation_enabled', "false") === "true"; + return $this->config->getAppValue(Application::APP_ID, 'federation_enabled', 'false') === 'true'; } /** * @param IUser $user + * @param string $remoteId * @param int $roomType * @param string $roomName * @param string $roomToken @@ -110,6 +122,8 @@ class FederationManager { * @throws DBException * @throws UnauthorizedException * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException + * @throws CannotReachRemoteException */ public function acceptRemoteRoomShare(IUser $user, int $shareId) { $invitation = $this->invitationMapper->getInvitationById($shareId); @@ -119,6 +133,13 @@ class FederationManager { // Add user to the room $room = $this->manager->getRoomById($invitation->getRoomId()); + if ( + !$this->notifications->sendShareAccepted($room->getServerUrl(), $invitation->getRemoteId(), $invitation->getAccessToken()) + ) { + throw new CannotReachRemoteException(); + } + + $participant = [ [ 'actorType' => Attendee::ACTOR_USERS, @@ -128,26 +149,28 @@ class FederationManager { 'remoteId' => $invitation->getRemoteId(), ] ]; - $this->participantService->addUsers($room, $participant); + $this->participantService->addUsers($room, $participant, $user); $this->invitationMapper->delete($invitation); - - // TODO: Send SHARE_ACCEPTED notification } /** * @throws DBException * @throws UnauthorizedException * @throws MultipleObjectsReturnedException + * @throws DoesNotExistException */ public function rejectRemoteRoomShare(IUser $user, int $shareId) { $invitation = $this->invitationMapper->getInvitationById($shareId); if ($invitation->getUserId() !== $user->getUID()) { throw new UnauthorizedException('invitation is for a different user'); } + + $room = $this->manager->getRoomById($invitation->getRoomId()); + $this->invitationMapper->delete($invitation); - // TODO: Send SHARE_DECLINED notification + $this->notifications->sendShareDeclined($room->getServerUrl(), $invitation->getRemoteId(), $invitation->getAccessToken()); } /** diff --git a/lib/Federation/Notifications.php b/lib/Federation/Notifications.php new file mode 100644 index 000000000..465538d00 --- /dev/null +++ b/lib/Federation/Notifications.php @@ -0,0 +1,244 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Gary Kim <gary@garykim.dev> + * + * @author Gary Kim <gary@garykim.dev> + * + * @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\FederatedFileSharing\AddressHandler; +use OCA\Talk\AppInfo\Application; +use OCA\Talk\BackgroundJob\RetryJob; +use OCA\Talk\Exceptions\RoomHasNoModeratorException; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Room; +use OCP\BackgroundJob\IJobList; +use OCP\Federation\ICloudFederationFactory; +use OCP\Federation\ICloudFederationNotification; +use OCP\Federation\ICloudFederationProviderManager; +use OCP\HintException; +use OCP\IUser; +use OCP\IUserManager; +use Psr\Log\LoggerInterface; + +class Notifications { + /** @var ICloudFederationFactory */ + private $cloudFederationFactory; + + /** @var LoggerInterface */ + private $logger; + + /** @var ICloudFederationProviderManager */ + private $federationProviderManager; + + /** @var IJobList */ + private $jobList; + + /** @var IUserManager */ + private $userManager; + + /** @var AddressHandler */ + private $addressHandler; + + public function __construct( + ICloudFederationFactory $cloudFederationFactory, + AddressHandler $addressHandler, + LoggerInterface $logger, + ICloudFederationProviderManager $federationProviderManager, + IJobList $jobList, + IUserManager $userManager + ) { + $this->cloudFederationFactory = $cloudFederationFactory; + $this->logger = $logger; + $this->federationProviderManager = $federationProviderManager; + $this->jobList = $jobList; + $this->userManager = $userManager; + $this->addressHandler = $addressHandler; + } + + /** + * @throws HintException + * @throws RoomHasNoModeratorException + * @throws \OCP\DB\Exception + */ + public function sendRemoteShare(string $providerId, string $token, string $shareWith, string $sharedBy, + string $sharedByFederatedId, string $shareType, Room $room, Attendee $roomOwnerAttendee): bool { + [$user, $remote] = $this->addressHandler->splitUserRemote($shareWith); + + $roomName = $room->getName(); + $roomType = $room->getType(); + $roomToken = $room->getToken(); + + if (!($user && $remote)) { + $this->logger->info( + "could not share $roomToken, invalid contact $shareWith", + ['app' => Application::APP_ID] + ); + return false; + } + + /** @var IUser|null $roomOwner */ + $roomOwner = null; + if ($roomOwnerAttendee) { + $roomOwner = $this->userManager->get($roomOwnerAttendee->getActorId()); + } else { + throw new RoomHasNoModeratorException(); + } + + $remote = $this->prepareRemoteUrl($remote); + + $share = $this->cloudFederationFactory->getCloudFederationShare( + $user . '@' . $remote, + $roomToken, + '', + $providerId, + $roomOwner->getCloudId(), + $roomOwner->getDisplayName(), + $sharedByFederatedId, + $sharedBy, + $token, + $shareType, + FederationManager::TALK_ROOM_RESOURCE + ); + + // Put room name info in the share + $protocol = $share->getProtocol(); + $protocol['roomName'] = $roomName; + $protocol['roomType'] = $roomType; + $protocol['name'] = FederationManager::TALK_PROTOCOL_NAME; + $share->setProtocol($protocol); + + $response = $this->federationProviderManager->sendShare($share); + if (is_array($response)) { + return true; + } + $this->logger->info( + "failed sharing $roomToken with $shareWith", + ['app' => Application::APP_ID] + ); + + return false; + } + + /** + * send remote share acceptance notification to remote server + * + * @param string $remote remote server domain + * @param string $id share id + * @param string $token share secret token + * @return bool success + */ + public function sendShareAccepted(string $remote, string $id, string $token): bool { + $remote = $this->prepareRemoteUrl($remote); + + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage( + 'SHARE_ACCEPTED', + FederationManager::TALK_ROOM_RESOURCE, + $id, + [ + 'sharedSecret' => $token, + 'message' => 'Recipient accepted the share', + ]); + $response = $this->federationProviderManager->sendNotification($remote, $notification); + if (!is_array($response)) { + $this->logger->info( + "failed to send share accepted notification for share from $remote", + ['app' => Application::APP_ID] + ); + return false; + } + return true; + } + + public function sendShareDeclined(string $remote, string $id, string $token): bool { + $remote = $this->prepareRemoteUrl($remote); + + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage( + 'SHARE_DECLINED', + FederationManager::TALK_ROOM_RESOURCE, + $id, + [ + 'sharedSecret' => $token, + 'message' => 'Recipient declined the share', + ] + ); + $response = $this->federationProviderManager->sendNotification($remote, $notification); + if (!is_array($response)) { + $this->logger->info( + "failed to send share declined notification for share from $remote", + ['app' => Application::APP_ID] + ); + return false; + } + return true; + } + + public function sendRemoteUnShare(string $remote, string $id, string $token): void { + $remote = $this->prepareRemoteUrl($remote); + + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage( + 'SHARE_UNSHARED', + FederationManager::TALK_ROOM_RESOURCE, + $id, + [ + 'sharedSecret' => $token, + 'message' => 'This room has been unshared', + ] + ); + + $this->sendUpdateToRemote($remote, $notification); + } + + public function sendUpdateDataToRemote(string $remote, array $data = [], int $try = 0): void { + $notification = $this->cloudFederationFactory->getCloudFederationNotification(); + $notification->setMessage( + $data['notificationType'], + $data['resourceType'], + $data['providerId'], + $data['notification'] + ); + $this->sendUpdateToRemote($remote, $notification, $try); + } + + public 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, + [ + 'remote' => $remote, + 'data' => json_encode($notification->getMessage()), + 'try' => $try, + ] + ); + } + } + + private function prepareRemoteUrl(string $remote): string { + if ($this->addressHandler->urlContainProtocol($remote)) { + return 'https://' . $remote; + } + return $remote; + } +} diff --git a/lib/Manager.php b/lib/Manager.php index ecbfdd8cd..1de00d120 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -43,10 +43,10 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\ICache; use OCP\IConfig; use OCP\IDBConnection; +use OCP\IGroupManager; use OCP\IL10N; use OCP\IUser; use OCP\IUserManager; -use OCP\IGroupManager; use OCP\Security\IHasher; use OCP\Security\ISecureRandom; diff --git a/lib/Model/AttendeeMapper.php b/lib/Model/AttendeeMapper.php index 261b949b0..89a9460d3 100644 --- a/lib/Model/AttendeeMapper.php +++ b/lib/Model/AttendeeMapper.php @@ -73,7 +73,25 @@ class AttendeeMapper extends QBMapper { $query = $this->db->getQueryBuilder(); $query->select('*') ->from($this->getTableName()) - ->where($query->expr()->eq('id', $query->createNamedParameter($id))); + ->where($query->expr()->eq('id', $query->createNamedParameter($id, IQueryBuilder::PARAM_INT))); + + return $this->findEntity($query); + } + + /** + * @param int $id + * @param string $token + * @return Attendee + * @throws DBException + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + */ + public function getByRemoteIdAndToken(int $id, string $token): Attendee { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('remote_id', $query->createNamedParameter($id, IQueryBuilder::PARAM_STR))) + ->andWhere($query->expr()->eq('access_token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR))); return $this->findEntity($query); } @@ -100,6 +118,25 @@ class AttendeeMapper extends QBMapper { /** * @param int $roomId + * @param array $participantType + * @return Attendee[] + * @throws DBException + */ + public function getActorsByParticipantTypes(int $roomId, array $participantType): array { + $query = $this->db->getQueryBuilder(); + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('room_id', $query->createNamedParameter($roomId, IQueryBuilder::PARAM_INT))); + + if (!empty($participantType)) { + $query->andWhere($query->expr()->in('participant_type', $query->createNamedParameter($participantType, IQueryBuilder::PARAM_INT_ARRAY))); + } + + return $this->findEntities($query); + } + + /** + * @param int $roomId * @param string $actorType * @param int|null $lastJoinedCall * @return int diff --git a/lib/Service/ParticipantService.php b/lib/Service/ParticipantService.php index 6019c8cbf..90b5712c3 100644 --- a/lib/Service/ParticipantService.php +++ b/lib/Service/ParticipantService.php @@ -40,6 +40,8 @@ use OCA\Talk\Events\RoomEvent; use OCA\Talk\Exceptions\InvalidPasswordException; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\Federation\FederationManager; +use OCA\Talk\Federation\Notifications; use OCA\Talk\Model\Attendee; use OCA\Talk\Model\AttendeeMapper; use OCA\Talk\Model\SelectHelper; @@ -57,9 +59,9 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IDBConnection; use OCP\IGroup; +use OCP\IGroupManager; use OCP\IUser; use OCP\IUserManager; -use OCP\IGroupManager; use OCP\Security\ISecureRandom; class ParticipantService { @@ -85,6 +87,8 @@ class ParticipantService { private $groupManager; /** @var MembershipService */ private $membershipService; + /** @var Notifications */ + private $notifications; /** @var ITimeFactory */ private $timeFactory; @@ -99,6 +103,7 @@ class ParticipantService { IUserManager $userManager, IGroupManager $groupManager, MembershipService $membershipService, + Notifications $notifications, ITimeFactory $timeFactory) { $this->serverConfig = $serverConfig; $this->talkConfig = $talkConfig; @@ -112,6 +117,7 @@ class ParticipantService { $this->groupManager = $groupManager; $this->membershipService = $membershipService; $this->timeFactory = $timeFactory; + $this->notifications = $notifications; } public function updateParticipantType(Room $room, Participant $participant, int $participantType): void { @@ -221,7 +227,7 @@ class ParticipantService { 'displayName' => $user->getDisplayName(), // need to use "USER" here, because "USER_SELF_JOINED" only works for public calls 'participantType' => Participant::USER, - ]]); + ]], $user); } elseif ($room->getType() === Room::PUBLIC_CALL) { // User joining a public room, without being invited $this->addUsers($room, [[ @@ -229,7 +235,7 @@ class ParticipantService { 'actorId' => $user->getUID(), 'displayName' => $user->getDisplayName(), 'participantType' => Participant::USER_SELF_JOINED, - ]]); + ]], $user); } else { // shouldn't happen unless some code called joinRoom without previous checks throw new UnauthorizedException('Participant is not allowed to join'); @@ -304,8 +310,10 @@ class ParticipantService { /** * @param Room $room * @param array $participants + * @param IUser|null $addedBy User that is attempting to add these users (must be set for federated users to be added) + * @throws \Exception thrown if $addedBy is not set when adding a federated user */ - public function addUsers(Room $room, array $participants): void { + public function addUsers(Room $room, array $participants, ?IUser $addedBy = null): void { if (empty($participants)) { return; } @@ -322,6 +330,14 @@ class ParticipantService { $readPrivacy = Participant::PRIVACY_PUBLIC; if ($participant['actorType'] === Attendee::ACTOR_USERS) { $readPrivacy = $this->talkConfig->getUserReadPrivacy($participant['actorId']); + } elseif ($participant['actorType'] === Attendee::ACTOR_FEDERATED_USERS) { + if ($addedBy === null) { + throw new \Exception('$addedBy must be set to add a federated user'); + } + $participant['accessToken'] = $this->secureRandom->generate( + FederationManager::TOKEN_LENGTH, + ISecureRandom::CHAR_HUMAN_READABLE + ); } $attendee = new Attendee(); @@ -341,8 +357,12 @@ class ParticipantService { $attendee->setLastReadMessage($lastMessage); $attendee->setReadPrivacy($readPrivacy); try { - $this->attendeeMapper->insert($attendee); + $entity = $this->attendeeMapper->insert($attendee); $attendees[] = $attendee; + + if ($attendee->getActorType() === Attendee::ACTOR_FEDERATED_USERS) { + $this->notifications->sendRemoteShare((string) $entity->getId(), $participant['accessToken'], $participant['actorId'], $addedBy->getDisplayName(), $addedBy->getCloudId(), 'user', $room, $this->getHighestPermissionAttendee($room)); + } } catch (Exception $e) { if ($e->getReason() !== Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { throw $e; @@ -358,6 +378,30 @@ class ParticipantService { } } + public function getHighestPermissionAttendee(Room $room): ?Attendee { + try { + $roomOwners = $this->attendeeMapper->getActorsByParticipantTypes($room->getId(), [Participant::OWNER]); + + if (!empty($roomOwners)) { + foreach ($roomOwners as $owner) { + if ($owner->getActorType() === Attendee::ACTOR_USERS) { + return $owner; + } + } + } + $roomModerators = $this->attendeeMapper->getActorsByParticipantTypes($room->getId(), [Participant::MODERATOR]); + if (!empty($roomOwners)) { + foreach ($roomModerators as $moderator) { + if ($moderator->getActorType() === Attendee::ACTOR_USERS) { + return $moderator; + } + } + } + } catch (Exception $e) { + } + return null; + } + /** * @param Room $room * @param IGroup $group diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index 5e1000af7..9667f7193 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -77,7 +77,7 @@ class RoomService { 'displayName' => $targetUser->getDisplayName(), 'participantType' => Participant::OWNER, ], - ]); + ], $actor); } return $room; @@ -130,7 +130,7 @@ class RoomService { 'actorType' => Attendee::ACTOR_USERS, 'actorId' => $owner->getUID(), 'participantType' => Participant::OWNER, - ]]); + ]], null); } return $room; |