Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
['name' => 'APIv2#getDownloadCount', 'url' => '/api/v2/activity/downloads/count', 'verb' => 'GET'],
// Before the catch-all below, and a sub-path of it so `{filter}` cannot
// swallow it either way
['name' => 'APIv2#getTeam', 'url' => '/api/v2/activity/team/{teamId}', 'verb' => 'GET'],
['name' => 'APIv2#getHistogram', 'url' => '/api/v2/activity/{filter}/histogram', 'verb' => 'GET'],
['name' => 'APIv2#getFilter', 'url' => '/api/v2/activity/{filter}', 'verb' => 'GET'],
],
Expand Down
37 changes: 35 additions & 2 deletions docs/endpoint-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,39 @@ With type filter
GET /ocs/v2.php/apps/activity/api/v2/activity/{filter}
```

With a Team's roster and resource activity

```
GET /ocs/v2.php/apps/activity/api/v2/activity/team/{teamId}
```

The Team endpoint is available only to current Team members. It accepts the
same pagination, ordering, search, date, and actor parameters as the default
stream. It includes roster activity from before the current member's join date.
File activity is limited to the Team's current `files` resources that are
readable through the member's own mounts and ACLs, including older events for
those files. A file resource is included directly; a folder resource includes
its full readable subtree. Deleted files, files no longer reachable through any
Team resource, and nested external mounts are not part of this scope.

File scopes are resolved from their roots without recursively listing all
descendants. Activity queries join `filecache` and restrict candidates to the
resource's storage and subtree. Roots are queried in bounded chunks, so Teams
with many file resources do not produce unbounded queries. Candidates are then
checked through the member's mounts, including read access to intermediate
folders. Pagination continues past inaccessible candidates until another
visible activity is found or the matching activities are exhausted.

Other Team resource providers can opt in to the stream by resolving their
resources into Activity object scopes. Activity then filters on those object
types and IDs without inferring scope from the Team's member roster. A resource
may be shared with other Teams or accounts; it only needs to be currently
reachable through this Team's resources.

Recipient copies are normalized to the viewer's file paths and deduplicated
before pagination. An empty Team page returns `200 OK`; a matching ETag returns
`304 Not Modified`. The existing personal stream retains its empty-page `304`.

Supported type filters can be obtained from:

```
Expand Down Expand Up @@ -93,11 +126,11 @@ Status Code | Description
----------- | -----------
`200 OK` | Activities
`204 No Content` | The user has selected no activities to be listed in the stream
`304 Not Modified` | ETag/If-None-Match are the same or the end of the activity list was reached
`304 Not Modified` | ETag/If-None-Match are the same, or the personal stream is empty
`400 Bad Request` | The search term is too short or too long, or `from` is after `to`
`403 Forbidden` | The offset activity belongs to a different user
`403 Forbidden` | The user is not logged in
`404 Not Found` | The filter is unknown
`404 Not Found` | The filter is unknown, or the Team does not exist or the user is not one of its members

## Headers

Expand Down
69 changes: 52 additions & 17 deletions lib/Controller/APIv2Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
use OCA\Activity\Exception\InvalidSearchCriteriaException;
use OCA\Activity\GroupHelper;
use OCA\Activity\SearchCriteria;
use OCA\Activity\TeamActivityResolver;
use OCA\Activity\TeamActivityScope;
use OCA\Activity\UserSettings;
use OCA\Activity\ViewInfoCache;
use OCP\Activity\IFilter;
Expand Down Expand Up @@ -70,6 +72,7 @@ public function __construct(
protected ViewInfoCache $infoCache,
protected INotificationManager $notificationManager,
protected IDateTimeZone $dateTimeZone,
protected TeamActivityResolver $teamActivityResolver,
) {
parent::__construct($appName, $request);
$this->activityManager = $activityManager;
Expand Down Expand Up @@ -131,6 +134,27 @@ public function getFilter(string $filter, int $since = 0, int $limit = 50, bool
return $this->get($filter, $since, $limit, $previews, $object_type, $object_id, $sort, $search, $from, $to, $actor);
}

/**
* @param string $search Only return activities whose file path contains this substring
* @param int $from Only return activities at or after this Unix timestamp
* @param int $to Only return activities at or before this Unix timestamp
* @param string $actor Only return activities authored by this account
*/
#[NoAdminRequired]
public function getTeam(string $teamId, int $since = 0, int $limit = 50, bool $previews = false, string $sort = 'desc', string $search = '', int $from = 0, int $to = 0, string $actor = ''): DataResponse {
$user = $this->userSession->getUser();
if (!$user instanceof IUser) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}

$scope = $this->teamActivityResolver->getScope($teamId, $user->getUID());
if ($scope === null) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

return $this->get('all', $since, $limit, $previews, '', 0, $sort, $search, $from, $to, $actor, $scope);
}

/**
* @param string $object_type Object type to count downloads for (must be 'files')
* @param int $object_id File ID
Expand Down Expand Up @@ -250,7 +274,7 @@ public function listFilters(): DataResponse {
return new DataResponse($filters);
}

protected function get(string $filter, int $since, int $limit, bool $previews, string $filterObjectType, int $filterObjectId, string $sort, string $search = '', int $from = 0, int $to = 0, string $actor = ''): DataResponse {
protected function get(string $filter, int $since, int $limit, bool $previews, string $filterObjectType, int $filterObjectId, string $sort, string $search = '', int $from = 0, int $to = 0, string $actor = '', ?TeamActivityScope $teamScope = null): DataResponse {
try {
$this->validateParameters($filter, $since, $limit, $previews, $filterObjectType, $filterObjectId, $sort, $search, $from, $to, $actor);
} catch (InvalidFilterException) {
Expand All @@ -263,21 +287,32 @@ protected function get(string $filter, int $since, int $limit, bool $previews, s

$this->activityManager->setRequirePNG(false);
try {
$response = $this->data->get(
$this->helper,
$this->settings,
$this->user,

$this->since,
$this->limit,
$this->sort,

$this->filter,
$this->objectType,
$this->objectId,
false,
$this->searchCriteria
);
$response = $teamScope === null
? $this->data->get(
$this->helper,
$this->settings,
$this->user,

$this->since,
$this->limit,
$this->sort,

$this->filter,
$this->objectType,
$this->objectId,
false,
$this->searchCriteria
)
: $this->data->getTeam(
$this->helper,
$this->settings,
$this->user,
$this->since,
$this->limit,
$this->sort,
$teamScope,
$this->searchCriteria
);
} catch (\OutOfBoundsException) {
// Invalid since argument
return new DataResponse([], Http::STATUS_FORBIDDEN);
Expand All @@ -288,7 +323,7 @@ protected function get(string $filter, int $since, int $limit, bool $previews, s
$this->activityManager->setRequirePNG(false);

$headers = $this->generateHeaders($response['headers'], $response['has_more'], $response['data']);
if (empty($response['data']) || $this->request->getHeader('If-None-Match') === $headers['ETag']) {
if (($teamScope === null && empty($response['data'])) || $this->request->getHeader('If-None-Match') === $headers['ETag']) {
return new DataResponse([], Http::STATUS_NOT_MODIFIED, $headers);
}

Expand Down
Loading
Loading