summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2024-03-15 17:45:48 +0100
committerGitHub <noreply@github.com>2024-03-15 17:45:48 +0100
commit61bea6f438d1ebe4ca63dc0dbc4270ad480e92e2 (patch)
tree8793b6fa0994626e4b5d1812453c22025f9914d2
parent046c66610ac0f11d1a96656714c353cb656b4563 (diff)
parent99cafa2ceb247aa5541cc80ca3b4fcbd9d07c91d (diff)
Merge pull request #11814 from nextcloud/feature/noid/federated-chat-reminders
feat(federation): Implement reminders for federated messages
-rw-r--r--docs/chat.md3
-rw-r--r--lib/Controller/ChatController.php56
-rw-r--r--lib/Manager.php22
-rw-r--r--lib/Model/ReminderMapper.php3
-rw-r--r--lib/Notification/Notifier.php6
-rw-r--r--lib/Service/ProxyCacheMessageService.php128
-rw-r--r--lib/Service/ReminderService.php62
-rw-r--r--lib/Service/RoomFormatter.php5
-rw-r--r--openapi-full.json36
-rw-r--r--openapi.json36
-rw-r--r--src/types/openapi/openapi-full.ts16
-rw-r--r--src/types/openapi/openapi.ts16
-rw-r--r--tests/integration/features/federation/chat.feature7
-rw-r--r--tests/integration/features/federation/reminder.feature75
-rw-r--r--tests/php/Controller/ChatControllerTest.php4
15 files changed, 422 insertions, 53 deletions
diff --git a/docs/chat.md b/docs/chat.md
index 7280253e7..161aceb18 100644
--- a/docs/chat.md
+++ b/docs/chat.md
@@ -351,6 +351,7 @@ See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob
## Set reminder for chat message
* Required capability: `remind-me-later`
+* Federation capability: `federation-v1`
* Method: `POST`
* Endpoint: `/chat/{token}/{messageId}/reminder`
* Data:
@@ -379,6 +380,7 @@ See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob
## Get reminder for chat message
* Required capability: `remind-me-later`
+* Federation capability: `federation-v1`
* Method: `GET`
* Endpoint: `/chat/{token}/{messageId}/reminder`
@@ -403,6 +405,7 @@ See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob
## Delete reminder for chat message
* Required capability: `remind-me-later`
+* Federation capability: `federation-v1`
* Method: `DELETE`
* Endpoint: `/chat/{token}/{messageId}/reminder`
diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php
index bad6ee7ea..c1bd4907a 100644
--- a/lib/Controller/ChatController.php
+++ b/lib/Controller/ChatController.php
@@ -31,6 +31,7 @@ use OCA\Talk\Chat\AutoComplete\Sorter;
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\Chat\ReactionManager;
+use OCA\Talk\Exceptions\CannotReachRemoteException;
use OCA\Talk\Federation\Authenticator;
use OCA\Talk\GuestManager;
use OCA\Talk\MatterbridgeManager;
@@ -54,6 +55,7 @@ use OCA\Talk\Service\AttachmentService;
use OCA\Talk\Service\AvatarService;
use OCA\Talk\Service\BotService;
use OCA\Talk\Service\ParticipantService;
+use OCA\Talk\Service\ProxyCacheMessageService;
use OCA\Talk\Service\ReminderService;
use OCA\Talk\Service\RoomFormatter;
use OCA\Talk\Service\SessionService;
@@ -127,6 +129,7 @@ class ChatController extends AEnvironmentAwareController {
protected ITrustedDomainHelper $trustedDomainHelper,
private IL10N $l,
protected Authenticator $federationAuthenticator,
+ protected ProxyCacheMessageService $pcmService,
) {
parent::__construct($appName, $request);
}
@@ -914,20 +917,21 @@ class ChatController extends AEnvironmentAwareController {
* @psalm-param non-negative-int $messageId
* @param int $timestamp Timestamp of the reminder
* @psalm-param non-negative-int $timestamp
- * @return DataResponse<Http::STATUS_CREATED, TalkChatReminder, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
+ * @return DataResponse<Http::STATUS_CREATED, TalkChatReminder, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error?: string}, array{}>
*
* 201: Reminder created successfully
* 404: Message not found
*/
+ #[FederationSupported]
#[NoAdminRequired]
#[RequireModeratorOrNoLobby]
#[RequireLoggedInParticipant]
#[UserRateLimit(limit: 60, period: 3600)]
public function setReminder(int $messageId, int $timestamp): DataResponse {
try {
- $this->chatManager->getComment($this->room, (string) $messageId);
- } catch (NotFoundException) {
- return new DataResponse([], Http::STATUS_NOT_FOUND);
+ $this->validateMessageExists($messageId, sync: true);
+ } catch (DoesNotExistException) {
+ return new DataResponse(['error' => 'message'], Http::STATUS_NOT_FOUND);
}
$reminder = $this->reminderService->setReminder(
@@ -945,30 +949,32 @@ class ChatController extends AEnvironmentAwareController {
*
* @param int $messageId ID of the message
* @psalm-param non-negative-int $messageId
- * @return DataResponse<Http::STATUS_OK, TalkChatReminder, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array<empty>, array{}>
+ * @return DataResponse<Http::STATUS_OK, TalkChatReminder, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error?: string}, array{}>
*
* 200: Reminder returned
* 404: No reminder found
* 404: Message not found
*/
+ #[FederationSupported]
#[NoAdminRequired]
#[RequireModeratorOrNoLobby]
#[RequireLoggedInParticipant]
public function getReminder(int $messageId): DataResponse {
try {
- $this->chatManager->getComment($this->room, (string) $messageId);
- } catch (NotFoundException) {
- return new DataResponse([], Http::STATUS_NOT_FOUND);
+ $this->validateMessageExists($messageId);
+ } catch (DoesNotExistException) {
+ return new DataResponse(['error' => 'message'], Http::STATUS_NOT_FOUND);
}
try {
$reminder = $this->reminderService->getReminder(
$this->participant->getAttendee()->getActorId(),
+ $this->room->getToken(),
$messageId,
);
return new DataResponse($reminder->jsonSerialize(), Http::STATUS_OK);
} catch (DoesNotExistException) {
- return new DataResponse([], Http::STATUS_NOT_FOUND);
+ return new DataResponse(['error' => 'reminder'], Http::STATUS_NOT_FOUND);
}
}
@@ -977,19 +983,20 @@ class ChatController extends AEnvironmentAwareController {
*
* @param int $messageId ID of the message
* @psalm-param non-negative-int $messageId
- * @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, array<empty>, array{}>
+ * @return DataResponse<Http::STATUS_OK|Http::STATUS_NOT_FOUND, array{error?: string}, array{}>
*
* 200: Reminder deleted successfully
* 404: Message not found
*/
+ #[FederationSupported]
#[NoAdminRequired]
#[RequireModeratorOrNoLobby]
#[RequireLoggedInParticipant]
public function deleteReminder(int $messageId): DataResponse {
try {
- $this->chatManager->getComment($this->room, (string) $messageId);
- } catch (NotFoundException) {
- return new DataResponse([], Http::STATUS_NOT_FOUND);
+ $this->validateMessageExists($messageId);
+ } catch (DoesNotExistException) {
+ return new DataResponse(['error' => 'message'], Http::STATUS_NOT_FOUND);
}
$this->reminderService->deleteReminder(
@@ -1002,6 +1009,29 @@ class ChatController extends AEnvironmentAwareController {
}
/**
+ * @throws DoesNotExistException
+ * @throws CannotReachRemoteException
+ */
+ protected function validateMessageExists(int $messageId, bool $sync = false): void {
+ if ($this->room->isFederatedConversation()) {
+ try {
+ $this->pcmService->findByRemote($this->room->getRemoteServer(), $this->room->getRemoteToken(), $messageId);
+ } catch (DoesNotExistException) {
+ if ($sync) {
+ $this->pcmService->syncRemoteMessage($this->room, $this->participant, $messageId);
+ }
+ }
+ return;
+ }
+
+ try {
+ $this->chatManager->getComment($this->room, (string)$messageId);
+ } catch (NotFoundException $e) {
+ throw new DoesNotExistException($e->getMessage());
+ }
+ }
+
+ /**
* Clear the chat history
*
* @return DataResponse<Http::STATUS_OK|Http::STATUS_ACCEPTED, TalkChatMessage, array{X-Chat-Last-Common-Read?: numeric-string}>|DataResponse<Http::STATUS_FORBIDDEN, array<empty>, array{}>
diff --git a/lib/Manager.php b/lib/Manager.php
index b9a81c65e..5b6f7c183 100644
--- a/lib/Manager.php
+++ b/lib/Manager.php
@@ -883,6 +883,28 @@ class Manager {
}
/**
+ * @param string[] $tokens
+ * @return array<string, Room>
+ */
+ public function getRoomsByToken(array $tokens): array {
+ $query = $this->db->getQueryBuilder();
+ $helper = new SelectHelper();
+ $helper->selectRoomsTable($query);
+ $query->from('talk_rooms', 'r')
+ ->where($query->expr()->in('r.token', $query->createNamedParameter($tokens, IQueryBuilder::PARAM_STR_ARRAY)));
+
+ $result = $query->executeQuery();
+ $rooms = [];
+ while ($row = $result->fetch()) {
+ $room = $this->createRoomObject($row);
+ $rooms[$room->getToken()] = $room;
+ }
+ $result->closeCursor();
+
+ return $rooms;
+ }
+
+ /**
* @param string|null $userId
* @param string|null $sessionId
* @return Room
diff --git a/lib/Model/ReminderMapper.php b/lib/Model/ReminderMapper.php
index 51fada9b9..c14a9273f 100644
--- a/lib/Model/ReminderMapper.php
+++ b/lib/Model/ReminderMapper.php
@@ -49,11 +49,12 @@ class ReminderMapper extends QBMapper {
/**
* @throws DoesNotExistException
*/
- public function findForUserAndMessage(string $userId, int $messageId): Reminder {
+ public function findForUserAndMessage(string $userId, string $token, int $messageId): Reminder {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('user_id', $query->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
+ ->andWhere($query->expr()->eq('token', $query->createNamedParameter($token, IQueryBuilder::PARAM_STR)))
->andWhere($query->expr()->eq('message_id', $query->createNamedParameter($messageId, IQueryBuilder::PARAM_INT)));
return $this->findEntity($query);
diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php
index 7896ac6cf..88e93aa64 100644
--- a/lib/Notification/Notifier.php
+++ b/lib/Notification/Notifier.php
@@ -625,7 +625,7 @@ class Notifier implements INotifier {
'name' => $shortenMessage,
];
if ($notification->getSubject() === 'reminder') {
- if ($comment->getActorId() === $notification->getUser()) {
+ if ($message->getActorId() === $notification->getUser()) {
// TRANSLATORS Reminder for a message you sent in the conversation {call}
$subject = $l->t('Reminder: You in {call}') . "\n{message}";
} elseif ($room->getType() === Room::TYPE_ONE_TO_ONE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
@@ -716,7 +716,7 @@ class Notifier implements INotifier {
}
} elseif ($notification->getSubject() === 'reminder') {
if ($room->getType() === Room::TYPE_ONE_TO_ONE || $room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
- if ($comment->getActorId() === $notification->getUser()) {
+ if ($message->getActorId() === $notification->getUser()) {
$subject = $l->t('Reminder: You in private conversation {call}');
} elseif ($room->getType() === Room::TYPE_ONE_TO_ONE_FORMER) {
$subject = $l->t('Reminder: A deleted user in private conversation {call}');
@@ -724,7 +724,7 @@ class Notifier implements INotifier {
$subject = $l->t('Reminder: {user} in private conversation');
}
} elseif ($richSubjectUser) {
- if ($comment->getActorId() === $notification->getUser()) {
+ if ($message->getActorId() === $notification->getUser()) {
$subject = $l->t('Reminder: You in conversation {call}');
} else {
$subject = $l->t('Reminder: {user} in conversation {call}');
diff --git a/lib/Service/ProxyCacheMessageService.php b/lib/Service/ProxyCacheMessageService.php
new file mode 100644
index 000000000..a7ebb1c8e
--- /dev/null
+++ b/lib/Service/ProxyCacheMessageService.php
@@ -0,0 +1,128 @@
+<?php
+
+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\Service;
+
+use OCA\Talk\Exceptions\CannotReachRemoteException;
+use OCA\Talk\Model\Message;
+use OCA\Talk\Model\ProxyCacheMessage;
+use OCA\Talk\Model\ProxyCacheMessageMapper;
+use OCA\Talk\Participant;
+use OCA\Talk\ResponseDefinitions;
+use OCA\Talk\Room;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Http;
+use OCP\DB\Exception as DBException;
+use Psr\Log\LoggerInterface;
+
+/**
+ * @psalm-import-type TalkChatMessageWithParent from ResponseDefinitions
+ */
+class ProxyCacheMessageService {
+ public function __construct(
+ protected ProxyCacheMessageMapper $mapper,
+ protected LoggerInterface $logger,
+ ) {
+ }
+
+ /**
+ * @throws DoesNotExistException
+ */
+ public function findByRemote(string $remoteServerUrl, string $remoteToken, int $remoteMessageId): ProxyCacheMessage {
+ return $this->mapper->findByRemote($remoteServerUrl, $remoteToken, $remoteMessageId);
+ }
+
+ /**
+ * @throws \InvalidArgumentException
+ * @throws CannotReachRemoteException
+ */
+ public function syncRemoteMessage(Room $room, Participant $participant, int $messageId): ProxyCacheMessage {
+ if (!$room->isFederatedConversation()) {
+ throw new \InvalidArgumentException('room');
+ }
+
+ /** @var \OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController $proxy */
+ $proxy = \OCP\Server::get(\OCA\Talk\Federation\Proxy\TalkV1\Controller\ChatController::class);
+ $ocsResponse = $proxy->getMessageContext($room, $participant, $messageId, 1);
+
+ if ($ocsResponse->getStatus() !== Http::STATUS_OK || !isset($ocsResponse->getData()[0])) {
+ throw new \InvalidArgumentException('message');
+ }
+
+ /** @var TalkChatMessageWithParent $messageData */
+ $messageData = $ocsResponse->getData()[0];
+
+ $proxy = new ProxyCacheMessage();
+ $proxy->setLocalToken($room->getToken());
+ $proxy->setRemoteServerUrl($room->getRemoteServer());
+ $proxy->setRemoteToken($room->getRemoteToken());
+ $proxy->setRemoteMessageId($messageData['id']);
+ $proxy->setActorType($messageData['actorType']);
+ $proxy->setActorId($messageData['actorId']);
+ $proxy->setActorDisplayName($messageData['actorDisplayName']);
+ $proxy->setMessageType($messageData['messageType']);
+ $proxy->setSystemMessage($messageData['systemMessage']);
+ if ($messageData['expirationTimestamp']) {
+ $proxy->setExpirationDatetime(new \DateTime('@' . $messageData['expirationTimestamp']));
+ }
+ $proxy->setCreationDatetime(new \DateTime('@' . $messageData['timestamp']));
+ $proxy->setMessage($messageData['message']);
+ $proxy->setMessageParameters(json_encode($messageData['messageParameters']));
+
+ $metaData = [];
+ if (!empty($messageData['lastEditActorType']) && !empty($messageData['lastEditActorId'])) {
+ $metaData[Message::METADATA_LAST_EDITED_BY_TYPE] = $messageData['lastEditActorType'];
+ $metaData[Message::METADATA_LAST_EDITED_BY_ID] = $messageData['lastEditActorId'];
+ }
+ if (!empty($messageData['lastEditTimestamp'])) {
+ $metaData[Message::METADATA_LAST_EDITED_TIME] = $messageData['lastEditTimestamp'];
+ }
+ if (!empty($messageData['silent'])) {
+ $metaData[Message::METADATA_SILENT] = $messageData['silent'];
+ }
+ $proxy->setMetaData(json_encode($metaData));
+
+ try {
+ $this->mapper->insert($proxy);
+ } 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->getReason() !== DBException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
+ $this->logger->error('Error saving proxy cache message failed: ' . $e->getMessage(), ['exception' => $e]);
+ throw $e;
+ }
+
+ $proxy = $this->mapper->findByRemote(
+ $room->getRemoteServer(),
+ $room->getRemoteToken(),
+ $messageData['id'],
+ );
+ }
+
+ return $proxy;
+ }
+}
diff --git a/lib/Service/ReminderService.php b/lib/Service/ReminderService.php
index f88bbdf36..13396b3b8 100644
--- a/lib/Service/ReminderService.php
+++ b/lib/Service/ReminderService.php
@@ -28,6 +28,8 @@ namespace OCA\Talk\Service;
use OCA\Talk\AppInfo\Application;
use OCA\Talk\Chat\ChatManager;
+use OCA\Talk\Manager;
+use OCA\Talk\Model\ProxyCacheMessage;
use OCA\Talk\Model\Reminder;
use OCA\Talk\Model\ReminderMapper;
use OCP\AppFramework\Db\DoesNotExistException;
@@ -38,12 +40,14 @@ class ReminderService {
protected IManager $notificationManager,
protected ReminderMapper $reminderMapper,
protected ChatManager $chatManager,
+ protected ProxyCacheMessageService $pcmService,
+ protected Manager $manager,
) {
}
public function setReminder(string $userId, string $token, int $messageId, int $timestamp): Reminder {
try {
- $reminder = $this->reminderMapper->findForUserAndMessage($userId, $messageId);
+ $reminder = $this->reminderMapper->findForUserAndMessage($userId, $token, $messageId);
$reminder->setDateTime(new \DateTime('@' . $timestamp));
$this->reminderMapper->update($reminder);
@@ -62,13 +66,13 @@ class ReminderService {
/**