|DataResponse * * 200: Avatar uploaded successfully * 400: Avatar invalid */ #[PublicPage] #[RequireModeratorParticipant] #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/room/{token}/avatar', requirements: [ 'apiVersion' => '(v1)', 'token' => '[a-z0-9]{4,30}', ])] public function uploadAvatar(): DataResponse { try { $file = $this->request->getUploadedFile('file'); $this->avatarService->setAvatarFromRequest($this->getRoom(), $file); return new DataResponse($this->roomFormatter->formatRoom( $this->getResponseFormat(), [], $this->getRoom(), $this->participant, )); } catch (InvalidArgumentException $e) { return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } catch (\Exception $e) { $this->logger->error('Failed to post avatar', [ 'exception' => $e, ]); return new DataResponse(['message' => $this->l->t('An error occurred. Please contact your administrator.')], Http::STATUS_BAD_REQUEST); } } /** * Set an emoji as avatar * * @param string $emoji Emoji * @param ?string $color Color of the emoji * @return DataResponse|DataResponse * * 200: Avatar set successfully * 400: Setting emoji avatar is not possible */ #[PublicPage] #[RequireModeratorParticipant] #[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/room/{token}/avatar/emoji', requirements: [ 'apiVersion' => '(v1)', 'token' => '[a-z0-9]{4,30}', ])] public function emojiAvatar(string $emoji, ?string $color): DataResponse { try { $this->avatarService->setAvatarFromEmoji($this->getRoom(), $emoji, $color); return new DataResponse($this->roomFormatter->formatRoom( $this->getResponseFormat(), [], $this->getRoom(), $this->participant, )); } catch (InvalidArgumentException $e) { return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST); } catch (\Exception $e) { $this->logger->error('Failed to post avatar', [ 'exception' => $e, ]); return new DataResponse(['message' => $this->l->t('An error occurred. Please contact your administrator.')], Http::STATUS_BAD_REQUEST); } } /** * Get the avatar of a room * * @param bool $darkTheme Theme used for background * @return FileDisplayResponse * * 200: Room avatar returned */ #[FederationSupported] #[PublicPage] #[NoCSRFRequired] #[AllowWithoutParticipantWhenPendingInvitation] #[RequireParticipantOrLoggedInAndListedConversation] #[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)] #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/room/{token}/avatar', requirements: [ 'apiVersion' => '(v1)', 'token' => '[a-z0-9]{4,30}', ])] public function getAvatar(bool $darkTheme = false): FileDisplayResponse { // Cache for 1 day $cacheDuration = 60 * 60 * 24; if ($this->room->isFederatedConversation()) { /** @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, $this->invitation, $darkTheme); } catch (CannotReachRemoteException) { // Falling back to a local "globe" avatar for indicating the federation // Cache for 15 minutes only $cacheDuration = 15 * 60; } } $file = $this->avatarService->getAvatar($this->getRoom(), $this->userSession->getUser(), $darkTheme); $response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $file->getMimeType()]); $response->cacheFor($cacheDuration, false, true); return $response; } /** * Get the dark mode avatar of a room * * @return FileDisplayResponse * * 200: Room avatar returned */ #[FederationSupported] #[PublicPage] #[NoCSRFRequired] #[AllowWithoutParticipantWhenPendingInvitation] #[RequireParticipantOrLoggedInAndListedConversation] #[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)] #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/room/{token}/avatar/dark', requirements: [ 'apiVersion' => '(v1)', 'token' => '[a-z0-9]{4,30}', ])] public function getAvatarDark(): FileDisplayResponse { return $this->getAvatar(true); } /** * Get the avatar of a cloudId user when inviting users while creating a conversation * * @param int $size Avatar size * @psalm-param 64|512 $size * @param string $cloudId Federation CloudID to get the avatar for * @param bool $darkTheme Theme used for background * @return FileDisplayResponse * * 200: User avatar returned */ #[FederationSupported] #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] #[NoAdminRequired] #[NoCSRFRequired] #[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)] #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/proxy/new/user-avatar/{size}', requirements: [ 'apiVersion' => '(v1)', 'size' => '(64|512)', ])] public function getUserProxyAvatarWithoutRoom(int $size, string $cloudId, bool $darkTheme = false): FileDisplayResponse { return $this->getUserProxyAvatar($size, $cloudId, $darkTheme); } /** * Get the dark mode avatar of a cloudId user when inviting users while creating a conversation * * @param int $size Avatar size * @psalm-param 64|512 $size * @param string $cloudId Federation CloudID to get the avatar for * @return FileDisplayResponse * * 200: User avatar returned */ #[FederationSupported] #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] #[NoAdminRequired] #[NoCSRFRequired] #[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)] #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/proxy/new/user-avatar/{size}/dark', requirements: [ 'apiVersion' => '(v1)', 'size' => '(64|512)', ])] public function getUserProxyAvatarDarkWithoutRoom(int $size, string $cloudId): FileDisplayResponse { return $this->getUserProxyAvatar($size, $cloudId, true); } /** * Get the avatar of a cloudId user * * @param int $size Avatar size * @psalm-param 64|512 $size * @param string $cloudId Federation CloudID to get the avatar for * @param bool $darkTheme Theme used for background * @return FileDisplayResponse * * 200: User avatar returned */ #[FederationSupported] #[BruteForceProtection(action: 'talkRoomToken')] #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] #[PublicPage] #[NoCSRFRequired] #[AllowWithoutParticipantWhenPendingInvitation] #[RequireLoggedInParticipant] #[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)] #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/proxy/{token}/user-avatar/{size}', requirements: [ 'apiVersion' => '(v1)', 'token' => '[a-z0-9]{4,30}', 'size' => '(64|512)', ])] public function getUserProxyAvatar(int $size, string $cloudId, bool $darkTheme = false): FileDisplayResponse { try { $resolvedCloudId = $this->cloudIdManager->resolveCloudId($cloudId); } catch (\InvalidArgumentException) { return $this->getPlaceholderResponse($darkTheme); } $ownId = $this->cloudIdManager->getCloudId($this->userSession->getUser()->getCloudId(), null); /** * Reach out to the remote server to get the avatar */ if ($ownId->getRemote() !== $resolvedCloudId->getRemote()) { /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\AvatarController $proxy */ $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\AvatarController::class); try { return $proxy->getUserProxyAvatar($resolvedCloudId->getRemote(), $resolvedCloudId->getUser(), $size, $darkTheme); } catch (CannotReachRemoteException) { // Falling back to a local "user" avatar return $this->getPlaceholderResponse($darkTheme); } } /** * We are the server that hosts the user, so getting it from the avatar manager */ try { $avatar = $this->avatarManager->getAvatar($resolvedCloudId->getUser()); $avatarFile = $avatar->getFile($size, $darkTheme); } catch (\Exception) { return $this->getPlaceholderResponse($darkTheme); } $response = new FileDisplayResponse( $avatarFile, Http::STATUS_OK, ['Content-Type' => $avatarFile->getMimeType()], ); // Cache for 1 day $response->cacheFor(60 * 60 * 24, false, true); return $response; } /** * Get the dark mode avatar of a cloudId user * * @param int $size Avatar size * @psalm-param 64|512 $size * @param string $cloudId Federation CloudID to get the avatar for * @return FileDisplayResponse * * 200: User avatar returned */ #[FederationSupported] #[BruteForceProtection(action: 'talkRoomToken')] #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)] #[PublicPage] #[NoCSRFRequired] #[AllowWithoutParticipantWhenPendingInvitation] #[RequireLoggedInParticipant] #[RequestHeader(name: 'x-nextcloud-federation', description: 'Set to 1 when the request is performed by another Nextcloud Server to indicate a federation request', indirect: true)] #[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/proxy/{token}/user-avatar/{size}/dark', requirements: [ 'apiVersion' => '(v1)', 'token' => '[a-z0-9]{4,30}', 'size' => '(64|512)', ])] public function getUserProxyAvatarDark(int $size, string $cloudId): FileDisplayResponse { return $this->getUserProxyAvatar($size, $cloudId, true); } /** * Get the placeholder avatar * * @param bool $darkTheme Theme used for background * @return FileDisplayResponse * * 200: User avatar returned */ protected function getPlaceholderResponse(bool $darkTheme): FileDisplayResponse { $file = $this->avatarService->getPersonPlaceholder($darkTheme); $response = new FileDisplayResponse( $file, Http::STATUS_OK, ['Content-Type' => $file->getMimeType()], ); $response->cacheFor(60 * 15, false, true); return $response; } /** * Delete the avatar of a room * * @return DataResponse * * 200: Avatar removed successfully */ #[PublicPage] #[RequireModeratorParticipant] #[ApiRoute(verb: 'DELETE', url: '/api/{apiVersion}/room/{token}/avatar', requirements: [ 'apiVersion' => '(v1)', 'token' => '[a-z0-9]{4,30}', ])] public function deleteAvatar(): DataResponse { $this->avatarService->deleteAvatar($this->getRoom()); return new DataResponse($this->roomFormatter->formatRoom( $this->getResponseFormat(), [], $this->getRoom(), $this->participant, )); } }