From 0d1fc13e36e40a6cd8dfa80915ea77f06b835f80 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Thu, 25 Jan 2024 15:39:03 +0100 Subject: [PATCH] feat(chat): Store and expose whether a message or call was silent Signed-off-by: Joas Schilling --- docs/capabilities.md | 1 + docs/chat.md | 47 ++++++++------- lib/Capabilities.php | 1 + lib/Chat/ChatManager.php | 60 ++++++++++++------- lib/Chat/Parser/SystemMessage.php | 17 +++++- lib/Chat/SystemMessage/Listener.php | 3 +- lib/Model/Message.php | 5 ++ lib/ResponseDefinitions.php | 1 + openapi-backend-sipbridge.json | 3 + openapi-federation.json | 3 + openapi-full.json | 3 + openapi.json | 3 + .../features/bootstrap/FeatureContext.php | 11 +++- .../features/callapi/notifications.feature | 12 +++- .../features/chat-1/notifications.feature | 6 ++ tests/php/CapabilitiesTest.php | 1 + 16 files changed, 126 insertions(+), 51 deletions(-) diff --git a/docs/capabilities.md b/docs/capabilities.md index fd45c3d7e13..257b747455f 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -142,3 +142,4 @@ ## 19 * `delete-messages-unlimited` - Whether messages can be deleted at any time (used to be restricted to 6 hours after posting) * `edit-messages` - Whether messages can be edited (restricted to 24 hours after posting) +* `silent-send-state` - Whether messages contain a flag that they were sent silently diff --git a/docs/chat.md b/docs/chat.md index a892072cb5d..5a013458dd8 100644 --- a/docs/chat.md +++ b/docs/chat.md @@ -43,29 +43,30 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1`: since Nextcloud 13 - Data: Array of messages, each message has at least: -| field | type | Description | -|----------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | int | ID of the comment | -| `token` | string | Conversation token | -| `actorType` | string | See [Constants - Actor types of chat messages](constants.md#actor-types-of-chat-messages) | -| `actorId` | string | Actor id of the message author | -| `actorDisplayName` | string | Display name of the message author (can be empty for type `deleted_users` and `guests`) | -| `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`, `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)) | -| `messageParameters` | array | Message parameters for `message` (see [Rich Object String](https://github.com/nextcloud/server/issues/1706)) | -| `expirationTimestamp` | int | Unix time stamp when the message expires and show be removed from the clients UI without further note or warning (only available with `message-expiration` capability) | -| `parent` | array | **Optional:** See `Parent data` below | -| `reactions` | int[] | **Optional:** An array map with relation between reaction emoji and total count of reactions with this emoji | -| `reactionsSelf` | string[] | **Optional:** When the user reacted this is the list of emojis the user reacted with | -| `markdown` | bool | **Optional:** Whether the message should be rendered as markdown or shown as plain text | -| `lastEditActorType` | string | Actor type of the last editing author - See [Constants - Actor types of chat messages](constants.md#actor-types-of-chat-messages) (only available with `edit-messages` capability and when the message was actually edited) | -| `lastEditActorId` | string | Actor id of the last editing author (only available with `edit-messages` capability and when the message was actually edited) | -| `lastEditActorDisplayName` | string | Display name of the last editing author (only available with `edit-messages` capability and when the message was actually edited) (can be empty for type `deleted_users` and `guests`) | -| `lastEditTimestamp` | int | Unix time stamp when the message was last edited (only available with `edit-messages` capability and when the message was actually edited) | +| field | type | Description | +|----------------------------|----------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | int | ID of the comment | +| `token` | string | Conversation token | +| `actorType` | string | See [Constants - Actor types of chat messages](constants.md#actor-types-of-chat-messages) | +| `actorId` | string | Actor id of the message author | +| `actorDisplayName` | string | Display name of the message author (can be empty for type `deleted_users` and `guests`) | +| `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`, `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)) | +| `messageParameters` | array | Message parameters for `message` (see [Rich Object String](https://github.com/nextcloud/server/issues/1706)) | +| `expirationTimestamp` | int | Unix time stamp when the message expires and show be removed from the clients UI without further note or warning (only available with `message-expiration` capability) | +| `parent` | array | **Optional:** See `Parent data` below | +| `reactions` | int[] | **Optional:** An array map with relation between reaction emoji and total count of reactions with this emoji | +| `reactionsSelf` | string[] | **Optional:** When the user reacted this is the list of emojis the user reacted with | +| `markdown` | bool | **Optional:** Whether the message should be rendered as markdown or shown as plain text | +| `lastEditActorType` | string | **Optional:** Actor type of the last editing author - See [Constants - Actor types of chat messages](constants.md#actor-types-of-chat-messages) (only available with `edit-messages` capability and when the message was actually edited) | +| `lastEditActorId` | string | **Optional:** Actor id of the last editing author (only available with `edit-messages` capability and when the message was actually edited) | +| `lastEditActorDisplayName` | string | **Optional:** Display name of the last editing author (only available with `edit-messages` capability and when the message was actually edited) (can be empty for type `deleted_users` and `guests`) | +| `lastEditTimestamp` | int | **Optional:** Unix time stamp when the message was last edited (only available with `edit-messages` capability and when the message was actually edited) | +| `silent` | bool | **Optional:** Whether the message was sent silently (only available with `silent-send-state` capability) | #### Parent data diff --git a/lib/Capabilities.php b/lib/Capabilities.php index 2e82b872aac..83f01d33c86 100644 --- a/lib/Capabilities.php +++ b/lib/Capabilities.php @@ -172,6 +172,7 @@ public function getCapabilities(): array { 'sip-support-dialout', 'delete-messages-unlimited', 'edit-messages', + 'silent-send-state', ], 'config' => [ 'attachments' => [ diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php index 5b3fc73a2ec..8aeb8f3b68e 100644 --- a/lib/Chat/ChatManager.php +++ b/lib/Chat/ChatManager.php @@ -153,6 +153,12 @@ public function addSystemMessage( $comment->setVerb(self::VERB_SYSTEM); } + if ($silent) { + $comment->setMetaData([ + 'silent' => true, + ]); + } + $this->setMessageExpiration($chat, $comment); $shouldFlush = $this->notificationManager->defer(); @@ -291,6 +297,12 @@ public function sendMessage(Room $chat, ?Participant $participant, string $actor } } + if ($silent) { + $comment->setMetaData([ + 'silent' => true, + ]); + } + $event = new BeforeChatMessageSentEvent($chat, $comment, $participant, $silent); $this->dispatcher->dispatchTyped($event); @@ -509,35 +521,41 @@ public function editMessage(Room $chat, IComment $comment, Participant $particip $metaData['last_edited_time'] = $editTime->getTimestamp(); $comment->setMetaData($metaData); - $mentionsBefore = $comment->getMentions(); - $usersDirectlyMentionedBefore = $this->notifier->getMentionedUserIds($comment); - $usersToNotifyBefore = $this->notifier->getUsersToNotify($chat, $comment, []); + $wasSilent = $metaData['silent'] ?? false; + + if (!$wasSilent) { + $mentionsBefore = $comment->getMentions(); + $usersDirectlyMentionedBefore = $this->notifier->getMentionedUserIds($comment); + $usersToNotifyBefore = $this->notifier->getUsersToNotify($chat, $comment, []); + } $comment->setMessage($message, self::MAX_CHAT_LENGTH); - $mentionsAfter = $comment->getMentions(); + if (!$wasSilent) { + $mentionsAfter = $comment->getMentions(); + } $this->commentsManager->save($comment); $this->referenceManager->invalidateCache($chat->getToken()); - $removedMentions = empty($mentionsAfter) ? $mentionsBefore : array_udiff($mentionsBefore, $mentionsAfter, [$this, 'compareMention']); - $addedMentions = empty($mentionsBefore) ? $mentionsAfter : array_udiff($mentionsAfter, $mentionsBefore, [$this, 'compareMention']); + if (!$wasSilent) { + $removedMentions = empty($mentionsAfter) ? $mentionsBefore : array_udiff($mentionsBefore, $mentionsAfter, [$this, 'compareMention']); + $addedMentions = empty($mentionsBefore) ? $mentionsAfter : array_udiff($mentionsAfter, $mentionsBefore, [$this, 'compareMention']); - // FIXME Not needed when it was silent, once it's stored in metadata - if (!empty($removedMentions)) { - $usersToNotifyAfter = $this->notifier->getUsersToNotify($chat, $comment, []); - $removedUsersMentioned = array_udiff($usersToNotifyBefore, $usersToNotifyAfter, [$this, 'compareMention']); - $userIds = array_column($removedUsersMentioned, 'id'); - $this->notifier->removeMentionNotificationAfterEdit($chat, $comment, $userIds); - } + if (!empty($removedMentions)) { + $usersToNotifyAfter = $this->notifier->getUsersToNotify($chat, $comment, []); + $removedUsersMentioned = array_udiff($usersToNotifyBefore, $usersToNotifyAfter, [$this, 'compareMention']); + $userIds = array_column($removedUsersMentioned, 'id'); + $this->notifier->removeMentionNotificationAfterEdit($chat, $comment, $userIds); + } - // FIXME silent support, once it's stored in metadata - if (!empty($addedMentions)) { - $usersDirectlyMentionedAfter = $this->notifier->getMentionedUserIds($comment); - $addedUsersDirectMentioned = array_diff($usersDirectlyMentionedAfter, $usersDirectlyMentionedBefore); + if (!empty($addedMentions)) { + $usersDirectlyMentionedAfter = $this->notifier->getMentionedUserIds($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); + $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); + } } } diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php index ef6bd835e3e..b85207f2f73 100644 --- a/lib/Chat/Parser/SystemMessage.php +++ b/lib/Chat/Parser/SystemMessage.php @@ -183,9 +183,20 @@ protected function parseMessage(Message $chatMessage): void { $parsedMessage = $this->l->t('An administrator removed the description'); } } elseif ($message === 'call_started') { - $parsedMessage = $this->l->t('{actor} started a call'); - if ($currentUserIsActor) { - $parsedMessage = $this->l->t('You started a call'); + $metaData = $comment->getMetaData() ?? []; + $silentCall = $metaData['silent'] ?? false; + if ($silentCall) { + if ($currentUserIsActor) { + $parsedMessage = $this->l->t('You started a silent call'); + } else { + $parsedMessage = $this->l->t('{actor} started a silent call'); + } + } else { + if ($currentUserIsActor) { + $parsedMessage = $this->l->t('You started a call'); + } else { + $parsedMessage = $this->l->t('{actor} started a call'); + } } } elseif ($message === 'call_joined') { $parsedMessage = $this->l->t('{actor} joined the call'); diff --git a/lib/Chat/SystemMessage/Listener.php b/lib/Chat/SystemMessage/Listener.php index 80af7f69b64..8bc0a07ff2a 100644 --- a/lib/Chat/SystemMessage/Listener.php +++ b/lib/Chat/SystemMessage/Listener.php @@ -134,7 +134,8 @@ protected function sendSystemMessageAboutBeginOfCall(BeforeParticipantModifiedEv if ($this->participantService->hasActiveSessionsInCall($event->getRoom())) { $this->sendSystemMessage($event->getRoom(), 'call_joined', [], $event->getParticipant()); } else { - $this->sendSystemMessage($event->getRoom(), 'call_started', [], $event->getParticipant()); + $silent = $event->getDetail(AParticipantModifiedEvent::DETAIL_IN_CALL_SILENT) ?? false; + $this->sendSystemMessage($event->getRoom(), 'call_started', [], $event->getParticipant(), silent: $silent); } } diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 2482cfcd191..f2123b1a785 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -220,6 +220,11 @@ public function toArray(string $format): array { $data['deleted'] = true; } + $metaData = $this->comment->getMetaData() ?? []; + if ($metaData['silent']) { + $data['silent'] = true; + } + return $data; } } diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php index 729a98d4741..ffa1df8dbcc 100644 --- a/lib/ResponseDefinitions.php +++ b/lib/ResponseDefinitions.php @@ -87,6 +87,7 @@ * lastEditActorId?: string, * lastEditActorType?: string, * lastEditTimestamp?: int, + * silent?: bool, * } * * @psalm-type TalkChatMessageWithParent = TalkChatMessage&array{parent?: TalkChatMessage} diff --git a/openapi-backend-sipbridge.json b/openapi-backend-sipbridge.json index 32805876afa..a7aeb7d66ff 100644 --- a/openapi-backend-sipbridge.json +++ b/openapi-backend-sipbridge.json @@ -116,6 +116,9 @@ "lastEditTimestamp": { "type": "integer", "format": "int64" + }, + "silent": { + "type": "boolean" } } }, diff --git a/openapi-federation.json b/openapi-federation.json index 2818cfac7dc..d172eb2463f 100644 --- a/openapi-federation.json +++ b/openapi-federation.json @@ -116,6 +116,9 @@ "lastEditTimestamp": { "type": "integer", "format": "int64" + }, + "silent": { + "type": "boolean" } } }, diff --git a/openapi-full.json b/openapi-full.json index f57872b2ec0..92792f15fd6 100644 --- a/openapi-full.json +++ b/openapi-full.json @@ -273,6 +273,9 @@ "lastEditTimestamp": { "type": "integer", "format": "int64" + }, + "silent": { + "type": "boolean" } } }, diff --git a/openapi.json b/openapi.json index 13f90ffc08f..3ec32478788 100644 --- a/openapi.json +++ b/openapi.json @@ -214,6 +214,9 @@ "lastEditTimestamp": { "type": "integer", "format": "int64" + }, + "silent": { + "type": "boolean" } } }, diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 5cb001b2c55..5ecb12a154d 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -2648,7 +2648,7 @@ protected function compareDataResponse(TableNode $formData = null) { } } - Assert::assertEquals($expected, array_map(function ($message) use ($includeParents, $includeReferenceId, $includeReactions, $includeReactionsSelf, $includeLastEdit) { + Assert::assertEquals($expected, array_map(function ($message, $expected) use ($includeParents, $includeReferenceId, $includeReactions, $includeReactionsSelf, $includeLastEdit) { $data = [ 'room' => self::$tokenToIdentifier[$message['token']], 'actorType' => $message['actorType'], @@ -2665,6 +2665,9 @@ protected function compareDataResponse(TableNode $formData = null) { if ($includeReferenceId) { $data['referenceId'] = $message['referenceId']; } + if (isset($expected['silent'])) { + $data['silent'] = isset($message['silent']) ? json_encode($message['silent']) : '!ISSET'; + } if ($includeReactions) { $data['reactions'] = json_encode($message['reactions'], JSON_UNESCAPED_UNICODE); } @@ -2684,7 +2687,7 @@ protected function compareDataResponse(TableNode $formData = null) { } } return $data; - }, $messages)); + }, $messages, $expected)); } /** @@ -2773,6 +2776,10 @@ public function userSeesTheFollowingSystemMessagesInRoom($user, $identifier, $st $data['messageParameters'] = json_encode($message['messageParameters']); } + if (isset($expected['silent'])) { + $data['silent'] = isset($message['silent']) ? json_encode($message['silent']) : '!ISSET'; + } + return $data; }, $messages, $expected)); } diff --git a/tests/integration/features/callapi/notifications.feature b/tests/integration/features/callapi/notifications.feature index 3d2ec863230..47fa7d56408 100644 --- a/tests/integration/features/callapi/notifications.feature +++ b/tests/integration/features/callapi/notifications.feature @@ -27,12 +27,17 @@ Feature: callapi/notifications Given user "participant1" joins room "room" with 200 (v4) Given user "participant2" joins room "room" with 200 (v4) Given user "participant1" joins call "room" with 200 (v4) + Then user "participant2" sees the following system messages in room "room" with 200 + | room | actorType | actorId | systemMessage | message | silent | messageParameters | + | room | users | participant1 | call_started | {actor} started a call | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | + | room | users | participant1 | user_added | {actor} added you | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"user":{"type":"user","id":"participant2","name":"participant2-displayname"}} | + | room | users | participant1 | conversation_created | {actor} created the conversation | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | Then user "participant2" has the following notifications | app | object_type | object_id | subject | | spreed | call | room | A group call has started in room | Given user "participant1" leaves call "room" with 200 (v4) Then user "participant2" has the following notifications - | app | object_type | object_id | subject | + | app | object_type | object_id | subject | | spreed | call | room | You missed a group call in room | Scenario: Silent call does not trigger notifications @@ -44,6 +49,11 @@ Feature: callapi/notifications Given user "participant2" joins room "room" with 200 (v4) Given user "participant1" joins call "room" with 200 (v4) | silent | true | + Then user "participant2" sees the following system messages in room "room" with 200 + | room | actorType | actorId | systemMessage | message | silent | messageParameters | + | room | users | participant1 | call_started | {actor} started a silent call | true | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | + | room | users | participant1 | user_added | {actor} added you | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"user":{"type":"user","id":"participant2","name":"participant2-displayname"}} | + | room | users | participant1 | conversation_created | {actor} created the conversation | !ISSET | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"}} | Then user "participant2" has the following notifications | app | object_type | object_id | subject | Given user "participant1" leaves call "room" with 200 (v4) diff --git a/tests/integration/features/chat-1/notifications.feature b/tests/integration/features/chat-1/notifications.feature index 8d66570e29a..e74482e1d8c 100644 --- a/tests/integration/features/chat-1/notifications.feature +++ b/tests/integration/features/chat-1/notifications.feature @@ -43,6 +43,9 @@ Feature: chat/notifications Then user "participant2" has the following notifications | app | object_type | object_id | subject | | spreed | chat | one-to-one room/Message 1 | participant1-displayname sent you a private message | + Then user "participant2" sees the following messages in room "one-to-one room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | silent | + | one-to-one room | users | participant1 | participant1-displayname | Message 1 | [] | !ISSET | Scenario: Silent sent message when recipient is offline in the one-to-one When user "participant1" creates room "one-to-one room" (v4) @@ -54,6 +57,9 @@ Feature: chat/notifications When user "participant1" silent sends message "Message 1" to room "one-to-one room" with 201 Then user "participant2" has the following notifications | app | object_type | object_id | subject | + Then user "participant2" sees the following messages in room "one-to-one room" with 200 + | room | actorType | actorId | actorDisplayName | message | messageParameters | silent | + | one-to-one room | users | participant1 | participant1-displayname | Message 1 | [] | true | Scenario: Normal message when recipient disabled notifications in the one-to-one When user "participant1" creates room "one-to-one room" (v4) diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php index de01a6845f7..00d569fb914 100644 --- a/tests/php/CapabilitiesTest.php +++ b/tests/php/CapabilitiesTest.php @@ -146,6 +146,7 @@ public function setUp(): void { 'sip-support-dialout', 'delete-messages-unlimited', 'edit-messages', + 'silent-send-state', 'message-expiration', 'reactions', ];