feat(profile): add API for shared resources

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/63002/head
Ferdinand Thiessen 2 weeks ago
parent e161c8a99e
commit 16b784601a
No known key found for this signature in database
GPG Key ID: 7E849AE05218500F
  1. 0
      apps/profile/.noopenapi
  2. 2
      apps/profile/composer/composer/autoload_classmap.php
  3. 2
      apps/profile/composer/composer/autoload_static.php
  4. 177
      apps/profile/lib/Controller/ProfileApiController.php
  5. 21
      apps/profile/lib/ResponseDefinitions.php
  6. 204
      apps/profile/openapi.json
  7. 152
      openapi.json

@ -8,8 +8,10 @@ $baseDir = $vendorDir;
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'OCA\\Profile\\AppInfo\\Application' => $baseDir . '/../lib/AppInfo/Application.php',
'OCA\\Profile\\Controller\\ProfileApiController' => $baseDir . '/../lib/Controller/ProfileApiController.php',
'OCA\\Profile\\Controller\\ProfilePageController' => $baseDir . '/../lib/Controller/ProfilePageController.php',
'OCA\\Profile\\Listener\\LoadAdditionalEntriesListener' => $baseDir . '/../lib/Listener/LoadAdditionalEntriesListener.php',
'OCA\\Profile\\Listener\\ProfilePickerReferenceListener' => $baseDir . '/../lib/Listener/ProfilePickerReferenceListener.php',
'OCA\\Profile\\Reference\\ProfilePickerReferenceProvider' => $baseDir . '/../lib/Reference/ProfilePickerReferenceProvider.php',
'OCA\\Profile\\ResponseDefinitions' => $baseDir . '/../lib/ResponseDefinitions.php',
);

@ -23,10 +23,12 @@ class ComposerStaticInitProfile
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'OCA\\Profile\\AppInfo\\Application' => __DIR__ . '/..' . '/../lib/AppInfo/Application.php',
'OCA\\Profile\\Controller\\ProfileApiController' => __DIR__ . '/..' . '/../lib/Controller/ProfileApiController.php',
'OCA\\Profile\\Controller\\ProfilePageController' => __DIR__ . '/..' . '/../lib/Controller/ProfilePageController.php',
'OCA\\Profile\\Listener\\LoadAdditionalEntriesListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalEntriesListener.php',
'OCA\\Profile\\Listener\\ProfilePickerReferenceListener' => __DIR__ . '/..' . '/../lib/Listener/ProfilePickerReferenceListener.php',
'OCA\\Profile\\Reference\\ProfilePickerReferenceProvider' => __DIR__ . '/..' . '/../lib/Reference/ProfilePickerReferenceProvider.php',
'OCA\\Profile\\ResponseDefinitions' => __DIR__ . '/..' . '/../lib/ResponseDefinitions.php',
);
public static function getInitializer(ClassLoader $loader)

@ -0,0 +1,177 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Profile\Controller;
use OCP\App\IAppManager;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\ApiRoute;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCS\OCSNotFoundException;
use OCP\AppFramework\OCSController;
use OCP\Calendar\ICalendarQuery;
use OCP\Calendar\IManager;
use OCP\IDateTimeFormatter;
use OCP\IRequest;
use OCP\IURLGenerator;
use OCP\IUserManager;
use OCP\IUserSession;
use OCP\Share\IManager as IShareManager;
use OCP\Share\IShare;
/**
* @psalm-import-type ProfileSharedResource from \OCA\Profile\ResponseDefinitions
*/
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)]
class ProfileApiController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
private IUserSession $userSession,
private IUserManager $userManager,
private IManager $calendarManager,
private IShareManager $shareManager,
private IDateTimeFormatter $formatter,
private IURLGenerator $urlGenerator,
private IAppManager $appManager,
) {
parent::__construct($appName, $request);
}
/**
* Get resources shared between the current user and the specified user.
*
* @param string $userId - The user ID of the user to get shared resources with.
* @return DataResponse<Http::STATUS_OK, list<ProfileSharedResource>, array{}>
* @throws OCSNotFoundException - The specified user does not exist.
*
* 200: The shared resources between the current user and the specified user.
* 404: The specified user does not exist.
*/
#[NoCSRFRequired]
#[NoAdminRequired]
#[ApiRoute(verb: 'GET', url: '/api/v1/resources/{userId}')]
public function getResources(string $userId): DataResponse {
$user = $this->userManager->get($userId);
if (!$user) {
throw new OCSNotFoundException();
}
$files = $this->getSharedNodes($userId);
$events = $this->getSharedCalendarEvents($userId);
$entries = array_values(array_merge($files, $events));
return new DataResponse($entries);
}
/**
* Get all upcoming events shared between a user and the current user.
*
* If the calendar app is disabled for the current user no events will be returned.
*
* @param string $userId - The user ID of the user to get shared events with.
* @return list<ProfileSharedResource>
*/
private function getSharedCalendarEvents(string $userId) {
if (!$this->appManager->isEnabledForUser('calendar', $this->userSession->getUser())) {
return [];
}
$mePrincipal = 'principals/users/' . $this->userSession->getUser()->getUID();
$query = $this->calendarManager->newQuery($mePrincipal);
$query->setSearchPattern($userId);
$query->addType('VEVENT');
$query->addSearchProperty(ICalendarQuery::SEARCH_PROPERTY_ATTENDEE);
$query->addSearchProperty(ICalendarQuery::SEARCH_PROPERTY_ORGANIZER);
$now = new \DateTimeImmutable('now');
$query->setTimerangeStart($now->modify('-1 hour'));
$query->setLimit(9);
$events = $this->calendarManager->searchForPrincipal($query);
$result = [];
foreach ($events as $event) {
if (isset($event['objects'][0]['STATUS']) && $event['objects'][0]['STATUS'][0] === 'CANCELLED') {
continue;
}
$end = $event['objects'][0]['DTEND'][0];
if ($now->diff($end)->invert === 1) {
// already ended, skip
continue;
}
$calendarUid = $event['objects'][0]['UID'][0];
if (isset($event['RECURRENCE-ID'])) {
$recurrenceId = $event['RECURRENCE-ID'][0];
$href = $this->urlGenerator->linkToRouteAbsolute('calendar.object.indexuid.recurrenceId', ['uid' => $calendarUid, 'recurrenceId' => $recurrenceId]);
} else {
$href = $this->urlGenerator->linkToRouteAbsolute('calendar.object.indexuid', ['uid' => $calendarUid]);
}
$start = \DateTime::createFromImmutable($event['objects'][0]['DTSTART'][0]);
$result[] = [
'label' => $event['objects'][0]['SUMMARY'][0],
'text' => $this->formatter->formatTimeSpan($start, \DateTime::createFromImmutable($now)),
'href' => $href,
'img' => $this->urlGenerator->getAbsoluteURL($this->appManager->getAppIcon('calendar')),
];
}
return $result;
}
/**
* @return ProfileSharedResource[]
*/
private function getSharedNodes(string $userId): array {
$outgoingShares = [];
$offset = 0;
while (count($outgoingShares) < 5) {
$shares = $this->shareManager->getSharesBy($userId, IShare::TYPE_USER, limit: 50, offset: $offset);
$outgoingShares = array_merge($outgoingShares, array_filter($shares, fn ($share) => $share->getSharedWith() === $this->userSession->getUser()->getUID()));
$offset += 50;
if (count($shares) < 50) {
break;
}
}
$incomingShares = [];
$offset = 0;
while (count($incomingShares) < 5) {
$shares = $this->shareManager->getSharesBy($this->userSession->getUser()->getUID(), IShare::TYPE_USER, limit: 50, offset: $offset);
$incomingShares = array_merge($incomingShares, array_filter($shares, fn ($share) => $share->getSharedWith() === $userId));
$offset += 50;
if (count($shares) < 50) {
break;
}
}
$shares = array_slice(array_merge($outgoingShares, $incomingShares), 0, 5);
usort($shares, fn ($a, $b) => $a->getNode()->getMTime() <=> $b->getNode()->getMTime());
$files = [];
foreach ($shares as $share) {
$files[] = [
'label' => $share->getNode()->getName(),
'text' => $this->formatter->formatTimeSpan($share->getNode()->getMTime()),
'href' => $this->urlGenerator->linkToRouteAbsolute('files.view.index', [
'dir' => $share->getNode()->getParent()->getPath(),
'fileid' => $share->getNode()->getId(),
]),
'img' => $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', [
'fileId' => $share->getNode()->getId(),
'mimeFallback' => true,
]),
];
}
return $files;
}
}

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\Profile;
/**
* @psalm-type ProfileSharedResource array{
* label: string,
* text: string,
* href: string,
* img: string
* }
*/
class ResponseDefinitions {
}

@ -0,0 +1,204 @@
{
"openapi": "3.0.3",
"info": {
"title": "profile",
"version": "0.0.1",
"description": "This application provides the profile",
"license": {
"name": "AGPL-3.0-or-later"
}
},
"components": {
"securitySchemes": {
"basic_auth": {
"type": "http",
"scheme": "basic"
},
"bearer_auth": {
"type": "http",
"scheme": "bearer"
}
},
"schemas": {
"OCSMeta": {
"type": "object",
"required": [
"status",
"statuscode"
],
"properties": {
"status": {
"type": "string"
},
"statuscode": {
"type": "integer"
},
"message": {
"type": "string"
},
"totalitems": {
"type": "string"
},
"itemsperpage": {
"type": "string"
}
}
},
"SharedResource": {
"type": "object",
"required": [
"label",
"text",
"href",
"img"
],
"properties": {
"label": {
"type": "string"
},
"text": {
"type": "string"
},
"href": {
"type": "string"
},
"img": {
"type": "string"
}
}
}
}
},
"paths": {
"/ocs/v2.php/apps/profile/api/v1/resources/{userId}": {
"get": {
"operationId": "profile_api-get-resources",
"summary": "Get resources shared between the current user and the specified user.",
"tags": [
"profile_api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "userId",
"in": "path",
"description": "- The user ID of the user to get shared resources with.",
"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": "The shared resources between the current user and the specified user.",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/SharedResource"
}
}
}
}
}
}
}
}
},
"404": {
"description": "- The specified user does not exist.",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
}
},
"tags": []
}

@ -3624,6 +3624,29 @@
}
}
},
"ProfileSharedResource": {
"type": "object",
"required": [
"label",
"text",
"href",
"img"
],
"properties": {
"label": {
"type": "string"
},
"text": {
"type": "string"
},
"href": {
"type": "string"
},
"img": {
"type": "string"
}
}
},
"ProvisioningApiCapabilities": {
"type": "object",
"required": [
@ -29932,6 +29955,135 @@
}
}
},
"/ocs/v2.php/apps/profile/api/v1/resources/{userId}": {
"get": {
"operationId": "profile-profile_api-get-resources",
"summary": "Get resources shared between the current user and the specified user.",
"tags": [
"profile/profile_api"
],
"security": [
{
"bearer_auth": []
},
{
"basic_auth": []
}
],
"parameters": [
{
"name": "userId",
"in": "path",
"description": "- The user ID of the user to get shared resources with.",
"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": "The shared resources between the current user and the specified user.",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ProfileSharedResource"
}
}
}
}
}
}
}
}
},
"404": {
"description": "- The specified user does not exist.",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
},
"401": {
"description": "Current user is not logged in",
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"ocs"
],
"properties": {
"ocs": {
"type": "object",
"required": [
"meta",
"data"
],
"properties": {
"meta": {
"$ref": "#/components/schemas/OCSMeta"
},
"data": {}
}
}
}
}
}
}
}
}
}
},
"/ocs/v2.php/cloud/apps": {
"get": {
"operationId": "provisioning_api-apps-get-apps",

Loading…
Cancel
Save