From 117a9857ec08545911d129978e344d1808366091 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Wed, 13 Dec 2023 14:18:22 +0100 Subject: [PATCH] feat(chat): Add basic handling of editing Signed-off-by: Joas Schilling --- docs/chat.md | 7 +++- lib/Chat/ChatManager.php | 41 +++++++++++++++++++++ lib/Controller/ChatController.php | 60 ++++++++++++++++++++++++++++++- lib/Model/Message.php | 10 ++++++ 4 files changed, 116 insertions(+), 2 deletions(-) diff --git a/docs/chat.md b/docs/chat.md index b42428e131f2..3ecd79da616c 100644 --- a/docs/chat.md +++ b/docs/chat.md @@ -314,12 +314,17 @@ See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob * Required capability: `edit-messages` * Method: `PUT` * Endpoint: `/chat/{token}/{messageId}` +* Data: + +| field | type | Description | +|--------------------|--------|-----------------------------------| +| `message` | string | The message the user wants to say | * Response: - Status code: + `200 OK` - When editing was successful + `202 Accepted` - When editing was successful but Matterbridge is enabled so the message was leaked to other services - + `400 Bad Request` The message is already older than 24 hours or another reason why deleting is not okay + + `400 Bad Request` The message is already older than 24 hours or another reason why editing is not okay + `403 Forbidden` When the message is not from the current user and the user not a moderator + `403 Forbidden` When the conversation is read-only + `404 Not Found` When the conversation or chat message could not be found for the participant diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index 99473b1d1f74..96d4763821b6 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -476,6 +476,47 @@ public function deleteMessage(Room $chat, IComment $comment, Participant $partic ); } + /** + * @param Room $chat + * @param IComment $comment + * @param Participant $participant + * @param \DateTime $editTime + * @param string $message + * @return IComment + */ + public function editMessage(Room $chat, IComment $comment, Participant $participant, \DateTime $editTime, string $message): IComment { + // if ($comment->getVerb() === self::VERB_OBJECT_SHARED) { + // $messageData = json_decode($comment->getMessage(), true); + // $this->unshareFileOnMessageDelete($chat, $participant, $messageData); + // $this->removePollOnMessageDelete($chat, $participant, $messageData, $deletionTime); + // } + + // $this->attachmentService->deleteAttachmentByMessageId((int) $comment->getId()); + + $metaData = $comment->getMetaData() ?? []; + $metaData['last_edited_by_type'] = $participant->getAttendee()->getActorType(); + $metaData['last_edited_by_id'] = $participant->getAttendee()->getActorId(); + $metaData['last_edited_time'] = $editTime->getTimestamp(); + $comment->setMetaData($metaData); + $comment->setMessage($message, self::MAX_CHAT_LENGTH); + $this->commentsManager->save($comment); + $this->referenceManager->invalidateCache($chat->getToken()); + + // TODO update mentions/notifications + + return $this->addSystemMessage( + $chat, + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId(), + json_encode(['message' => 'message_edited', 'parameters' => ['message' => $comment->getId()]]), + $this->timeFactory->getDateTime(), + false, + null, + $comment, + true + ); + } + public function clearHistory(Room $chat, string $actorType, string $actorId): IComment { $this->commentsManager->deleteCommentsAtObject('chat', (string) $chat->getId()); diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index bd23f064b5a8..914535ce9c0b 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -751,6 +751,7 @@ public function deleteMessage(int $messageId): DataResponse { * Edit a chat message * * @param int $messageId ID of the message + * @param string $message the message to send * @psalm-param non-negative-int $messageId * @return DataResponse|DataResponse, array{}> * @@ -766,7 +767,64 @@ public function deleteMessage(int $messageId): DataResponse { #[RequireParticipant] #[RequirePermission(permission: RequirePermission::CHAT)] #[RequireReadWriteConversation] - public function editMessage(int $messageId): DataResponse { + public function editMessage(int $messageId, string $message): DataResponse { + try { + $comment = $this->chatManager->getComment($this->room, (string) $messageId); + } catch (NotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + $attendee = $this->participant->getAttendee(); + $isOwnMessage = $comment->getActorType() === $attendee->getActorType() + && $comment->getActorId() === $attendee->getActorId(); + + // Special case for if the message is a bridged message, then the message is the bridge bot's message. + $isOwnMessage = $isOwnMessage || ($comment->getActorType() === Attendee::ACTOR_BRIDGED && $attendee->getActorId() === MatterbridgeManager::BRIDGE_BOT_USERID); + if (!$isOwnMessage + && (!$this->participant->hasModeratorPermissions(false) + || $this->room->getType() === Room::TYPE_ONE_TO_ONE + || $this->room->getType() === Room::TYPE_ONE_TO_ONE_FORMER)) { + // Actor is not a moderator or not the owner of the message + return new DataResponse([], Http::STATUS_FORBIDDEN); + } + + if ($comment->getVerb() !== ChatManager::VERB_MESSAGE && $comment->getVerb() !== ChatManager::VERB_OBJECT_SHARED) { + // System message (since the message is not parsed, it has type "system") + return new DataResponse([], Http::STATUS_METHOD_NOT_ALLOWED); + } + + $maxAge = $this->timeFactory->getDateTime(); + $maxAge->sub(new \DateInterval('P1D')); + if ($comment->getCreationDateTime() < $maxAge) { + // Message is too old + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + $systemMessageComment = $this->chatManager->editMessage( + $this->room, + $comment, + $this->participant, + $this->timeFactory->getDateTime(), + $message + ); + + $systemMessage = $this->messageParser->createMessage($this->room, $this->participant, $systemMessageComment, $this->l); + $this->messageParser->parseMessage($systemMessage); + + $comment = $this->chatManager->getComment($this->room, (string) $messageId); + $parseMessage = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l); + $this->messageParser->parseMessage($parseMessage); + + $data = $systemMessage->toArray($this->getResponseFormat()); + $data['parent'] = $parseMessage->toArray($this->getResponseFormat()); + + $bridge = $this->matterbridgeManager->getBridgeOfRoom($this->room); + + $headers = []; + if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) { + $headers = ['X-Chat-Last-Common-Read' => (string) $this->chatManager->getLastCommonReadMessage($this->room)]; + } + return new DataResponse($data, $bridge['enabled'] ? Http::STATUS_ACCEPTED : Http::STATUS_OK, $headers); } /** diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 595c165231fa..d76b311736ed 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -190,6 +190,16 @@ public function toArray(string $format): array { 'markdown' => $this->getMessageType() === ChatManager::VERB_SYSTEM ? false : true, ]; + $metaData = $this->getComment()->getMetaData(); + if (!empty($metaData)) { + if (isset($metaData['last_edited_by_type'], $metaData['last_edited_by_id'], $metaData['last_edited_time'])) { + $data['lastEditActorDisplayName'] = $metaData['last_edited_by_id']; // FIXME @see MessageParser::setActor + $data['lastEditActorId'] = $metaData['last_edited_by_id']; + $data['lastEditActorType'] = $metaData['last_edited_by_type']; + $data['lastEditTimestamp'] = $metaData['last_edited_time']; + } + } + if ($this->getMessageType() === ChatManager::VERB_MESSAGE_DELETED) { $data['deleted'] = true; }