diff options
-rw-r--r-- | .drone.yml | 130 | ||||
-rw-r--r-- | appinfo/routes.php | 1 | ||||
-rw-r--r-- | appinfo/routes/routesReactionController.php | 33 | ||||
-rw-r--r-- | docs/chat.md | 2 | ||||
-rw-r--r-- | docs/index.md | 1 | ||||
-rw-r--r-- | docs/reaction.md | 20 | ||||
-rw-r--r-- | lib/Chat/Parser/Listener.php | 12 | ||||
-rw-r--r-- | lib/Chat/Parser/ReactionParser.php | 43 | ||||
-rw-r--r-- | lib/Chat/ReactionManager.php | 54 | ||||
-rw-r--r-- | lib/Controller/ChatController.php | 6 | ||||
-rw-r--r-- | lib/Controller/ReactionController.php | 95 | ||||
-rw-r--r-- | lib/Model/Message.php | 2 | ||||
-rw-r--r-- | mkdocs.yml | 1 | ||||
-rw-r--r-- | tests/integration/features/bootstrap/FeatureContext.php | 19 | ||||
-rw-r--r-- | tests/integration/features/reaction/react.feature | 31 | ||||
-rw-r--r-- | tests/php/Controller/ChatControllerTest.php | 5 |
16 files changed, 454 insertions, 1 deletions
diff --git a/.drone.yml b/.drone.yml index d25ce0823..78fc3fea9 100644 --- a/.drone.yml +++ b/.drone.yml @@ -178,6 +178,43 @@ trigger: --- kind: pipeline +name: int-sqlite-reaction + +steps: + - name: integration-reaction + image: ghcr.io/nextcloud/continuous-integration-php8.0:latest + environment: + APP_NAME: spreed + CORE_BRANCH: master + GUESTS_BRANCH: master + DATABASEHOST: sqlite + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - wget https://raw.githubusercontent.com/nextcloud/travis_ci/master/before_install.sh + - bash ./before_install.sh $APP_NAME $CORE_BRANCH $DATABASEHOST + - cd ../server + - git clone --depth 1 -b "$GUESTS_BRANCH" https://github.com/nextcloud/guests apps/guests + - ./occ app:enable $APP_NAME + - cd apps/$APP_NAME + + # Run integration tests + - cd tests/integration/ + - bash run.sh features/reaction + +services: + - name: cache + image: ghcr.io/nextcloud/continuous-integration-redis:latest + +trigger: + branch: + - master + - stable* + event: + - pull_request + - push + +--- +kind: pipeline name: int-sqlite-sharing steps: @@ -477,6 +514,53 @@ trigger: --- kind: pipeline +name: int-mysql-reaction + +steps: + - name: integration-reaction + image: ghcr.io/nextcloud/continuous-integration-php8.0:latest + environment: + APP_NAME: spreed + CORE_BRANCH: master + GUESTS_BRANCH: master + DATABASEHOST: mysql + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - wget https://raw.githubusercontent.com/nextcloud/travis_ci/master/before_install.sh + - bash ./before_install.sh $APP_NAME $CORE_BRANCH $DATABASEHOST + - cd ../server + - git clone --depth 1 -b "$GUESTS_BRANCH" https://github.com/nextcloud/guests apps/guests + - ./occ app:enable $APP_NAME + - cd apps/$APP_NAME + + # Run integration tests + - cd tests/integration/ + - bash run.sh features/reaction + +services: + - name: cache + image: ghcr.io/nextcloud/continuous-integration-redis:latest + - name: mysql + image: ghcr.io/nextcloud/continuous-integration-mariadb-10.4:10.4 + environment: + MYSQL_ROOT_PASSWORD: owncloud + MYSQL_USER: oc_autotest + MYSQL_PASSWORD: owncloud + MYSQL_DATABASE: oc_autotest + command: [ "--innodb_large_prefix=true", "--innodb_file_format=barracuda", "--innodb_file_per_table=true" ] + tmpfs: + - /var/lib/mysql + +trigger: + branch: + - master + - stable* + event: +# - pull_request + - push + +--- +kind: pipeline name: int-mysql-sharing steps: @@ -791,6 +875,52 @@ trigger: --- kind: pipeline +name: int-pgsql-reaction + +steps: + - name: integration-reaction + image: ghcr.io/nextcloud/continuous-integration-php8.0:latest + environment: + APP_NAME: spreed + CORE_BRANCH: master + GUESTS_BRANCH: master + DATABASEHOST: pgsql + commands: + - bash tests/drone-run-integration-tests.sh || exit 0 + - wget https://raw.githubusercontent.com/nextcloud/travis_ci/master/before_install.sh + - bash ./before_install.sh $APP_NAME $CORE_BRANCH $DATABASEHOST + - cd ../server + - git clone --depth 1 -b "$GUESTS_BRANCH" https://github.com/nextcloud/guests apps/guests + - ./occ app:enable $APP_NAME + - cd apps/$APP_NAME + + # Run integration tests + - cd tests/integration/ + - bash run.sh features/reaction + +services: + - name: cache + image: ghcr.io/nextcloud/continuous-integration-redis:latest + - name: pgsql + image: ghcr.io/nextcloud/continuous-integration-postgres-13:postgres-13 + environment: + POSTGRES_USER: oc_autotest + POSTGRES_DB: oc_autotest_dummy + POSTGRES_HOST_AUTH_METHOD: trust + POSTGRES_PASSWORD: + tmpfs: + - /var/lib/postgresql/data + +trigger: + branch: + - master + - stable* + event: +# - pull_request + - push + +--- +kind: pipeline name: int-pgsql-sharing steps: diff --git a/appinfo/routes.php b/appinfo/routes.php index e888b7006..fde29617e 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -36,6 +36,7 @@ return array_merge_recursive( include(__DIR__ . '/routes/routesMatterbridgeSettingsController.php'), include(__DIR__ . '/routes/routesPageController.php'), include(__DIR__ . '/routes/routesPublicShareAuthController.php'), + include(__DIR__ . '/routes/routesReactionController.php'), include(__DIR__ . '/routes/routesRoomController.php'), include(__DIR__ . '/routes/routesSettingsController.php'), include(__DIR__ . '/routes/routesSignalingController.php'), diff --git a/appinfo/routes/routesReactionController.php b/appinfo/routes/routesReactionController.php new file mode 100644 index 000000000..bed62fb15 --- /dev/null +++ b/appinfo/routes/routesReactionController.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +return [ + 'ocs' => [ + ['name' => 'Reaction#react', 'url' => '/api/{apiVersion}/reaction/{token}/{messageId}', 'verb' => 'POST', 'requirements' => [ + 'apiVersion' => 'v1', + 'token' => '^[a-z0-9]{4,30}$', + ]], + ], +]; diff --git a/docs/chat.md b/docs/chat.md index ff03979ef..dd496a7b4 100644 --- a/docs/chat.md +++ b/docs/chat.md @@ -50,6 +50,7 @@ Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` `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)) `parent` | array | **Optional:** See `Parent data` below + `reactions` | array | **Optional:** An array map with relation between reaction emoji and total of reactions with this emoji #### Parent data @@ -324,3 +325,4 @@ See [OCP\RichObjectStrings\Definitions](https://github.com/nextcloud/server/blob * `matterbridge_config_removed` - {actor} removed the Matterbridge configuration * `matterbridge_config_enabled` - {actor} started Matterbridge * `matterbridge_config_disabled` - {actor} stopped Matterbridge + diff --git a/docs/index.md b/docs/index.md index 0e19b823f..3e1dec1dc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -16,6 +16,7 @@ * [Participant API](participant.md) * [Call API](call.md) * [Chat API](chat.md) +* [Reaction API](reaction.md) * [Webinar API](webinar.md) * [Internal Signaling API](internal-signaling.md) * [Standalone Signaling API](https://nextcloud-spreed-signaling.readthedocs.io/en/latest/) diff --git a/docs/reaction.md b/docs/reaction.md new file mode 100644 index 000000000..54a58b75a --- /dev/null +++ b/docs/reaction.md @@ -0,0 +1,20 @@ +# Reaction API + +Base endpoint is: `/ocs/v2.php/apps/spreed/api/v1` + +## React to a message + +* Method: `POST` +* Endpoint: `/chat/{token}/{messageId}` +* Data: + + field | type | Description + ---|---|--- + `reaction` | string | the reaction emoji + +* Response: + - Status code: + + `201 Created` + + `400 Bad Request` In case of any other error + + `404 Not Found` When the conversation or message to react could not be found for the participant + + `409 Conflict` User already did this reaction to this message diff --git a/lib/Chat/Parser/Listener.php b/lib/Chat/Parser/Listener.php index 83e26f673..593d9bd6e 100644 --- a/lib/Chat/Parser/Listener.php +++ b/lib/Chat/Parser/Listener.php @@ -100,6 +100,18 @@ class Listener { $dispatcher->addListener(MessageParser::EVENT_MESSAGE_PARSE, static function (ChatMessageEvent $event) { $chatMessage = $event->getMessage(); + if ($chatMessage->getMessageType() !== 'reaction') { + return; + } + + /** @var ReactionParser $parser */ + $parser = \OC::$server->get(ReactionParser::class); + $parser->parseMessage($chatMessage); + }); + + $dispatcher->addListener(MessageParser::EVENT_MESSAGE_PARSE, static function (ChatMessageEvent $event) { + $chatMessage = $event->getMessage(); + if ($chatMessage->getMessageType() !== 'comment_deleted') { return; } diff --git a/lib/Chat/Parser/ReactionParser.php b/lib/Chat/Parser/ReactionParser.php new file mode 100644 index 000000000..3ec9d8801 --- /dev/null +++ b/lib/Chat/Parser/ReactionParser.php @@ -0,0 +1,43 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Chat\Parser; + +use OCA\Talk\Model\Message; + +class ReactionParser { + /** + * @param Message $message + * @throws \OutOfBoundsException + */ + public function parseMessage(Message $message): void { + $comment = $message->getComment(); + if (!in_array($comment->getVerb(), ['reaction'])) { + throw new \OutOfBoundsException('Not a reaction'); + } + $message->setMessageType('system'); + $message->setMessage($message->getMessage(), [], $comment->getVerb()); + } +} diff --git a/lib/Chat/ReactionManager.php b/lib/Chat/ReactionManager.php new file mode 100644 index 000000000..4ea721a7d --- /dev/null +++ b/lib/Chat/ReactionManager.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Chat; + +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCP\Comments\IComment; +use OCP\Comments\ICommentsManager; + +class ReactionManager { + /** @var ICommentsManager|CommentsManager */ + private $commentsManager; + + public function __construct(CommentsManager $commentsManager) { + $this->commentsManager = $commentsManager; + } + + public function addReactionMessage(Room $chat, Participant $participant, int $messageId, string $reaction): IComment { + $comment = $this->commentsManager->create( + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId(), + 'chat', + (string) $chat->getId() + ); + $comment->setParentId((string) $messageId); + $comment->setMessage($reaction); + $comment->setVerb('reaction'); + $this->commentsManager->save($comment); + return $comment; + } +} diff --git a/lib/Controller/ChatController.php b/lib/Controller/ChatController.php index 312965dc3..e97bc4fc6 100644 --- a/lib/Controller/ChatController.php +++ b/lib/Controller/ChatController.php @@ -27,6 +27,7 @@ namespace OCA\Talk\Controller; use OCA\Talk\Chat\AutoComplete\SearchPlugin; use OCA\Talk\Chat\AutoComplete\Sorter; use OCA\Talk\Chat\ChatManager; +use OCA\Talk\Chat\CommentsManager; use OCA\Talk\Chat\MessageParser; use OCA\Talk\GuestManager; use OCA\Talk\MatterbridgeManager; @@ -68,6 +69,9 @@ class ChatController extends AEnvironmentAwareController { /** @var IAppManager */ private $appManager; + /** @var CommentsManager */ + private $commentsManager; + /** @var ChatManager */ private $chatManager; @@ -121,6 +125,7 @@ class ChatController extends AEnvironmentAwareController { IRequest $request, IUserManager $userManager, IAppManager $appManager, + CommentsManager $commentsManager, ChatManager $chatManager, ParticipantService $participantService, SessionService $sessionService, @@ -141,6 +146,7 @@ class ChatController extends AEnvironmentAwareController { $this->userId = $UserId; $this->userManager = $userManager; $this->appManager = $appManager; + $this->commentsManager = $commentsManager; $this->chatManager = $chatManager; $this->participantService = $participantService; $this->sessionService = $sessionService; diff --git a/lib/Controller/ReactionController.php b/lib/Controller/ReactionController.php new file mode 100644 index 000000000..0343ff061 --- /dev/null +++ b/lib/Controller/ReactionController.php @@ -0,0 +1,95 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2021 Vitor Mattos <vitor@php.rio> + * + * @author Vitor Mattos <vitor@php.rio> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +namespace OCA\Talk\Controller; + +use OCA\Talk\Chat\ChatManager; +use OCA\Talk\Chat\CommentsManager; +use OCA\Talk\Chat\ReactionManager; +use OCP\AppFramework\Http; +use OCP\AppFramework\Http\DataResponse; +use OCP\Comments\NotFoundException; +use OCP\IRequest; + +class ReactionController extends AEnvironmentAwareController { + /** @var CommentsManager */ + private $commentsManager; + /** @var ChatManager */ + private $chatManager; + /** @var ReactionManager */ + private $reactionManager; + + public function __construct(string $appName, + IRequest $request, + CommentsManager $commentsManager, + ChatManager $chatManager, + ReactionManager $reactionManager) { + parent::__construct($appName, $request); + + $this->commentsManager = $commentsManager; + $this->chatManager = $chatManager; + $this->reactionManager = $reactionManager; + } + + /** + * @NoAdminRequired + * @RequireParticipant + * @RequireReadWriteConversation + * @RequireModeratorOrNoLobby + * + * @param int $messageId for reaction + * @param string $reaction the reaction emoji + * @return DataResponse + */ + public function react(int $messageId, string $reaction): DataResponse { + $participant = $this->getParticipant(); + try { + // Verify that messageId is part of the room + $this->chatManager->getComment($this->getRoom(), (string) $messageId); + } catch (NotFoundException $e) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } + + try { + // Verify already reacted whith the same reaction + $this->commentsManager->getReactionComment( + $messageId, + $participant->getAttendee()->getActorType(), + $participant->getAttendee()->getActorId(), + $reaction + ); + return new DataResponse([], Http::STATUS_CONFLICT); + } catch (NotFoundException $e) { + } + + try { + $this->reactionManager->addReactionMessage($this->getRoom(), $participant, $messageId, $reaction); + } catch (\Exception $e) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse([], Http::STATUS_CREATED); + } +} diff --git a/lib/Model/Message.php b/lib/Model/Message.php index 30f1fa6d9..118adad72 100644 --- a/lib/Model/Message.php +++ b/lib/Model/Message.php @@ -163,6 +163,7 @@ class Message { return $this->getMessageType() !== 'system' && $this->getMessageType() !== 'command' && $this->getMessageType() !== 'comment_deleted' && + $this->getMessageType() !== 'reaction' && \in_array($this->getActorType(), [Attendee::ACTOR_USERS, Attendee::ACTOR_GUESTS]); } @@ -180,6 +181,7 @@ class Message { 'messageType' => $this->getMessageType(), 'isReplyable' => $this->isReplyable(), 'referenceId' => (string) $this->getComment()->getReferenceId(), + 'reactions' => $this->getComment()->getReactions(), ]; if ($this->getMessageType() === 'comment_deleted') { diff --git a/mkdocs.yml b/mkdocs.yml index 28860dae3..d83858d64 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -26,6 +26,7 @@ nav: - 'Participants management': 'participant.md' - 'Call management': 'call.md' - 'Chat management': 'chat.md' + - 'Reaction management': 'reaction.md' - 'Webinar management': 'webinar.md' - 'Settings': 'settings.md' - 'Integration by other apps': 'integration.md' diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 58814dd15..aaf63c134 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -1559,6 +1559,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { } $includeParents = in_array('parentMessage', $formData->getRow(0), true); $includeReferenceId = in_array('referenceId', $formData->getRow(0), true); + $includeReactions = in_array('reactions', $formData->getRow(0), true); $count = count($formData->getHash()); Assert::assertCount($count, $messages, 'Message count does not match'); @@ -1567,7 +1568,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { $messages[$i]['messageParameters'] = 'IGNORE'; } } - Assert::assertEquals($formData->getHash(), array_map(function ($message) use ($includeParents, $includeReferenceId) { + Assert::assertEquals($formData->getHash(), array_map(function ($message) use ($includeParents, $includeReferenceId, $includeReactions) { $data = [ 'room' => self::$tokenToIdentifier[$message['token']], 'actorType' => $message['actorType'], @@ -1584,6 +1585,9 @@ class FeatureContext implements Context, SnippetAcceptingContext { if ($includeReferenceId) { $data['referenceId'] = $message['referenceId']; } + if ($includeReactions) { + $data['reactions'] = json_encode($message['reactions'], JSON_UNESCAPED_UNICODE); + } return $data; }, $messages)); } @@ -2079,6 +2083,19 @@ class FeatureContext implements Context, SnippetAcceptingContext { $this->setCurrentUser($currentUs |