Skip to content

Commit

Permalink
Merge pull request #11674 from nextcloud/feat/11272/unread-info-for-f…
Browse files Browse the repository at this point in the history
…ederated-users

feat(federation): Federate unread information to proxies
  • Loading branch information
nickvergessen authored Feb 29, 2024
2 parents c07fb6f + c2a9f36 commit 84f7ad3
Show file tree
Hide file tree
Showing 19 changed files with 619 additions and 58 deletions.
8 changes: 8 additions & 0 deletions docs/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,10 @@ See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob
+ `404 Not Found` When the room could not be found for the participant,
or the participant is a guest.

- Data in case of `200 OK`:
+ **Without** `federation-v1` capability empty
+ **With** `federation-v1` capability, see array definition in [Get user´s conversations](conversation.md#get-user-s-conversations)

- Header:

| field | type | Description |
Expand All @@ -444,6 +448,10 @@ See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob
+ `404 Not Found` When the room could not be found for the participant,
or the participant is a guest.

- Data in case of `200 OK`:
+ **Without** `federation-v1` capability empty
+ **With** `federation-v1` capability, see array definition in [Get user´s conversations](conversation.md#get-user-s-conversations)

- Header:

| field | type | Description |
Expand Down
24 changes: 20 additions & 4 deletions lib/Chat/ChatManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -177,25 +177,31 @@ public function addSystemMessage(
if ($sendNotifications) {
/** @var ?IComment $captionComment */
$captionComment = null;
$alreadyNotifiedUsers = $usersDirectlyMentioned = [];
$alreadyNotifiedUsers = $usersDirectlyMentioned = $federatedUsersDirectlyMentioned = [];
if ($messageType === 'file_shared') {
if (isset($messageDecoded['parameters']['metaData']['caption'])) {
$captionComment = clone $comment;
$captionComment->setMessage($messageDecoded['parameters']['metaData']['caption'], self::MAX_CHAT_LENGTH);
$usersDirectlyMentioned = $this->notifier->getMentionedUserIds($captionComment);
$federatedUsersDirectlyMentioned = $this->notifier->getMentionedCloudIds($captionComment);
}
if ($replyTo instanceof IComment) {
$alreadyNotifiedUsers = $this->notifier->notifyReplyToAuthor($chat, $comment, $replyTo, $silent);
if ($replyTo->getActorType() === Attendee::ACTOR_USERS) {
$usersDirectlyMentioned[] = $replyTo->getActorId();
} elseif ($replyTo->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
$federatedUsersDirectlyMentioned[] = $replyTo->getActorId();
}
}
}

$alreadyNotifiedUsers = $this->notifier->notifyMentionedUsers($chat, $captionComment ?? $comment, $alreadyNotifiedUsers, $silent);
if (!empty($alreadyNotifiedUsers)) {
$userIds = array_column($alreadyNotifiedUsers, 'id');
$this->participantService->markUsersAsMentioned($chat, $userIds, (int) $comment->getId(), $usersDirectlyMentioned);
$this->participantService->markUsersAsMentioned($chat, Attendee::ACTOR_USERS, $userIds, (int) $comment->getId(), $usersDirectlyMentioned);
}
if (!empty($federatedUsersDirectlyMentioned)) {
$this->participantService->markUsersAsMentioned($chat, Attendee::ACTOR_FEDERATED_USERS, $federatedUsersDirectlyMentioned, (int) $comment->getId(), $federatedUsersDirectlyMentioned);
}

$this->notifier->notifyOtherParticipant($chat, $comment, [], $silent);
Expand Down Expand Up @@ -326,17 +332,23 @@ public function sendMessage(Room $chat, ?Participant $participant, string $actor

$alreadyNotifiedUsers = [];
$usersDirectlyMentioned = $this->notifier->getMentionedUserIds($comment);
$federatedUsersDirectlyMentioned = $this->notifier->getMentionedCloudIds($comment);
if ($replyTo instanceof IComment) {
$alreadyNotifiedUsers = $this->notifier->notifyReplyToAuthor($chat, $comment, $replyTo, $silent);
if ($replyTo->getActorType() === Attendee::ACTOR_USERS) {
$usersDirectlyMentioned[] = $replyTo->getActorId();
} elseif ($replyTo->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
$federatedUsersDirectlyMentioned[] = $replyTo->getActorId();
}
}

$alreadyNotifiedUsers = $this->notifier->notifyMentionedUsers($chat, $comment, $alreadyNotifiedUsers, $silent);
if (!empty($alreadyNotifiedUsers)) {
$userIds = array_column($alreadyNotifiedUsers, 'id');
$this->participantService->markUsersAsMentioned($chat, $userIds, (int) $comment->getId(), $usersDirectlyMentioned);
$this->participantService->markUsersAsMentioned($chat, Attendee::ACTOR_USERS, $userIds, (int) $comment->getId(), $usersDirectlyMentioned);
}
if (!empty($federatedUsersDirectlyMentioned)) {
$this->participantService->markUsersAsMentioned($chat, Attendee::ACTOR_FEDERATED_USERS, $federatedUsersDirectlyMentioned, (int) $comment->getId(), $federatedUsersDirectlyMentioned);
}

// User was not mentioned, send a normal notification
Expand Down Expand Up @@ -561,12 +573,16 @@ public function editMessage(Room $chat, IComment $comment, Participant $particip

if (!empty($addedMentions)) {
$usersDirectlyMentionedAfter = $this->notifier->getMentionedUserIds($comment);
$federatedUsersDirectlyMentionedAfter = $this->notifier->getMentionedCloudIds($comment);
$addedUsersDirectMentioned = array_diff($usersDirectlyMentionedAfter, $usersDirectlyMentionedBefore);

$alreadyNotifiedUsers = $this->notifier->notifyMentionedUsers($chat, $comment, $usersToNotifyBefore, silent: false);
if (!empty($alreadyNotifiedUsers)) {
$userIds = array_column($alreadyNotifiedUsers, 'id');
$this->participantService->markUsersAsMentioned($chat, $userIds, (int) $comment->getId(), $addedUsersDirectMentioned);
$this->participantService->markUsersAsMentioned($chat, Attendee::ACTOR_USERS, $userIds, (int) $comment->getId(), $addedUsersDirectMentioned);
}
if (!empty($federatedUsersDirectlyMentionedAfter)) {
$this->participantService->markUsersAsMentioned($chat, Attendee::ACTOR_FEDERATED_USERS, $federatedUsersDirectlyMentionedAfter, (int) $comment->getId(), $federatedUsersDirectlyMentionedAfter);
}
}
}
Expand Down
56 changes: 53 additions & 3 deletions lib/Chat/Notifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,21 @@ private function addMentionAllToList(Room $chat, array $list): array {
* @psalm-return array<int, array{id: string, type: string, reason: string}>
*/
public function notifyReplyToAuthor(Room $chat, IComment $comment, IComment $replyTo, bool $silent): array {
if ($replyTo->getActorType() !== Attendee::ACTOR_USERS) {
// No reply notification when the replyTo-author was not a user
if ($replyTo->getActorType() !== Attendee::ACTOR_USERS && $replyTo->getActorType() !== Attendee::ACTOR_FEDERATED_USERS) {
// No reply notification when the replyTo-author was not a user or federated user
return [];
}

if ($replyTo->getActorType() === Attendee::ACTOR_FEDERATED_USERS) {
return [
[
'id' => $replyTo->getActorId(),
'type' => $replyTo->getActorType(),
'reason' => 'reply',
],
];
}

if (!$this->shouldMentionedUserBeNotified($replyTo->getActorId(), $comment, $chat)) {
return [];
}
Expand Down Expand Up @@ -407,6 +417,19 @@ public function getMentionedUserIds(IComment $comment): array {
}, $mentionedUsers);
}

/**
* Returns the cloud IDs of the federated users mentioned in the given comment.
*
* @param IComment $comment
* @return string[] the mentioned cloud IDs
*/
public function getMentionedCloudIds(IComment $comment): array {
$mentionedFederatedUsers = $this->getMentionedFederatedUsers($comment);
return array_map(static function ($mentionedUser) {
return $mentionedUser['id'];
}, $mentionedFederatedUsers);
}

/**
* @param IComment $comment
* @return array[]
Expand All @@ -427,7 +450,34 @@ private function getMentionedUsers(IComment $comment): array {

$mentionedUsers[] = [
'id' => $mention['id'],
'type' => 'users',
'type' => Attendee::ACTOR_USERS,
'reason' => 'direct',
];
}
return $mentionedUsers;
}

/**
* @param IComment $comment
* @return array[]
* @psalm-return array<int, array{type: string, id: string, reason: string}>
*/
private function getMentionedFederatedUsers(IComment $comment): array {
$mentions = $comment->getMentions();

if (empty($mentions)) {
return [];
}

$mentionedUsers = [];
foreach ($mentions as $mention) {
if ($mention['type'] !== 'federated_user') {
continue;
}

$mentionedUsers[] = [
'id' => $mention['id'],
'type' => Attendee::ACTOR_FEDERATED_USERS,
'reason' => 'direct',
];
}
Expand Down
46 changes: 36 additions & 10 deletions lib/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
use OCA\Talk\Service\BotService;
use OCA\Talk\Service\ParticipantService;
use OCA\Talk\Service\ReminderService;
use OCA\Talk\Service\RoomFormatter;
use OCA\Talk\Service\SessionService;
use OCA\Talk\Share\Helper\FilesMetadataCache;
use OCA\Talk\Share\RoomShareProvider;
Expand Down Expand Up @@ -90,6 +91,7 @@
* @psalm-import-type TalkChatMessage from ResponseDefinitions
* @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
* @psalm-import-type TalkChatReminder from ResponseDefinitions
* @psalm-import-type TalkRoom from ResponseDefinitions
*/
class ChatController extends AEnvironmentAwareController {
/** @var string[] */
Expand All @@ -102,6 +104,7 @@ public function __construct(
private IUserManager $userManager,
private IAppManager $appManager,
private ChatManager $chatManager,
private RoomFormatter $roomFormatter,
private ReactionManager $reactionManager,
private ParticipantService $participantService,
private SessionService $sessionService,
Expand Down Expand Up @@ -1045,31 +1048,54 @@ public function clearHistory(): DataResponse {
*
* @param int $lastReadMessage ID if the last read message
* @psalm-param non-negative-int $lastReadMessage
* @return DataResponse<Http::STATUS_OK, array<empty>, array{X-Chat-Last-Common-Read?: numeric-string}>
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{X-Chat-Last-Common-Read?: numeric-string}>
*
* 200: Read marker set successfully
*/
#[NoAdminRequired]
#[RequireParticipant]
#[FederationSupported]
#[PublicPage]
#[RequireAuthenticatedParticipant]
public function setReadMarker(int $lastReadMessage): DataResponse {
if ($this->room->getRemoteServer() !== '') {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController::class);
return $proxy->setReadMarker($this->room, $this->participant, $this->getResponseFormat(), $lastReadMessage);
}

$this->participantService->updateLastReadMessage($this->participant, $lastReadMessage);
$headers = [];
if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) {
$headers = ['X-Chat-Last-Common-Read' => (string) $this->chatManager->getLastCommonReadMessage($this->room)];
$attendee = $this->participant->getAttendee();

$headers = $lastCommonRead = [];
if ($attendee->getReadPrivacy() === Participant::PRIVACY_PUBLIC) {
$lastCommonRead[$this->room->getId()] = $this->chatManager->getLastCommonReadMessage($this->room);
$headers = ['X-Chat-Last-Common-Read' => (string) $lastCommonRead[$this->room->getId()]];
}
return new DataResponse([], Http::STATUS_OK, $headers);

return new DataResponse($this->roomFormatter->formatRoom(
$this->getResponseFormat(),
$lastCommonRead,
$this->room,
$this->participant,
), Http::STATUS_OK, $headers);
}

/**
* Mark a chat as unread
*
* @return DataResponse<Http::STATUS_OK, array<empty>, array{X-Chat-Last-Common-Read?: numeric-string}>
* @return DataResponse<Http::STATUS_OK, TalkRoom, array{X-Chat-Last-Common-Read?: numeric-string}>
*
* 200: Read marker set successfully
*/
#[NoAdminRequired]
#[RequireParticipant]
#[FederationSupported]
#[PublicPage]
#[RequireAuthenticatedParticipant]
public function markUnread(): DataResponse {
if ($this->room->getRemoteServer() !== '') {
/** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */
$proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController::class);
return $proxy->markUnread($this->room, $this->participant, $this->getResponseFormat());
}

$message = $this->room->getLastMessage();
$unreadId = 0;

Expand Down
6 changes: 6 additions & 0 deletions lib/Federation/BackendNotifier.php
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ public function sendRoomModifiedUpdate(
/**
* Send information to remote participants that a message was posted
* Sent from Host server to Remote participant server
*
* @param array{unreadMessages: int, unreadMention: bool, unreadMentionDirect: bool} $unreadInfo
*/
public function sendMessageUpdate(
string $remoteServer,
Expand All @@ -286,6 +288,7 @@ public function sendMessageUpdate(
string $accessToken,
string $localToken,
array $messageData,
array $unreadInfo,
): void {
$remote = $this->prepareRemoteUrl($remoteServer);

Expand All @@ -299,6 +302,7 @@ public function sendMessageUpdate(
'sharedSecret' => $accessToken,
'remoteToken' => $localToken,
'messageData' => $messageData,
'unreadInfo' => $unreadInfo,
],
);

Expand All @@ -324,6 +328,8 @@ public function sendUpdateDataToRemote(string $remote, array $data, int $try): v
}

protected function sendUpdateToRemote(string $remote, ICloudFederationNotification $notification, int $try = 0): void {
\OC::$server->getLogger()->error('sendUpdateToRemote');
\OC::$server->getLogger()->error(json_encode($notification->getMessage()));
$response = $this->federationProviderManager->sendNotification($remote, $notification);
if (!is_array($response)) {
$this->jobList->add(RetryJob::class,
Expand Down
48 changes: 40 additions & 8 deletions lib/Federation/CloudFederationProviderTalk.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use OCA\Talk\Events\AAttendeeRemovedEvent;
use OCA\Talk\Events\ARoomModifiedEvent;
use OCA\Talk\Events\AttendeesAddedEvent;
use OCA\Talk\Exceptions\ParticipantNotFoundException;
use OCA\Talk\Exceptions\RoomNotFoundException;
use OCA\Talk\Manager;
use OCA\Talk\Model\Attendee;
Expand Down Expand Up @@ -309,7 +310,7 @@ private function roomModified(int $remoteAttendeeId, array $notification): array

/**
* @param int $remoteAttendeeId
* @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, messageData: array{remoteMessageId: int, actorType: string, actorId: string, actorDisplayName: string, messageType: string, systemMessage: string, expirationDatetime: string, message: string, messageParameter: string}} $notification
* @param array{remoteServerUrl: string, sharedSecret: string, remoteToken: string, messageData: array{remoteMessageId: int, actorType: string, actorId: string, actorDisplayName: string, messageType: string, systemMessage: string, expirationDatetime: string, message: string, messageParameter: string}, unreadInfo: array{unreadMessages: int, unreadMention: bool, unreadMentionDirect: bool}} $notification
* @return array
* @throws ActionNotSupportedException
* @throws AuthenticationFailedException
Expand Down Expand Up @@ -338,19 +339,50 @@ private function messagePosted(int $remoteAttendeeId, array $notification): arra
$message->setActorDisplayName($notification['messageData']['actorDisplayName']);
$message->setMessageType($notification['messageData']['messageType']);
$message->setSystemMessage($notification['messageData']['systemMessage']);
$message->setExpirationDateTime(new \DateTimeImmutable($notification['messageData']['expirationDatetime']));
if ($notification['messageData']['expirationDatetime']) {
$message->setExpirationDatetime(new \DateTimeImmutable($notification['messageData']['expirationDatetime']));
}
$message->setMessage($notification['messageData']['message']);
$message->setMessageParameters($notification['messageData']['messageParameter']);
$this->proxyCacheMessagesMapper->insert($message);
try {
$this->proxyCacheMessagesMapper->insert($message);

if ($this->proxyCacheMessages instanceof ICache) {
$cacheKey = sha1(json_encode([$notification['remoteServerUrl'], $notification['remoteToken']]));
$cacheData = $this->proxyCacheMessages->get($cacheKey);
if ($cacheData === null || $cacheData < $notification['messageData']['remoteMessageId']) {
$this->proxyCacheMessages->set($cacheKey, $notification['messageData']['remoteMessageId'], 300);
$lastMessageId = $room->getLastMessageId();
if ($notification['messageData']['remoteMessageId'] > $lastMessageId) {
$lastMessageId = (int) $notification['messageData']['remoteMessageId'];
}
$this->roomService->setLastMessageInfo($room, $lastMessageId, new \DateTime());

if ($this->proxyCacheMessages instanceof ICache) {
$cacheKey = sha1(json_encode([$notification['remoteServerUrl'], $notification['remoteToken']]));
$cacheData = $this->proxyCacheMessages->get($cacheKey);
if ($cacheData === null || $cacheData < $notification['messageData']['remoteMessageId']) {
$this->proxyCacheMessages->set($cacheKey, $notification['messageData']['remoteMessageId'], 300);
}
}
} catch (DBException $e) {
// DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION happens when
// multiple users are in the same conversation. We are therefore
// informed multiple times about the same remote message.
if ($e->getCode() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
$this->logger->error('Error saving proxy cache message failed: ' . $e->getMessage(), ['exception' => $e]);
throw $e;
}
}

try {
$participant = $this->participantService->getParticipant($room, $invite->getUserId(), false);
} catch (ParticipantNotFoundException) {
throw new ShareNotFound();
}

$this->participantService->updateUnreadInfoForProxyParticipant(
$participant,
$notification['unreadInfo']['unreadMessages'],
$notification['unreadInfo']['unreadMention'],
$notification['unreadInfo']['unreadMentionDirect'],
);

return [];
}

Expand Down
Loading

0 comments on commit 84f7ad3

Please sign in to comment.