Skip to content

Commit

Permalink
WIP: add async requestToExApp, OCS API endpoints (#290)
Browse files Browse the repository at this point in the history
This PR introduces async requestToExApp Public functions + two OCS
endpoints for requestToExApp and exAppRequestWithUserInit.
Some logging level adjustments.

---------

Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
  • Loading branch information
andrey18106 committed May 10, 2024
1 parent 37be01e commit de235e2
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 40 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,22 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

## [2.6.0 - 2024-05-xx]
## [2.6.0 - 2024-05-10]

### Added

- Added File Actions v2 version with redirect to the ExApp UI. #284
- Added async requestToExApp public functions. #290
- Added OCS API for synchronous requestToExApp functions. #290

### Changed

- Reworked scopes for database/cache requests optimization, drop old ex_app_scopes table. #285
- Corrected "Download ExApp logs" button availability in "Test deploy". #289

### Fixed

- Fixed incorrect init_timeout setting key in the UI. #288

## [2.5.1 - 2024-05-02]

Expand Down
4 changes: 4 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@
['name' => 'OCSExApp#getExAppsList', 'url' => '/api/v1/ex-app/{list}', 'verb' => 'GET'],
['name' => 'OCSExApp#getExApp', 'url' => '/api/v1/ex-app/info/{appId}', 'verb' => 'GET'],

// Requests to ExApps
['name' => 'OCSExApp#requestToExApp', 'url' => '/api/v1/ex-app/request/{appId}/', 'verb' => 'POST'],
['name' => 'OCSExApp#aeRequestToExApp', 'url' => '/api/v1/ex-app/request/{appId}/{$userId}', 'verb' => 'POST'],

// ExApps actions
['name' => 'OCSExApp#setExAppEnabled', 'url' => '/api/v1/ex-app/{appId}/enabled', 'verb' => 'PUT'],

Expand Down
43 changes: 43 additions & 0 deletions docs/tech_details/api/exapp.rst
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,46 @@ The response data is a JSON array of ExApp objects with the following attributes
"last_check_time": "timestamp of last successful Nextcloud->ExApp connection check",
"system": "true/false flag indicating system ExApp",
}
Make Requests to ExApps
^^^^^^^^^^^^^^^^^^^^^^^

There are two endpoints for making requests to ExApps:

1. Synchronous request: ``POST /apps/app_api/api/v1/ex-app/request/{appid}``
2. Synchronous request with ExApp user setup: ``POST /apps/app_api/api/v1/ex-app/request/{appid}/{userId}``

Request data
************

The request data params are the same as in ``lib/PublicFunction.php``:

.. code-block:: json
{
"route": "relative route to ExApp API endpoint",
"method": "GET/POST/PUT/DELETE",
"params": {},
"options": {},
}
.. note::

``userId`` and ``appId`` is taken from url params


Response data
*************

Successful request to ExApp OCS data response structure is the following:

.. code-block:: json
{
"status_code": "HTTP status code",
"body": "response data from ExApp",
"headers": "response headers from ExApp",
}
If there is an error, the response object will have only an ``error`` attribute with the error message.
51 changes: 51 additions & 0 deletions lib/Controller/OCSExAppController.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,16 @@
use OCP\IRequest;

class OCSExAppController extends OCSController {
protected $request;

public function __construct(
IRequest $request,
private readonly AppAPIService $service,
private readonly ExAppService $exAppService,
) {
parent::__construct(Application::APP_ID, $request);

$this->request = $request;
}

#[NoCSRFRequired]
Expand Down Expand Up @@ -70,4 +73,52 @@ public function setExAppEnabled(string $appId, int $enabled): DataResponse {

return new DataResponse();
}

#[NoCSRFRequired]
public function requestToExApp(
string $appId,
string $route,
?string $userId = null,
string $method = 'POST',
array $params = [],
array $options = [],
): DataResponse {
$exApp = $this->exAppService->getExApp($appId);
if ($exApp === null) {
return new DataResponse(['error' => sprintf('ExApp `%s` not found', $appId)]);
}
$response = $this->service->requestToExApp($exApp, $route, $userId, $method, $params, $options, $this->request);
if (is_array($response) && isset($response['error'])) {
return new DataResponse($response, Http::STATUS_BAD_REQUEST);
}
return new DataResponse([
'status_code' => $response->getStatusCode(),
'headers' => $response->getHeaders(),
'body' => $response->getBody(),
]);
}

#[NoCSRFRequired]
public function aeRequestToExApp(
string $appId,
string $route,
?string $userId = null,
string $method = 'POST',
array $params = [],
array $options = [],
): DataResponse {
$exApp = $this->exAppService->getExApp($appId);
if ($exApp === null) {
return new DataResponse(['error' => sprintf('ExApp `%s` not found', $appId)]);
}
$response = $this->service->aeRequestToExApp($exApp, $route, $userId, $method, $params, $options, $this->request);
if (is_array($response) && isset($response['error'])) {
return new DataResponse($response, Http::STATUS_BAD_REQUEST);
}
return new DataResponse([
'status_code' => $response->getStatusCode(),
'headers' => $response->getHeaders(),
'body' => $response->getBody(),
]);
}
}
47 changes: 45 additions & 2 deletions lib/PublicFunctions.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use OCA\AppAPI\Service\AppAPIService;
use OCA\AppAPI\Service\ExAppService;
use OCP\Http\Client\IPromise;
use OCP\Http\Client\IResponse;
use OCP\IRequest;

Expand All @@ -28,7 +29,7 @@ public function exAppRequest(
array $params = [],
array $options = [],
?IRequest $request = null,
): array|IResponse {
): array|IResponse {
$exApp = $this->exAppService->getExApp($appId);
if ($exApp === null) {
return ['error' => sprintf('ExApp `%s` not found', $appId)];
Expand All @@ -47,11 +48,53 @@ public function exAppRequestWithUserInit(
array $params = [],
array $options = [],
?IRequest $request = null,
): array|IResponse {
): array|IResponse {
$exApp = $this->exAppService->getExApp($appId);
if ($exApp === null) {
return ['error' => sprintf('ExApp `%s` not found', $appId)];
}
return $this->service->aeRequestToExApp($exApp, $route, $userId, $method, $params, $options, $request);
}

/**
* Async request to ExApp with AppAPI auth headers
*
* @throws \Exception if ExApp not found
*/
public function asyncExAppRequest(
string $appId,
string $route,
?string $userId = null,
string $method = 'POST',
array $params = [],
array $options = [],
?IRequest $request = null,
): IPromise {
$exApp = $this->exAppService->getExApp($appId);
if ($exApp === null) {
throw new \Exception(sprintf('ExApp `%s` not found', $appId));
}
return $this->service->requestToExAppAsync($exApp, $route, $userId, $method, $params, $options, $request);
}

/**
* Async request to ExApp with AppAPI auth headers and ExApp user initialization
*
* @throws \Exception if ExApp not found or failed to setup ExApp user
*/
public function asyncExAppRequestWithUserInit(
string $appId,
string $route,
?string $userId = null,
string $method = 'POST',
array $params = [],
array $options = [],
?IRequest $request = null,
): IPromise {
$exApp = $this->exAppService->getExApp($appId);
if ($exApp === null) {
throw new \Exception(sprintf('ExApp `%s` not found', $appId));
}
return $this->service->aeRequestToExAppAsync($exApp, $route, $userId, $method, $params, $options, $request);
}
}
82 changes: 45 additions & 37 deletions lib/Service/AppAPIService.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OCP\DB\Exception;
use OCP\Http\Client\IClient;
use OCP\Http\Client\IClientService;
use OCP\Http\Client\IPromise;
use OCP\Http\Client\IResponse;
use OCP\IConfig;
use OCP\IRequest;
Expand Down Expand Up @@ -96,29 +97,39 @@ private function requestToExAppInternal(
array $options,
): array|IResponse {
try {
switch ($method) {
case 'GET':
$response = $this->client->get($uri, $options);
break;
case 'POST':
$response = $this->client->post($uri, $options);
break;
case 'PUT':
$response = $this->client->put($uri, $options);
break;
case 'DELETE':
$response = $this->client->delete($uri, $options);
break;
default:
return ['error' => 'Bad HTTP method'];
}
return $response;
return match ($method) {
'GET' => $this->client->get($uri, $options),
'POST' => $this->client->post($uri, $options),
'PUT' => $this->client->put($uri, $options),
'DELETE' => $this->client->delete($uri, $options),
default => ['error' => 'Bad HTTP method'],
};
} catch (\Exception $e) {
$this->logger->error(sprintf('Error during request to ExApp %s: %s', $exApp->getAppid(), $e->getMessage()), ['exception' => $e]);
$this->logger->warning(sprintf('Error during request to ExApp %s: %s', $exApp->getAppid(), $e->getMessage()), ['exception' => $e]);
return ['error' => $e->getMessage()];
}
}

/**
* @throws \Exception
*/
public function aeRequestToExAppAsync(
ExApp $exApp,
string $route,
?string $userId = null,
string $method = 'POST',
array $params = [],
array $options = [],
?IRequest $request = null,
): IPromise {
$this->exAppUsersService->setupExAppUser($exApp->getAppid(), $userId);
$requestData = $this->prepareRequestToExApp($exApp, $route, $userId, $method, $params, $options, $request);
return $this->requestToExAppInternalAsync($exApp, $method, $requestData['url'], $requestData['options']);
}

/**
* @throws \Exception
*/
public function requestToExAppAsync(
ExApp $exApp,
string $route,
Expand All @@ -127,35 +138,32 @@ public function requestToExAppAsync(
array $params = [],
array $options = [],
?IRequest $request = null,
): void {
): IPromise {
$requestData = $this->prepareRequestToExApp($exApp, $route, $userId, $method, $params, $options, $request);
$this->requestToExAppInternalAsync($exApp, $method, $requestData['url'], $requestData['options']);
return $this->requestToExAppInternalAsync($exApp, $method, $requestData['url'], $requestData['options']);
}

/**
* @throws \Exception if bad HTTP method
*/
private function requestToExAppInternalAsync(
ExApp $exApp,
string $method,
string $uri,
#[\SensitiveParameter]
array $options,
): void {
switch ($method) {
case 'POST':
$promise = $this->client->postAsync($uri, $options);
break;
case 'PUT':
$promise = $this->client->putAsync($uri, $options);
break;
case 'DELETE':
$promise = $this->client->deleteAsync($uri, $options);
break;
default:
$this->logger->error('Bad HTTP method: requestToExAppAsync accepts only `POST`, `PUT` and `DELETE`');
return;
}
$promise->then(function (IResponse $response) use ($exApp) {
}, function (\Exception $exception) use ($exApp) {
): IPromise {
$promise = match ($method) {
'GET' => $this->client->getAsync($uri, $options),
'POST' => $this->client->postAsync($uri, $options),
'PUT' => $this->client->putAsync($uri, $options),
'DELETE' => $this->client->deleteAsync($uri, $options),
default => throw new \Exception('Bad HTTP method'),
};
$promise->then(onRejected: function (\Exception $exception) use ($exApp) {
$this->logger->warning(sprintf('Error during requestToExAppAsync %s: %s', $exApp->getAppid(), $exception->getMessage()), ['exception' => $exception]);
});
return $promise;
}

private function prepareRequestToExApp(
Expand Down

0 comments on commit de235e2

Please sign in to comment.