Browse Source

feat(chat): Store and expose whether a message or call was silent

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/11449/head
Joas Schilling 2 years ago
parent
commit
0d1fc13e36
No known key found for this signature in database GPG Key ID: 74434EFE0D2E2205
  1. 1
      docs/capabilities.md
  2. 47
      docs/chat.md
  3. 1
      lib/Capabilities.php
  4. 60
      lib/Chat/ChatManager.php
  5. 17
      lib/Chat/Parser/SystemMessage.php
  6. 3
      lib/Chat/SystemMessage/Listener.php
  7. 5
      lib/Model/Message.php
  8. 1
      lib/ResponseDefinitions.php
  9. 3
      openapi-backend-sipbridge.json
  10. 3
      openapi-federation.json
  11. 3
      openapi-full.json
  12. 3
      openapi.json
  13. 11
      tests/integration/features/bootstrap/FeatureContext.php
  14. 12
      tests/integration/features/callapi/notifications.feature
  15. 6
      tests/integration/features/chat-1/notifications.feature
  16. 1
      tests/php/CapabilitiesTest.php

1
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

47
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

1
lib/Capabilities.php

@ -172,6 +172,7 @@ class Capabilities implements IPublicCapability {
'sip-support-dialout',
'delete-messages-unlimited',
'edit-messages',
'silent-send-state',
],
'config' => [
'attachments' => [

60
lib/Chat/ChatManager.php

@ -153,6 +153,12 @@ class ChatManager {
$comment->setVerb(self::VERB_SYSTEM);
}
if ($silent) {
$comment->setMetaData([
'silent' => true,
]);
}
$this->setMessageExpiration($chat, $comment);
$shouldFlush = $this->notificationManager->defer();
@ -291,6 +297,12 @@ class ChatManager {
}
}
if ($silent) {
$comment->setMetaData([
'silent' => true,
]);
}
$event = new BeforeChatMessageSentEvent($chat, $comment, $participant, $silent);
$this->dispatcher->dispatchTyped($event);
@ -509,35 +521,41 @@ class ChatManager {
$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);
}
}
}

17
lib/Chat/Parser/SystemMessage.php

@ -183,9 +183,20 @@ class SystemMessage implements IEventListener {
$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');

3
lib/Chat/SystemMessage/Listener.php

@ -134,7 +134,8 @@ class Listener implements IEventListener {
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);
}
}

5
lib/Model/Message.php

@ -220,6 +220,11 @@ class Message {
$data['deleted'] = true;
}
$metaData = $this->comment->getMetaData() ?? [];
if ($metaData['silent']) {
$data['silent'] = true;
}
return $data;
}
}

1
lib/ResponseDefinitions.php

@ -87,6 +87,7 @@ namespace OCA\Talk;
* lastEditActorId?: string,
* lastEditActorType?: string,
* lastEditTimestamp?: int,
* silent?: bool,
* }
*
* @psalm-type TalkChatMessageWithParent = TalkChatMessage&array{parent?: TalkChatMessage}

3
openapi-backend-sipbridge.json

@ -116,6 +116,9 @@
"lastEditTimestamp": {
"type": "integer",
"format": "int64"
},
"silent": {
"type": "boolean"
}
}
},

3
openapi-federation.json

@ -116,6 +116,9 @@
"lastEditTimestamp": {
"type": "integer",
"format": "int64"
},
"silent": {
"type": "boolean"
}
}
},

3
openapi-full.json

@ -273,6 +273,9 @@
"lastEditTimestamp": {
"type": "integer",
"format": "int64"
},
"silent": {
"type": "boolean"
}
}
},

3
openapi.json

@ -214,6 +214,9 @@
"lastEditTimestamp": {
"type": "integer",
"format": "int64"
},
"silent": {
"type": "boolean"
}
}
},

11
tests/integration/features/bootstrap/FeatureContext.php

@ -2648,7 +2648,7 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
}
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 @@ class FeatureContext implements Context, SnippetAcceptingContext {
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 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
}
return $data;
}, $messages));
}, $messages, $expected));
}
/**
@ -2773,6 +2776,10 @@ class FeatureContext implements Context, SnippetAcceptingContext {
$data['messageParameters'] = json_encode($message['messageParameters']);
}
if (isset($expected['silent'])) {
$data['silent'] = isset($message['silent']) ? json_encode($message['silent']) : '!ISSET';
}
return $data;
}, $messages, $expected));
}

12
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)

6
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)

1
tests/php/CapabilitiesTest.php

@ -146,6 +146,7 @@ class CapabilitiesTest extends TestCase {
'sip-support-dialout',
'delete-messages-unlimited',
'edit-messages',
'silent-send-state',
'message-expiration',
'reactions',
];

Loading…
Cancel
Save