Skip to content

Commit 62ce0c2

Browse files
committed
Implement get ocm invites route.
1 parent 96d8cce commit 62ce0c2

26 files changed

+1195
-321
lines changed

Makefile

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
# SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
33
# SPDX-License-Identifier: AGPL-3.0-or-later
44

5-
app_name=$(notdir $(CURDIR))
6-
project_directory=$(CURDIR)/../$(app_name)
5+
app_name="contacts"
6+
project_folder="nextcloud-contacts"
7+
8+
project_directory=$(CURDIR)/../$(project_folder)
79
appstore_build_directory=$(CURDIR)/build/artifacts
810
appstore_package_name=$(appstore_build_directory)/$(app_name)
911

@@ -20,3 +22,55 @@ clean-dev:
2022
# Builds the source package for the app store, ignores php and js tests
2123
appstore:
2224
krankerl package
25+
26+
# Builds the source package for the app store, ignores php and js tests
27+
# command: make version={version_number} buildapp
28+
# concatenate cd, ls and tar commands with '&&' otherwise the script context will remain the root instead of build
29+
.PHONY: buildapp
30+
buildapp:
31+
make check-version
32+
33+
version=$(version)
34+
35+
make clean-buildapp
36+
37+
mkdir -p $(appstore_build_directory)
38+
cd build && \
39+
ln -s ../ $(app_name) && \
40+
tar cvzfh $(appstore_build_directory)/$(app_name)_$(version).tar.gz \
41+
--exclude="$(app_name)/build" \
42+
--exclude="$(app_name)/release" \
43+
--exclude="$(app_name)/tests" \
44+
--exclude="$(app_name)/src" \
45+
--exclude="$(app_name)/tests" \
46+
--exclude="$(app_name)/vite.config.js" \
47+
--exclude="$(app_name)/*.log" \
48+
--exclude="$(app_name)/phpunit*xml" \
49+
--exclude="$(app_name)/composer.*" \
50+
--exclude="$(app_name)/node_modules" \
51+
--exclude="$(app_name)/js/node_modules" \
52+
--exclude="$(app_name)/js/tests" \
53+
--exclude="$(app_name)/js/test" \
54+
--exclude="$(app_name)/js/*.log" \
55+
--exclude="$(app_name)/js/package.json" \
56+
--exclude="$(app_name)/js/bower.json" \
57+
--exclude="$(app_name)/js/karma.*" \
58+
--exclude="$(app_name)/js/protractor.*" \
59+
--exclude="$(app_name)/package.json" \
60+
--exclude="$(app_name)/bower.json" \
61+
--exclude="$(app_name)/karma.*" \
62+
--exclude="$(app_name)/protractor\.*" \
63+
--exclude="$(app_name)/.*" \
64+
--exclude="$(app_name)/js/.*" \
65+
--exclude-vcs \
66+
$(app_name) && \
67+
rm $(app_name)
68+
69+
clean-buildapp:
70+
rm -rf ${appstore_build_directory}
71+
72+
check-version:
73+
@if [ "${version}" = "" ]; then\
74+
echo "Error: You must set version, eg. make -e version=v0.0.1 buildapp";\
75+
exit 1;\
76+
fi

appinfo/routes.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@
1414
['name' => 'contacts#direct', 'url' => '/direct/contact/{contact}', 'verb' => 'GET'],
1515
['name' => 'contacts#directcircle', 'url' => '/direct/circle/{singleId}', 'verb' => 'GET'],
1616

17+
['name' => 'federated_invites#get_invites', 'url' => '/ocm/invitations', 'verb' => 'GET'],
1718
['name' => 'federated_invites#create_invite', 'url' => '/ocm/invitations', 'verb' => 'POST'],
18-
['name' => 'federated_invites#invite_accepted', 'url' => '/ocm/invitations/accept', 'verb' => 'PATCH'],
19+
['name' => 'federated_invites#invite_accepted', 'url' => '/ocm/invitations/{token}/accept', 'verb' => 'PATCH'],
1920
['name' => 'federated_invites#invite_accept_dialog', 'url' => $inviteAcceptDialogPath, 'verb' => 'GET'],
2021

2122
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],

lib/AppInfo/Application.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
*/
77
namespace OCA\Contacts\AppInfo;
88

9+
use OCA\CloudFederationAPI\Events\FederatedInviteAcceptedEvent;
910
use OCA\Contacts\Dav\PatchPlugin;
1011
use OCA\Contacts\Event\LoadContactsOcaApiEvent;
12+
use OCA\Contacts\Listener\FederatedInviteAcceptedListener;
1113
use OCA\Contacts\Listener\LoadContactsFilesActions;
1214
use OCA\Contacts\Listener\LoadContactsOcaApi;
1315
use OCA\Files\Event\LoadAdditionalScriptsEvent;
@@ -32,6 +34,7 @@ public function __construct() {
3234
public function register(IRegistrationContext $context): void {
3335
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadContactsFilesActions::class);
3436
$context->registerEventListener(LoadContactsOcaApiEvent::class, LoadContactsOcaApi::class);
37+
$context->registerEventListener(FederatedInviteAcceptedEvent::class, FederatedInviteAcceptedListener::class);
3538
}
3639

3740
public function boot(IBootContext $context): void {

lib/Controller/FederatedInvitesController.php

Lines changed: 63 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@
1010
use Exception;
1111
use OC\App\CompareVersion;
1212
use OCA\DAV\CardDAV\CardDavBackend;
13-
use OCA\CloudFederationAPI\Db\FederatedInviteMapper;
1413
use OCA\Contacts\AppInfo\Application;
14+
use OCA\Contacts\Db\FederatedInvite;
15+
use OCA\Contacts\Db\FederatedInviteMapper;
16+
use OCA\Contacts\Service\FederatedInvitesService;
1517
use OCA\Contacts\Service\GroupSharingService;
1618
use OCA\Contacts\Service\SocialApiService;
1719
use OCA\FederatedFileSharing\AddressHandler;
1820
use OCP\App\IAppManager;
1921
use OCP\AppFramework\Http;
2022
use OCP\AppFramework\Http\DataResponse;
2123
use OCP\AppFramework\Http\TemplateResponse;
24+
use OCP\AppFramework\Utility\ITimeFactory;
2225
use OCP\Contacts\IManager;
2326
use OCP\Http\Client\IClientService;
2427
use OCP\IConfig;
@@ -30,6 +33,7 @@
3033
use OCP\IUserSession;
3134
use OCP\L10N\IFactory;
3235
use Psr\Log\LoggerInterface;
36+
use Sabre\DAV\UUIDUtil;
3337

3438
/**
3539
* Controller for federated invites related routes.
@@ -43,24 +47,27 @@ public function __construct(
4347
IRequest $request,
4448
private AddressHandler $addressHandler,
4549
private CardDavBackend $cardDavBackend,
50+
private FederatedInviteMapper $federatedInviteMapper,
51+
private FederatedInvitesService $federatedInvitesService,
52+
private IAppManager $appManager,
4653
private IClientService $httpClient,
4754
private IConfig $config,
4855
private IInitialStateService $initialStateService,
4956
private IFactory $languageFactory,
5057
private IManager $contactsManager,
5158
private IUserSession $userSession,
5259
private SocialApiService $socialApiService,
53-
private IAppManager $appManager,
60+
private ITimeFactory $timeFactory,
5461
private CompareVersion $compareVersion,
5562
private GroupSharingService $groupSharingService,
5663
private IL10N $il10,
5764
private IURLGenerator $urlGenerator,
5865
private IUserManager $userManager,
59-
private FederatedInviteMapper $federatedInviteMapper,
6066
private LoggerInterface $logger,
6167
) {
6268
parent::__construct(
6369
$request,
70+
$federatedInvitesService,
6471
$config,
6572
$initialStateService,
6673
$languageFactory,
@@ -73,6 +80,28 @@ public function __construct(
7380
);
7481
}
7582

83+
/**
84+
* @NoAdminRequired
85+
* @NoCSRFRequired
86+
*
87+
* Returns all open (not yet accepted) invites.
88+
*
89+
* @return DataResponse
90+
*/
91+
public function getInvites(): DataResponse {
92+
$_invites = $this->federatedInviteMapper->findOpenInvitesByUiddd($this->userSession->getUser()->getUID());
93+
$invites = [];
94+
foreach ($_invites as $invite) {
95+
if ($invite instanceof FederatedInvite) {
96+
array_push(
97+
$invites,
98+
$invite->jsonSerialize()
99+
);
100+
}
101+
}
102+
return new DataResponse($invites, Http::STATUS_OK);
103+
}
104+
76105
/**
77106
* @NoAdminRequired
78107
* @NoCSRFRequired
@@ -82,9 +111,7 @@ public function __construct(
82111
* @param string $token
83112
* @param string $provider
84113
*/
85-
public function inviteAcceptDialog(string $token = "", string $provider = ""): TemplateResponse
86-
{
87-
$this->logger->debug(" - FederatedInvitesController inviteAcceptDialog method: setting initial state ($token, $provider) and returning PageController index ", ['app' => Application::APP_ID]);
114+
public function inviteAcceptDialog(string $token = "", string $provider = ""): TemplateResponse {
88115
$this->initialStateService->provideInitialState(Application::APP_ID, 'inviteToken', $token);
89116
$this->initialStateService->provideInitialState(Application::APP_ID, 'inviteProvider', $provider);
90117
// TODO read from config
@@ -99,11 +126,29 @@ public function inviteAcceptDialog(string $token = "", string $provider = ""): T
99126
*
100127
* Creates an invitation to exchange contact info for the user with the specified uid.
101128
*
102-
* @return DataResponse
129+
* @param string $email the recipient email to send the invitation to
130+
* @param string $message the optional message to send with the invitation
131+
* @return DataResponse with data signature ['token' | 'error'] - the token of the invitation or an error message in case of error
103132
*/
104-
public function createInvite(string $uid = ''): DataResponse
105-
{
106-
return new DataResponse(['message' => "Route not implemented"], Http::STATUS_NOT_IMPLEMENTED);
133+
public function createInvite(string $email = null, string $message = null): DataResponse {
134+
if(!isset($email)) {
135+
return new DataResponse(['error' => 'Recipient email is required'], Http::STATUS_BAD_REQUEST);
136+
}
137+
$invite = new FederatedInvite();
138+
$invite->setUserId($this->userSession->getUser()->getUID());
139+
$token = UUIDUtil::getUUID();
140+
$invite->setToken($token);
141+
$invite->setCreatedAt($this->timeFactory->getTime());
142+
// TODO get expiration period from config
143+
// take 30 days
144+
$invite->setExpiredAt($invite->getCreatedAt() + 2592000000);
145+
$invite->setRecipientEmail($email);
146+
$invite->setAccepted(false);
147+
$this->federatedInviteMapper->insert($invite);
148+
149+
// TODO send email
150+
151+
return new DataResponse(['token' => $token], Http::STATUS_OK);
107152
}
108153

109154
/**
@@ -115,12 +160,12 @@ public function createInvite(string $uid = ''): DataResponse
115160
*
116161
* @param string $token the token of the invite
117162
* @param string $provider the provider of the sender of the invite
118-
* @return DataResponse with data signature ['contact' | 'message'] - the new contact url or a message in case of error
163+
* @return DataResponse with data signature ['contact' | 'error'] - the new contact url or an error message in case of error
119164
*/
120165
public function inviteAccepted(string $token = "", string $provider = ""): DataResponse {
121166
if ($token === "" || $provider === "") {
122167
$this->logger->error("Both token and provider must be specified. Received: token=$token, provider=$provider", ['app' => Application::APP_ID]);
123-
return new DataResponse(['message' => 'Both token and provider must be specified.'], Http::STATUS_NOT_FOUND);
168+
return new DataResponse(['error' => 'Both token and provider must be specified.'], Http::STATUS_NOT_FOUND);
124169
}
125170
try {
126171
// delegate further to OCM /invite-accepted
@@ -147,14 +192,14 @@ public function inviteAccepted(string $token = "", string $provider = ""): DataR
147192
$responseData = $response->getBody();
148193
$data = json_decode($responseData, true);
149194
$newContact = $this->socialApiService->createFederatedContact(
150-
// nextcloud cloud id format, ie. the ocm address
195+
// the ocm address: nextcloud cloud id format
151196
$data['userID'] . "@" . $this->addressHandler->removeProtocolFromUrl($provider),
152197
$data['email'],
153198
$data['name'],
154199
$localUser->getUID(),
155200
);
156201
if (!isset($newContact)) {
157-
return new DataResponse(['message' => 'An unexpected error occurred trying to accept invite: could not create new contact'], Http::STATUS_NOT_FOUND);
202+
return new DataResponse(['error' => 'An unexpected error occurred trying to accept invite: could not create new contact'], Http::STATUS_NOT_FOUND);
158203
}
159204
$this->logger->info("Created new contact with UID: " . $newContact['UID'] . " for user with UID: " . $localUser->getUID(), ['app' => Application::APP_ID]);
160205

@@ -172,15 +217,15 @@ public function inviteAccepted(string $token = "", string $provider = ""): DataR
172217
$statusCode = $e->getCode();
173218
switch ($statusCode) {
174219
case Http::STATUS_BAD_REQUEST:
175-
return new DataResponse(['message' => 'Invalid, non existing or expired token'], $e->getCode());
220+
return new DataResponse(['error' => 'Invalid, non existing or expired token'], $e->getCode());
176221
case Http::STATUS_CONFLICT:
177-
return new DataResponse(['message' => 'Invite already accepted'], $e->getCode());
222+
return new DataResponse(['error' => 'Invite already accepted'], $e->getCode());
178223
}
179224
$this->logger->error("An unexpected error occurred accepting invite with token=$token and provider=$provider. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]);
180-
return new DataResponse(['message' => 'An unexpected error occurred trying to accept invite.'], Http::STATUS_NOT_FOUND);
225+
return new DataResponse(['error' => 'An unexpected error occurred trying to accept invite.'], Http::STATUS_NOT_FOUND);
181226
} catch (Exception $e) {
182227
$this->logger->error("An unexpected error occurred accepting invite with token=$token and provider=$provider. Stacktrace: " . $e->getTraceAsString(), ['app' => Application::APP_ID]);
183-
return new DataResponse(['message' => 'An unexpected error occurred trying to accept invite'], Http::STATUS_NOT_FOUND);
228+
return new DataResponse(['error' => 'An unexpected error occurred trying to accept invite'], Http::STATUS_NOT_FOUND);
184229
}
185230
}
186231
}

lib/Controller/PageController.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
use OC\App\CompareVersion;
1111
use OCA\Contacts\AppInfo\Application;
12+
use OCA\Contacts\Service\FederatedInvitesService;
1213
use OCA\Contacts\Service\GroupSharingService;
1314
use OCA\Contacts\Service\SocialApiService;
1415
use OCP\App\IAppManager;
@@ -25,6 +26,7 @@ class PageController extends Controller {
2526

2627
public function __construct(
2728
IRequest $request,
29+
private FederatedInvitesService $federatedInvitesService,
2830
private IConfig $config,
2931
private IInitialStateService $initialStateService,
3032
private IFactory $languageFactory,
@@ -67,6 +69,7 @@ public function index(): TemplateResponse {
6769
$isTalkEnabled = $this->appManager->isEnabledForUser('spreed') === true;
6870

6971
$isTalkVersionCompatible = $this->compareVersion->isCompatible($talkVersion ? $talkVersion : '0.0.0', 2);
72+
$isOcmInvitesEnabled = $this->federatedInvitesService->isOcmInvitesEnabled();
7073

7174
$this->initialStateService->provideInitialState(Application::APP_ID, 'isGroupSharingEnabled', $isGroupSharingEnabled);
7275
$this->initialStateService->provideInitialState(Application::APP_ID, 'locales', $locales);
@@ -77,6 +80,7 @@ public function index(): TemplateResponse {
7780
$this->initialStateService->provideInitialState(Application::APP_ID, 'isContactsInteractionEnabled', $isContactsInteractionEnabled);
7881
$this->initialStateService->provideInitialState(Application::APP_ID, 'isCirclesEnabled', $isCirclesEnabled && $isCircleVersionCompatible);
7982
$this->initialStateService->provideInitialState(Application::APP_ID, 'isTalkEnabled', $isTalkEnabled && $isTalkVersionCompatible);
83+
$this->initialStateService->provideInitialState(Application::APP_ID, 'isOcmInvitesEnabled', $isOcmInvitesEnabled);
8084

8185
Util::addStyle(Application::APP_ID, 'contacts-main');
8286
Util::addScript(Application::APP_ID, 'contacts-main');

lib/Db/FederatedInvite.php

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Contacts\Db;
11+
12+
use OCA\CloudFederationAPI\Db\FederatedInvite as DbFederatedInvite;
13+
14+
class FederatedInvite extends DbFederatedInvite {
15+
16+
public function __construct() {
17+
}
18+
public function jsonSerialize(): array {
19+
return [
20+
'accepted' => $this->accepted,
21+
'acceptedAt' => $this->acceptedAt,
22+
'createdAt' => $this->createdAt,
23+
'expiredAt' => $this->expiredAt,
24+
'recipientEmail' => $this->recipientEmail,
25+
'recipientName' => $this->recipientName,
26+
'recipientProvider' => $this->recipientProvider,
27+
'recipientUserId' => $this->recipientUserId,
28+
'token' => $this->token,
29+
'userId' => $this->userId,
30+
];
31+
}
32+
33+
}

lib/Db/FederatedInviteMapper.php

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Contacts\Db;
11+
12+
use OCA\CloudFederationAPI\Db\FederatedInviteMapper as DbFederatedInviteMapper;
13+
use OCP\DB\QueryBuilder\IQueryBuilder;
14+
use OCP\IDBConnection;
15+
use Psr\Log\LoggerInterface;
16+
17+
/**
18+
* @template-extends QBMapper<FederatedInvite>
19+
*/
20+
class FederatedInviteMapper extends DbFederatedInviteMapper {
21+
22+
public function __construct(
23+
IDBConnection $db,
24+
private LoggerInterface $logger,
25+
) {
26+
parent::__construct($db, self::TABLE_NAME);
27+
}
28+
29+
/**
30+
* Returns all open federated invites of the specified user
31+
*
32+
* @return array a list of FederatedInvite objects
33+
*/
34+
public function findOpenInvitesByUiddd(string $userId):array {
35+
$qb = $this->db->getQueryBuilder();
36+
$qb->select('*')
37+
->from(self::TABLE_NAME)
38+
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)))
39+
->andWhere($qb->expr()->eq('accepted', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)));
40+
$entities = $this->findEntities($qb);
41+
foreach($entities as $entity) {
42+
$this->logger->debug(get_class($entity));
43+
}
44+
return $entities;
45+
}
46+
47+
}

0 commit comments

Comments
 (0)