summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
authorJoas Schilling <coding@schilljs.com>2024-03-07 12:18:03 +0100
committerJoas Schilling <coding@schilljs.com>2024-03-07 12:55:25 +0100
commit483659f7d252f6f1510aad42e914885f0f412550 (patch)
treedbf47c9f7c73f49e99e0c5e0c000d9b46179434e /lib
parentdac6f7a1a82f18dfd5e55d4d0a6b9b10507fb384 (diff)
feat(federation): Add endpoint to get the proxied avatar of other users
Signed-off-by: Joas Schilling <coding@schilljs.com>
Diffstat (limited to 'lib')
-rw-r--r--lib/Controller/AvatarController.php109
-rw-r--r--lib/Federation/Proxy/TalkV1/Controller/AvatarController.php35
-rw-r--r--lib/Federation/Proxy/TalkV1/ProxyRequest.php21
-rw-r--r--lib/Service/AvatarService.php5
4 files changed, 161 insertions, 9 deletions
diff --git a/lib/Controller/AvatarController.php b/lib/Controller/AvatarController.php
index ef3116c17..68fc4ca92 100644
--- a/lib/Controller/AvatarController.php
+++ b/lib/Controller/AvatarController.php
@@ -30,16 +30,21 @@ namespace OCA\Talk\Controller;
use InvalidArgumentException;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Middleware\Attribute\FederationSupported;
+use OCA\Talk\Middleware\Attribute\RequireLoggedInParticipant;
use OCA\Talk\Middleware\Attribute\RequireModeratorParticipant;
use OCA\Talk\Middleware\Attribute\RequireParticipantOrLoggedInAndListedConversation;
use OCA\Talk\ResponseDefinitions;
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\RoomFormatter;
use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\BruteForceProtection;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\FileDisplayResponse;
+use OCP\Federation\ICloudIdManager;
+use OCP\IAvatarManager;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserSession;
@@ -57,6 +62,8 @@ class AvatarController extends AEnvironmentAwareController {
protected IUserSession $userSession,
protected IL10N $l,
protected LoggerInterface $logger,
+ protected ICloudIdManager $cloudIdManager,
+ protected IAvatarManager $avatarManager,
) {
parent::__construct($appName, $request);
}
@@ -138,7 +145,7 @@ class AvatarController extends AEnvironmentAwareController {
#[NoCSRFRequired]
#[RequireParticipantOrLoggedInAndListedConversation]
public function getAvatar(bool $darkTheme = false): FileDisplayResponse {
- if ($this->room->getRemoteServer()) {
+ 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 {
@@ -171,6 +178,106 @@ class AvatarController extends AEnvironmentAwareController {
}
/**
+ * 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<Http::STATUS_OK, array{Content-Type: string}>
+ *
+ * 200: User avatar returned
+ */
+ #[FederationSupported]
+ #[BruteForceProtection(action: 'talkRoomToken')]
+ #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[RequireLoggedInParticipant]
+ 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<Http::STATUS_OK, array{Content-Type: string}>
+ *
+ * 200: User avatar returned
+ */
+ #[FederationSupported]
+ #[BruteForceProtection(action: 'talkRoomToken')]
+ #[OpenAPI(scope: OpenAPI::SCOPE_FEDERATION)]
+ #[PublicPage]
+ #[NoCSRFRequired]
+ #[RequireLoggedInParticipant]
+ 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<Http::STATUS_OK, array{Content-Type: string}>
+ *
+ * 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 * 60 * 24, false, true);
+ return $response;
+
+ }
+
+ /**
* Delete the avatar of a room
*
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{}>
diff --git a/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php b/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php
index d99964637..43592df6d 100644
--- a/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php
+++ b/lib/Federation/Proxy/TalkV1/Controller/AvatarController.php
@@ -77,4 +77,39 @@ class AvatarController {
$response->cacheFor(60 * 60 * 24, false, true);
return $response;
}
+
+ /**
+ * @see \OCA\Talk\Controller\AvatarController::getUserProxyAvatar()
+ *
+ * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>
+ * @throws CannotReachRemoteException
+ *
+ * 200: User avatar returned
+ */
+ public function getUserProxyAvatar(string $remoteServer, string $user, int $size, bool $darkTheme): FileDisplayResponse {
+ $proxy = $this->proxy->get(
+ null,
+ null,
+ $remoteServer . '/index.php/avatar/' . $user . '/' . $size . ($darkTheme ? '/dark' : ''),
+ );
+
+ if ($proxy->getStatusCode() !== Http::STATUS_OK) {
+ if ($proxy->getStatusCode() !== Http::STATUS_NOT_FOUND) {
+ $this->proxy->logUnexpectedStatusCode(__METHOD__, $proxy->getStatusCode(), (string) $proxy->getBody());
+ }
+ throw new CannotReachRemoteException('Avatar request had unexpected status code');
+ }
+
+ $content = $proxy->getBody();
+ if ($content === '') {
+ throw new CannotReachRemoteException('No avatar content received');
+ }
+
+ $file = new InMemoryFile($user, $content);
+
+ $response = new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => $file->getMimeType()]);
+ // Cache for 1 day
+ $response->cacheFor(60 * 60 * 24, false, true);
+ return $response;
+ }
}
diff --git a/lib/Federation/Proxy/TalkV1/ProxyRequest.php b/lib/Federation/Proxy/TalkV1/ProxyRequest.php
index 224a952d8..548553867 100644
--- a/lib/Federation/Proxy/TalkV1/ProxyRequest.php
+++ b/lib/Federation/Proxy/TalkV1/ProxyRequest.php
@@ -59,11 +59,11 @@ class ProxyRequest {
}
protected function generateDefaultRequestOptions(
- string $cloudId,
+ ?string $cloudId,
#[SensitiveParameter]
- string $accessToken,
+ ?string $accessToken,
): array {
- return [
+ $options = [
'verify' => !$this->config->getSystemValueBool('sharing.federation.allowSelfSignedCertificates'),
'nextcloud' => [
'allow_local_address' => $this->config->getSystemValueBool('allow_local_remote_servers'),
@@ -74,8 +74,13 @@ class ProxyRequest {
'OCS-APIRequest' => 'true',
],
'timeout' => 5,
- 'auth' => [urlencode($cloudId), $accessToken],
];
+
+ if ($cloudId !== null && $accessToken !== null) {
+ $options['auth'] = [urlencode($cloudId), $accessToken];
+ }
+
+ return $options;
}
protected function prependProtocolIfNotAvailable(string $url): string {
@@ -91,9 +96,9 @@ class ProxyRequest {
*/
protected function request(
string $verb,
- string $cloudId,
+ ?string $cloudId,
#[SensitiveParameter]
- string $accessToken,
+ ?string $accessToken,
string $url,
array $parameters,
): IResponse {
@@ -134,9 +139,9 @@ class ProxyRequest {
* @throws CannotReachRemoteException
*/
public function get(
- string $cloudId,
+ ?string $cloudId,
#[SensitiveParameter]
- string $accessToken,
+ ?string $accessToken,
string $url,
array $parameters = [],
): IResponse {
diff --git a/lib/Service/AvatarService.php b/lib/Service/AvatarService.php
index 82b701eed..70757074a 100644
--- a/lib/Service/AvatarService.php
+++ b/lib/Service/AvatarService.php
@@ -237,6 +237,11 @@ class AvatarService {
return new InMemoryFile($token, file_get_contents($this->getAvatarPath($room, $darkTheme)));
}
+ public function getPersonPlaceholder(bool $darkTheme = false): ISimpleFile {
+ $colorTone = $darkTheme ? 'dark' : 'bright';
+ return new InMemoryFile('fallback', file_get_contents(__DIR__ . '/../../img/icon-conversation-user-' . $colorTone . '.svg'));
+ }
+
protected function getEmojiAvatar(string $emoji, string $fillColor): string {
return str_replace([
'{letter}',