From 5be3f06e804790cbed89e7cf0922439011dbf435 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 11 Oct 2024 16:45:45 +0200 Subject: [PATCH] feat(calls): Allow moderators to download a call participants list Signed-off-by: Joas Schilling --- appinfo/routes/routesCallController.php | 2 + docs/capabilities.md | 1 + lib/Capabilities.php | 1 + lib/Controller/CallController.php | 50 +++++++++++ openapi-full.json | 87 +++++++++++++++++++ openapi.json | 87 +++++++++++++++++++ src/types/openapi/openapi-full.ts | 57 ++++++++++++ src/types/openapi/openapi.ts | 57 ++++++++++++ .../features/bootstrap/FeatureContext.php | 25 ++++++ .../features/callapi/public.feature | 7 +- 10 files changed, 373 insertions(+), 1 deletion(-) diff --git a/appinfo/routes/routesCallController.php b/appinfo/routes/routesCallController.php index fb260175f71..15c9062c322 100644 --- a/appinfo/routes/routesCallController.php +++ b/appinfo/routes/routesCallController.php @@ -15,6 +15,8 @@ 'ocs' => [ /** @see \OCA\Talk\Controller\CallController::getPeersForCall() */ ['name' => 'Call#getPeersForCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'GET', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\CallController::downloadParticipantsForCall() */ + ['name' => 'Call#downloadParticipantsForCall', 'url' => '/api/{apiVersion}/call/{token}/download', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::joinCall() */ ['name' => 'Call#joinCall', 'url' => '/api/{apiVersion}/call/{token}', 'verb' => 'POST', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\CallController::joinFederatedCall() */ diff --git a/docs/capabilities.md b/docs/capabilities.md index 0e3584a5a5e..f8bebf371cd 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -159,5 +159,6 @@ ## 20.1 * `archived-conversations` (local) - Conversations can be marked as archived which will hide them from the conversation list by default * `talk-polls-drafts` - Whether moderators can store and retrieve poll drafts +* `download-call-participants` - Whether the endpoints for moderators to download the call participants is available * `config => call => start-without-media` (local) - Boolean, whether media should be disabled when starting or joining a conversation * `config => call => max-duration` - Integer, maximum call duration in seconds. Please note that this should only be used with system cron and with a reasonable high value, due to the expended duration until the background job ran. diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 1ae5a29a02c..cb30c4bb443 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -105,6 +105,7 @@ class Capabilities implements IPublicCapability { 'edit-messages-note-to-self', 'archived-conversations', 'talk-polls-drafts', + 'download-call-participants', ]; public const LOCAL_FEATURES = [ diff --git a/lib/Controller/CallController.php b/lib/Controller/CallController.php index 5224bdd14df..d1530793082 100644 --- a/lib/Controller/CallController.php +++ b/lib/Controller/CallController.php @@ -17,6 +17,7 @@ use OCA\Talk\Middleware\Attribute\RequireCallEnabled; use OCA\Talk\Middleware\Attribute\RequireFederatedParticipant; use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby; +use OCA\Talk\Middleware\Attribute\RequireModeratorParticipant; use OCA\Talk\Middleware\Attribute\RequireParticipant; use OCA\Talk\Middleware\Attribute\RequirePermission; use OCA\Talk\Middleware\Attribute\RequireReadWriteConversation; @@ -33,7 +34,9 @@ use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\BruteForceProtection; use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\DataDownloadResponse; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\Response; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IRequest; use OCP\IUserManager; @@ -111,6 +114,53 @@ public function getPeersForCall(): DataResponse { return new DataResponse($result); } + /** + * Download the list of current call participants + * + * Required capability: `download-call-participants` + * + * @param 'csv'|'pdf' $format Download format + * @return DataDownloadResponse|Response + * + * 200: List of participants in the call downloaded in the requested format + * 400: No call in progress + */ + #[PublicPage] + #[RequireModeratorParticipant] + public function downloadParticipantsForCall(string $format = 'csv'): DataDownloadResponse|Response { + $timeout = $this->timeFactory->getTime() - Session::SESSION_TIMEOUT; + $participants = $this->participantService->getParticipantsInCall($this->room, $timeout); + + if (empty($participants)) { + return new Response(Http::STATUS_BAD_REQUEST); + } + + if ($format !== 'csv' && $format !== 'pdf') { + // Unsupported format + return new Response(Http::STATUS_BAD_REQUEST); + } + + if ($format !== 'csv') { + // FIXME Remove once pdf was implemented. + return new Response(Http::STATUS_BAD_REQUEST); + } + + $output = fopen('php://memory', 'w'); + fputcsv($output, [ + 'name', + 'type', + 'identifier', + ]); + + foreach ($participants as $participant) { + fputcsv($output, [$participant->getAttendee()->getDisplayName(), $participant->getAttendee()->getActorType(), $participant->getAttendee()->getActorId()]); + } + + fseek($output, 0); + + return new DataDownloadResponse(stream_get_contents($output), 'participants.csv', 'text/csv'); + } + /** * Join a call * diff --git a/openapi-full.json b/openapi-full.json index 6d1b91f9dfa..279744b5ed8 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -4544,6 +4544,93 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/download": { + "get": { + "operationId": "call-download-participants-for-call", + "summary": "Download the list of current call participants", + "description": "Required capability: `download-call-participants`", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "format", + "in": "query", + "description": "Download format", + "schema": { + "type": "string", + "default": "csv", + "enum": [ + "csv", + "pdf" + ] + } + }, + { + "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": "List of participants in the call downloaded in the requested format", + "content": { + "text/csv": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "No call in progress" + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { "post": { "operationId": "call-join-federated-call", diff --git a/openapi.json b/openapi.json index 01a1a41f86d..1ac6ced0651 100644 --- a/openapi.json +++ b/openapi.json @@ -4431,6 +4431,93 @@ } } }, + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/download": { + "get": { + "operationId": "call-download-participants-for-call", + "summary": "Download the list of current call participants", + "description": "Required capability: `download-call-participants`", + "tags": [ + "call" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "apiVersion", + "in": "path", + "required": true, + "schema": { + "type": "string", + "enum": [ + "v4" + ], + "default": "v4" + } + }, + { + "name": "token", + "in": "path", + "required": true, + "schema": { + "type": "string", + "pattern": "^[a-z0-9]{4,30}$" + } + }, + { + "name": "format", + "in": "query", + "description": "Download format", + "schema": { + "type": "string", + "default": "csv", + "enum": [ + "csv", + "pdf" + ] + } + }, + { + "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": "List of participants in the call downloaded in the requested format", + "content": { + "text/csv": { + "schema": { + "type": "string", + "format": "binary" + } + }, + "application/pdf": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "No call in progress" + } + } + } + }, "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { "post": { "operationId": "call-join-federated-call", diff --git a/src/types/openapi/openapi-full.ts b/src/types/openapi/openapi-full.ts index 3e80ea9d133..883a78aab87 100644 --- a/src/types/openapi/openapi-full.ts +++ b/src/types/openapi/openapi-full.ts @@ -261,6 +261,26 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/download": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Download the list of current call participants + * @description Required capability: `download-call-participants` + */ + get: operations["call-download-participants-for-call"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { parameters: { query?: never; @@ -3443,6 +3463,43 @@ export interface operations { }; }; }; + "call-download-participants-for-call": { + parameters: { + query?: { + /** @description Download format */ + format?: "csv" | "pdf"; + }; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of participants in the call downloaded in the requested format */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/csv": string; + "application/pdf": string; + }; + }; + /** @description No call in progress */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; "call-update-federated-call-flags": { parameters: { query?: never; diff --git a/src/types/openapi/openapi.ts b/src/types/openapi/openapi.ts index 13ccc9c713b..2b877740f83 100644 --- a/src/types/openapi/openapi.ts +++ b/src/types/openapi/openapi.ts @@ -261,6 +261,26 @@ export type paths = { patch?: never; trace?: never; }; + "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/download": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Download the list of current call participants + * @description Required capability: `download-call-participants` + */ + get: operations["call-download-participants-for-call"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/ocs/v2.php/apps/spreed/api/{apiVersion}/call/{token}/federation": { parameters: { query?: never; @@ -2924,6 +2944,43 @@ export interface operations { }; }; }; + "call-download-participants-for-call": { + parameters: { + query?: { + /** @description Download format */ + format?: "csv" | "pdf"; + }; + header: { + /** @description Required to be true for the API request to pass */ + "OCS-APIRequest": boolean; + }; + path: { + apiVersion: "v4"; + token: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of participants in the call downloaded in the requested format */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/csv": string; + "application/pdf": string; + }; + }; + /** @description No call in progress */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; "call-update-federated-call-flags": { parameters: { query?: never; diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 9c3b52093c3..84c4cc85314 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -2233,6 +2233,31 @@ public function userSeesPeersInCall(string $user, int $numPeers, string $identif } } + /** + * @Then /^user "([^"]*)" downloads call participants from "([^"]*)" as "(csv)" with (\d+) \((v4)\)$/ + */ + public function userDownloadsPeersInCall(string $user, string $identifier, string $format, int $statusCode, string $apiVersion, ?TableNode $tableNode = null): void { + $this->setCurrentUser($user); + $this->sendRequest('GET', '/apps/spreed/api/' . $apiVersion . '/call/' . self::$identifierToToken[$identifier] . '/download', [ + 'format' => $format, + ]); + $this->assertStatusCode($this->response, $statusCode); + + if ($statusCode !== 200) { + return; + } + + $expected = []; + foreach ($tableNode->getRows() as $row) { + if ($row[1] === 'guests') { + $row[2] = self::$sessionNameToActorId[$row[2]]; + } + $expected[] = implode(',', $row); + } + + Assert::assertEquals(implode("\n", $expected) . "\n", $this->response->getBody()->getContents()); + } + /** * @Then /^user "([^"]*)" (silent sends|sends) message ("[^"]*"|'[^']*') to room "([^"]*)" with (\d+)(?: \((v1)\))?$/ * diff --git a/tests/integration/features/callapi/public.feature b/tests/integration/features/callapi/public.feature index 2ae08a9f489..de88c15c985 100644 --- a/tests/integration/features/callapi/public.feature +++ b/tests/integration/features/callapi/public.feature @@ -89,12 +89,17 @@ Feature: callapi/public Then user "participant1" joins call "room" with 200 (v4) Then user "participant1" sees 1 peers in call "room" with 200 (v4) And user "guest" sees 0 peers in call "room" with 404 (v4) - Then user "guest" joins room "room" with 200 (v4) + Then user "guest" joins room "room" with 200 (v4) session name "guest1" Then user "participant1" sees 1 peers in call "room" with 200 (v4) And user "guest" sees 1 peers in call "room" with 200 (v4) And user "guest" joins call "room" with 200 (v4) Then user "participant1" sees 2 peers in call "room" with 200 (v4) And user "guest" sees 2 peers in call "room" with 200 (v4) + And user "participant2" downloads call participants from "room" as "csv" with 403 (v4) + And user "participant1" downloads call participants from "room" as "csv" with 200 (v4) + | name | type | identifier | + | participant1-displayname | users | participant1 | + | | guests | guest1 | Then user "guest" leaves call "room" with 200 (v4) Then user "participant1" sees 1 peers in call "room" with 200 (v4) And user "guest" sees 1 peers in call "room" with 200 (v4)