Skip to content

Commit de49bff

Browse files
committed
feat: Add the possibility to create invitation links to circles
Signed-off-by: Kostiantyn Miakshyn <molodchick@gmail.com>
1 parent 8796983 commit de49bff

15 files changed

+743
-4
lines changed

appinfo/routes.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
['name' => 'Local#editName', 'url' => '/circles/{circleId}/name', 'verb' => 'PUT'],
3939
['name' => 'Local#editDescription', 'url' => '/circles/{circleId}/description', 'verb' => 'PUT'],
4040
['name' => 'Local#editSetting', 'url' => '/circles/{circleId}/setting', 'verb' => 'PUT'],
41+
['name' => 'Local#createInvitation', 'url' => '/circles/{circleId}/invitation', 'verb' => 'PUT'],
42+
['name' => 'Local#revokeInvitation', 'url' => '/circles/{circleId}/invitation', 'verb' => 'DELETE'],
4143
['name' => 'Local#editConfig', 'url' => '/circles/{circleId}/config', 'verb' => 'PUT'],
4244
['name' => 'Local#link', 'url' => '/link/{circleId}/{singleId}', 'verb' => 'GET'],
4345

lib/Controller/LocalController.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,6 +590,47 @@ public function link(string $circleId, string $singleId): DataResponse {
590590
}
591591
}
592592

593+
/**
594+
* @NoAdminRequired
595+
*
596+
* @param string $circleId
597+
*
598+
* @return DataResponse
599+
* @throws OCSException
600+
*/
601+
public function createInvitation(string $circleId): DataResponse {
602+
try {
603+
$this->setCurrentFederatedUser();
604+
605+
$outcome = $this->circleService->createInvitation($circleId);
606+
607+
return new DataResponse($this->serializeArray($outcome));
608+
} catch (\Exception $e) {
609+
$this->e($e, ['circleId' => $circleId]);
610+
throw new OCSException($e->getMessage(), (int)$e->getCode(), $e);
611+
}
612+
}
613+
614+
/**
615+
* @NoAdminRequired
616+
*
617+
* @param string $circleId
618+
*
619+
* @return DataResponse
620+
* @throws OCSException
621+
*/
622+
public function revokeInvitation(string $circleId): DataResponse {
623+
try {
624+
$this->setCurrentFederatedUser();
625+
626+
$outcome = $this->circleService->revokeInvitation($circleId);
627+
628+
return new DataResponse($this->serializeArray($outcome));
629+
} catch (\Exception $e) {
630+
$this->e($e, ['circleId' => $circleId]);
631+
throw new OCSException($e->getMessage(), (int)$e->getCode(), $e);
632+
}
633+
}
593634

594635
/**
595636
* @return void

lib/Db/CircleInvitationRequest.php

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Circles\Db;
11+
12+
use OCA\Circles\Exceptions\InvalidIdException;
13+
use OCA\Circles\Model\CircleInvitation;
14+
15+
/**
16+
* Class CircleInvitationRequest
17+
*
18+
* @package OCA\Circles\Db
19+
*/
20+
class CircleInvitationRequest extends CircleRequestBuilder {
21+
/**
22+
* @param CircleInvitation $circleInvitation
23+
*
24+
* @throws InvalidIdException
25+
*/
26+
public function save(CircleInvitation $circleInvitation): void {
27+
$this->confirmValidId($circleInvitation->getCircleId());
28+
29+
$qb = $this->getCircleInvitationInsertSql();
30+
$qb->setValue('circle_id', $qb->createNamedParameter($circleInvitation->getCircleId()))
31+
->setValue('invitation_code', $qb->createNamedParameter($circleInvitation->getInvitationCode()))
32+
->setValue('created_by', $qb->createNamedParameter($circleInvitation->getCreatedBy()));
33+
$qb->executeStatement();
34+
}
35+
36+
/**
37+
* @param CircleInvitation $circleInvitation
38+
*
39+
* @throws InvalidIdException
40+
*/
41+
public function replace(CircleInvitation $circleInvitation): void {
42+
$this->delete($circleInvitation->getCircleId());
43+
$this->save($circleInvitation);
44+
}
45+
46+
/**
47+
* @param string $circleId
48+
*/
49+
public function delete(string $circleId): void {
50+
$qb = $this->getCircleInvitationDeleteSql();
51+
$qb->limitToCircleId($circleId);
52+
53+
$qb->executeStatement();
54+
}
55+
}

lib/Db/CircleRequest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ public function getCircles(?IFederatedUser $initiator, CircleProbe $probe): arra
167167
$qb->limitToInitiator(CoreQueryBuilder::CIRCLE, $initiator);
168168
$qb->orderBy($qb->generateAlias(CoreQueryBuilder::CIRCLE, CoreQueryBuilder::INITIATOR) . '.level', 'desc');
169169
$qb->addOrderBy(CoreQueryBuilder::CIRCLE . '.display_name', 'asc');
170+
$qb->leftJoinCircleInvitation(CoreQueryBuilder::CIRCLE);
170171
}
171172
if ($probe->hasFilterMember()) {
172173
$qb->limitToDirectMembership(CoreQueryBuilder::CIRCLE, $probe->getFilterMember());
@@ -369,6 +370,7 @@ public function getCircle(
369370
$qb->limitToUniqueId($id);
370371
$qb->filterCircles(CoreQueryBuilder::CIRCLE, $probe);
371372
$qb->leftJoinOwner(CoreQueryBuilder::CIRCLE);
373+
$qb->leftJoinCircleInvitation(CoreQueryBuilder::CIRCLE);
372374
// $qb->setOptions(
373375
// [CoreRequestBuilder::CIRCLE, CoreRequestBuilder::INITIATOR], [
374376
// 'mustBeMember' => false,

lib/Db/CircleRequestBuilder.php

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ protected function getCircleInsertSql(): CoreQueryBuilder {
3333
return $qb;
3434
}
3535

36+
/**
37+
* @return CoreQueryBuilder&IQueryBuilder
38+
*/
39+
protected function getCircleInvitationInsertSql(): CoreQueryBuilder {
40+
$qb = $this->getQueryBuilder();
41+
$qb->insert(self::TABLE_INVITATIONS)
42+
->setValue('created', $qb->createNamedParameter($this->timezoneService->getUTCDate()));
43+
44+
return $qb;
45+
}
3646

3747
/**
3848
* @return CoreQueryBuilder&IQueryBuilder
@@ -44,7 +54,6 @@ protected function getCircleUpdateSql(): CoreQueryBuilder {
4454
return $qb;
4555
}
4656

47-
4857
/**
4958
* @param string $alias
5059
* @param bool $single
@@ -65,7 +74,6 @@ protected function getCircleSelectSql(
6574
return $qb;
6675
}
6776

68-
6977
/**
7078
* Base of the Sql Delete request
7179
*
@@ -78,6 +86,17 @@ protected function getCircleDeleteSql(): CoreQueryBuilder {
7886
return $qb;
7987
}
8088

89+
/**
90+
* Base of the Sql Delete request
91+
*
92+
* @return CoreQueryBuilder&IQueryBuilder
93+
*/
94+
protected function getCircleInvitationDeleteSql(): CoreQueryBuilder {
95+
$qb = $this->getQueryBuilder();
96+
$qb->delete(self::TABLE_INVITATIONS);
97+
98+
return $qb;
99+
}
81100

82101
/**
83102
* @param CoreQueryBuilder&IQueryBuilder $qb

lib/Db/CoreQueryBuilder.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ class CoreQueryBuilder extends ExtendedQueryBuilder {
5959
public const TOKEN = 'u';
6060
public const OPTIONS = 'v';
6161
public const HELPER = 'w';
62-
62+
public const INVITATION = 'x';
6363

6464
public static $SQL_PATH = [
6565
self::SINGLE => [
@@ -69,6 +69,7 @@ class CoreQueryBuilder extends ExtendedQueryBuilder {
6969
self::OPTIONS => [
7070
],
7171
self::MEMBER,
72+
self::INVITATION,
7273
self::OWNER => [
7374
self::BASED_ON
7475
],
@@ -839,6 +840,31 @@ public function leftJoinOwner(string $alias, string $field = 'unique_id'): void
839840
$this->leftJoinBasedOn($aliasMember);
840841
}
841842

843+
/**
844+
* @param string $alias
845+
* @param string $field
846+
*
847+
* @throws RequestBuilderException
848+
*/
849+
public function leftJoinCircleInvitation(string $alias, string $field = 'unique_id'): void {
850+
if ($this->getType() !== QueryBuilder::SELECT) {
851+
return;
852+
}
853+
854+
try {
855+
$aliasInvitation = $this->generateAlias($alias, self::INVITATION, $options);
856+
$getData = $this->getBool('getData', $options, false);
857+
} catch (RequestBuilderException $e) {
858+
return;
859+
}
860+
861+
$expr = $this->expr();
862+
$this->generateCircleInvitationSelectAlias($aliasInvitation)
863+
->leftJoin(
864+
$alias, CoreRequestBuilder::TABLE_INVITATIONS, $aliasInvitation,
865+
$expr->eq($aliasInvitation . '.circle_id', $alias . '.' . $field),
866+
);
867+
}
842868

843869
/**
844870
* @param CircleProbe $probe
@@ -1584,6 +1610,23 @@ private function generateMemberSelectAlias(string $alias, array $default = []):
15841610
return $this;
15851611
}
15861612

1613+
/**
1614+
* @param string $alias
1615+
* @param array $default
1616+
*
1617+
* @return $this
1618+
*/
1619+
private function generateCircleInvitationSelectAlias(string $alias, array $default = []): self {
1620+
$this->generateSelectAlias(
1621+
CoreRequestBuilder::$tables[CoreRequestBuilder::TABLE_INVITATIONS],
1622+
$alias,
1623+
$alias,
1624+
$default
1625+
);
1626+
1627+
return $this;
1628+
}
1629+
15871630

15881631
/**
15891632
* @param string $alias

lib/Db/CoreRequestBuilder.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ class CoreRequestBuilder {
3232
public const TABLE_STORAGES = 'storages';
3333

3434
public const TABLE_CIRCLE = 'circles_circle';
35+
public const TABLE_INVITATIONS = 'circles_invitations';
3536
public const TABLE_MEMBER = 'circles_member';
3637
public const TABLE_MEMBERSHIP = 'circles_membership';
3738
public const TABLE_REMOTE = 'circles_remote';
@@ -64,6 +65,12 @@ class CoreRequestBuilder {
6465
'contact_groupname',
6566
'creation'
6667
],
68+
self::TABLE_INVITATIONS => [
69+
'circle_id',
70+
'invitation_code',
71+
'created_by',
72+
'created',
73+
],
6774
self::TABLE_MEMBER => [
6875
'circle_id',
6976
'member_id',
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
6+
/**
7+
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
8+
* SPDX-License-Identifier: AGPL-3.0-or-later
9+
*/
10+
11+
namespace OCA\Circles\Exceptions;
12+
13+
class CircleInvitationNotFoundException extends FederatedItemNotFoundException {
14+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Circles\FederatedItems;
11+
12+
use OCA\Circles\Db\CircleInvitationRequest;
13+
use OCA\Circles\Exceptions\CircleNameTooShortException;
14+
use OCA\Circles\Exceptions\RequestBuilderException;
15+
use OCA\Circles\IFederatedItem;
16+
use OCA\Circles\Model\CircleInvitation;
17+
use OCA\Circles\Model\Federated\FederatedEvent;
18+
use OCA\Circles\Model\Helpers\MemberHelper;
19+
use OCA\Circles\Service\EventService;
20+
use OCA\Circles\Tools\Traits\TDeserialize;
21+
use OCP\Security\ISecureRandom;
22+
23+
/**
24+
* Class CircleCreateInvitation
25+
*
26+
* @package OCA\Circles\FederatedItems
27+
*/
28+
class CircleCreateInvitation implements IFederatedItem {
29+
use TDeserialize;
30+
31+
public function __construct(
32+
private CircleInvitationRequest $circleInvitationRequest,
33+
private EventService $eventService,
34+
private ISecureRandom $random,
35+
) {
36+
}
37+
38+
/**
39+
* @param FederatedEvent $event
40+
*
41+
* @throws RequestBuilderException
42+
* @throws CircleNameTooShortException
43+
*/
44+
public function verify(FederatedEvent $event): void {
45+
$circle = $event->getCircle();
46+
47+
$initiatorHelper = new MemberHelper($circle->getInitiator());
48+
$initiatorHelper->mustBeAdmin();
49+
50+
$new = clone $circle;
51+
52+
$invitationCode = $this->random->generate(
53+
16,
54+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
55+
);
56+
57+
$circleInvitation = new CircleInvitation();
58+
$circleInvitation->setCircleId($circle->getSingleId());
59+
$circleInvitation->setInvitationCode($invitationCode);
60+
$circleInvitation->setCreatedBy($circle->getInitiator()->getUserId());
61+
62+
$new->setCircleInvitation($circleInvitation);
63+
$event->getData()->sObj('circle_invitation', $circleInvitation);
64+
65+
$event->setOutcome($this->serialize($new));
66+
}
67+
68+
/**
69+
* @param FederatedEvent $event
70+
*
71+
* @throws RequestBuilderException
72+
*/
73+
public function manage(FederatedEvent $event): void {
74+
/** @var CircleInvitation $circleInvitation */
75+
$circleInvitation = $event->getData()->gObj('circle_invitation');
76+
$this->circleInvitationRequest->replace($circleInvitation);
77+
78+
// todo: do we need separate event here?
79+
$this->eventService->circleEditing($event);
80+
}
81+
82+
/**
83+
* @param FederatedEvent $event
84+
* @param array $results
85+
*/
86+
public function result(FederatedEvent $event, array $results): void {
87+
// todo: do we need separate event here?
88+
$this->eventService->circleEdited($event, $results);
89+
}
90+
}

0 commit comments

Comments
 (0)