Browse Source

feat: Implement team provider api

Signed-off-by: Julius Härtl <jus@bitgrid.net>
Julius Härtl 2 months ago
parent
commit
c7813bfdaf

+ 97 - 0
core/Controller/TeamsApiController.php

@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * @copyright Copyright (c) 2024 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+namespace OC\Core\Controller;
+
+use OCA\Core\ResponseDefinitions;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\ApiRoute;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\IRequest;
+use OCP\Teams\ITeamManager;
+use OCP\Teams\Team;
+
+/**
+ * @psalm-import-type CoreTeamResource from ResponseDefinitions
+ * @psalm-import-type CoreTeam from ResponseDefinitions
+ * @property $userId string
+ */
+class TeamsApiController extends \OCP\AppFramework\OCSController {
+	public function __construct(
+		string $appName,
+		IRequest $request,
+		private ITeamManager $teamManager,
+		private ?string $userId,
+	) {
+		parent::__construct($appName, $request);
+	}
+
+	/**
+	 * Get all resources of a team
+	 *
+	 * @param string $teamId Unique id of the team
+	 * @return DataResponse<Http::STATUS_OK, array{resources: CoreTeamResource[]}, array{}>
+	 *
+	 * 200: Resources returned
+	 */
+	#[NoAdminRequired]
+	#[ApiRoute(verb: 'GET', url: '/{teamId}/resources', root: '/teams')]
+	public function resolveOne(string $teamId): DataResponse {
+		/**
+		 * @var CoreTeamResource[] $resolvedResources
+		 * @psalm-suppress PossiblyNullArgument The route is limited to logged-in users
+		 */
+		$resolvedResources = $this->teamManager->getSharedWith($teamId, $this->userId);
+
+		return new DataResponse(['resources' => $resolvedResources]);
+	}
+
+	/**
+	 * Get all teams of a resource
+	 *
+	 * @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<Http::STATUS_OK, array{teams: CoreTeam[]}, array{}>
+	 *
+	 * 200: Teams returned
+	 */
+	#[NoAdminRequired]
+	#[ApiRoute(verb: 'GET', url: '/resources/{providerId}/{resourceId}', root: '/teams')]
+	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 CoreTeam[] $teams */
+		$teams = 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);
+			return $response;
+		}, $teams);
+
+		return new DataResponse([
+			'teams' => $teams,
+		]);
+	}
+}

+ 15 - 0
core/ResponseDefinitions.php

@@ -161,6 +161,21 @@ namespace OCA\Core;
  *      numberOfImages: int,
  *      completionExpectedAt: ?int,
  *  }
+ *
+ * @psalm-type CoreTeam = array{
+ *      id: string,
+ *      name: string,
+ *      icon: string,
+ * }
+ *
+ * @psalm-type CoreTeamResource = array{
+ *       id: int,
+ *       label: string,
+ *       url: string,
+ *       iconSvg: ?string,
+ *       iconURL: ?string,
+ *       iconEmoji: ?string,
+ *   }
  */
 class ResponseDefinitions {
 }

+ 225 - 0
core/openapi.json

@@ -406,6 +406,60 @@
                     }
                 }
             },
+            "Team": {
+                "type": "object",
+                "required": [
+                    "id",
+                    "name",
+                    "icon"
+                ],
+                "properties": {
+                    "id": {
+                        "type": "string"
+                    },
+                    "name": {
+                        "type": "string"
+                    },
+                    "icon": {
+                        "type": "string"
+                    }
+                }
+            },
+            "TeamResource": {
+                "type": "object",
+                "required": [
+                    "id",
+                    "label",
+                    "url",
+                    "iconSvg",
+                    "iconURL",
+                    "iconEmoji"
+                ],
+                "properties": {
+                    "id": {
+                        "type": "integer",
+                        "format": "int64"
+                    },
+                    "label": {
+                        "type": "string"
+                    },
+                    "url": {
+                        "type": "string"
+                    },
+                    "iconSvg": {
+                        "type": "string",
+                        "nullable": true
+                    },
+                    "iconURL": {
+                        "type": "string",
+                        "nullable": true
+                    },
+                    "iconEmoji": {
+                        "type": "string",
+                        "nullable": true
+                    }
+                }
+            },
             "TextProcessingTask": {
                 "type": "object",
                 "required": [
@@ -3009,6 +3063,177 @@
                 }
             }
         },
+        "/ocs/v2.php/teams/{teamId}/resources": {
+            "get": {
+                "operationId": "teams_api-resolve-one",
+                "summary": "Get all resources of a team",
+                "tags": [
+                    "teams_api"
+                ],
+                "security": [
+                    {
+                        "bearer_auth": []
+                    },
+                    {
+                        "basic_auth": []
+                    }
+                ],
+                "parameters": [
+                    {
+                        "name": "teamId",
+                        "in": "path",
+                        "description": "Unique id of the team",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        }
+                    },
+                    {
+                        "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": "Resources returned",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "type": "object",
+                                    "required": [
+                                        "ocs"
+                                    ],
+                                    "properties": {
+                                        "ocs": {
+                                            "type": "object",
+                                            "required": [
+                                                "meta",
+                                                "data"
+                                            ],
+                                            "properties": {
+                                                "meta": {
+                                                    "$ref": "#/components/schemas/OCSMeta"
+                                                },
+                                                "data": {
+                                                    "type": "object",
+                                                    "required": [
+                                                        "resources"
+                                                    ],
+                                                    "properties": {
+                                                        "resources": {
+                                                            "type": "array",
+                                                            "items": {
+                                                                "$ref": "#/components/schemas/TeamResource"
+                                                            }
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
+        "/ocs/v2.php/teams/resources/{providerId}/{resourceId}": {
+            "get": {
+                "operationId": "teams_api-list-teams",
+                "summary": "Get all teams of a resource",
+                "tags": [
+                    "teams_api"
+                ],
+                "security": [
+                    {
+                        "bearer_auth": []
+                    },
+                    {
+                        "basic_auth": []
+                    }
+                ],
+                "parameters": [
+                    {
+                        "name": "providerId",
+                        "in": "path",
+                        "description": "Identifier of the provider (e.g. deck, talk, collectives)",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        }
+                    },
+                    {
+                        "name": "resourceId",
+                        "in": "path",
+                        "description": "Unique id of the resource to list teams for (e.g. deck board id)",
+                        "required": true,
+                        "schema": {
+                            "type": "string"
+                        }
+                    },
+                    {
+                        "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": "Teams returned",
+                        "content": {
+                            "application/json": {
+                                "schema": {
+                                    "type": "object",
+                                    "required": [
+                                        "ocs"
+                                    ],
+                                    "properties": {
+                                        "ocs": {
+                                            "type": "object",
+                                            "required": [
+                                                "meta",
+                                                "data"
+                                            ],
+                                            "properties": {
+                                                "meta": {
+                                                    "$ref": "#/components/schemas/OCSMeta"
+                                                },
+                                                "data": {
+                                                    "type": "object",
+                                                    "required": [
+                                                        "teams"
+                                                    ],
+                                                    "properties": {
+                                                        "teams": {
+                                                            "type": "array",
+                                                            "items": {
+                                                                "$ref": "#/components/schemas/Team"
+                                                            }
+                                                        }
+                                                    }
+                                                }
+                                            }
+                                        }
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        },
         "/ocs/v2.php/textprocessing/tasktypes": {
             "get": {
                 "operationId": "text_processing_api-task-types",

+ 2 - 0
lib/composer/composer/LICENSE

@@ -1,3 +1,4 @@
+
 Copyright (c) Nils Adermann, Jordi Boggiano
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -17,3 +18,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 THE SOFTWARE.
+

+ 6 - 0
lib/composer/composer/autoload_classmap.php

@@ -691,6 +691,10 @@ return array(
     'OCP\\Talk\\IConversation' => $baseDir . '/lib/public/Talk/IConversation.php',
     'OCP\\Talk\\IConversationOptions' => $baseDir . '/lib/public/Talk/IConversationOptions.php',
     'OCP\\Talk\\ITalkBackend' => $baseDir . '/lib/public/Talk/ITalkBackend.php',
+    'OCP\\Teams\\ITeamManager' => $baseDir . '/lib/public/Teams/ITeamManager.php',
+    'OCP\\Teams\\ITeamResourceProvider' => $baseDir . '/lib/public/Teams/ITeamResourceProvider.php',
+    'OCP\\Teams\\Team' => $baseDir . '/lib/public/Teams/Team.php',
+    'OCP\\Teams\\TeamResource' => $baseDir . '/lib/public/Teams/TeamResource.php',
     'OCP\\Template' => $baseDir . '/lib/public/Template.php',
     'OCP\\TextProcessing\\Events\\AbstractTextProcessingEvent' => $baseDir . '/lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php',
     'OCP\\TextProcessing\\Events\\TaskFailedEvent' => $baseDir . '/lib/public/TextProcessing/Events/TaskFailedEvent.php',
@@ -1158,6 +1162,7 @@ return array(
     'OC\\Core\\Controller\\ReferenceController' => $baseDir . '/core/Controller/ReferenceController.php',
     'OC\\Core\\Controller\\SearchController' => $baseDir . '/core/Controller/SearchController.php',
     'OC\\Core\\Controller\\SetupController' => $baseDir . '/core/Controller/SetupController.php',
+    'OC\\Core\\Controller\\TeamsApiController' => $baseDir . '/core/Controller/TeamsApiController.php',
     'OC\\Core\\Controller\\TextProcessingApiController' => $baseDir . '/core/Controller/TextProcessingApiController.php',
     'OC\\Core\\Controller\\TextToImageApiController' => $baseDir . '/core/Controller/TextToImageApiController.php',
     'OC\\Core\\Controller\\TranslationApiController' => $baseDir . '/core/Controller/TranslationApiController.php',
@@ -1794,6 +1799,7 @@ return array(
     'OC\\Tags' => $baseDir . '/lib/private/Tags.php',
     'OC\\Talk\\Broker' => $baseDir . '/lib/private/Talk/Broker.php',
     'OC\\Talk\\ConversationOptions' => $baseDir . '/lib/private/Talk/ConversationOptions.php',
+    'OC\\Teams\\TeamManager' => $baseDir . '/lib/private/Teams/TeamManager.php',
     'OC\\TempManager' => $baseDir . '/lib/private/TempManager.php',
     'OC\\TemplateLayout' => $baseDir . '/lib/private/TemplateLayout.php',
     'OC\\Template\\Base' => $baseDir . '/lib/private/Template/Base.php',

+ 6 - 0
lib/composer/composer/autoload_static.php

@@ -724,6 +724,10 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OCP\\Talk\\IConversation' => __DIR__ . '/../../..' . '/lib/public/Talk/IConversation.php',
         'OCP\\Talk\\IConversationOptions' => __DIR__ . '/../../..' . '/lib/public/Talk/IConversationOptions.php',
         'OCP\\Talk\\ITalkBackend' => __DIR__ . '/../../..' . '/lib/public/Talk/ITalkBackend.php',
+        'OCP\\Teams\\ITeamManager' => __DIR__ . '/../../..' . '/lib/public/Teams/ITeamManager.php',
+        'OCP\\Teams\\ITeamResourceProvider' => __DIR__ . '/../../..' . '/lib/public/Teams/ITeamResourceProvider.php',
+        'OCP\\Teams\\Team' => __DIR__ . '/../../..' . '/lib/public/Teams/Team.php',
+        'OCP\\Teams\\TeamResource' => __DIR__ . '/../../..' . '/lib/public/Teams/TeamResource.php',
         'OCP\\Template' => __DIR__ . '/../../..' . '/lib/public/Template.php',
         'OCP\\TextProcessing\\Events\\AbstractTextProcessingEvent' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Events/AbstractTextProcessingEvent.php',
         'OCP\\TextProcessing\\Events\\TaskFailedEvent' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Events/TaskFailedEvent.php',
@@ -1191,6 +1195,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\Core\\Controller\\ReferenceController' => __DIR__ . '/../../..' . '/core/Controller/ReferenceController.php',
         'OC\\Core\\Controller\\SearchController' => __DIR__ . '/../../..' . '/core/Controller/SearchController.php',
         'OC\\Core\\Controller\\SetupController' => __DIR__ . '/../../..' . '/core/Controller/SetupController.php',
+        'OC\\Core\\Controller\\TeamsApiController' => __DIR__ . '/../../..' . '/core/Controller/TeamsApiController.php',
         'OC\\Core\\Controller\\TextProcessingApiController' => __DIR__ . '/../../..' . '/core/Controller/TextProcessingApiController.php',
         'OC\\Core\\Controller\\TextToImageApiController' => __DIR__ . '/../../..' . '/core/Controller/TextToImageApiController.php',
         'OC\\Core\\Controller\\TranslationApiController' => __DIR__ . '/../../..' . '/core/Controller/TranslationApiController.php',
@@ -1827,6 +1832,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
         'OC\\Tags' => __DIR__ . '/../../..' . '/lib/private/Tags.php',
         'OC\\Talk\\Broker' => __DIR__ . '/../../..' . '/lib/private/Talk/Broker.php',
         'OC\\Talk\\ConversationOptions' => __DIR__ . '/../../..' . '/lib/private/Talk/ConversationOptions.php',
+        'OC\\Teams\\TeamManager' => __DIR__ . '/../../..' . '/lib/private/Teams/TeamManager.php',
         'OC\\TempManager' => __DIR__ . '/../../..' . '/lib/private/TempManager.php',
         'OC\\TemplateLayout' => __DIR__ . '/../../..' . '/lib/private/TemplateLayout.php',
         'OC\\Template\\Base' => __DIR__ . '/../../..' . '/lib/private/Template/Base.php',

+ 66 - 3
lib/composer/composer/installed.json

@@ -1,5 +1,68 @@
 {
-    "packages": [],
-    "dev": false,
-    "dev-package-names": []
+    "packages": [
+        {
+            "name": "bamarni/composer-bin-plugin",
+            "version": "1.8.2",
+            "version_normalized": "1.8.2.0",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/bamarni/composer-bin-plugin.git",
+                "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/bamarni/composer-bin-plugin/zipball/92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880",
+                "reference": "92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880",
+                "shasum": ""
+            },
+            "require": {
+                "composer-plugin-api": "^2.0",
+                "php": "^7.2.5 || ^8.0"
+            },
+            "require-dev": {
+                "composer/composer": "^2.0",
+                "ext-json": "*",
+                "phpstan/extension-installer": "^1.1",
+                "phpstan/phpstan": "^1.8",
+                "phpstan/phpstan-phpunit": "^1.1",
+                "phpunit/phpunit": "^8.5 || ^9.5",
+                "symfony/console": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
+                "symfony/finder": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0",
+                "symfony/process": "^2.8.52 || ^3.4.35 || ^4.4 || ^5.0 || ^6.0"
+            },
+            "time": "2022-10-31T08:38:03+00:00",
+            "type": "composer-plugin",
+            "extra": {
+                "class": "Bamarni\\Composer\\Bin\\BamarniBinPlugin"
+            },
+            "installation-source": "dist",
+            "autoload": {
+                "psr-4": {
+                    "Bamarni\\Composer\\Bin\\": "src"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "description": "No conflicts for your bin dependencies",
+            "keywords": [
+                "composer",
+                "conflict",
+                "dependency",
+                "executable",
+                "isolation",
+                "tool"
+            ],
+            "support": {
+                "issues": "https://github.com/bamarni/composer-bin-plugin/issues",
+                "source": "https://github.com/bamarni/composer-bin-plugin/tree/1.8.2"
+            },
+            "install-path": "../bamarni/composer-bin-plugin"
+        }
+    ],
+    "dev": true,
+    "dev-package-names": [
+        "bamarni/composer-bin-plugin"
+    ]
 }

+ 12 - 3
lib/composer/composer/installed.php

@@ -3,21 +3,30 @@
         'name' => '__root__',
         'pretty_version' => 'dev-master',
         'version' => 'dev-master',
-        'reference' => '559a758533026559cf632ed1b3d74f6b1ebfb481',
+        'reference' => 'b6abfc4cba2d1ef4fdd8f2c22bbff46796b9485e',
         'type' => 'library',
         'install_path' => __DIR__ . '/../../../',
         'aliases' => array(),
-        'dev' => false,
+        'dev' => true,
     ),
     'versions' => array(
         '__root__' => array(
             'pretty_version' => 'dev-master',
             'version' => 'dev-master',
-            'reference' => '559a758533026559cf632ed1b3d74f6b1ebfb481',
+            'reference' => 'b6abfc4cba2d1ef4fdd8f2c22bbff46796b9485e',
             'type' => 'library',
             'install_path' => __DIR__ . '/../../../',
             'aliases' => array(),
             'dev_requirement' => false,
         ),
+        'bamarni/composer-bin-plugin' => array(
+            'pretty_version' => '1.8.2',
+            'version' => '1.8.2.0',
+            'reference' => '92fd7b1e6e9cdae19b0d57369d8ad31a37b6a880',
+            'type' => 'composer-plugin',
+            'install_path' => __DIR__ . '/../bamarni/composer-bin-plugin',
+            'aliases' => array(),
+            'dev_requirement' => true,
+        ),
     ),
 );

+ 30 - 0
lib/private/AppFramework/Bootstrap/RegistrationContext.php

@@ -54,6 +54,7 @@ use OCP\Share\IPublicShareTemplateProvider;
 use OCP\SpeechToText\ISpeechToTextProvider;
 use OCP\Support\CrashReport\IReporter;
 use OCP\Talk\ITalkBackend;
+use OCP\Teams\ITeamResourceProvider;
 use OCP\TextProcessing\IProvider as ITextProcessingProvider;
 use OCP\Translation\ITranslationProvider;
 use OCP\UserMigration\IMigrator as IUserMigrator;
@@ -158,6 +159,9 @@ class RegistrationContext {
 	/** @var PreviewProviderRegistration[] */
 	private array $previewProviders = [];
 
+	/** @var ServiceRegistration<ITeamResourceProvider>[] */
+	private array $teamResourceProviders = [];
+
 	public function __construct(LoggerInterface $logger) {
 		$this->logger = $logger;
 	}
@@ -357,6 +361,13 @@ class RegistrationContext {
 				);
 			}
 
+			public function registerTeamResourceProvider(string $class) : void {
+				$this->context->registerTeamResourceProvider(
+					$this->appId,
+					$class
+				);
+			}
+
 			public function registerCalendarRoomBackend(string $class): void {
 				$this->context->registerCalendarRoomBackend(
 					$this->appId,
@@ -531,6 +542,17 @@ class RegistrationContext {
 		);
 	}
 
+
+	/**
+	 * @psalm-param class-string<ITeamResourceProvider> $class
+	 */
+	public function registerTeamResourceProvider(string $appId, string $class) {
+		$this->teamResourceProviders[] = new ServiceRegistration(
+			$appId,
+			$class
+		);
+	}
+
 	/**
 	 * @psalm-param class-string<IUserMigrator> $migratorClass
 	 */
@@ -870,4 +892,12 @@ class RegistrationContext {
 	public function getSetupChecks(): array {
 		return $this->setupChecks;
 	}
+
+
+	/**
+	 * @return ServiceRegistration<ITeamResourceProvider>[]
+	 */
+	public function getTeamResourceProviders(): array {
+		return $this->teamResourceProviders;
+	}
 }

+ 3 - 0
lib/private/Server.php

@@ -158,6 +158,7 @@ use OC\SpeechToText\SpeechToTextManager;
 use OC\SystemTag\ManagerFactory as SystemTagManagerFactory;
 use OC\Tagging\TagMapper;
 use OC\Talk\Broker;
+use OC\Teams\TeamManager;
 use OC\Template\JSCombiner;
 use OC\Translation\TranslationManager;
 use OC\User\AvailabilityCoordinator;
@@ -265,6 +266,7 @@ use OCP\SpeechToText\ISpeechToTextManager;
 use OCP\SystemTag\ISystemTagManager;
 use OCP\SystemTag\ISystemTagObjectMapper;
 use OCP\Talk\IBroker;
+use OCP\Teams\ITeamManager;
 use OCP\Translation\ITranslationManager;
 use OCP\User\Events\BeforeUserDeletedEvent;
 use OCP\User\Events\BeforeUserLoggedInEvent;
@@ -1297,6 +1299,7 @@ class Server extends ServerContainer implements IServerContainer {
 		$this->registerAlias(\OCP\Collaboration\Resources\IManager::class, \OC\Collaboration\Resources\Manager::class);
 
 		$this->registerAlias(IReferenceManager::class, ReferenceManager::class);
+		$this->registerAlias(ITeamManager::class, TeamManager::class);
 
 		$this->registerDeprecatedAlias('SettingsManager', \OC\Settings\Manager::class);
 		$this->registerAlias(\OCP\Settings\IManager::class, \OC\Settings\Manager::class);

+ 119 - 0
lib/private/Teams/TeamManager.php

@@ -0,0 +1,119 @@
+<?php
+/**
+ * @copyright Copyright (c) 2024 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+namespace OC\Teams;
+
+use OC\AppFramework\Bootstrap\Coordinator;
+use OCA\Circles\CirclesManager;
+use OCA\Circles\Exceptions\CircleNotFoundException;
+use OCA\Circles\Model\Circle;
+use OCA\Circles\Model\Member;
+use OCP\IURLGenerator;
+use OCP\Server;
+use OCP\Teams\ITeamManager;
+use OCP\Teams\ITeamResourceProvider;
+use OCP\Teams\Team;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\NotFoundExceptionInterface;
+
+class TeamManager implements ITeamManager {
+
+	/** @var ?ITeamResourceProvider[] */
+	private ?array $providers = null;
+
+	public function __construct(
+		private Coordinator $bootContext,
+		private IURLGenerator $urlGenerator,
+		private ?CirclesManager $circlesManager,
+	) {
+	}
+
+	public function hasTeamSupport(): bool {
+		return $this->circlesManager !== null;
+	}
+
+	public function getProviders(): array {
+		if ($this->providers !== null) {
+			return $this->providers;
+		}
+
+		$this->providers = [];
+		foreach ($this->bootContext->getRegistrationContext()->getTeamResourceProviders() as $providerRegistration) {
+			try {
+				/** @var ITeamResourceProvider $provider */
+				$provider = Server::get($providerRegistration->getService());
+				$this->providers[$provider->getId()] = $provider;
+			} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
+			}
+		}
+		return $this->providers;
+	}
+
+	public function getProvider(string $providerId): ITeamResourceProvider {
+		$providers = $this->getProviders();
+		if (isset($providers[$providerId])) {
+			return $providers[$providerId];
+		}
+
+		throw new \RuntimeException('No provider found for id ' .$providerId);
+	}
+
+	public function getSharedWith(string $teamId, string $userId): array {
+		if ($this->getTeam($teamId, $userId) === null) {
+			return [];
+		}
+
+		$resources = [];
+
+		foreach ($this->getProviders() as $provider) {
+			array_push($resources, ...$provider->getSharedWith($teamId));
+		}
+
+		return $resources;
+	}
+
+	public function getTeamsForResource(string $providerId, string $resourceId, string $userId): array {
+		$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;
+			}
+
+			return new Team(
+				$teamId,
+				$team->getDisplayName(),
+				$this->urlGenerator->linkToRouteAbsolute('contacts.contacts.directcircle', ['singleId' => $teamId]),
+			);
+		}, $provider->getTeamsForResource($resourceId))));
+	}
+
+	private function getTeam(string $teamId, string $userId): ?Circle {
+		try {
+			$federatedUser = $this->circlesManager->getFederatedUser($userId, Member::TYPE_USER);
+			$this->circlesManager->startSession($federatedUser);
+			return $this->circlesManager->getCircle($teamId);
+		} catch (CircleNotFoundException) {
+			return null;
+		}
+	}
+}

+ 8 - 0
lib/public/AppFramework/Bootstrap/IRegistrationContext.php

@@ -351,6 +351,14 @@ interface IRegistrationContext {
 	 */
 	public function registerCalendarRoomBackend(string $class): void;
 
+	/**
+	 * @param string $class
+	 * @psalm-param class-string<\OCP\Calendar\Room\IBackend> $actionClass
+	 * @return void
+	 * @since 29.0.0
+	 */
+	public function registerTeamResourceProvider(string $class): void;
+
 	/**
 	 * Register an implementation of \OCP\UserMigration\IMigrator that
 	 * will handle the implementation of a migrator

+ 58 - 0
lib/public/Teams/ITeamManager.php

@@ -0,0 +1,58 @@
+<?php
+/**
+ * @copyright Copyright (c) 2024 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\Teams;
+
+/**
+ * @since 29.0.0
+ */
+interface ITeamManager {
+	/**
+	 * Get all providers that have registered as a team resource provider
+	 *
+	 * @return ITeamResourceProvider[]
+	 * @since 29.0.0
+	 */
+	public function getProviders(): array;
+
+	/**
+	 * Get a specific team resource provider by its id
+	 *
+	 * @since 29.0.0
+	 */
+	public function getProvider(string $providerId): ITeamResourceProvider;
+
+	/**
+	 * Returns all team resources for a given team and user
+	 *
+	 * @return TeamResource[]
+	 * @since 29.0.0
+	 */
+	public function getSharedWith(string $teamId, string $userId): array;
+
+	/**
+	 * Returns all teams for a given resource and user
+	 *
+	 * @since 29.0.0
+	 */
+	public function getTeamsForResource(string $providerId, string $resourceId, string $userId): array;
+}

+ 76 - 0
lib/public/Teams/ITeamResourceProvider.php

@@ -0,0 +1,76 @@
+<?php
+/**
+ * @copyright Copyright (c) 2024 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\Teams;
+
+/**
+ * Implement a provider of resources that are shared or owned by a team
+ *
+ * @since 29.0.0
+ */
+interface ITeamResourceProvider {
+
+	/**
+	 * Unique identifier used to identify the provider (app id)
+	 *
+	 * @since 29.0.0
+	 */
+	public function getId(): string;
+
+	/**
+	 * User visible name of the provider (app name)
+	 *
+	 * @since 29.0.0
+	 */
+	public function getName(): string;
+
+	/**
+	 * Svg icon to show next to the provider (app icon)
+	 *
+	 * @since 29.0.0
+	 */
+	public function getIconSvg(): string;
+
+	/**
+	 * Return all resources that are shared to the given team id for the current provider
+	 *
+	 * @param string $teamId
+	 * @return TeamResource[]
+	 * @since 29.0.0
+	 */
+	public function getSharedWith(string $teamId): array;
+
+	/**
+	 * Check if a resource is shared with the given team
+	 *
+	 * @since 29.0.0
+	 */
+	public function isSharedWithTeam(string $teamId, string $resourceId): bool;
+
+	/**
+	 * Return team ids that a resource is shared with or owned by
+	 *
+	 * @return string[]
+	 * @since 29.0.0
+	 */
+	public function getTeamsForResource(string $resourceId): array;
+}

+ 73 - 0
lib/public/Teams/Team.php

@@ -0,0 +1,73 @@
+<?php
+/**
+ * @copyright Copyright (c) 2024 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\Teams;
+
+/**
+ * Simple abstraction to represent a team in the public API
+ *
+ * In the backend a team is a circle identified by the circles singleId
+ *
+ * @since 29.0.0
+ */
+class Team implements \JsonSerializable {
+
+	/**
+	 * @since 29.0.0
+	 */
+	public function __construct(private string $teamId, private string $displayName, private ?string $link) {
+	}
+
+	/**
+	 * Unique identifier of the team (singleId of the circle)
+	 *
+	 * @since 29.0.0
+	 */
+	public function getId(): string {
+		return $this->teamId;
+	}
+
+	/**
+	 * @since 29.0.0
+	 */
+	public function getDisplayName(): string {
+		return $this->displayName;
+	}
+
+	/**
+	 * @since 29.0.0
+	 */
+	public function getLink(): ?string {
+		return $this->link;
+	}
+
+	/**
+	 * @since 29.0.0
+	 */
+	public function jsonSerialize(): array {
+		return [
+			'teamId' => $this->teamId,
+			'displayName' => $this->displayName,
+			'link' => $this->link,
+		];
+	}
+}

+ 129 - 0
lib/public/Teams/TeamResource.php

@@ -0,0 +1,129 @@
+<?php
+/**
+ * @copyright Copyright (c) 2024 Julius Härtl <jus@bitgrid.net>
+ *
+ * @author Julius Härtl <jus@bitgrid.net>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ */
+
+namespace OCP\Teams;
+
+/**
+ * @since 29.0.0
+ */
+class TeamResource implements \JsonSerializable {
+	/**
+	 * @since 29.0.0
+	 */
+	public function __construct(
+		private ITeamResourceProvider $teamResourceProvider,
+		private string $resourceId,
+		private string $label,
+		private string $url,
+		private ?string $iconSvg = null,
+		private ?string $iconURL = null,
+		private ?string $iconEmoji = null,
+	) {
+	}
+
+	/**
+	 * Returns the provider details for the current resource
+	 *
+	 * @since 29.0.0
+	 */
+	public function getProvider(): ITeamResourceProvider {
+		return $this->teamResourceProvider;
+	}
+
+	/**
+	 * Unique id of the resource (e.g. primary key id)
+	 * @since 29.0.0
+	 */
+	public function getId(): string {
+		return $this->resourceId;
+	}
+
+	/**
+	 * User visible label when listing resources
+	 *
+	 * @since 29.0.0
+	 */
+	public function getLabel(): string {
+		return $this->label;
+	}
+
+	/**
+	 * Absolute url to navigate the user to the resource
+	 *
+	 * @since 29.0.0
+	 */
+	public function getUrl(): string {
+		return $this->url;
+	}
+
+	/**
+	 * Svg icon to show next to the name for the resource
+	 *
+	 * From all icons the first one returning not null will be picked in order: iconEmoji, iconSvg, iconUrl
+	 *
+	 * @since 29.0.0
+	 */
+	public function getIconSvg(): ?string {
+		return $this->iconSvg;
+	}
+
+	/**
+	 * Image url of the icon to show next to the name for the resource
+	 *
+	 * From all icons the first one returning not null will be picked in order: iconEmoji, iconSvg, iconUrl
+	 *
+	 * @since 29.0.0
+	 */
+	public function getIconURL(): ?string {
+		return $this->iconURL;
+	}
+
+	/**
+	 * Emoji show next to the name for the resource
+	 *
+	 * From all icons the first one returning not null will be picked in order: iconEmoji, iconSvg, iconUrl
+	 *
+	 * @since 29.0.0
+	 */
+	public function getIconEmoji(): ?string {
+		return $this->iconEmoji;
+	}
+
+	/**
+	 * @since 29.0.0
+	 */
+	public function jsonSerialize(): array {
+		return [
+			'id' => $this->resourceId,
+			'label' => $this->label,
+			'url' => $this->url,
+			'iconSvg' => $this->iconSvg,
+			'iconURL' => $this->iconURL,
+			'iconEmoji' => $this->iconEmoji,
+			'provider' => [
+				'id' => $this->teamResourceProvider->getId(),
+				'name' => $this->teamResourceProvider->getName(),
+				'icon' => $this->teamResourceProvider->getIconSvg(),
+			]
+		];
+	}
+}

+ 4 - 0
psalm.xml

@@ -91,6 +91,10 @@
 		<MoreSpecificReturnType errorLevel="error"/>
 		<UndefinedClass>
 			<errorLevel type="suppress">
+				<referencedClass name="OCA\Circles\CirclesManager"/>
+				<referencedClass name="OCA\Circles\Exceptions\CircleNotFoundException"/>
+				<referencedClass name="OCA\Circles\Model\Circle"/>
+				<referencedClass name="OCA\Circles\Model\Member"/>
 				<referencedClass name="OCA\GroupFolders\Mount\GroupFolderStorage"/>
 				<referencedClass name="OCA\TwoFactorNextcloudNotification\Controller\APIController"/>
 				<referencedClass name="OCA\GlobalSiteSelector\Service\SlaveService"/>