diff options
author | Joas Schilling <213943+nickvergessen@users.noreply.github.com> | 2021-07-16 09:38:31 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-07-16 09:38:31 +0200 |
commit | 56e1d3db96b55b1c9910398f0a39844de9e51f96 (patch) | |
tree | f793d088a37d2699293158b1bbe95da30af17e6e | |
parent | c4dc1af6213c8c21cc5b17545bb4989324a1afd5 (diff) | |
parent | 06dd757d480fd71c0783f1aa4d795d03a0086d91 (diff) |
Merge pull request #5775 from nextcloud/enh/5723/add-database
Implement Recieving Room Shares
-rw-r--r-- | appinfo/info.xml | 2 | ||||
-rw-r--r-- | appinfo/routes.php | 21 | ||||
-rw-r--r-- | lib/BackgroundJob/RemoveEmptyRooms.php | 6 | ||||
-rw-r--r-- | lib/Controller/FederationController.php | 87 | ||||
-rw-r--r-- | lib/Federation/CloudFederationProviderTalk.php | 265 | ||||
-rw-r--r-- | lib/Federation/FederationManager.php | 159 | ||||
-rw-r--r-- | lib/Manager.php | 44 | ||||
-rw-r--r-- | lib/Migration/Version13000Date20210625232111.php | 102 | ||||
-rw-r--r-- | lib/Model/Attendee.php | 17 | ||||
-rw-r--r-- | lib/Model/AttendeeMapper.php | 25 | ||||
-rw-r--r-- | lib/Model/Invitation.php | 73 | ||||
-rw-r--r-- | lib/Model/InvitationMapper.php | 106 | ||||
-rw-r--r-- | lib/Model/SelectHelper.php | 3 | ||||
-rw-r--r-- | lib/Notification/Notifier.php | 53 | ||||
-rw-r--r-- | lib/Room.php | 12 | ||||
-rw-r--r-- | lib/Service/ParticipantService.php | 6 | ||||
-rw-r--r-- | psalm.xml | 2 | ||||
-rw-r--r-- | tests/php/Notification/NotifierTest.php | 7 | ||||
-rw-r--r-- | tests/php/RoomTest.php | 1 |
19 files changed, 982 insertions, 9 deletions
diff --git a/appinfo/info.xml b/appinfo/info.xml index fcd28db7a..52561d744 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -16,7 +16,7 @@ And in the works for the [coming versions](https://github.com/nextcloud/spreed/m ]]></description> - <version>13.0.0-dev</version> + <version>13.0.0-dev.1</version> <licence>agpl</licence> <author>Daniel Calviño Sánchez</author> diff --git a/appinfo/routes.php b/appinfo/routes.php index 26ec3ccbd..59143fd5c 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -524,6 +524,27 @@ return [ ], /** + * Federation + */ + + [ + 'name' => 'Federation#acceptShare', + 'url' => 'api/{apiVersion}/federation/invitation/{id}', + 'verb' => 'POST', + 'requirements' => [ + 'apiVersion' => 'v1', + ], + ], + [ + 'name' => 'Federation#rejectShare', + 'url' => 'api/{apiVersion}/federation/invitation/{id}', + 'verb' => 'DELETE', + 'requirements' => [ + 'apiVersion' => 'v1', + ], + ], + + /** * PublicShareAuth */ [ diff --git a/lib/BackgroundJob/RemoveEmptyRooms.php b/lib/BackgroundJob/RemoveEmptyRooms.php index af9738809..3c620c171 100644 --- a/lib/BackgroundJob/RemoveEmptyRooms.php +++ b/lib/BackgroundJob/RemoveEmptyRooms.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace OCA\Talk\BackgroundJob; +use OCA\Talk\Federation\FederationManager; use OCA\Talk\Service\ParticipantService; use OCP\AppFramework\Utility\ITimeFactory; use OCP\BackgroundJob\TimedJob; @@ -46,6 +47,9 @@ class RemoveEmptyRooms extends TimedJob { /** @var LoggerInterface */ protected $logger; + /** @var FederationManager */ + protected $federationManager; + protected $numDeletedRooms = 0; public function __construct(ITimeFactory $timeFactory, @@ -77,7 +81,7 @@ class RemoveEmptyRooms extends TimedJob { return; } - if ($this->participantService->getNumberOfActors($room) === 0 && $room->getObjectType() !== 'file') { + if ($this->participantService->getNumberOfActors($room) === 0 && $room->getObjectType() !== 'file' && $this->federationManager->getNumberOfInvitations($room) === 0) { $room->deleteRoom(); $this->numDeletedRooms++; } diff --git a/lib/Controller/FederationController.php b/lib/Controller/FederationController.php new file mode 100644 index 000000000..ea951d593 --- /dev/null +++ b/lib/Controller/FederationController.php @@ -0,0 +1,87 @@ +<?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\Controller; + +use OCA\Talk\AppInfo\Application; +use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\Federation\FederationManager; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\OCSController; +use OCP\DB\Exception as DBException; +use OCP\IRequest; +use OCP\IUser; +use OCP\IUserSession; + +class FederationController extends OCSController { + /** @var FederationManager */ + private $federationManager; + + /** @var IUserSession */ + private $userSession; + + public function __construct(IRequest $request, FederationManager $federationManager, IUserSession $userSession) { + parent::__construct(Application::APP_ID, $request); + $this->federationManager = $federationManager; + $this->userSession = $userSession; + } + + /** + * @NoAdminRequired + * + * @param int $id + * @return DataResponse + * @throws UnauthorizedException + * @throws DBException + * @throws MultipleObjectsReturnedException + */ + public function acceptShare(int $id): DataResponse { + $user = $this->userSession->getUser(); + if (!$user instanceof IUser) { + throw new UnauthorizedException(); + } + $this->federationManager->acceptRemoteRoomShare($user, $id); + return new DataResponse(); + } + + /** + * @NoAdminRequired + * + * @param int $id + * @return DataResponse + * @throws UnauthorizedException + * @throws DBException + * @throws MultipleObjectsReturnedException + */ + public function rejectShare(int $id): DataResponse { + $user = $this->userSession->getUser(); + if (!$user instanceof IUser) { + throw new UnauthorizedException(); + } + $this->federationManager->rejectRemoteRoomShare($user, $id); + return new DataResponse(); + } +} diff --git a/lib/Federation/CloudFederationProviderTalk.php b/lib/Federation/CloudFederationProviderTalk.php new file mode 100644 index 000000000..3e89bda07 --- /dev/null +++ b/lib/Federation/CloudFederationProviderTalk.php @@ -0,0 +1,265 @@ +<?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 Exception; +use OCA\FederatedFileSharing\AddressHandler; +use OCA\Talk\AppInfo\Application; +use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\AttendeeMapper; +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; +use OCP\AppFramework\Http; +use OCP\DB\Exception as DBException; +use OCP\Federation\Exceptions\ActionNotSupportedException; +use OCP\Federation\Exceptions\AuthenticationFailedException; +use OCP\Federation\Exceptions\BadRequestException; +use OCP\Federation\Exceptions\ProviderCouldNotAddShareException; +use OCP\Federation\ICloudFederationProvider; +use OCP\Federation\ICloudFederationShare; +use OCP\HintException; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; +use OCP\Notification\IManager as INotificationManager; +use OCP\Share\Exceptions\ShareNotFound; + +class CloudFederationProviderTalk implements ICloudFederationProvider { + + /** @var IUserManager */ + private $userManager; + + /** @var AddressHandler */ + private $addressHandler; + + /** @var FederationManager */ + private $federationManager; + + /** @var INotificationManager */ + private $notificationManager; + + /** @var IURLGenerator */ + private $urlGenerator; + + /** @var ParticipantService */ + private $participantService; + + /** @var AttendeeMapper */ + private $attendeeMapper; + + /** @var Manager */ + private $manager; + + public function __construct( + IUserManager $userManager, + AddressHandler $addressHandler, + FederationManager $federationManager, + INotificationManager $notificationManager, + IURLGenerator $urlGenerator, + ParticipantService $participantService, + AttendeeMapper $attendeeMapper, + Manager $manager + ) { + $this->userManager = $userManager; + $this->addressHandler = $addressHandler; + $this->federationManager = $federationManager; + $this->notificationManager = $notificationManager; + $this->urlGenerator = $urlGenerator; + $this->participantService = $participantService; + $this->attendeeMapper = $attendeeMapper; + $this->manager = $manager; + } + + /** + * @inheritDoc + */ + public function getShareType(): string { + return 'talk-room'; + } + + /** + * @inheritDoc + * @throws HintException + * @throws DBException + */ + public function shareReceived(ICloudFederationShare $share): string { + if (!$this->federationManager->isEnabled()) { + throw new ProviderCouldNotAddShareException('Server does not support talk federation', '', Http::STATUS_SERVICE_UNAVAILABLE); + } + if (!in_array($share->getShareType(), $this->getSupportedShareTypes(), true)) { + throw new ProviderCouldNotAddShareException('Support for sharing with non-users not implemented yet', '', Http::STATUS_NOT_IMPLEMENTED); + // TODO: Implement group shares + } + + if (!is_numeric($share->getShareType())) { + throw new ProviderCouldNotAddShareException('shareType is not a number', '', Http::STATUS_BAD_REQUEST); + } + + $shareSecret = $share->getShareSecret(); + $shareWith = $share->getShareWith(); + $remoteId = $share->getProviderId(); + $roomToken = $share->getResourceName(); + $roomName = $share->getProtocol()['roomName']; + $roomType = (int) $share->getShareType(); + $sharedBy = $share->getSharedByDisplayName(); + $sharedByFederatedId = $share->getSharedBy(); + $owner = $share->getOwnerDisplayName(); + $ownerFederatedId = $share->getOwner(); + [, $remote] = $this->addressHandler->splitUserRemote($ownerFederatedId); + + // if no explicit information about the person who created the share was send + // we assume that the share comes from the owner + if ($sharedByFederatedId === null) { + $sharedBy = $owner; + $sharedByFederatedId = $ownerFederatedId; + } + + if ($remote && $shareSecret && $shareWith && $roomToken && $remoteId && is_string($roomName) && $roomName && $owner) { + $shareWith = $this->userManager->get($shareWith); + if ($shareWith === null) { + throw new ProviderCouldNotAddShareException('User does not exist', '',Http::STATUS_BAD_REQUEST); + } + + $shareId = (string) $this->federationManager->addRemoteRoom($shareWith, $remoteId, $roomType, $roomName, $roomToken, $remote, $shareSecret); + + $this->notifyAboutNewShare($shareWith, $shareId, $sharedByFederatedId, $sharedBy, $roomName, $roomToken, $remote); + return $shareId; + } + throw new ProviderCouldNotAddShareException('required request data not found', '', Http::STATUS_BAD_REQUEST); + } + + /** + * @inheritDoc + */ + public function notificationReceived($notificationType, $providerId, array $notification): array { + if (!is_numeric($providerId)) { + throw new BadRequestException(['providerId']); + } + switch ($notificationType) { + case 'SHARE_ACCEPTED': + return $this->shareAccepted((int) $providerId, $notification); + case 'SHARE_DECLINED': + return $this->shareDeclined((int) $providerId, $notification); + case 'SHARE_UNSHARED': + return []; // TODO: Implement + case 'REQUEST_RESHARE': + return []; // TODO: Implement + case 'RESHARE_UNDO': + return []; // TODO: Implement + case 'RESHARE_CHANGE_PERMISSION': + return []; // TODO: Implement + } + return []; + // TODO: Implement notificationReceived() method. + } + + /** + * @throws ActionNotSupportedException + * @throws ShareNotFound + * @throws AuthenticationFailedException + */ + private function shareAccepted(int $id, array $notification): array { + $attendee = $this->getAttendeeAndValidate($id, $notification['sharedSecret']); + + // TODO: Add activity for share accepted + + return []; + } + + /** + * @throws ActionNotSupportedException + * @throws ShareNotFound + * @throws AuthenticationFailedException + */ + private function shareDeclined(int $id, array $notification): array { + $attendee = $this->getAttendeeAndValidate($id, $notification['sharedSecret']); + + $room = $this->manager->getRoomById($attendee->getRoomId()); + $participant = new Participant($room, $attendee, null); + $this->participantService->removeAttendee($room, $participant, Room::PARTICIPANT_LEFT); + return []; + } + + /** + * @throws AuthenticationFailedException + * @throws ActionNotSupportedException + * @throws ShareNotFound + */ + private function getAttendeeAndValidate(int $id, string $sharedSecret): Attendee { + if (!$this->federationManager->isEnabled()) { + throw new ActionNotSupportedException('Server does not support Talk federation'); + } + + try { + $attendee = $this->attendeeMapper->getById($id); + } catch (Exception $ex) { + throw new ShareNotFound(); + } + if ($attendee->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) { + throw new ShareNotFound(); + } + if ($attendee->getAccessToken() !== $sharedSecret) { + throw new AuthenticationFailedException(); + } + 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) + ->setUser($shareWith->getUID()) + ->setDateTime(new \DateTime()) + ->setObject('remote_talk_share', $shareId) + ->setSubject('remote_talk_share', [ + 'sharedByDisplayName' => $sharedByName, + 'sharedByFederatedId' => $sharedByFederatedId, + 'roomName' => $roomName, + 'serverUrl' => $serverUrl, + 'roomToken' => $roomToken, + ]); + + $declineAction = $notification->createAction(); + $declineAction->setLabel('decline') + ->setLink($this->urlGenerator->linkToOCSRouteAbsolute('spreed.Federation.rejectShare', ['id' => $shareId]), 'DELETE'); + $notification->addAction($declineAction); + + $acceptAction = $notification->createAction(); + $acceptAction->setLabel('accept') + ->setLink($this->urlGenerator->linkToOCSRouteAbsolute('spreed.Federation.acceptShare', ['id' => $shareId]), 'POST'); + $notification->addAction($acceptAction); + + $this->notificationManager->notify($notification); + } + + /** + * @inheritDoc + */ + public function getSupportedShareTypes() { + return ['user']; + } +} diff --git a/lib/Federation/FederationManager.php b/lib/Federation/FederationManager.php new file mode 100644 index 000000000..691d83a6c --- /dev/null +++ b/lib/Federation/FederationManager.php @@ -0,0 +1,159 @@ +<?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\Talk\AppInfo\Application; +use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Exceptions\UnauthorizedException; +use OCA\Talk\Manager; +use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\Invitation; +use OCA\Talk\Model\InvitationMapper; +use OCA\Talk\Room; +use OCA\Talk\Service\ParticipantService; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\DB\Exception as DBException; +use OCP\IConfig; +use OCP\IUser; + +/** + * Class FederationManager + * + * @package OCA\Talk\Federation + * + * FederationManager handles incoming federated rooms + */ +class FederationManager { + /** @var IConfig */ + private $config; + + /** @var Manager */ + private $manager; + + /** @var ParticipantService */ + private $participantService; + + /** @var InvitationMapper */ + private $invitationMapper; + + public function __construct( + IConfig $config, + Manager $manager, + ParticipantService $participantService, + InvitationMapper $invitationMapper + ) { + $this->config = $config; + $this->manager = $manager; + $this->participantService = $participantService; + $this->invitationMapper = $invitationMapper; + } + + /** + * Determine if Talk federation is enabled on this instance + * @return bool + */ + public function isEnabled(): bool { + // TODO: Set to default true once implementation is complete + return $this->config->getAppValue(Application::APP_ID, 'federation_enabled', "false") === "true"; + } + + /** + * @param IUser $user + * @param int $roomType + * @param string $roomName + * @param string $roomToken + * @param string $remoteUrl + * @param string $sharedSecret + * @return int share id for this specific remote room share + * @throws DBException + */ + public function addRemoteRoom(IUser $user, string $remoteId, int $roomType, string $roomName, string $roomToken, string $remoteUrl, string $sharedSecret): int { + try { + $room = $this->manager->getRoomByToken($roomToken, null, $remoteUrl); + } catch (RoomNotFoundException $ex) { + $room = $this->manager->createRemoteRoom($roomType, $roomName, $roomToken, $remoteUrl); + } + $invitation = new Invitation(); + $invitation->setUserId($user->getUID()); + $invitation->setRoomId($room->getId()); + $invitation->setAccessToken($sharedSecret); + $invitation->setRemoteId($remoteId); + $invitation = $this->invitationMapper->insert($invitation); + + return $invitation->getId(); + } + + /** + * @throws DBException + * @throws UnauthorizedException + * @throws MultipleObjectsReturnedException + */ + public function acceptRemoteRoomShare(IUser $user, int $shareId) { + $invitation = $this->invitationMapper->getInvitationById($shareId); + if ($invitation->getUserId() !== $user->getUID()) { + throw new UnauthorizedException('invitation is for a different user'); + } + + // Add user to the room + $room = $this->manager->getRoomById($invitation->getRoomId()); + $participant = [ + [ + 'actorType' => Attendee::ACTOR_USERS, + 'actorId' => $user->getUID(), + 'displayName' => $user->getDisplayName(), + 'accessToken' => $invitation->getAccessToken(), + 'remoteId' => $invitation->getRemoteId(), + ] + ]; + $this->participantService->addUsers($room, $participant); + + $this->invitationMapper->delete($invitation); + + // TODO: Send SHARE_ACCEPTED notification + } + + /** + * @throws DBException + * @throws UnauthorizedException + * @throws MultipleObjectsReturnedException + */ + 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'); + } + $this->invitationMapper->delete($invitation); + + // TODO: Send SHARE_DECLINED notification + } + + /** + * @throws DBException + */ + public function getNumberOfInvitations(Room $room): int { + return $this->invitationMapper->countInvitationsForRoom($room); + } +} diff --git a/lib/Manager.php b/lib/Manager.php index aeb52b200..ecbfdd8cd 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -37,6 +37,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\IComment; use OCP\Comments\ICommentsManager; use OCP\Comments\NotFoundException; +use OCP\DB\Exception as DBException; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; use OCP\ICache; @@ -185,6 +186,7 @@ class Manager { (string) $row['name'], (string) $row['description'], (string) $row['password'], + (string) $row['server_url'], (int) $row['active_guests'], (int) $row['call_flag'], $activeSince, @@ -628,7 +630,7 @@ class Manager { * @return Room * @throws RoomNotFoundException */ - public function getRoomByActor(string $token, string $actorType, string $actorId, ?string $sessionId = null): Room { + public function getRoomByActor(string $token, string $actorType, string $actorId, ?string $sessionId = null, ?string $serverUrl = null): Room { $query = $this->db->getQueryBuilder(); $helper = new SelectHelper(); $helper->selectRoomsTable($query); @@ -641,6 +643,12 @@ class Manager { )) ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); + if ($serverUrl === null) { + $query->andWhere($query->expr()->isNull('r.server_url')); + } else { + $query->andWhere($query->expr()->eq('r.server_url', $query->createNamedParameter($serverUrl))); + } + if ($sessionId !== null) { $helper->selectSessionsTable($query); $query->leftJoin('a', 'talk_sessions', 's', $query->expr()->andX( @@ -676,10 +684,10 @@ class Manager { * @return Room * @throws RoomNotFoundException */ - public function getRoomByToken(string $token, ?string $preloadUserId = null): Room { + public function getRoomByToken(string $token, ?string $preloadUserId = null, ?string $serverUrl = null): Room { $preloadUserId = $preloadUserId === '' ? null : $preloadUserId; if ($preloadUserId !== null) { - return $this->getRoomByActor($token, Attendee::ACTOR_USERS, $preloadUserId); + return $this->getRoomByActor($token, Attendee::ACTOR_USERS, $preloadUserId, null, $serverUrl); } $query = $this->db->getQueryBuilder(); @@ -688,6 +696,13 @@ class Manager { $query->from('talk_rooms', 'r') ->where($query->expr()->eq('r.token', $query->createNamedParameter($token))); + if ($serverUrl === null) { + $query->andWhere($query->expr()->isNull('r.server_url')); + } else { + $query->andWhere($query->expr()->eq('r.server_url', $query->createNamedParameter($serverUrl))); + } + + $result = $query->execute(); $row = $result->fetch(); $result->closeCursor(); @@ -908,6 +923,29 @@ class Manager { return $room; } + /** + * @param int $type + * @param string $name + * @return Room + * @throws DBException + */ + public function createRemoteRoom(int $type, string $name, string $token, string $serverUrl): Room { + $qb = $this->db->getQueryBuilder(); + + $qb->insert('talk_rooms') + ->values([ + 'name' => $qb->createNamedParameter($name), + 'type' => $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT), + 'token' => $qb->createNamedParameter($token), + 'server_url' => $qb->createNamedParameter($serverUrl), + ]); + + $qb->executeStatement(); + $roomId = $qb->getLastInsertId(); + + return $this->getRoomById($roomId); + } + public function resolveRoomDisplayName(Room $room, string $userId): string { < |