Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2021-02-03 18:17:16 +0300
committerGitHub <noreply@github.com>2021-02-03 18:17:16 +0300
commitf6008b9e4d25dbd5c9d18331df733f1d8dec9cf6 (patch)
tree4beab925c833faf07bc1115f35a70bc096255868
parent96f7529e466c7d696696c1b7285a2a829fc20698 (diff)
parentaebc61cde02a6fc2938db4f3d06ff945d46ea08e (diff)
Merge pull request #5034 from nextcloud/feature/noid/allow-sharing-rich-objects-to-chats
Allow to share RichObjects to chats
-rw-r--r--appinfo/routes.php9
-rw-r--r--docs/capabilities.md1
-rw-r--r--docs/chat.md34
-rw-r--r--lib/Capabilities.php1
-rw-r--r--lib/Chat/ChatManager.php5
-rw-r--r--lib/Chat/Parser/SystemMessage.php4
-rw-r--r--lib/Controller/ChatController.php146
-rw-r--r--tests/integration/features/bootstrap/FeatureContext.php30
-rw-r--r--tests/integration/features/chat/rich-object-share.feature20
-rw-r--r--tests/php/CapabilitiesTest.php1
-rw-r--r--tests/php/Controller/ChatControllerTest.php93
11 files changed, 309 insertions, 35 deletions
diff --git a/appinfo/routes.php b/appinfo/routes.php
index db32cbd72..87e15f034 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -181,6 +181,15 @@ return [
'token' => '^[a-z0-9]{4,30}$',
],
],
+ [
+ 'name' => 'Chat#shareObjectToChat',
+ 'url' => '/api/{apiVersion}/chat/{token}/share',
+ 'verb' => 'POST',
+ 'requirements' => [
+ 'apiVersion' => 'v1',
+ 'token' => '^[a-z0-9]{4,30}$',
+ ],
+ ],
/**
* Conversation (Room)
diff --git a/docs/capabilities.md b/docs/capabilities.md
index 2383394cb..5c1e5a77b 100644
--- a/docs/capabilities.md
+++ b/docs/capabilities.md
@@ -67,3 +67,4 @@ title: Capabilities
## 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.
+* `rich-object-sharing` - Rich objects can be shared to chats. See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob/master/lib/public/RichObjectStrings/Definitions.php) for more details on supported rich objects and required data.
diff --git a/docs/chat.md b/docs/chat.md
index 3d03bf5e1..a2f0c746c 100644
--- a/docs/chat.md
+++ b/docs/chat.md
@@ -96,6 +96,40 @@ 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)
+## Share a rich object to the chat
+
+See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob/master/lib/public/RichObjectStrings/Definitions.php) for more details on supported rich objects and required data.
+
+* Required capability: `rich-object-sharing`
+* Method: `POST`
+* Endpoint: `/chat/{token}/share`
+* Data:
+
+ field | type | Description
+ ------|------|------------
+ `objectType` | string | The object type
+ `objectId` | string | The object id
+ `metaData` | string | JSON encoded array of the rich objects data
+ `actorDisplayName` | string | Guest display name (ignored for logged in users)
+ `referenceId` | string | A reference string to be able to identify the message again in a "get messages" request, should be a random sha256 (only available with `chat-reference-id` capability)
+
+* Response:
+ - Status code:
+ + `201 Created`
+ + `400 Bad Request` In case the meta data is invalid
+ + `403 Forbidden` When the conversation is read-only
+ + `404 Not Found` When the conversation could not be found for the participant
+ + `412 Precondition Failed` When the lobby is active and the user is not a moderator
+ + `413 Payload Too Large` When the message was longer than the allowed limit of 32000 characters (or 1000 until Nextcloud 16.0.1, check the `spreed => config => chat => max-length` capability for the limit)
+
+ - 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 message, as defined in [Receive chat messages of a conversation](#receive-chat-messages-of-a-conversation)
## Deleting a chat message
diff --git a/lib/Capabilities.php b/lib/Capabilities.php
index 668574f0d..0e938ff22 100644
--- a/lib/Capabilities.php
+++ b/lib/Capabilities.php
@@ -88,6 +88,7 @@ class Capabilities implements IPublicCapability {
'phonebook-search',
'raise-hand',
'room-description',
+ 'rich-object-sharing',
],
'config' => [
'attachments' => [
diff --git a/lib/Chat/ChatManager.php b/lib/Chat/ChatManager.php
index 4202a3a46..2209dfa72 100644
--- a/lib/Chat/ChatManager.php
+++ b/lib/Chat/ChatManager.php
@@ -123,7 +123,10 @@ class ChatManager {
$comment->setMessage($message, self::MAX_CHAT_LENGTH);
$comment->setCreationDateTime($creationDateTime);
if ($referenceId !== null) {
- $comment->setReferenceId($referenceId);
+ $referenceId = trim(substr($referenceId, 0, 40));
+ if ($referenceId !== '') {
+ $comment->setReferenceId($referenceId);
+ }
}
if ($parentId !== null) {
$comment->setParentId((string) $parentId);
diff --git a/lib/Chat/Parser/SystemMessage.php b/lib/Chat/Parser/SystemMessage.php
index ab1534480..563351799 100644
--- a/lib/Chat/Parser/SystemMessage.php
+++ b/lib/Chat/Parser/SystemMessage.php
@@ -329,6 +329,10 @@ class SystemMessage {
$parsedMessage = $this->l->t('You shared a file which is no longer available');
}
}
+ } elseif ($message === 'object_shared') {
+ $parsedParameters['object'] = $parameters['metaData'];
+ $parsedMessage = '{object}';
+ $chatMessage->setMessageType('comment');
} elseif ($message === 'matterbridge_config_added') {
$parsedMessage = $this->l->t('{actor} set up Matterbridge to synchronize this conversation with other chats.');
if ($currentUserIsActor) {
diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php
index 4c5706086..49257cbca 100644
--- a/lib/Controller/ChatController.php
+++ b/lib/Controller/ChatController.php
@@ -51,6 +51,8 @@ use OCP\EventDispatcher\IEventDispatcher;
use OCP\IL10N;
use OCP\IRequest;
use OCP\IUserManager;
+use OCP\RichObjectStrings\InvalidObjectExeption;
+use OCP\RichObjectStrings\IValidator;
use OCP\User\Events\UserLiveStatusEvent;
use OCP\UserStatus\IManager as IUserStatusManager;
use OCP\UserStatus\IUserStatus;
@@ -108,6 +110,9 @@ class ChatController extends AEnvironmentAwareController {
/** @var IEventDispatcher */
protected $eventDispatcher;
+ /** @var IValidator */
+ protected $richObjectValidator;
+
/** @var IL10N */
private $l;
@@ -129,6 +134,7 @@ class ChatController extends AEnvironmentAwareController {
ISearchResult $searchResult,
ITimeFactory $timeFactory,
IEventDispatcher $eventDispatcher,
+ IValidator $richObjectValidator,
IL10N $l) {
parent::__construct($appName, $request);
@@ -148,9 +154,58 @@ class ChatController extends AEnvironmentAwareController {
$this->searchResult = $searchResult;
$this->timeFactory = $timeFactory;
$this->eventDispatcher = $eventDispatcher;
+ $this->richObjectValidator = $richObjectValidator;
$this->l = $l;
}
+ protected function getActorInfo(string $actorDisplayName = ''): array {
+ if ($this->userId === null) {
+ $actorType = Attendee::ACTOR_GUESTS;
+ $sessionId = $this->session->getSessionForRoom($this->room->getToken());
+ // The character limit for actorId is 64, but the spreed-session is
+ // 256 characters long, so it has to be hashed to get an ID that
+ // fits (except if there is no session, as the actorId should be
+ // empty in that case but sha1('') would generate a hash too
+ // instead of returning an empty string).
+ $actorId = $sessionId ? sha1($sessionId) : 'failed-to-get-session';
+
+ if ($sessionId && $actorDisplayName) {
+ $this->guestManager->updateName($this->room, $this->participant, $actorDisplayName);
+ }
+ } else {
+ $actorType = Attendee::ACTOR_USERS;
+ $actorId = $this->userId;
+ }
+
+ return [$actorType, $actorId];
+ }
+
+ public function parseCommentToResponse(IComment $comment, Message $parentMessage = null): DataResponse {
+ $chatMessage = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l);
+ $this->messageParser->parseMessage($chatMessage);
+
+ if (!$chatMessage->getVisibility()) {
+ $response = new DataResponse([], Http::STATUS_CREATED);
+ if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) {
+ $response->addHeader('X-Chat-Last-Common-Read', $this->chatManager->getLastCommonReadMessage($this->room));
+ }
+ return $response;
+ }
+
+ $this->participantService->updateLastReadMessage($this->participant, (int) $comment->getId());
+
+ $data = $chatMessage->toArray();
+ if ($parentMessage instanceof Message) {
+ $data['parent'] = $parentMessage->toArray();
+ }
+
+ $response = new DataResponse($data, Http::STATUS_CREATED);
+ if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) {
+ $response->addHeader('X-Chat-Last-Common-Read', $this->chatManager->getLastCommonReadMessage($this->room));
+ }
+ return $response;
+ }
+
/**
* @PublicPage
* @RequireParticipant
@@ -171,24 +226,7 @@ class ChatController extends AEnvironmentAwareController {
* found".
*/
public function sendMessage(string $message, string $actorDisplayName = '', string $referenceId = '', int $replyTo = 0): DataResponse {
- if ($this->userId === null) {
- $actorType = Attendee::ACTOR_GUESTS;
- $sessionId = $this->session->getSessionForRoom($this->room->getToken());
- // The character limit for actorId is 64, but the spreed-session is
- // 256 characters long, so it has to be hashed to get an ID that
- // fits (except if there is no session, as the actorId should be
- // empty in that case but sha1('') would generate a hash too
- // instead of returning an empty string).
- $actorId = $sessionId ? sha1($sessionId) : 'failed-to-get-session';
-
- if ($sessionId && $actorDisplayName) {
- $this->guestManager->updateName($this->room, $this->participant, $actorDisplayName);
- }
- } else {
- $actorType = Attendee::ACTOR_USERS;
- $actorId = $this->userId;
- }
-
+ [$actorType, $actorId] = $this->getActorInfo($actorDisplayName);
if (!$actorId) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
@@ -220,29 +258,69 @@ class ChatController extends AEnvironmentAwareController {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
- $chatMessage = $this->messageParser->createMessage($this->room, $this->participant, $comment, $this->l);
- $this->messageParser->parseMessage($chatMessage);
+ return $this->parseCommentToResponse($comment, $parentMessage);
+ }
- if (!$chatMessage->getVisibility()) {
- $response = new DataResponse([], Http::STATUS_CREATED);
- if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) {
- $response->addHeader('X-Chat-Last-Common-Read', $this->chatManager->getLastCommonReadMessage($this->room));
- }
- return $response;
+ /**
+ * @PublicPage
+ * @RequireParticipant
+ * @RequireReadWriteConversation
+ * @RequireModeratorOrNoLobby
+ *
+ * Sends a rich-object to the given room.
+ *
+ * The author and timestamp are automatically set to the current user/guest
+ * and time.
+ *
+ * @param string $objectType
+ * @param string $objectId
+ * @param string $metaData
+ * @param string $actorDisplayName
+ * @param string $referenceId
+ * @return DataResponse the status code is "201 Created" if successful, and
+ * "404 Not found" if the room or session for a guest user was not
+ * found".
+ */
+ public function shareObjectToChat(string $objectType, string $objectId, string $metaData = '', string $actorDisplayName = '', string $referenceId = ''): DataResponse {
+ [$actorType, $actorId] = $this->getActorInfo($actorDisplayName);
+ if (!$actorId) {
+ return new DataResponse([], Http::STATUS_NOT_FOUND);
}
- $this->participantService->updateLastReadMessage($this->participant, (int) $comment->getId());
+ $data = $metaData !== '' ? json_decode($metaData, true) : [];
+ if (!is_array($data)) {
+ $data = [];
+ }
+ $data['type'] = $objectType;
+ $data['id'] = $objectId;
- $data = $chatMessage->toArray();
- if ($parentMessage instanceof Message) {
- $data['parent'] = $parentMessage->toArray();
+ try {
+ $this->richObjectValidator->validate('{object}', ['object' => $data]);
+ } catch (InvalidObjectExeption $e) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
- $response = new DataResponse($data, Http::STATUS_CREATED);
- if ($this->participant->getAttendee()->getReadPrivacy() === Participant::PRIVACY_PUBLIC) {
- $response->addHeader('X-Chat-Last-Common-Read', $this->chatManager->getLastCommonReadMessage($this->room));
+ $this->participantService->ensureOneToOneRoomIsFilled($this->room);
+ $creationDateTime = $this->timeFactory->getDateTime('now', new \DateTimeZone('UTC'));
+
+ $message = json_encode([
+ 'message' => 'object_shared',
+ 'parameters' => [
+ 'objectType' => $objectType,
+ 'objectId' => $objectId,
+ 'metaData' => $data,
+ ],
+ ]);
+
+ try {
+ $comment = $this->chatManager->addSystemMessage($this->room, $actorType, $actorId, $message, $creationDateTime, true, $referenceId);
+ } catch (MessageTooLongException $e) {
+ return new DataResponse([], Http::STATUS_REQUEST_ENTITY_TOO_LARGE);
+ } catch (\Exception $e) {
+ return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
- return $response;
+
+ return $this->parseCommentToResponse($comment);
}
/**
diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php
index feb7af04d..4ccad6689 100644
--- a/tests/integration/features/bootstrap/FeatureContext.php
+++ b/tests/integration/features/bootstrap/FeatureContext.php
@@ -1103,6 +1103,36 @@ class FeatureContext implements Context, SnippetAcceptingContext {
}
/**
+ * @Then /^user "([^"]*)" shares rich-object "([^"]*)" "([^"]*)" '([^']*)' to room "([^"]*)" with (\d+)(?: \((v(1|2|3))\))?$/
+ *
+ * @param string $user
+ * @param string $type
+ * @param string $id
+ * @param string $metaData
+ * @param string $identifier
+ * @param string $statusCode
+ * @param string $apiVersion
+ */
+ public function userSharesRichObjectToRoom($user, $type, $id, $metaData, $identifier, $statusCode, $apiVersion = 'v1') {
+ $this->setCurrentUser($user);
+ $this->sendRequest(
+ 'POST', '/apps/spreed/api/' . $apiVersion . '/chat/' . self::$identifierToToken[$identifier] . '/share',
+ new TableNode([
+ ['objectType', $type],
+ ['objectId', $id],
+ ['metaData', $metaData],
+ ])
+ );
+ $this->assertStatusCode($this->response, $statusCode);
+ sleep(1); // make sure Postgres manages the order of the messages
+
+ $response = $this->getDataFromResponse($this->response);
+ if (isset($response['id'])) {
+ self::$messages['shared::' . $type . '::' . $id] = $response['id'];
+ }
+ }
+
+ /**
* @Then /^user "([^"]*)" deletes message "([^"]*)" from room "([^"]*)" with (\d+)(?: \((v(1|2|3))\))?$/
*
* @param string $user
diff --git a/tests/integration/features/chat/rich-object-share.feature b/tests/integration/features/chat/rich-object-share.feature
new file mode 100644
index 000000000..1c9d4152d
--- /dev/null
+++ b/tests/integration/features/chat/rich-object-share.feature
@@ -0,0 +1,20 @@
+Feature: chat/public
+ Background:
+ Given user "participant1" exists
+
+ Scenario: Share a rich object to a chat
+ Given user "participant1" creates room "public room"
+ | roomType | 3 |
+ | roomName | room |
+ When user "participant1" shares rich-object "call" "R4nd0mT0k3n" '{"name":"Another room","call-type":"group"}' to room "public room" with 201 (v1)
+ Then user "participant1" sees the following messages in room "public room" with 200
+ | room | actorType | actorId | actorDisplayName | message | messageParameters |
+ | public room | users | participant1 | participant1-displayname | {object} | {"actor":{"type":"user","id":"participant1","name":"participant1-displayname"},"object":{"name":"Another room","call-type":"group","type":"call","id":"R4nd0mT0k3n"}} |
+
+
+ Scenario: Share an invalid rich object to a chat
+ Given user "participant1" creates room "public room"
+ | roomType | 3 |
+ | roomName | room |
+ When user "participant1" shares rich-object "call" "R4nd0mT0k3n" '{"MISSINGname":"Another room","call-type":"group"}' to room "public room" with 400 (v1)
+ Then user "participant1" sees the following messages in room "public room" with 200
diff --git a/tests/php/CapabilitiesTest.php b/tests/php/CapabilitiesTest.php
index 690e74c71..714ded031 100644
--- a/tests/php/CapabilitiesTest.php
+++ b/tests/php/CapabilitiesTest.php
@@ -85,6 +85,7 @@ class CapabilitiesTest extends TestCase {
'phonebook-search',
'raise-hand',
'room-description',
+ 'rich-object-sharing',
];
}
diff --git a/tests/php/Controller/ChatControllerTest.php b/tests/php/Controller/ChatControllerTest.php
index 30086496c..eb3f0f029 100644
--- a/tests/php/Controller/ChatControllerTest.php
+++ b/tests/php/Controller/ChatControllerTest.php
@@ -47,6 +47,7 @@ use OCP\IL10N;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
+use OCP\RichObjectStrings\IValidator;
use OCP\UserStatus\IManager as IUserStatusManager;
use PHPUnit\Framework\Constraint\Callback;
use PHPUnit\Framework\MockObject\MockObject;
@@ -86,6 +87,8 @@ class ChatControllerTest extends TestCase {
protected $eventDispatcher;
/** @var ITimeFactory|MockObject */
protected $timeFactory;
+ /** @var IValidator|MockObject */
+ protected $richObjectValidator;
/** @var IL10N|MockObject */
private $l;
@@ -117,6 +120,7 @@ class ChatControllerTest extends TestCase {
$this->searchResult = $this->createMock(ISearchResult::class);
$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
+ $this->richObjectValidator = $this->createMock(IValidator::class);
$this->l = $this->createMock(IL10N::class);
$this->room = $this->createMock(Room::class);
@@ -151,6 +155,7 @@ class ChatControllerTest extends TestCase {
$this->searchResult,
$this->timeFactory,
$this->eventDispatcher,
+ $this->richObjectValidator,
$this->l
);
}
@@ -613,6 +618,94 @@ class ChatControllerTest extends TestCase {
$this->assertEquals($expected, $response);
}
+ public function testShareObjectToChatByUser() {
+ $participant = $this->createMock(Participant::class);
+
+ $richData = [
+ 'call-type' => 'one2one',
+ 'type' => 'call',
+ 'id' => 'R4nd0mToken',
+ ];
+
+ $date = new \DateTime();
+ $this->timeFactory->expects($this->once())
+ ->method('getDateTime')
+ ->willReturn($date);
+ /** @var IComment|MockObject $comment */
+ $comment = $this->newComment(42, 'user', $this->userId, $date, 'testMessage');
+ $this->chatManager->expects($this->once())
+ ->method('addSystemMessage')
+ ->with($this->room,
+ 'users',
+ $this->userId,
+ json_encode([
+ 'message' => 'object_shared',
+ 'parameters' => [
+ 'objectType' => 'call',
+ 'objectId' => 'R4nd0mToken',
+ 'metaData' => [
+ 'call-type' => 'one2one',
+ 'type' => 'call',
+ 'id' => 'R4nd0mToken',
+ ],
+ ],
+ ]),
+ $this->newMessageDateTimeConstraint
+ )
+ ->willReturn($comment);
+
+ $chatMessage = $this->createMock(Message::class);
+ $chatMessage->expects($this->once())
+ ->method('getVisibility')
+ ->willReturn(true);
+ $chatMessage->expects($this->once())
+ ->method('toArray')
+ ->willReturn([
+ 'id' => 42,
+ 'token' => 'testToken',
+ 'actorType' => 'users',
+ 'actorId' => $this->userId,
+ 'actorDisplayName' => 'displayName',
+ 'timestamp' => $date->getTimestamp(),
+ 'message' => '{object}',
+ 'messageParameters' => $richData,
+ 'systemMessage' => '',
+ 'messageType' => 'comment',
+ 'isReplyable' => true,
+ 'referenceId' => '',
+ ]);
+
+ $this->messageParser->expects($this->once())
+ ->method('createMessage')
+ ->with($this->room, $participant, $comment, $this->l)
+ ->willReturn($chatMessage);
+
+ $this->messageParser->expects($this->once())
+ ->method('parseMessage')
+ ->with($chatMessage);
+
+ $this->controller->setRoom($this->room);
+ $this->controller->setParticipant($participant);
+ $response = $this->controller->shareObjectToChat($richData['type'], $richData['id'], json_encode(['call-type' => $richData['call-type']]));
+ $expected = new DataResponse([
+ 'id' => 42,
+ 'token' => 'testToken',
+ 'actorType' => 'users',
+ 'actorId' => $this->userId,
+ 'actorDisplayName' => 'displayName',
+ 'timestamp' => $date->getTimestamp(),
+ 'message' => '{object}',
+ 'messageParameters' => $richData,
+ 'systemMessage' => '',
+ 'messageType' => 'comment',
+ 'isReplyable' => true,
+ 'referenceId' => '',
+ ], Http::STATUS_CREATED);
+
+ $this->assertEquals($expected->getStatus(), $response->getStatus());
+ $this->assertEquals($expected->getData(), $response->getData());
+ }
+
public function testReceiveHistoryByUser() {
$offset = 23;
$limit = 4;