From f65db249631ecb4c8f9fb2fd86e831fd711340e7 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 12 Mar 2024 10:24:41 +0100 Subject: [PATCH 1/3] fix(federation): Expose local token with the invite so UI can render the avatar Signed-off-by: Joas Schilling --- lib/Controller/FederationController.php | 2 +- lib/Model/Invitation.php | 3 +-- lib/ResponseDefinitions.php | 2 +- openapi-federation.json | 7 +++---- openapi-full.json | 7 +++---- src/types/openapi/openapi-federation.ts | 3 +-- src/types/openapi/openapi-full.ts | 3 +-- 7 files changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/Controller/FederationController.php b/lib/Controller/FederationController.php index 5cc0ef4410c..2a8072672a4 100644 --- a/lib/Controller/FederationController.php +++ b/lib/Controller/FederationController.php @@ -180,7 +180,6 @@ public function getShares(): DataResponse { * @return TalkFederationInvite|null */ protected function enrichInvite(Invitation $invitation): ?array { - try { $room = $this->talkManager->getRoomById($invitation->getLocalRoomId()); } catch (RoomNotFoundException) { @@ -189,6 +188,7 @@ protected function enrichInvite(Invitation $invitation): ?array { $federationInvite = $invitation->jsonSerialize(); $federationInvite['roomName'] = $room->getName(); + $federationInvite['localToken'] = $room->getToken(); return $federationInvite; } } diff --git a/lib/Model/Invitation.php b/lib/Model/Invitation.php index f0c7377db9a..558fbb0104d 100644 --- a/lib/Model/Invitation.php +++ b/lib/Model/Invitation.php @@ -79,14 +79,13 @@ public function __construct() { } /** - * @return array{id: int, localRoomId: int, localCloudId: string, remoteAttendeeId: int, remoteServerUrl: string, remoteToken: string, state: int, userId: string, inviterCloudId: string, inviterDisplayName: string} + * @return array{id: int, localCloudId: string, remoteAttendeeId: int, remoteServerUrl: string, remoteToken: string, state: int, userId: string, inviterCloudId: string, inviterDisplayName: string} */ public function jsonSerialize(): array { return [ 'id' => $this->getId(), 'userId' => $this->getUserId(), 'state' => $this->getState(), - 'localRoomId' => $this->getLocalRoomId(), 'localCloudId' => $this->getLocalCloudId(), 'remoteServerUrl' => $this->getRemoteServerUrl(), 'remoteToken' => $this->getRemoteToken(), diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 1ddcbacccbd..4189222dad3 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -117,7 +117,7 @@ * id: int, * state: int, * localCloudId: string, - * localRoomId: int, + * localToken: string, * remoteAttendeeId: int, * remoteServerUrl: string, * remoteToken: string, diff --git a/openapi-federation.json b/openapi-federation.json index 94d773b18c1..e71a6f6da54 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -316,7 +316,7 @@ "id", "state", "localCloudId", - "localRoomId", + "localToken", "remoteAttendeeId", "remoteServerUrl", "remoteToken", @@ -337,9 +337,8 @@ "localCloudId": { "type": "string" }, - "localRoomId": { - "type": "integer", - "format": "int64" + "localToken": { + "type": "string" }, "remoteAttendeeId": { "type": "integer", diff --git a/openapi-full.json b/openapi-full.json index fa1e3f6e14c..50e739aad5f 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -517,7 +517,7 @@ "id", "state", "localCloudId", - "localRoomId", + "localToken", "remoteAttendeeId", "remoteServerUrl", "remoteToken", @@ -538,9 +538,8 @@ "localCloudId": { "type": "string" }, - "localRoomId": { - "type": "integer", - "format": "int64" + "localToken": { + "type": "string" }, "remoteAttendeeId": { "type": "integer", diff --git a/src/types/openapi/openapi-federation.ts b/src/types/openapi/openapi-federation.ts index 1988ac0d983..a4893fc9401 100644 --- a/src/types/openapi/openapi-federation.ts +++ b/src/types/openapi/openapi-federation.ts @@ -137,8 +137,7 @@ export type components = { /** Format: int64 */ state: number; localCloudId: string; - /** Format: int64 */ - localRoomId: number; + localToken: string; /** Format: int64 */ remoteAttendeeId: number; remoteServerUrl: string; diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 71be708bb1a..98121ac6349 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -664,8 +664,7 @@ export type components = { /** Format: int64 */ state: number; localCloudId: string; - /** Format: int64 */ - localRoomId: number; + localToken: string; /** Format: int64 */ remoteAttendeeId: number; remoteServerUrl: string; From 04d51b3514cb95273b26bd9a265a2c1ada0eb68c Mon Sep 17 00:00:00 2001 From: Maksim Sukharev Date: Tue, 12 Mar 2024 11:32:48 +0100 Subject: [PATCH 2/3] fix(federation): show proxy avatars in InvitationHandler Signed-off-by: Maksim Sukharev --- src/components/ConversationIcon.vue | 2 +- .../LeftSidebar/InvitationHandler.vue | 33 +++++++++++-------- src/stores/__tests__/federation.spec.js | 5 +-- src/stores/federation.ts | 4 +-- 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/components/ConversationIcon.vue b/src/components/ConversationIcon.vue index 8c60e3178af..b294dd3f26d 100644 --- a/src/components/ConversationIcon.vue +++ b/src/components/ConversationIcon.vue @@ -179,7 +179,7 @@ export default { if (this.item.isDummyConversation) { // Prevent a 404 when trying to load an avatar before the conversation data is actually loaded // Also used in new conversation / invitation handler dialog - const isFed = this.item.isFederatedConversation && 'icon-conversation-federation' + const isFed = this.item.remoteServer && 'icon-conversation-federation' const type = this.item.type === CONVERSATION.TYPE.PUBLIC ? 'icon-conversation-public' : 'icon-conversation-group' const theme = isDarkTheme ? 'dark' : 'bright' return `${isFed || type} icon--dummy icon--${theme}` diff --git a/src/components/LeftSidebar/InvitationHandler.vue b/src/components/LeftSidebar/InvitationHandler.vue index 64091804d7c..cadc008337e 100644 --- a/src/components/LeftSidebar/InvitationHandler.vue +++ b/src/components/LeftSidebar/InvitationHandler.vue @@ -39,12 +39,10 @@ {{ item.roomName }} - - {{ t('spreed', 'From {user} at {remoteServer}', { - user: item.inviterDisplayName, - remoteServer: item.remoteServerUrl, - }) }} - + diff --git a/src/stores/__tests__/federation.spec.js b/src/stores/__tests__/federation.spec.js index 749ab1367a6..31237749c2b 100644 --- a/src/stores/__tests__/federation.spec.js +++ b/src/stores/__tests__/federation.spec.js @@ -16,7 +16,7 @@ describe('federationStore', () => { id: 2, userId: 'user0', state: 0, - localRoomId: 10, + localToken: 'TOKEN_LOCAL_2', remoteServerUrl: 'remote.nextcloud.com', remoteToken: 'TOKEN_2', remoteAttendeeId: 11, @@ -28,7 +28,7 @@ describe('federationStore', () => { id: 1, userId: 'user0', state: 1, - localRoomId: 9, + localToken: 'TOKEN_LOCAL_1', remoteServerUrl: 'remote.nextcloud.com', remoteToken: 'TOKEN_1', remoteAttendeeId: 11, @@ -158,6 +158,7 @@ describe('federationStore', () => { const room = { id: 10, + token: 'TOKEN_LOCAL_2' } const acceptResponse = generateOCSResponse({ payload: room }) acceptShare.mockResolvedValueOnce(acceptResponse) diff --git a/src/stores/federation.ts b/src/stores/federation.ts index d7a56c9b34d..5dd3af83f95 100644 --- a/src/stores/federation.ts +++ b/src/stores/federation.ts @@ -72,7 +72,7 @@ export const useFederationStore = defineStore('federation', { const { id, name } = notification.messageRichParameters.user1 const invitation: FederationInvite = { id: notification.objectId, - localRoomId: 0, + localToken: '', localCloudId: notification.user + '@' + getBaseUrl().replace('https://', ''), remoteAttendeeId: 0, remoteServerUrl, @@ -99,7 +99,7 @@ export const useFederationStore = defineStore('federation', { Vue.delete(this.pendingShares[id], 'loading') Vue.set(this.acceptedShares, id, { ...this.pendingShares[id], - localRoomId: conversation.id, + localToken: conversation.token, state: FEDERATION.STATE.ACCEPTED, }) Vue.delete(this.pendingShares, id) From 5907dc435c8e533c85d357d0be9089538b83cadb Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Tue, 12 Mar 2024 15:06:12 +0100 Subject: [PATCH 3/3] fix(federation): Allow accessing avatars with pending invites Signed-off-by: Joas Schilling --- .../AEnvironmentAwareController.php | 10 +++++ lib/Controller/AvatarController.php | 7 +++- .../TalkV1/Controller/AvatarController.php | 11 +++-- ...ithoutParticipantWhenPendingInvitation.php | 36 ++++++++++++++++ lib/Middleware/InjectionMiddleware.php | 41 +++++++++++++++++++ lib/Model/InvitationMapper.php | 14 +++++++ 6 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 lib/Middleware/Attribute/AllowWithoutParticipantWhenPendingInvitation.php diff --git a/lib/Controller/AEnvironmentAwareController.php b/lib/Controller/AEnvironmentAwareController.php index 2110e82b60f..672cbe75733 100644 --- a/lib/Controller/AEnvironmentAwareController.php +++ b/lib/Controller/AEnvironmentAwareController.php @@ -28,6 +28,7 @@ namespace OCA\Talk\Controller; use OC\AppFramework\Http\Dispatcher; +use OCA\Talk\Model\Invitation; use OCA\Talk\Participant; use OCA\Talk\Room; use OCP\AppFramework\OCSController; @@ -36,6 +37,7 @@ abstract class AEnvironmentAwareController extends OCSController { protected int $apiVersion = 1; protected ?Room $room = null; protected ?Participant $participant = null; + protected ?Invitation $invitation = null; public function setAPIVersion(int $apiVersion): void { $this->apiVersion = $apiVersion; @@ -61,6 +63,14 @@ public function getParticipant(): ?Participant { return $this->participant; } + public function setInvitation(Invitation $invitation): void { + $this->invitation = $invitation; + } + + public function getInvitation(): ?Invitation { + return $this->invitation; + } + /** * Following the logic of {@see Dispatcher::executeController} * @return string Either 'json' or 'xml' diff --git a/lib/Controller/AvatarController.php b/lib/Controller/AvatarController.php index 68fc4ca920f..8e98a9a1dd8 100644 --- a/lib/Controller/AvatarController.php +++ b/lib/Controller/AvatarController.php @@ -29,6 +29,7 @@ use InvalidArgumentException; use OCA\Talk\Exceptions\CannotReachRemoteException; +use OCA\Talk\Middleware\Attribute\AllowWithoutParticipantWhenPendingInvitation; use OCA\Talk\Middleware\Attribute\FederationSupported; use OCA\Talk\Middleware\Attribute\RequireLoggedInParticipant; use OCA\Talk\Middleware\Attribute\RequireModeratorParticipant; @@ -143,13 +144,14 @@ public function emojiAvatar(string $emoji, ?string $color): DataResponse { #[FederationSupported] #[PublicPage] #[NoCSRFRequired] + #[AllowWithoutParticipantWhenPendingInvitation] #[RequireParticipantOrLoggedInAndListedConversation] public function getAvatar(bool $darkTheme = false): FileDisplayResponse { if ($this->room->getRemoteServer() !== '') { /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\AvatarController $proxy */ $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\AvatarController::class); try { - return $proxy->getAvatar($this->room, $this->participant, $darkTheme); + return $proxy->getAvatar($this->room, $this->participant, $this->invitation, $darkTheme); } catch (CannotReachRemoteException) { // Falling back to a local "globe" avatar for indicating the federation } @@ -172,6 +174,7 @@ public function getAvatar(bool $darkTheme = false): FileDisplayResponse { #[FederationSupported] #[PublicPage] #[NoCSRFRequired] + #[AllowWithoutParticipantWhenPendingInvitation] #[RequireParticipantOrLoggedInAndListedConversation] public function getAvatarDark(): FileDisplayResponse { return $this->getAvatar(true); @@ -193,6 +196,7 @@ public function getAvatarDark(): FileDisplayResponse { #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] #[PublicPage] #[NoCSRFRequired] + #[AllowWithoutParticipantWhenPendingInvitation] #[RequireLoggedInParticipant] public function getUserProxyAvatar(int $size, string $cloudId, bool $darkTheme = false): FileDisplayResponse { try { @@ -252,6 +256,7 @@ public function getUserProxyAvatar(int $size, string $cloudId, bool $darkTheme = #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] #[PublicPage] #[NoCSRFRequired] + #[AllowWithoutParticipantWhenPendingInvitation] #[RequireLoggedInParticipant] public function getUserProxyAvatarDark(int $size, string $cloudId): FileDisplayResponse { return $this->getUserProxyAvatar($size, $cloudId, true); diff --git a/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php b/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php index dd5cde00f0d..7fa1496131b 100644 --- a/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php +++ b/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php @@ -28,6 +28,7 @@ use OCA\Talk\Exceptions\CannotReachRemoteException; use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest; +use OCA\Talk\Model\Invitation; use OCA\Talk\Participant; use OCA\Talk\ResponseDefinitions; use OCA\Talk\Room; @@ -53,10 +54,14 @@ public function __construct( * * 200: Room avatar returned */ - public function getAvatar(Room $room, Participant $participant, bool $darkTheme): FileDisplayResponse { + public function getAvatar(Room $room, ?Participant $participant, ?Invitation $invitation, bool $darkTheme): FileDisplayResponse { + if ($participant === null && $invitation === null) { + throw new CannotReachRemoteException('Must receive either participant or invitation'); + } + $proxy = $this->proxy->get( - $participant->getAttendee()->getInvitedCloudId(), - $participant->getAttendee()->getAccessToken(), + $participant ? $participant->getAttendee()->getInvitedCloudId() : $invitation->getLocalCloudId(), + $participant ? $participant->getAttendee()->getAccessToken() : $invitation->getAccessToken(), $room->getRemoteServer() . '/ocs/v2.php/apps/spreed/api/v1/room/' . $room->getRemoteToken() . '/avatar' . ($darkTheme ? '/dark' : ''), ); diff --git a/lib/Middleware/Attribute/AllowWithoutParticipantWhenPendingInvitation.php b/lib/Middleware/Attribute/AllowWithoutParticipantWhenPendingInvitation.php new file mode 100644 index 00000000000..4feb981386e --- /dev/null +++ b/lib/Middleware/Attribute/AllowWithoutParticipantWhenPendingInvitation.php @@ -0,0 +1,36 @@ + + * + * @author Joas Schilling + * + * @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 . + */ + +namespace OCA\Talk\Middleware\Attribute; + +use Attribute; +use OCA\Talk\Middleware\InjectionMiddleware; + +/** + * @see InjectionMiddleware::getRoomByInvite() + */ +#[Attribute(Attribute::TARGET_METHOD)] +class AllowWithoutParticipantWhenPendingInvitation extends RequireRoom { +} diff --git a/lib/Middleware/InjectionMiddleware.php b/lib/Middleware/InjectionMiddleware.php index fa07547d95d..81279fcd359 100644 --- a/lib/Middleware/InjectionMiddleware.php +++ b/lib/Middleware/InjectionMiddleware.php @@ -30,6 +30,7 @@ use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Federation\Authenticator; use OCA\Talk\Manager; +use OCA\Talk\Middleware\Attribute\AllowWithoutParticipantWhenPendingInvitation; use OCA\Talk\Middleware\Attribute\FederationSupported; use OCA\Talk\Middleware\Attribute\RequireAuthenticatedParticipant; use OCA\Talk\Middleware\Attribute\RequireLoggedInModeratorParticipant; @@ -46,12 +47,14 @@ use OCA\Talk\Middleware\Exceptions\NotAModeratorException; use OCA\Talk\Middleware\Exceptions\ReadOnlyException; use OCA\Talk\Model\Attendee; +use OCA\Talk\Model\InvitationMapper; use OCA\Talk\Participant; use OCA\Talk\Room; use OCA\Talk\Service\ParticipantService; use OCA\Talk\TalkSession; use OCA\Talk\Webinary; use OCP\AppFramework\Controller; +use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\RedirectResponse; @@ -74,6 +77,7 @@ public function __construct( protected ICloudIdManager $cloudIdManager, protected IThrottler $throttler, protected IURLGenerator $url, + protected InvitationMapper $invitationMapper, protected Authenticator $federationAuthenticator, protected ?string $userId, ) { @@ -98,6 +102,15 @@ public function beforeController(Controller $controller, string $methodName): vo $apiVersion = $this->request->getParam('apiVersion'); $controller->setAPIVersion((int) substr($apiVersion, 1)); + if (!empty($reflectionMethod->getAttributes(AllowWithoutParticipantWhenPendingInvitation::class))) { + try { + $this->getRoomByInvite($controller); + return; + } catch (RoomNotFoundException|ParticipantNotFoundException) { + // Falling back to bellow checks + } + } + if (!empty($reflectionMethod->getAttributes(RequireAuthenticatedParticipant::class))) { $this->getLoggedInOrGuest($controller, false, requireFederationWhenNotLoggedIn: true); } @@ -232,6 +245,34 @@ protected function getLoggedInOrGuest(AEnvironmentAwareController $controller, b } } + /** + * @param AEnvironmentAwareController $controller + * @throws RoomNotFoundException + * @throws ParticipantNotFoundException + */ + protected function getRoomByInvite(AEnvironmentAwareController $controller): void { + if ($this->userId === null) { + throw new ParticipantNotFoundException('No user available'); + } + + $room = $controller->getRoom(); + if (!$room instanceof Room) { + $token = $this->request->getParam('token'); + $room = $this->manager->getRoomByToken($token); + $controller->setRoom($room); + } + + $participant = $controller->getParticipant(); + if (!$participant instanceof Participant) { + try { + $invitation = $this->invitationMapper->getInvitationsForUserByLocalRoom($room, $this->userId); + $controller->setInvitation($invitation); + } catch (DoesNotExistException $e) { + throw new ParticipantNotFoundException('No invite available', $e->getCode(), $e); + } + } + } + /** * @param AEnvironmentAwareController $controller * @throws FederationUnsupportedFeatureException diff --git a/lib/Model/InvitationMapper.php b/lib/Model/InvitationMapper.php index 22c44271e55..ceb4593f740 100644 --- a/lib/Model/InvitationMapper.php +++ b/lib/Model/InvitationMapper.php @@ -95,6 +95,20 @@ public function getInvitationsForUser(IUser $user): array { return $this->findEntities($qb); } + /** + * @throws DoesNotExistException + */ + public function getInvitationsForUserByLocalRoom(Room $room, string $userId): Invitation { + $query = $this->db->getQueryBuilder(); + + $query->select('*') + ->from($this->getTableName()) + ->where($query->expr()->eq('user_id', $query->createNamedParameter($userId))) + ->andWhere($query->expr()->eq('local_room_id', $query->createNamedParameter($room->getId()))); + + return $this->findEntity($query); + } + public function countInvitationsForLocalRoom(Room $room): int { $qb = $this->db->getQueryBuilder();