From 2552950299fda7da394bda220d2e83e65b0f3ba0 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Mon, 13 Oct 2025 13:55:05 +0200 Subject: [PATCH 1/2] fix(core): Fix TeamsApiController typing Signed-off-by: provokateurin --- core/Controller/TeamsApiController.php | 14 +++--- core/ResponseDefinitions.php | 29 ++++++++---- core/openapi-full.json | 63 +++++++++++++++++++++----- core/openapi.json | 63 +++++++++++++++++++++----- lib/public/Teams/Team.php | 6 +++ lib/public/Teams/TeamResource.php | 14 ++++++ openapi.json | 63 +++++++++++++++++++++----- 7 files changed, 201 insertions(+), 51 deletions(-) diff --git a/core/Controller/TeamsApiController.php b/core/Controller/TeamsApiController.php index 2eb33a0c254..ab29fdef77e 100644 --- a/core/Controller/TeamsApiController.php +++ b/core/Controller/TeamsApiController.php @@ -17,10 +17,12 @@ use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\Teams\ITeamManager; use OCP\Teams\Team; +use OCP\Teams\TeamResource; /** * @psalm-import-type CoreTeamResource from ResponseDefinitions * @psalm-import-type CoreTeam from ResponseDefinitions + * @psalm-import-type CoreTeamWithResources from ResponseDefinitions * @property $userId string */ class TeamsApiController extends OCSController { @@ -44,13 +46,10 @@ class TeamsApiController extends OCSController { #[NoAdminRequired] #[ApiRoute(verb: 'GET', url: '/{teamId}/resources', root: '/teams')] public function resolveOne(string $teamId): DataResponse { - /** - * @var list $resolvedResources - * @psalm-suppress PossiblyNullArgument The route is limited to logged-in users - */ + /** @psalm-suppress PossiblyNullArgument The route is limited to logged-in users */ $resolvedResources = $this->teamManager->getSharedWith($teamId, $this->userId); - return new DataResponse(['resources' => $resolvedResources]); + return new DataResponse(['resources' => array_map(static fn (TeamResource $resource) => $resource->jsonSerialize(), $resolvedResources)]); } /** @@ -58,7 +57,7 @@ class TeamsApiController extends OCSController { * * @param string $providerId Identifier of the provider (e.g. deck, talk, collectives) * @param string $resourceId Unique id of the resource to list teams for (e.g. deck board id) - * @return DataResponse}, array{}> + * @return DataResponse}, array{}> * * 200: Teams returned */ @@ -67,11 +66,10 @@ class TeamsApiController extends OCSController { public function listTeams(string $providerId, string $resourceId): DataResponse { /** @psalm-suppress PossiblyNullArgument The route is limited to logged-in users */ $teams = $this->teamManager->getTeamsForResource($providerId, $resourceId, $this->userId); - /** @var list $teams */ $teams = array_values(array_map(function (Team $team) { $response = $team->jsonSerialize(); /** @psalm-suppress PossiblyNullArgument The route is limited to logged in users */ - $response['resources'] = $this->teamManager->getSharedWith($team->getId(), $this->userId); + $response['resources'] = array_map(static fn (TeamResource $resource) => $resource->jsonSerialize(), $this->teamManager->getSharedWith($team->getId(), $this->userId)); return $response; }, $teams)); diff --git a/core/ResponseDefinitions.php b/core/ResponseDefinitions.php index 5a0c00524ee..a52b8a0bc0b 100644 --- a/core/ResponseDefinitions.php +++ b/core/ResponseDefinitions.php @@ -149,19 +149,28 @@ namespace OC\Core; * } * * @psalm-type CoreTeam = array{ - * id: string, - * name: string, - * icon: string, + * teamId: string, + * displayName: string, + * link: ?string, * } * * @psalm-type CoreTeamResource = array{ - * id: int, - * label: string, - * url: string, - * iconSvg: ?string, - * iconURL: ?string, - * iconEmoji: ?string, - * } + * id: string, + * label: string, + * url: string, + * iconSvg: ?string, + * iconURL: ?string, + * iconEmoji: ?string, + * provider: array{ + * id: string, + * name: string, + * icon: string, + * }, + * } + * + * @psalm-type CoreTeamWithResources = CoreTeam&array{ + * resources: list, + * } * * @psalm-type CoreTaskProcessingShape = array{ * name: string, diff --git a/core/openapi-full.json b/core/openapi-full.json index a029ee84d9c..805944296b7 100644 --- a/core/openapi-full.json +++ b/core/openapi-full.json @@ -898,19 +898,20 @@ "Team": { "type": "object", "required": [ - "id", - "name", - "icon" + "teamId", + "displayName", + "link" ], "properties": { - "id": { + "teamId": { "type": "string" }, - "name": { + "displayName": { "type": "string" }, - "icon": { - "type": "string" + "link": { + "type": "string", + "nullable": true } } }, @@ -922,12 +923,12 @@ "url", "iconSvg", "iconURL", - "iconEmoji" + "iconEmoji", + "provider" ], "properties": { "id": { - "type": "integer", - "format": "int64" + "type": "string" }, "label": { "type": "string" @@ -946,9 +947,49 @@ "iconEmoji": { "type": "string", "nullable": true + }, + "provider": { + "type": "object", + "required": [ + "id", + "name", + "icon" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + } + } } } }, + "TeamWithResources": { + "allOf": [ + { + "$ref": "#/components/schemas/Team" + }, + { + "type": "object", + "required": [ + "resources" + ], + "properties": { + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamResource" + } + } + } + } + ] + }, "TextProcessingTask": { "type": "object", "required": [ @@ -6306,7 +6347,7 @@ "teams": { "type": "array", "items": { - "$ref": "#/components/schemas/Team" + "$ref": "#/components/schemas/TeamWithResources" } } } diff --git a/core/openapi.json b/core/openapi.json index 471e6567b84..f1f04cb02d9 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -898,19 +898,20 @@ "Team": { "type": "object", "required": [ - "id", - "name", - "icon" + "teamId", + "displayName", + "link" ], "properties": { - "id": { + "teamId": { "type": "string" }, - "name": { + "displayName": { "type": "string" }, - "icon": { - "type": "string" + "link": { + "type": "string", + "nullable": true } } }, @@ -922,12 +923,12 @@ "url", "iconSvg", "iconURL", - "iconEmoji" + "iconEmoji", + "provider" ], "properties": { "id": { - "type": "integer", - "format": "int64" + "type": "string" }, "label": { "type": "string" @@ -946,9 +947,49 @@ "iconEmoji": { "type": "string", "nullable": true + }, + "provider": { + "type": "object", + "required": [ + "id", + "name", + "icon" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + } + } } } }, + "TeamWithResources": { + "allOf": [ + { + "$ref": "#/components/schemas/Team" + }, + { + "type": "object", + "required": [ + "resources" + ], + "properties": { + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TeamResource" + } + } + } + } + ] + }, "TextProcessingTask": { "type": "object", "required": [ @@ -6306,7 +6347,7 @@ "teams": { "type": "array", "items": { - "$ref": "#/components/schemas/Team" + "$ref": "#/components/schemas/TeamWithResources" } } } diff --git a/lib/public/Teams/Team.php b/lib/public/Teams/Team.php index 474ebaed84f..cc70f6fbd45 100644 --- a/lib/public/Teams/Team.php +++ b/lib/public/Teams/Team.php @@ -50,6 +50,12 @@ class Team implements \JsonSerializable { } /** + * @return array{ + * teamId: string, + * displayName: string, + * link: ?string, + * } + * * @since 29.0.0 */ public function jsonSerialize(): array { diff --git a/lib/public/Teams/TeamResource.php b/lib/public/Teams/TeamResource.php index acb98380562..a15faacf79b 100644 --- a/lib/public/Teams/TeamResource.php +++ b/lib/public/Teams/TeamResource.php @@ -94,6 +94,20 @@ class TeamResource implements \JsonSerializable { } /** + * @return array{ + * id: string, + * label: string, + * url: string, + * iconSvg: ?string, + * iconURL: ?string, + * iconEmoji: ?string, + * provider: array{ + * id: string, + * name: string, + * icon: string, + * }, + * } + * * @since 29.0.0 */ public function jsonSerialize(): array { diff --git a/openapi.json b/openapi.json index 8307e44dda7..e86802a938d 100644 --- a/openapi.json +++ b/openapi.json @@ -940,19 +940,20 @@ "CoreTeam": { "type": "object", "required": [ - "id", - "name", - "icon" + "teamId", + "displayName", + "link" ], "properties": { - "id": { + "teamId": { "type": "string" }, - "name": { + "displayName": { "type": "string" }, - "icon": { - "type": "string" + "link": { + "type": "string", + "nullable": true } } }, @@ -964,12 +965,12 @@ "url", "iconSvg", "iconURL", - "iconEmoji" + "iconEmoji", + "provider" ], "properties": { "id": { - "type": "integer", - "format": "int64" + "type": "string" }, "label": { "type": "string" @@ -988,9 +989,49 @@ "iconEmoji": { "type": "string", "nullable": true + }, + "provider": { + "type": "object", + "required": [ + "id", + "name", + "icon" + ], + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "icon": { + "type": "string" + } + } } } }, + "CoreTeamWithResources": { + "allOf": [ + { + "$ref": "#/components/schemas/CoreTeam" + }, + { + "type": "object", + "required": [ + "resources" + ], + "properties": { + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CoreTeamResource" + } + } + } + } + ] + }, "CoreTextProcessingTask": { "type": "object", "required": [ @@ -9819,7 +9860,7 @@ "teams": { "type": "array", "items": { - "$ref": "#/components/schemas/CoreTeam" + "$ref": "#/components/schemas/CoreTeamWithResources" } } } From 9ba3ce27cb3f38da46183a458421ba1008eeea01 Mon Sep 17 00:00:00 2001 From: Maxence Lange Date: Mon, 8 Sep 2025 13:02:34 -0100 Subject: [PATCH 2/2] fix(team-api): get all teams details in a single request Signed-off-by: Maxence Lange Signed-off-by: provokateurin --- build/psalm-baseline.xml | 5 +++ core/Controller/TeamsApiController.php | 8 ++--- lib/private/Teams/TeamManager.php | 45 ++++++++++++++++++++------ lib/public/Teams/ITeamManager.php | 8 +++++ 4 files changed, 53 insertions(+), 13 deletions(-) diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 472eac7295d..ddd0d864b4f 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -4297,6 +4297,11 @@ + + + + + diff --git a/core/Controller/TeamsApiController.php b/core/Controller/TeamsApiController.php index ab29fdef77e..0dac1920ff8 100644 --- a/core/Controller/TeamsApiController.php +++ b/core/Controller/TeamsApiController.php @@ -66,15 +66,15 @@ class TeamsApiController extends OCSController { public function listTeams(string $providerId, string $resourceId): DataResponse { /** @psalm-suppress PossiblyNullArgument The route is limited to logged-in users */ $teams = $this->teamManager->getTeamsForResource($providerId, $resourceId, $this->userId); - $teams = array_values(array_map(function (Team $team) { + $sharesPerTeams = $this->teamManager->getSharedWithList(array_map(fn (Team $team): string => $team->getId(), $teams), $this->userId); + $listTeams = array_values(array_map(function (Team $team) use ($sharesPerTeams) { $response = $team->jsonSerialize(); - /** @psalm-suppress PossiblyNullArgument The route is limited to logged in users */ - $response['resources'] = array_map(static fn (TeamResource $resource) => $resource->jsonSerialize(), $this->teamManager->getSharedWith($team->getId(), $this->userId)); + $response['resources'] = array_map(static fn (TeamResource $resource) => $resource->jsonSerialize(), $sharesPerTeams[$team->getId()] ?? []); return $response; }, $teams)); return new DataResponse([ - 'teams' => $teams, + 'teams' => $listTeams, ]); } } diff --git a/lib/private/Teams/TeamManager.php b/lib/private/Teams/TeamManager.php index 13d6cc459a9..c5ede021d6d 100644 --- a/lib/private/Teams/TeamManager.php +++ b/lib/private/Teams/TeamManager.php @@ -84,24 +84,38 @@ class TeamManager implements ITeamManager { return array_values($resources); } - public function getTeamsForResource(string $providerId, string $resourceId, string $userId): array { + public function getSharedWithList(array $teams, string $userId): array { if (!$this->hasTeamSupport()) { return []; } - $provider = $this->getProvider($providerId); - return array_values(array_filter(array_map(function ($teamId) use ($userId) { - $team = $this->getTeam($teamId, $userId); - if ($team === null) { - return null; + $resources = []; + foreach ($this->getProviders() as $provider) { + if (method_exists($provider, 'getSharedWithList')) { + $resources[] = $provider->getSharedWithList($teams, $userId); + } else { + foreach ($teams as $team) { + $resources[] = [$team->getId() => $provider->getSharedWith($team->getId())]; + } } + } + return array_merge_recursive(...$resources); + } + + public function getTeamsForResource(string $providerId, string $resourceId, string $userId): array { + if (!$this->hasTeamSupport()) { + return []; + } + + $provider = $this->getProvider($providerId); + return array_map(function (Circle $team) { return new Team( - $teamId, + $team->getSingleId(), $team->getDisplayName(), - $this->urlGenerator->linkToRouteAbsolute('contacts.contacts.directcircle', ['singleId' => $teamId]), + $this->urlGenerator->linkToRouteAbsolute('contacts.contacts.directcircle', ['singleId' => $team->getSingleId()]), ); - }, $provider->getTeamsForResource($resourceId)))); + }, $this->getTeams($provider->getTeamsForResource($resourceId), $userId)); } private function getTeam(string $teamId, string $userId): ?Circle { @@ -117,4 +131,17 @@ class TeamManager implements ITeamManager { return null; } } + + /** + * @return Circle[] + */ + private function getTeams(array $teams, string $userId): array { + if (!$this->hasTeamSupport()) { + return []; + } + + $federatedUser = $this->circlesManager->getFederatedUser($userId, Member::TYPE_USER); + $this->circlesManager->startSession($federatedUser); + return $this->circlesManager->getCirclesByIds($teams); + } } diff --git a/lib/public/Teams/ITeamManager.php b/lib/public/Teams/ITeamManager.php index 3f737b4229c..b39b8da157b 100644 --- a/lib/public/Teams/ITeamManager.php +++ b/lib/public/Teams/ITeamManager.php @@ -40,4 +40,12 @@ interface ITeamManager { * @since 29.0.0 */ public function getTeamsForResource(string $providerId, string $resourceId, string $userId): array; + + /** + * @param list $teams + * @return array> + * + * @since 33.0.0 + */ + public function getSharedWithList(array $teams, string $userId): array; }