Skip to content

Commit 683c657

Browse files
committed
feat: implement recipient api for unified sharing
Signed-off-by: Robin Appelman <robin@icewind.nl>
1 parent 61bb327 commit 683c657

5 files changed

Lines changed: 190 additions & 0 deletions

File tree

apps/sharing/lib/Controller/ApiV1Controller.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use NCU\Sharing\Share;
2727
use NCU\Sharing\ShareAccessContext;
2828
use NCU\Sharing\ShareState;
29+
use NCU\Sharing\ShareUser;
2930
use NCU\Sharing\ShareUserStatus;
3031
use NCU\Sharing\Source\IShareSourceType;
3132
use NCU\Sharing\Source\ShareSource;
@@ -125,6 +126,52 @@ public function searchRecipients(?array $filterRecipientTypeClasses, string $que
125126
}
126127
}
127128

129+
/**
130+
* Get recommended recipients for the current user, based on share frequency.
131+
*
132+
* @param ?list<class-string<IShareRecipientType>> $filterRecipientTypeClasses Type classes of recipients to filter by
133+
* @param int<1, 100> $limit The maximum number of participants
134+
* @param ?non-empty-string $id If provided, recipients that are already part of the share will not be returned.
135+
* @param ?non-empty-string $afterRecipientClass If all `after` values are provided, return recipients that come after the specified recipient.
136+
* @param ?non-empty-string $afterRecipientInstance If all `after` values are provided, return recipients that come after the specified recipient.
137+
* @param ?non-empty-string $afterRecipientValue If all `after` values are provided, return recipients that come after the specified recipient.
138+
* @return DataResponse<Http::STATUS_OK, list<SharingRecipient>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, string, array{}>
139+
*
140+
* 200: Recipients returned
141+
* 400: Invalid recipient search parameters
142+
* 404: Share used for filtering existing recipients does not exist
143+
*/
144+
#[NoAdminRequired]
145+
#[ApiRoute(verb: 'GET', url: '/api/v1/recipients/recommended')]
146+
public function recommendRecipients(
147+
?array $filterRecipientTypeClasses = null,
148+
int $limit = 5,
149+
?string $id = null,
150+
?string $afterRecipientClass = null,
151+
?string $afterRecipientInstance = null,
152+
?string $afterRecipientValue = null,
153+
): DataResponse {
154+
$afterRecipient = null;
155+
if (!is_null($afterRecipientClass) || !is_null($afterRecipientInstance) || !is_null($afterRecipientValue)) {
156+
if (is_null($afterRecipientClass)) {
157+
return new DataResponse('either all `after` values need to be null, or none of them', Http::STATUS_BAD_REQUEST);
158+
}
159+
if (is_null($afterRecipientInstance)) {
160+
return new DataResponse('either all `after` values need to be null, or none of them', Http::STATUS_BAD_REQUEST);
161+
}
162+
if (is_null($afterRecipientValue)) {
163+
return new DataResponse('either all `after` values need to be null, or none of them', Http::STATUS_BAD_REQUEST);
164+
}
165+
$afterRecipient = new ShareRecipient(
166+
$afterRecipientClass,
167+
$afterRecipientInstance,
168+
$afterRecipientValue
169+
);
170+
}
171+
$user = new ShareUser($this->accessContext->currentUser->getUID(), null);
172+
$this->manager->getRecipientsForUser($user, $filterRecipientTypeClasses, $id, $limit, $afterRecipient);
173+
}
174+
128175
/**
129176
* Generate a new secret.
130177
*

lib/private/Sharing/SharingBackend.php

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,99 @@ public function createSharePermissionDefaultValue(Share $share, string $permissi
14081408
);
14091409
}
14101410

1411+
#[\Override]
1412+
public function getRecipientsForUser(
1413+
ShareUser $user,
1414+
?array $filterRecipientTypeClasses = null,
1415+
?string $notInShare = null,
1416+
?int $count = 5,
1417+
?ShareRecipient $after = null,
1418+
): array {
1419+
$query = $this->connection->getTypedQueryBuilder();
1420+
$query->select('recipient_class_id', 'recipient_value', 'recipient_instance', 'initiator_user_id', 'initiator_user_id')
1421+
->selectAlias('count(*)', 'count')
1422+
->from('sharing_share_recipients', 'r')
1423+
->innerJoin('r', 'sharing_share', 's', $query->expr()->eq('r.share_id', 's.id'))
1424+
->where(
1425+
$query->expr()->orX([
1426+
$query->expr()->andX([
1427+
$query->expr()->eq('s.owner_user_id', $query->createNamedParameter($user->userId)),
1428+
$query->expr()->eq('s.owner_instance', $query->createNamedParameter($user->instance)),
1429+
]),
1430+
$query->expr()->andX([
1431+
$query->expr()->eq('r.initiator_user_id', $query->createNamedParameter($user->userId)),
1432+
$query->expr()->eq('r.initiator_instance', $query->createNamedParameter($user->instance)),
1433+
]),
1434+
])
1435+
)
1436+
->groupBy('r.*')
1437+
->orderBy('count', \SortDirection::Descending)
1438+
// sort by recipient to get a stable output, and allow "after" to be deterministic
1439+
->addOrderBy(
1440+
'recipient_instance', \SortDirection::Ascending
1441+
)
1442+
->addOrderBy(
1443+
'recipient_value', \SortDirection::Ascending
1444+
)
1445+
->addOrderBy('recipient_class_id', \SortDirection::Ascending);
1446+
1447+
if ($filterRecipientTypeClasses) {
1448+
$query = $query->andWhere(
1449+
$query->expr()->in('recipient_class_id', $query->createNamedParameter($filterRecipientTypeClasses, IQueryBuilder::PARAM_STR_ARRAY))
1450+
);
1451+
}
1452+
1453+
if ($notInShare) {
1454+
$fullRecipientId = $query->func()->concat('r.recipient_class_id', 'recipient_instance', 'recipient_value');
1455+
1456+
$subQuery = $this->connection->getTypedQueryBuilder();
1457+
$subQuery->selectAlias($fullRecipientId, 'recipient')
1458+
->from('sharing_share_recipients')
1459+
->where($query->expr()->eq('share_id', $query->createNamedParameter($notInShare)));
1460+
1461+
$query = $query->having(
1462+
$query->expr()->notIn(
1463+
$fullRecipientId,
1464+
$query->createFunction('(' . $subQuery->getSQL() . ')')
1465+
)
1466+
);
1467+
}
1468+
1469+
if ($count) {
1470+
$query->setMaxResults($count);
1471+
}
1472+
1473+
if ($after) {
1474+
$query = $query->andWhere(
1475+
$query->expr()->orX([
1476+
$query->expr()->andX([
1477+
$query->expr()->eq('s.recipient_instance', $query->createNamedParameter($after->instance)),
1478+
$query->expr()->eq('s.recipient_value', $query->createNamedParameter($user->userId)),
1479+
$query->expr()->gt('s.recipient_class_id', $query->createNamedParameter($user->userId)),
1480+
]),
1481+
$query->expr()->andX([
1482+
$query->expr()->eq('s.recipient_instance', $query->createNamedParameter($after->instance)),
1483+
$query->expr()->gt('s.recipient_value', $query->createNamedParameter($user->userId)),
1484+
]),
1485+
$query->expr()->gt('s.recipient_instance', $query->createNamedParameter($after->instance)),
1486+
])
1487+
);
1488+
}
1489+
1490+
$rows = $query->executeQuery()->fetchAll();
1491+
1492+
return array_map(fn (array $row) => new ShareRecipient(
1493+
$row['recipient_class_id'],
1494+
$row['recipient_value'],
1495+
$row['recipient_instance'],
1496+
null,
1497+
new ShareUser(
1498+
$row['initiator_user_id'],
1499+
$row['initiator_instance'],
1500+
)
1501+
), $rows);
1502+
}
1503+
14111504
private static function parseTimestamp(string $timestampMs): \DateTimeImmutable {
14121505
if (method_exists(\DateTimeImmutable::class, 'createFromTimestamp')) {
14131506
// with php 8.3 the method doesn't exist and psalm doesn't know the return type

lib/private/Sharing/SharingManager.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -769,6 +769,17 @@ public function getShares(
769769
return $this->backend->getShares($accessContext, $filterSourceTypeClass, $filterSourceTypeValue, $filterState, $filterUserStatus, $lastShareID, $limit);
770770
}
771771

772+
#[\Override]
773+
public function getRecipientsForUser(
774+
ShareUser $user,
775+
?array $filterRecipientTypeClasses = null,
776+
?string $notInShare = null,
777+
?int $count = 5,
778+
?ShareRecipient $after = null,
779+
): array {
780+
return $this->backend->getRecipientsForUser($user, $filterRecipientTypeClasses, $count, $after);
781+
}
782+
772783
#[\Override]
773784
public function handle(Event $event): void {
774785
if ($event instanceof SharesDefaultSetEvent) {

lib/unstable/Sharing/ISharingBackend.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use NCU\Sharing\Permission\ISharePermissionPreset;
1515
use NCU\Sharing\Permission\SharePermission;
1616
use NCU\Sharing\Property\ShareProperty;
17+
use NCU\Sharing\Recipient\IShareRecipientType;
1718
use NCU\Sharing\Recipient\ShareRecipient;
1819
use NCU\Sharing\Source\IShareSourceType;
1920
use NCU\Sharing\Source\ShareSource;
@@ -212,4 +213,23 @@ public function setLastUpdated(array $ids, \DateTimeImmutable $lastUpdated): voi
212213
* @experimental 35.0.0
213214
*/
214215
public function ensureDefaults(array $shares): array;
216+
217+
/**
218+
* Get a list of recipients a user has shared with, ordered by share count
219+
*
220+
* "shared with" includes both shares owned by the user, and reshares initiated by the user
221+
*
222+
* @param ?list<class-string<IShareRecipientType>> $filterRecipientTypeClasses
223+
* @param null|non-empty-string $notInShare
224+
* @param null|non-negative-int $count
225+
* @return ShareRecipient[]
226+
* @experimental 35.0.0
227+
*/
228+
public function getRecipientsForUser(
229+
ShareUser $user,
230+
?array $filterRecipientTypeClasses = null,
231+
?string $notInShare = null,
232+
?int $count = 5,
233+
?ShareRecipient $after = null,
234+
): array;
215235
}

lib/unstable/Sharing/ISharingManager.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,4 +225,23 @@ public function getShare(ShareAccessContext $accessContext, string $id): Share;
225225
* @experimental 35.0.0
226226
*/
227227
public function getShares(ShareAccessContext $accessContext, ?string $filterSourceTypeClass, ?string $filterSourceTypeValue, ?ShareState $filterState, ?ShareUserStatus $filterUserStatus, ?string $lastShareID, ?int $limit): array;
228+
229+
/**
230+
* Get a list of recipients a user has shared with, ordered by share count
231+
*
232+
* "shared with" includes both shares owned by the user, and reshares initiated by the user
233+
*
234+
* @param ?list<class-string<IShareRecipientType>> $filterRecipientTypeClasses
235+
* @param null|non-empty-string $notInShare
236+
* @param null|non-negative-int $count
237+
* @return ShareRecipient[]
238+
* @experimental 35.0.0
239+
*/
240+
public function getRecipientsForUser(
241+
ShareUser $user,
242+
?array $filterRecipientTypeClasses = null,
243+
?string $notInShare = null,
244+
?int $count = 5,
245+
?ShareRecipient $after = null,
246+
): array;
228247
}

0 commit comments

Comments
 (0)