Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(federation): Federate unread information to proxies #11674

Merged
merged 7 commits into from
Feb 29, 2024
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
Loading