From c7813bfdaf09626dfed13c63b28a3919018d4403 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julius=20H=C3=A4rtl?= Date: Mon, 26 Feb 2024 16:30:16 +0100 Subject: [PATCH] feat: Implement team provider api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Julius Härtl --- core/Controller/TeamsApiController.php | 97 ++++++++ core/ResponseDefinitions.php | 15 ++ core/openapi.json | 225 ++++++++++++++++++ lib/composer/composer/LICENSE | 2 + lib/composer/composer/autoload_classmap.php | 6 + lib/composer/composer/autoload_static.php | 6 + lib/composer/composer/installed.json | 69 +++++- lib/composer/composer/installed.php | 15 +- .../Bootstrap/RegistrationContext.php | 30 +++ lib/private/Server.php | 3 + lib/private/Teams/TeamManager.php | 119 +++++++++ .../Bootstrap/IRegistrationContext.php | 8 + lib/public/Teams/ITeamManager.php | 58 +++++ lib/public/Teams/ITeamResourceProvider.php | 76 ++++++ lib/public/Teams/Team.php | 73 ++++++ lib/public/Teams/TeamResource.php | 129 ++++++++++ psalm.xml | 4 + 17 files changed, 929 insertions(+), 6 deletions(-) create mode 100644 core/Controller/TeamsApiController.php create mode 100644 lib/private/Teams/TeamManager.php create mode 100644 lib/public/Teams/ITeamManager.php create mode 100644 lib/public/Teams/ITeamResourceProvider.php create mode 100644 lib/public/Teams/Team.php create mode 100644 lib/public/Teams/TeamResource.php diff --git a/core/Controller/TeamsApiController.php b/core/Controller/TeamsApiController.php new file mode 100644 index 00000000000..f937bcb847c --- /dev/null +++ b/core/Controller/TeamsApiController.php @@ -0,0 +1,97 @@ + + * + * @author Julius Härtl + * + * @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 . + */ + +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 + * + * 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 + * + * 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, + ]); + } +} diff --git a/core/ResponseDefinitions.php b/core/ResponseDefinitions.php index b8f73bcdcda..4a79c3ad3ec 100644 --- a/core/ResponseDefinitions.php +++ b/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 { } diff --git a/core/openapi.json b/core/openapi.json index 2c6b5f28ef2..8bb7beca406 100644 --- a/core/openapi.json +++ b/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", diff --git a/lib/composer/composer/LICENSE b/lib/composer/composer/LICENSE index 62ecfd8d004..f27399a042d 100644 --- a/lib/composer/composer/LICENSE +++ b/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. + diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index d5c89d0237f..e32e509e3ee 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/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', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 906a5ac1996..2e8a3c71c0a 100644 --- a/lib/composer/composer/autoload_static.php +++ b/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', diff --git a/lib/composer/composer/installed.json b/lib/composer/composer/installed.json index f20a6c47c6d..13ea12dca2a 100644 --- a/lib/composer/composer/installed.json +++ b/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" + ] } diff --git a/lib/composer/composer/installed.php b/lib/composer/composer/installed.php index ada5df14e4f..88d9c302532 100644 --- a/lib/composer/composer/installed.php +++ b/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, + ), ), ); diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index 120ee7ea9fa..6c51aafff9b 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/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[] */ + 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 $class + */ + public function registerTeamResourceProvider(string $appId, string $class) { + $this->teamResourceProviders[] = new ServiceRegistration( + $appId, + $class + ); + } + /** * @psalm-param class-string $migratorClass */ @@ -870,4 +892,12 @@ class RegistrationContext { public function getSetupChecks(): array { return $this->setupChecks; } + + + /** + * @return ServiceRegistration[] + */ + public function getTeamResourceProviders(): array { + return $this->teamResourceProviders; + } } diff --git a/lib/private/Server.php b/lib/private/Server.php index 1aedd7d06ac..df267839160 100644 --- a/lib/private/Server.php +++ b/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); diff --git a/lib/private/Teams/TeamManager.php b/lib/private/Teams/TeamManager.php new file mode 100644 index 00000000000..6651d3ce676 --- /dev/null +++ b/lib/private/Teams/TeamManager.php @@ -0,0 +1,119 @@ + + * + * @author Julius Härtl + * + * @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 . + */ + +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; + } + } +} diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index b39c8591a7e..f515180cef5 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/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 diff --git a/lib/public/Teams/ITeamManager.php b/lib/public/Teams/ITeamManager.php new file mode 100644 index 00000000000..51d8a1feb5a --- /dev/null +++ b/lib/public/Teams/ITeamManager.php @@ -0,0 +1,58 @@ + + * + * @author Julius Härtl + * + * @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 . + */ + +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; +} diff --git a/lib/public/Teams/ITeamResourceProvider.php b/lib/public/Teams/ITeamResourceProvider.php new file mode 100644 index 00000000000..722c877555e --- /dev/null +++ b/lib/public/Teams/ITeamResourceProvider.php @@ -0,0 +1,76 @@ + + * + * @author Julius Härtl + * + * @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 . + */ + +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; +} diff --git a/lib/public/Teams/Team.php b/lib/public/Teams/Team.php new file mode 100644 index 00000000000..d3d6c2d143d --- /dev/null +++ b/lib/public/Teams/Team.php @@ -0,0 +1,73 @@ + + * + * @author Julius Härtl + * + * @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 . + */ + +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, + ]; + } +} diff --git a/lib/public/Teams/TeamResource.php b/lib/public/Teams/TeamResource.php new file mode 100644 index 00000000000..569583bb393 --- /dev/null +++ b/lib/public/Teams/TeamResource.php @@ -0,0 +1,129 @@ + + * + * @author Julius Härtl + * + * @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 . + */ + +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(), + ] + ]; + } +} diff --git a/psalm.xml b/psalm.xml index ed840f89059..7ef84054074 100644 --- a/psalm.xml +++ b/psalm.xml @@ -91,6 +91,10 @@ + + + +