Skip to content

Commit

Permalink
feat(federation): Expose a header with the pending invitations on roo…
Browse files Browse the repository at this point in the history
…m list

Signed-off-by: Joas Schilling <coding@schilljs.com>
  • Loading branch information
nickvergessen committed Mar 28, 2024
1 parent 61a6b98 commit 6962f78
Show file tree
Hide file tree
Showing 18 changed files with 129 additions and 16 deletions.
9 changes: 5 additions & 4 deletions docs/conversation.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@

- Header:

| field | type | Description |
|------------------------------------|--------|----------------------------------------------------------------------------------------------------------------------------------------------------------|
| `X-Nextcloud-Talk-Hash` | string | Sha1 value over some config. When you receive a different value on subsequent requests, the capabilities and the signaling settings should be refreshed. |
| `X-Nextcloud-Talk-Modified-Before` | string | Timestamp from before the database request that can be used as `modifiedSince` parameter in the next request |
| field | type | Description |
|---------------------------------------|--------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `X-Nextcloud-Talk-Hash` | string | Sha1 value over some config. When you receive a different value on subsequent requests, the capabilities and the signaling settings should be refreshed. |
| `X-Nextcloud-Talk-Modified-Before` | string | Timestamp from before the database request that can be used as `modifiedSince` parameter in the next request |
| `X-Nextcloud-Talk-Federation-Invites` | string | *Optional:* Number of pending invites to federated conversations the user has. (Only available when the user can do federation and has at least one invite pending) |

- Data:
Array of conversations, each conversation has at least:
Expand Down
3 changes: 2 additions & 1 deletion lib/BackgroundJob/ResetAssignedSignalingServer.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

namespace OCA\Talk\BackgroundJob;

use OCA\Talk\CachePrefix;
use OCA\Talk\Manager;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
Expand All @@ -49,7 +50,7 @@ public function __construct(
$this->setInterval(60 * 5);
$this->setTimeSensitivity(IJob::TIME_SENSITIVE);

$this->cache = $cacheFactory->createDistributed('hpb_servers');
$this->cache = $cacheFactory->createDistributed(CachePrefix::SIGNALING_ASSIGNED_SERVER);
}

protected function run($argument): void {
Expand Down
36 changes: 36 additions & 0 deletions lib/CachePrefix.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php
/*
* @copyright Copyright (c) 2024 Joas Schilling <coding@schilljs.com>
*/

declare(strict_types=1);
/*
* @copyright Copyright (c) 2024 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\Talk;

class CachePrefix {
public const FEDERATED_PCM = 'talk/pcm/';
public const CHAT_LAST_MESSAGE_ID = 'talk/lastmsgid';
public const CHAT_UNREAD_COUNT = 'talk/unreadcount';
public const SIGNALING_ASSIGNED_SERVER = 'hpb_servers';
}
5 changes: 3 additions & 2 deletions lib/Chat/ChatManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use DateInterval;
use OC\Memcache\ArrayCache;
use OC\Memcache\NullCache;
use OCA\Talk\CachePrefix;
use OCA\Talk\Events\BeforeChatMessageSentEvent;
use OCA\Talk\Events\BeforeSystemMessageSentEvent;
use OCA\Talk\Events\ChatMessageSentEvent;
Expand Down Expand Up @@ -113,8 +114,8 @@ public function __construct(
protected IRequest $request,
protected LoggerInterface $logger,
) {
$this->cache = $cacheFactory->createDistributed('talk/lastmsgid');
$this->unreadCountCache = $cacheFactory->createDistributed('talk/unreadcount');
$this->cache = $cacheFactory->createDistributed(CachePrefix::CHAT_LAST_MESSAGE_ID);
$this->unreadCountCache = $cacheFactory->createDistributed(CachePrefix::CHAT_UNREAD_COUNT);
}

/**
Expand Down
1 change: 0 additions & 1 deletion lib/Controller/FederationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@
* @psalm-import-type TalkRoom from ResponseDefinitions
*/
class FederationController extends OCSController {

public function __construct(
IRequest $request,
private FederationManager $federationManager,
Expand Down
19 changes: 16 additions & 3 deletions lib/Controller/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ protected function getTalkHashHeader(): array {
* @param bool $includeStatus Include the user status
* @param int $modifiedSince Filter rooms modified after a timestamp
* @psalm-param non-negative-int $modifiedSince
* @return DataResponse<Http::STATUS_OK, TalkRoom[], array{X-Nextcloud-Talk-Hash: string, X-Nextcloud-Talk-Modified-Before: numeric-string}>
* @return DataResponse<Http::STATUS_OK, TalkRoom[], array{X-Nextcloud-Talk-Hash: string, X-Nextcloud-Talk-Modified-Before: numeric-string, X-Nextcloud-Talk-Federation-Invites?: numeric-string}>
*
* 200: Return list of rooms
*/
Expand All @@ -191,6 +191,7 @@ public function getRooms(int $noStatusUpdate = 0, bool $includeStatus = false, i

$event = new BeforeRoomsFetchEvent($this->userId);
$this->dispatcher->dispatchTyped($event);
$user = $this->userManager->get($this->userId);

if ($noStatusUpdate === 0) {
$isMobileApp = $this->request->isUserAgent([
Expand All @@ -201,7 +202,7 @@ public function getRooms(int $noStatusUpdate = 0, bool $includeStatus = false, i
if ($isMobileApp) {
// Bump the user status again
$event = new UserLiveStatusEvent(
$this->userManager->get($this->userId),
$user,
IUserStatus::ONLINE,
$this->timeFactory->getTime()
);
Expand Down Expand Up @@ -255,7 +256,19 @@ public function getRooms(int $noStatusUpdate = 0, bool $includeStatus = false, i
}
}

return new DataResponse($return, Http::STATUS_OK, array_merge($this->getTalkHashHeader(), ['X-Nextcloud-Talk-Modified-Before' => (string) $nextModifiedSince]));
/** @var array{X-Nextcloud-Talk-Modified-Before: numeric-string, X-Nextcloud-Talk-Federation-Invites?: numeric-string} $headers */
$headers = ['X-Nextcloud-Talk-Modified-Before' => (string) $nextModifiedSince];
if ($this->talkConfig->isFederationEnabledForUserId($user)) {
$numInvites = $this->federationManager->getNumberOfPendingInvitationsForUser($user);
if ($numInvites !== 0) {
$headers['X-Nextcloud-Talk-Federation-Invites'] = (string) $numInvites;
}
}

/** @var array{X-Nextcloud-Talk-Hash: string, X-Nextcloud-Talk-Modified-Before: numeric-string, X-Nextcloud-Talk-Federation-Invites?: numeric-string} $headers */
$headers = array_merge($this->getTalkHashHeader(), $headers);

return new DataResponse($return, Http::STATUS_OK, $headers);
}

/**
Expand Down
3 changes: 2 additions & 1 deletion lib/Federation/CloudFederationProviderTalk.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Exception;
use OCA\FederatedFileSharing\AddressHandler;
use OCA\Talk\AppInfo\Application;
use OCA\Talk\CachePrefix;
use OCA\Talk\Config;
use OCA\Talk\Events\AAttendeeRemovedEvent;
use OCA\Talk\Events\ARoomModifiedEvent;
Expand Down Expand Up @@ -97,7 +98,7 @@ public function __construct(
private UserConverter $userConverter,
ICacheFactory $cacheFactory,
) {
$this->proxyCacheMessages = $cacheFactory->isAvailable() ? $cacheFactory->createDistributed('talk/pcm/') : null;
$this->proxyCacheMessages = $cacheFactory->isAvailable() ? $cacheFactory->createDistributed(CachePrefix::FEDERATED_PCM) : null;
}

/**
Expand Down
4 changes: 4 additions & 0 deletions lib/Federation/FederationManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ public function getRemoteRoomShares(IUser $user): array {
return $this->invitationMapper->getInvitationsForUser($user);
}

public function getNumberOfPendingInvitationsForUser(IUser $user): int {
return $this->invitationMapper->countInvitationsForUser($user, Invitation::STATE_PENDING);
}

public function getNumberOfInvitations(Room $room): int {
return $this->invitationMapper->countInvitationsForLocalRoom($room);
}
Expand Down
3 changes: 2 additions & 1 deletion lib/Federation/Proxy/TalkV1/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

namespace OCA\Talk\Federation\Proxy\TalkV1\Controller;

use OCA\Talk\CachePrefix;
use OCA\Talk\Chat\Notifier;
use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Federation\Proxy\TalkV1\ProxyRequest;
Expand Down Expand Up @@ -57,7 +58,7 @@ public function __construct(
protected Notifier $notifier,
ICacheFactory $cacheFactory,
) {
$this->proxyCacheMessages = $cacheFactory->isAvailable() ? $cacheFactory->createDistributed('talk/pcm/') : null;
$this->proxyCacheMessages = $cacheFactory->isAvailable() ? $cacheFactory->createDistributed(CachePrefix::FEDERATED_PCM) : null;
}

/**
Expand Down
21 changes: 21 additions & 0 deletions lib/Model/InvitationMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,27 @@ public function getInvitationsForUser(IUser $user): array {
return $this->findEntities($qb);
}

/**
* @psalm-param Invitation::STATE_*|null $state
*/
public function countInvitationsForUser(IUser $user, ?int $state = null): int {
$qb = $this->db->getQueryBuilder();

$qb->select($qb->func()->count('*'))
->from($this->getTableName())
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($user->getUID())));

if ($state !== null) {
$qb->andWhere($qb->expr()->eq('state', $qb->createNamedParameter($state)));
}

$result = $qb->executeQuery();
$count = (int) $result->fetchOne();
$result->closeCursor();

return $count;
}

/**
* @throws DoesNotExistException
*/
Expand Down
5 changes: 3 additions & 2 deletions lib/Service/ParticipantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
use OCA\Circles\CirclesManager;
use OCA\Circles\Model\Circle;
use OCA\Circles\Model\Member;
use OCA\Talk\CachePrefix;
use OCA\Talk\Config;
use OCA\Talk\Events\AAttendeeRemovedEvent;
use OCA\Talk\Events\AParticipantModifiedEvent;
Expand Down Expand Up @@ -562,9 +563,9 @@ protected function updateRoomLastMessage(Room $room, IComment $message): void {
$roomService = Server::get(RoomService::class);
$roomService->setLastMessage($room, $message);

$lastMessageCache = $this->cacheFactory->createDistributed('talk/lastmsgid');
$lastMessageCache = $this->cacheFactory->createDistributed(CachePrefix::CHAT_LAST_MESSAGE_ID);
$lastMessageCache->remove($room->getToken());
$unreadCountCache = $this->cacheFactory->createDistributed('talk/unreadcount');
$unreadCountCache = $this->cacheFactory->createDistributed(CachePrefix::CHAT_UNREAD_COUNT);
$unreadCountCache->clear($room->getId() . '-');

$event = new SystemMessagesMultipleSentEvent($room, $message);
Expand Down
3 changes: 2 additions & 1 deletion lib/Signaling/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

namespace OCA\Talk\Signaling;

use OCA\Talk\CachePrefix;
use OCA\Talk\Config;
use OCA\Talk\Room;
use OCA\Talk\Service\RoomService;
Expand All @@ -42,7 +43,7 @@ public function __construct(
protected RoomService $roomService,
ICacheFactory $cacheFactory,
) {
$this->cache = $cacheFactory->createDistributed('hpb_servers');
$this->cache = $cacheFactory->createDistributed(CachePrefix::SIGNALING_ASSIGNED_SERVER);
}

public function isCompatibleSignalingServer(IResponse $response): bool {
Expand Down
5 changes: 5 additions & 0 deletions openapi-full.json
Original file line number Diff line number Diff line change
Expand Up @@ -9560,6 +9560,11 @@
"schema": {
"type": "string"
}
},
"X-Nextcloud-Talk-Federation-Invites": {
"schema": {
"type": "string"
}
}
},
"content": {
Expand Down
5 changes: 5 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -9447,6 +9447,11 @@
"schema": {
"type": "string"
}
},
"X-Nextcloud-Talk-Federation-Invites": {
"schema": {
"type": "string"
}
}
},
"content": {
Expand Down
1 change: 1 addition & 0 deletions src/types/openapi/openapi-full.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3640,6 +3640,7 @@ export type operations = {
headers: {
"X-Nextcloud-Talk-Hash"?: string;
"X-Nextcloud-Talk-Modified-Before"?: string;
"X-Nextcloud-Talk-Federation-Invites"?: string;
};
content: {
"application/json": {
Expand Down
1 change: 1 addition & 0 deletions src/types/openapi/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3463,6 +3463,7 @@ export type operations = {
headers: {
"X-Nextcloud-Talk-Hash"?: string;
"X-Nextcloud-Talk-Modified-Before"?: string;
"X-Nextcloud-Talk-Federation-Invites"?: string;
};
content: {
"application/json": {
Expand Down
14 changes: 14 additions & 0 deletions tests/integration/features/bootstrap/FeatureContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -3026,6 +3026,20 @@ public function hasChatLastCommonReadHeader($setOrLower, $message) {
}
}

/**
* @Then /^last response has federation invites header set to "([^"]*)"$/
*
* @param string $count
*/
public function hasFederationInvitesHeader(string $count): void {
if ($count === 'NULL') {
Assert::assertFalse($this->response->hasHeader('X-Nextcloud-Talk-Federation-Invites'), "Should not contain 'X-Nextcloud-Talk-Federation-Invites' header\n" . json_encode($this->response->getHeaders(), JSON_PRETTY_PRINT));
} else {
Assert::assertTrue($this->response->hasHeader('X-Nextcloud-Talk-Federation-Invites'), "Should contain 'X-Nextcloud-Talk-Federation-Invites' header\n" . json_encode($this->response->getHeaders(), JSON_PRETTY_PRINT));
Assert::assertEquals($count, $this->response->getHeader('X-Nextcloud-Talk-Federation-Invites')[0]);
}
}

/**
* @Then /^user "([^"]*)" creates (\d+) (automatic|manual|free) breakout rooms for "([^"]*)" with (\d+) \((v1)\)$/
*
Expand Down
7 changes: 7 additions & 0 deletions tests/integration/features/federation/invite.feature
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ Feature: federation/invite
| room | users | participant1 | federated_user_added | You invited {federated_user} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"federated_user":{"type":"user","id":"participant2","name":"participant2@localhost:8180","server":"http:\/\/localhost:8180"}} |
| room | users | participant1 | conversation_created | You created the conversation | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} |
And user "participant1" adds federated_user "participant2" to room "room" with 200 (v4)
Then user "participant2" is participant of the following rooms (v4)
| id | name | type |
Then last response has federation invites header set to "1"
When user "participant1" sees the following attendees in room "room" with 200 (v4)
| actorType | actorId | participantType |
| users | participant1 | 1 |
Expand All @@ -59,6 +62,10 @@ Feature: federation/invite
And user "participant2" accepts invite to room "room" of server "LOCAL" with 200 (v1)
| id | name | type | remoteServer | remoteToken |
| room | room | 3 | LOCAL | room |
Then user "participant2" is participant of the following rooms (v4)
| id | name | type | remoteServer | remoteToken |
| room | room | 3 | LOCAL | room |
Then last response has federation invites header set to "NULL"
And user "participant2" accepts invite to room "room" of server "LOCAL" with 400 (v1)
| error | state |
And user "participant2" declines invite to room "room" of server "LOCAL" with 400 (v1)
Expand Down

0 comments on commit 6962f78

Please sign in to comment.