Skip to content

Commit

Permalink
Merge pull request #4861 from nextcloud/enh/deleteMsg
Browse files Browse the repository at this point in the history
Delete chat messages
  • Loading branch information
nickvergessen authored Feb 2, 2021
2 parents b602859 + 11a861c commit 9bd20f3
Show file tree
Hide file tree
Showing 18 changed files with 613 additions and 28 deletions.
10 changes: 10 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,16 @@
'token' => '^[a-z0-9]{4,30}$',
],
],
[
'name' => 'Chat#deleteMessage',
'url' => '/api/{apiVersion}/chat/{token}/{messageId}',
'verb' => 'DELETE',
'requirements' => [
'apiVersion' => 'v1',
'token' => '^[a-z0-9]{4,30}$',
'messageId' => '^[0-9]+$',
],
],
[
'name' => 'Chat#setReadMarker',
'url' => '/api/{apiVersion}/chat/{token}/read',
Expand Down
3 changes: 3 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,6 @@ title: Capabilities
* `room-description` - A description can be get and set for conversations.
* `config => chat => read-privacy` - See `chat-read-status`
* `config => previews => max-gif-size` - Maximum size in bytes below which a GIF can be embedded directly in the page at render time. Bigger files will be rendered statically using the preview endpoint instead. Can be set with `occ config:app:set spreed max-gif-size --value=X` where X is the new value in bytes. Defaults to 3 MB.

## 12.0
* `delete-messages` - Allows to delete chat messages up to 6 hours for your own messages or when being a moderator. On deleting the message text will be replaced and a follow up system message will make sure clients and users update it in their cache and storage.
33 changes: 31 additions & 2 deletions docs/chat.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`actorDisplayName` | string | Display name of the message author
`timestamp` | int | Timestamp in seconds and UTC time zone
`systemMessage` | string | empty for normal chat message or the type of the system message (untranslated)
`messageType` | string | Currently known types are `comment`, `system` and `command`
`messageType` | string | Currently known types are `comment`, `comment_deleted`, `system` and `command`
`isReplyable` | bool | True if the user can post a reply to this message (only available with `chat-replies` capability)
`referenceId` | string | A reference string that was given while posting the message to be able to identify a sent message again (only available with `chat-reference-id` capability)
`message` | string | Message string with placeholders (see [Rich Object String](https://github.com/nextcloud/server/issues/1706))
Expand Down Expand Up @@ -96,6 +96,35 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
- Data:
The full message array of the new message, as defined in [Receive chat messages of a conversation](#receive-chat-messages-of-a-conversation)


## Deleting a chat message

* Method: `DELETE`
* Endpoint: `/chat/{token}/{messageId}`

* Response:
- Status code:
+ `200 OK` - When deleting was successful
+ `202 Accepted` - When deleting was successful but Matterbridge is enabled so the message was leaked to other services
+ `400 Bad Request` The message is already older than 6 hours
+ `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
+ `405 Method Not Allowed` When the message is not a normal chat message
+ `412 Precondition Failed` When the lobby is active and the user is not a moderator

- Header:

field | type | Description
------|------|------------
`X-Chat-Last-Common-Read` | int | ID of the last message read by every user that has read privacy set to public. When the user themself has it set to private the value the header is not set (only available with `chat-read-status` capability)

- Data:
The full message array of the new system message "You deleted a message", as defined in [Receive chat messages of a conversation](#receive-chat-messages-of-a-conversation)
The parent message is the object of the deleted message with the replaced text "Message deleted by you".
This message should not be displayed to the user but instead be used to remove the original message from any cache/storage of the device.


## Mark chat as read

* Method: `POST`
Expand All @@ -119,7 +148,6 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
`X-Chat-Last-Common-Read` | int | ID of the last message read by every user that has read privacy set to public. When the user themself has it set to private the value the header is not set (only available with `chat-read-status` capability)



## Get mention autocomplete suggestions

* Method: `GET`
Expand Down Expand Up @@ -177,3 +205,4 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`
* `moderator_demoted` - {actor} demoted {user} from moderator
* `guest_moderator_promoted` - {actor} promoted {user} to moderator
* `guest_moderator_demoted` - {actor} demoted {user} from moderator
* `message_deleted` - Message deleted by {actor} (Should not be shown to the user)
1 change: 1 addition & 0 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public function getCapabilities(): array {
'last-room-activity',
'no-ping',
'system-messages',
'delete-messages',
'mention-flag',
'in-call-flags',
'notification-levels',
Expand Down
55 changes: 54 additions & 1 deletion lib/Chat/ChatManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,28 @@ public function __construct(CommentsManager $commentsManager,
* @param \DateTime $creationDateTime
* @param bool $sendNotifications
* @param string|null $referenceId
* @param int|null $parentId
* @return IComment
*/
public function addSystemMessage(Room $chat, string $actorType, string $actorId, string $message, \DateTime $creationDateTime, bool $sendNotifications, ?string $referenceId = null): IComment {
public function addSystemMessage(
Room $chat,
string $actorType,
string $actorId,
string $message,
\DateTime $creationDateTime,
bool $sendNotifications,
?string $referenceId = null,
?int $parentId = null
): IComment {
$comment = $this->commentsManager->create($actorType, $actorId, 'chat', (string) $chat->getId());
$comment->setMessage($message, self::MAX_CHAT_LENGTH);
$comment->setCreationDateTime($creationDateTime);
if ($referenceId !== null) {
$comment->setReferenceId($referenceId);
}
if ($parentId !== null) {
$comment->setParentId((string) $parentId);
}
$comment->setVerb('system');

$event = new ChatEvent($chat, $comment);
Expand Down Expand Up @@ -231,6 +244,30 @@ public function sendMessage(Room $chat, Participant $participant, string $actorT
return $comment;
}

public function deleteMessage(Room $chat, int $messageId, string $actorType, string $actorId, \DateTime $deletionTime): IComment {
$comment = $this->getComment($chat, (string) $messageId);
$comment->setMessage(
json_encode([
'deleted_by_type' => $actorType,
'deleted_by_id' => $actorId,
'deleted_on' => $deletionTime->getTimestamp(),
])
);
$comment->setVerb('comment_deleted');
$this->commentsManager->save($comment);

return $this->addSystemMessage(
$chat,
$actorType,
$actorId,
json_encode(['message' => 'message_deleted', 'parameters' => ['message' => $messageId]]),
$this->timeFactory->getDateTime(),
false,
null,
$messageId
);
}

/**
* @param Room $chat
* @param string $parentId
Expand All @@ -247,6 +284,22 @@ public function getParentComment(Room $chat, string $parentId): IComment {
return $comment;
}

/**
* @param Room $chat
* @param string $messageId
* @return IComment
* @throws NotFoundException
*/
public function getComment(Room $chat, string $messageId): IComment {
$comment = $this->commentsManager->get($messageId);

if ($comment->getObjectType() !== 'chat' || $comment->getObjectId() !== (string) $chat->getId()) {
throw new NotFoundException('Message not found in the right context');
}

return $comment;
}

public function getLastReadMessageFromLegacy(Room $chat, IUser $user): int {
$marker = $this->commentsManager->getReadMark('chat', $chat->getId(), $user);
if ($marker === null) {
Expand Down
20 changes: 20 additions & 0 deletions lib/Chat/Parser/Listener.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,25 @@ public static function register(IEventDispatcher $dispatcher): void {
$event->stopPropagation();
}
});

$dispatcher->addListener(MessageParser::EVENT_MESSAGE_PARSE, static function (ChatMessageEvent $event) {
$chatMessage = $event->getMessage();

if ($chatMessage->getMessageType() !== 'comment_deleted') {
return;
}

/** @var SystemMessage $parser */
$parser = \OC::$server->query(SystemMessage::class);

try {
$parser->parseDeletedMessage($chatMessage);
$event->stopPropagation();
} catch (\OutOfBoundsException $e) {
// Unknown message, ignore
} catch (\RuntimeException $e) {
$event->stopPropagation();
}
}, 9999);// First things first
}
}
65 changes: 60 additions & 5 deletions lib/Chat/Parser/SystemMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ public function parseMessage(Message $chatMessage): void {

$message = $data['message'];
$parameters = $data['parameters'];
$parsedParameters = ['actor' => $this->getActor($comment)];
$parsedParameters = ['actor' => $this->getActorFromComment($comment)];

$participant = $chatMessage->getParticipant();
if (!$participant->isGuest()) {
Expand Down Expand Up @@ -354,6 +354,57 @@ public function parseMessage(Message $chatMessage): void {
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('You stopped Matterbridge.');
}
} elseif ($message === 'message_deleted') {
$parsedMessage = $this->l->t('{actor} deleted a message');
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('You deleted a message');
}
} else {
throw new \OutOfBoundsException('Unknown subject');
}

$chatMessage->setMessage($parsedMessage, $parsedParameters, $message);
}

/**
* @param Message $chatMessage
* @throws \OutOfBoundsException
*/
public function parseDeletedMessage(Message $chatMessage): void {
$this->l = $chatMessage->getL10n();
$data = json_decode($chatMessage->getMessage(), true);
if (!\is_array($data)) {
throw new \OutOfBoundsException('Invalid message');
}

$parsedParameters = ['actor' => $this->getActor($data['deleted_by_type'], $data['deleted_by_id'])];

$participant = $chatMessage->getParticipant();
$currentActorId = $participant->getAttendee()->getActorId();

$authorIsActor = $data['deleted_by_type'] === $chatMessage->getComment()->getActorType()
&& $data['deleted_by_id'] === $chatMessage->getComment()->getActorId();

if (!$participant->isGuest()) {
$currentUserIsActor = $parsedParameters['actor']['type'] === 'user' &&
$participant->getAttendee()->getActorType() === Attendee::ACTOR_USERS &&
$currentActorId === $parsedParameters['actor']['id'];
} else {
$currentUserIsActor = $parsedParameters['actor']['type'] === 'guest' &&
$participant->getAttendee()->getActorType() === 'guest' &&
$currentActorId === $parsedParameters['actor']['id'];
}

if ($chatMessage->getMessageType() === 'comment_deleted') {
$message = 'message_deleted';
$parsedMessage = $this->l->t('Message deleted by author');

if (!$authorIsActor) {
$parsedMessage = $this->l->t('Message deleted by {actor}');
}
if ($currentUserIsActor) {
$parsedMessage = $this->l->t('Message deleted by you');
}
} else {
throw new \OutOfBoundsException('Unknown subject');
}
Expand Down Expand Up @@ -430,12 +481,16 @@ protected function getFileFromShare(Participant $participant, string $shareId):
];
}

protected function getActor(IComment $comment): array {
if ($comment->getActorType() === Attendee::ACTOR_GUESTS) {
return $this->getGuest($comment->getActorId());
protected function getActorFromComment(IComment $comment): array {
return $this->getActor($comment->getActorType(), $comment->getActorId());
}

protected function getActor(string $actorType, string $actorId): array {
if ($actorType === Attendee::ACTOR_GUESTS) {
return $this->getGuest($actorId);
}

return $this->getUser($comment->getActorId());
return $this->getUser($actorId);
}

protected function getUser(string $uid): array {
Expand Down
69 changes: 69 additions & 0 deletions lib/Controller/ChatController.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use OCA\Talk\Chat\ChatManager;
use OCA\Talk\Chat\MessageParser;
use OCA\Talk\GuestManager;
use OCA\Talk\MatterbridgeManager;
use OCA\Talk\Model\Attendee;
use OCA\Talk\Model\Message;
use OCA\Talk\Model\Session;
Expand Down Expand Up @@ -92,6 +93,9 @@ class ChatController extends AEnvironmentAwareController {
/** @var IUserStatusManager */
private $statusManager;

/** @var MatterbridgeManager */
protected $matterbridgeManager;

/** @var SearchPlugin */
private $searchPlugin;

Expand Down Expand Up @@ -120,6 +124,7 @@ public function __construct(string $appName,
MessageParser $messageParser,
IManager $autoCompleteManager,
IUserStatusManager $statusManager,
MatterbridgeManager $matterbridgeManager,
SearchPlugin $searchPlugin,
ISearchResult $searchResult,
ITimeFactory $timeFactory,
Expand All @@ -138,6 +143,7 @@ public function __construct(string $appName,
$this->messageParser = $messageParser;
$this->autoCompleteManager = $autoCompleteManager;
$this->statusManager = $statusManager;
$this->matterbridgeManager = $matterbridgeManager;
$this->searchPlugin = $searchPlugin;
$this->searchResult = $searchResult;
$this->timeFactory = $timeFactory;
Expand Down Expand Up @@ -462,6 +468,69 @@ public function receiveMessages(int $lookIntoFuture,
return $response;
}

/**
* @NoAdminRequired
* @RequireParticipant
* @RequireReadWriteConversation
* @RequireModeratorOrNoLobby
*
* @param int $messageId
* @return DataResponse
*/
public function deleteMessage(int $messageId): DataResponse {
try {
$message = $this->chatManager->getComment($this->room, (string) $messageId);
} catch (NotFoundException $e) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

$attendee = $this->participant->getAttendee();
if (!$this->participant->hasModeratorPermissions(false)
&& ($message->getActorType() !== $attendee->getActorType()
|| $message->getActorId() !== $attendee->getActorId())) {
// Actor is not a moderator or not the owner of the message
return new DataResponse([], Http::STATUS_FORBIDDEN);
}

if ($message->getVerb() !== 'comment') {
// System message or file share (since the message is not parsed, it has type "system")
return new DataResponse([], Http::STATUS_METHOD_NOT_ALLOWED);
}

$maxDeleteAge = $this->timeFactory->getDateTime();
$maxDeleteAge->sub(new \DateInterval('PT6H'));
if ($message->getCreationDateTime() < $maxDeleteAge) {
// Message is too old
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}

$systemMessageComment = $this->chatManager->deleteMessage(
$this->room,
$messageId,
$attendee->getActorType(),
$attendee->getActorId(),
$this->timeFactory->getDateTime()
);

$systemMessage = $this->messageParser->createMessage($this->room, $this->participant, $systemMessageComment, $this->l);
$this->messageParser->parseMessage($systemMessage);

$comment = $this->chatManager->getComment($this->room, (string) $messageId);
$message = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l);
$this->messageParser->parseMessage($message);

$data = $systemMessage->toArray();
$data['parent'] = $message->toArray();

$bridge = $this->matterbridgeManager->getBridgeOfRoom($this->room);

$response = new DataResponse($data, $bridge['enabled'] ? Http::STATUS_ACCEPTED: Http::STATUS_OK);
if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) {
$response->addHeader('X-Chat-Last-Common-Read', $this->chatManager->getLastCommonReadMessage($this->room));
}
return $response;
}

/**
* @NoAdminRequired
* @RequireParticipant
Expand Down
Loading

0 comments on commit 9bd20f3

Please sign in to comment.