summaryrefslogtreecommitdiffstats
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
parentdac6f7a1a82f18dfd5e55d4d0a6b9b10507fb384 (diff)
feat(federation): Add endpoint to get the proxied avatar of other users
Signed-off-by: Joas Schilling <coding@schilljs.com>
-rw-r--r--appinfo/routes/routesAvatarController.php9
-rw-r--r--docs/avatar.md36
-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
-rw-r--r--openapi-federation.json187
-rw-r--r--openapi-full.json1203
-rw-r--r--src/types/openapi/openapi-federation.ts64
-rw-r--r--src/types/openapi/openapi-full.ts472
10 files changed, 1420 insertions, 721 deletions
diff --git a/appinfo/routes/routesAvatarController.php b/appinfo/routes/routesAvatarController.php
index db4b61a27..688dc1a28 100644
--- a/appinfo/routes/routesAvatarController.php
+++ b/appinfo/routes/routesAvatarController.php
@@ -27,6 +27,11 @@ $requirements = [
'apiVersion' => '(v1)',
'token' => '^[a-z0-9]{4,30}$',
];
+$requirementsWithSize = [
+ 'apiVersion' => '(v1)',
+ 'token' => '^[a-z0-9]{4,30}$',
+ 'size' => '(64|512)',
+];
return [
'ocs' => [
@@ -40,5 +45,9 @@ return [
['name' => 'Avatar#getAvatarDark', 'url' => '/api/{apiVersion}/room/{token}/avatar/dark', 'verb' => 'GET', 'requirements' => $requirements],
/** @see \OCA\Talk\Controller\AvatarController::deleteAvatar() */
['name' => 'Avatar#deleteAvatar', 'url' => '/api/{apiVersion}/room/{token}/avatar', 'verb' => 'DELETE', 'requirements' => $requirements],
+ /** @see \OCA\Talk\Controller\AvatarController::getUserProxyAvatar() */
+ ['name' => 'Avatar#getUserProxyAvatar', 'url' => '/api/{apiVersion}/proxy/{token}/user-avatar/{size}', 'verb' => 'GET', 'requirements' => $requirementsWithSize],
+ /** @see \OCA\Talk\Controller\AvatarController::getUserProxyAvatarDark() */
+ ['name' => 'Avatar#getUserProxyAvatarDark', 'url' => '/api/{apiVersion}/proxy/{token}/user-avatar/{size}/dark', 'verb' => 'GET', 'requirements' => $requirementsWithSize],
],
];
diff --git a/docs/avatar.md b/docs/avatar.md
index 7fc908c44..c15948924 100644
--- a/docs/avatar.md
+++ b/docs/avatar.md
@@ -92,3 +92,39 @@
+ `200 OK`
+ `404 Not Found` When the conversation could not be found for the participant
- Body: the image file
+
+## Get federated user avatar (binary)
+
+* Required capability: `federation-v1`
+* Method: `GET`
+* Endpoint: `/proxy/{token}/user-avatar/{size}`
+* Data:
+
+| field | type | Description |
+|-----------|--------|------------------------------------------|
+| `size` | int | Only 64 and 512 are supported |
+| `cloudId` | string | Federation CloudID to get the avatar for |
+
+* Response:
+ - Status code:
+ + `200 OK`
+ + `404 Not Found` When the conversation could not be found for the participant
+ - Body: the image file
+
+## Get dark mode federated user avatar (binary)
+
+* Required capability: `federation-v1`
+* Method: `GET`
+* Endpoint: `/proxy/{token}/user-avatar/{size}/dark`
+* Data:
+
+| field | type | Description |
+|-----------|--------|------------------------------------------|
+| `size` | int | Only 64 and 512 are supported |
+| `cloudId` | string | Federation CloudID to get the avatar for |
+
+* Response:
+ - Status code:
+ + `200 OK`
+ + `404 Not Found` When the conversation could not be found for the participant
+ - Body: the image file
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}',
diff --git a/openapi-federation.json b/openapi-federation.json
index 6445bbfeb..10bd1368e 100644
--- a/openapi-federation.json
+++ b/openapi-federation.json
@@ -731,6 +731,193 @@
}
},
"paths": {
+ "/ocs/v2.php/apps/spreed/api/{apiVersion}/proxy/{token}/user-avatar/{size}": {
+ "get": {
+ "operationId": "avatar-get-user-proxy-avatar",
+ "summary": "Get the avatar of a cloudId user",
+ "tags": [
+ "avatar"
+ ],
+ "security": [
+ {},
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "cloudId",
+ "in": "query",
+ "description": "Federation CloudID to get the avatar for",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "darkTheme",
+ "in": "query",
+ "description": "Theme used for background",
+ "schema": {
+ "type": "integer",
+ "default": 0,
+ "enum": [
+ 0,
+ 1
+ ]
+ }
+ },
+ {
+ "name": "apiVersion",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "enum": [
+ "v1"
+ ],
+ "default": "v1"
+ }
+ },
+ {
+ "name": "token",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "pattern": "^[a-z0-9]{4,30}$"
+ }
+ },
+ {
+ "name": "size",
+ "in": "path",
+ "description": "Avatar size",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64",
+ "enum": [
+ 64,
+ 512
+ ]
+ }
+ },
+ {
+ "name": "OCS-APIRequest",
+ "in": "header",
+ "description": "Required to be true for the API request to pass",
+ "required": true,
+ "schema": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "User avatar returned",
+ "content": {
+ "*/*": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/ocs/v2.php/apps/spreed/api/{apiVersion}/proxy/{token}/user-avatar/{size}/dark": {
+ "get": {
+ "operationId": "avatar-get-user-proxy-avatar-dark",
+ "summary": "Get the dark mode avatar of a cloudId user",
+ "tags": [
+ "avatar"
+ ],
+ "security": [
+ {},
+ {
+ "bearer_auth": []
+ },
+ {
+ "basic_auth": []
+ }
+ ],
+ "parameters": [
+ {
+ "name": "cloudId",
+ "in": "query",
+ "description": "Federation CloudID to get the avatar for",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+