diff options
author | Joas Schilling <coding@schilljs.com> | 2023-08-07 14:39:28 +0200 |
---|---|---|
committer | Joas Schilling <coding@schilljs.com> | 2023-08-08 10:46:57 +0200 |
commit | 13c5ced6ac827ae7e51415be01274eadb4a945b0 (patch) | |
tree | 82e1b610b9254d4b1205432696e956a49d545aef | |
parent | 49e69db76dca56bbd93ae841a17d581a25703e23 (diff) |
feat(bot): Allow reactions by bots
Signed-off-by: Joas Schilling <coding@schilljs.com>
-rw-r--r-- | appinfo/routes/routesBotController.php | 10 | ||||
-rw-r--r-- | lib/Chat/ReactionManager.php | 30 | ||||
-rw-r--r-- | lib/Controller/BotController.php | 139 | ||||
-rw-r--r-- | lib/Controller/ReactionController.php | 6 | ||||
-rw-r--r-- | tests/integration/features/bootstrap/FeatureContext.php | 9 | ||||
-rw-r--r-- | tests/integration/features/chat/bots.feature | 8 |
6 files changed, 161 insertions, 41 deletions
diff --git a/appinfo/routes/routesBotController.php b/appinfo/routes/routesBotController.php index f61d6aa1f..90910d2a3 100644 --- a/appinfo/routes/routesBotController.php +++ b/appinfo/routes/routesBotController.php @@ -28,6 +28,12 @@ $requirements = [ 'token' => '[a-z0-9]{4,30}', ]; +$requirementsWithMessageId = [ + 'apiVersion' => 'v1', + 'token' => '[a-z0-9]{4,30}', + 'messageId' => '[0-9]+', +]; + $requirementsWithBotId = [ 'apiVersion' => 'v1', 'token' => '[a-z0-9]{4,30}', @@ -38,6 +44,10 @@ return [ 'ocs' => [ /** @see \OCA\Talk\Controller\BotController::sendMessage() */ ['name' => 'Bot#sendMessage', 'url' => '/api/{apiVersion}/bot/{token}/message', 'verb' => 'POST', 'requirements' => $requirements], + /** @see \OCA\Talk\Controller\BotController::react() */ + ['name' => 'Bot#react', 'url' => '/api/{apiVersion}/bot/{token}/reaction/{messageId}', 'verb' => 'POST', 'requirements' => $requirementsWithMessageId], + /** @see \OCA\Talk\Controller\BotController::deleteReaction() */ + ['name' => 'Bot#deleteReaction', 'url' => '/api/{apiVersion}/bot/{token}/reaction/{messageId}', 'verb' => 'DELETE', 'requirements' => $requirementsWithMessageId], /** @see \OCA\Talk\Controller\BotController::listBots() */ ['name' => 'Bot#listBots', 'url' => '/api/{apiVersion}/bot/{token}', 'verb' => 'GET', 'requirements' => $requirements], /** @see \OCA\Talk\Controller\BotController::enableBot() */ diff --git a/lib/Chat/ReactionManager.php b/lib/Chat/ReactionManager.php index 52de4d53a..d00fb39f8 100644 --- a/lib/Chat/ReactionManager.php +++ b/lib/Chat/ReactionManager.php @@ -63,7 +63,8 @@ class ReactionManager { * Add reaction * * @param Room $chat - * @param Participant $participant + * @param string $actorType + * @param string $actorId * @param integer $messageId * @param string $reaction * @return IComment @@ -72,14 +73,14 @@ class ReactionManager { * @throws ReactionNotSupportedException * @throws ReactionOutOfContextException */ - public function addReactionMessage(Room $chat, Participant $participant, int $messageId, string $reaction): IComment { + public function addReactionMessage(Room $chat, string $actorType, string $actorId, int $messageId, string $reaction): IComment { $parentMessage = $this->getCommentToReact($chat, (string) $messageId); try { // Check if the user already reacted with the same reaction $this->commentsManager->getReactionComment( (int) $parentMessage->getId(), - $participant->getAttendee()->getActorType(), - $participant->getAttendee()->getActorId(), + $actorType, + $actorId, $reaction ); throw new ReactionAlreadyExistsException(); @@ -87,8 +88,8 @@ class ReactionManager { } $comment = $this->commentsManager->create( - $participant->getAttendee()->getActorType(), - $participant->getAttendee()->getActorId(), + $actorType, + $actorId, 'chat', (string) $chat->getId() ); @@ -105,7 +106,8 @@ class ReactionManager { * Delete reaction * * @param Room $chat - * @param Participant $participant + * @param string $actorType + * @param string $actorId * @param integer $messageId * @param string $reaction * @return IComment @@ -113,20 +115,20 @@ class ReactionManager { * @throws ReactionNotSupportedException * @throws ReactionOutOfContextException */ - public function deleteReactionMessage(Room $chat, Participant $participant, int $messageId, string $reaction): IComment { + public function deleteReactionMessage(Room $chat, string $actorType, string $actorId, int $messageId, string $reaction): IComment { // Just to verify that messageId is part of the room and throw error if not. $this->getCommentToReact($chat, (string) $messageId); $comment = $this->commentsManager->getReactionComment( $messageId, - $participant->getAttendee()->getActorType(), - $participant->getAttendee()->getActorId(), + $actorType, + $actorId, $reaction ); $comment->setMessage( json_encode([ - 'deleted_by_type' => $participant->getAttendee()->getActorType(), - 'deleted_by_id' => $participant->getAttendee()->getActorId(), + 'deleted_by_type' => $actorType, + 'deleted_by_id' => $actorId, 'deleted_on' => $this->timeFactory->getDateTime()->getTimestamp(), ]) ); @@ -135,8 +137,8 @@ class ReactionManager { $this->chatManager->addSystemMessage( $chat, - $participant->getAttendee()->getActorType(), - $participant->getAttendee()->getActorId(), + $actorType, + $actorId, json_encode(['message' => 'reaction_revoked', 'parameters' => ['message' => (int) $comment->getId()]]), $this->timeFactory->getDateTime(), false, diff --git a/lib/Controller/BotController.php b/lib/Controller/BotController.php index 6492bd95e..3e763dc13 100644 --- a/lib/Controller/BotController.php +++ b/lib/Controller/BotController.php @@ -27,6 +27,10 @@ declare(strict_types=1); namespace OCA\Talk\Controller; use OCA\Talk\Chat\ChatManager; +use OCA\Talk\Chat\ReactionManager; +use OCA\Talk\Exceptions\ReactionAlreadyExistsException; +use OCA\Talk\Exceptions\ReactionNotSupportedException; +use OCA\Talk\Exceptions\ReactionOutOfContextException; use OCA\Talk\Exceptions\UnauthorizedException; use OCA\Talk\Manager; use OCA\Talk\Middleware\Attribute\RequireLoggedInModeratorParticipant; @@ -63,42 +67,31 @@ class BotController extends AEnvironmentAwareController { protected BotServerMapper $botServerMapper, protected BotService $botService, protected Manager $manager, + protected ReactionManager $reactionManager, protected LoggerInterface $logger, ) { parent::__construct($appName, $request); } /** - * Sends a new chat message to the given room. - * - * The author and timestamp are automatically set to the current user/guest - * and time. - * - * @param string $token conversation token - * @param string $message the message to send - * @param string $referenceId for the message to be able to later identify it again - * @param int $replyTo Parent id which this message is a reply to - * @param bool $silent If sent silent the chat message will not create any notifications - * @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". + * @param string $token + * @param string $message + * @return Bot + * @throws \InvalidArgumentException When the request could not be linked with a bot */ - #[BruteForceProtection(action: 'bot')] - #[PublicPage] - public function sendMessage(string $token, string $message, string $referenceId = '', int $replyTo = 0, bool $silent = false): DataResponse { + protected function getBotFromHeaders(string $token, string $message): Bot { $random = $this->request->getHeader('X-Nextcloud-Talk-Bot-Random'); if (empty($random) || strlen($random) < 32) { $this->logger->error('Invalid Random received from bot response'); - return new DataResponse([], Http::STATUS_BAD_REQUEST); + throw new \InvalidArgumentException('Invalid Random received from bot response', Http::STATUS_BAD_REQUEST); } $checksum = $this->request->getHeader('X-Nextcloud-Talk-Bot-Signature'); if (empty($checksum)) { $this->logger->error('Invalid Signature received from bot response'); - return new DataResponse([], Http::STATUS_BAD_REQUEST); + throw new \InvalidArgumentException('Invalid Signature received from bot response', Http::STATUS_BAD_REQUEST); } $bots = $this->botService->getBotsForToken($token); - $bot = null; foreach ($bots as $botAttempt) { try { $this->checksumVerificationService->validateRequest( @@ -107,16 +100,40 @@ class BotController extends AEnvironmentAwareController { $botAttempt->getBotServer()->getSecret(), $message ); - $bot = $botAttempt; - break; + return $botAttempt; } catch (UnauthorizedException) { } } - if (!$bot instanceof Bot) { - $this->logger->debug('No valid Bot entry found'); - $response = new DataResponse([], Http::STATUS_UNAUTHORIZED); - $response->throttle(['action' => 'bot']); + $this->logger->debug('No valid Bot entry found'); + throw new \InvalidArgumentException('No valid Bot entry found', Http::STATUS_UNAUTHORIZED); + } + + /** + * Sends a new chat message to the given room. + * + * The author and timestamp are automatically set to the current user/guest + * and time. + * + * @param string $token conversation token + * @param string $message the message to send + * @param string $referenceId for the message to be able to later identify it again + * @param int $replyTo Parent id which this message is a reply to + * @param bool $silent If sent silent the chat message will not create any notifications + * @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". + */ + #[BruteForceProtection(action: 'bot')] + #[PublicPage] + public function sendMessage(string $token, string $message, string $referenceId = '', int $replyTo = 0, bool $silent = false): DataResponse { + try { + $bot = $this->getBotFromHeaders($token, $message); + } catch (\InvalidArgumentException $e) { + $response = new DataResponse([], $e->getCode()); + if ($e->getCode() === Http::STATUS_UNAUTHORIZED) { + $response->throttle(['action' => 'bot']); + } return $response; } @@ -149,6 +166,78 @@ class BotController extends AEnvironmentAwareController { return new DataResponse([], Http::STATUS_CREATED); } + #[BruteForceProtection(action: 'bot')] + #[PublicPage] + public function react(string $token, int $messageId, string $reaction): DataResponse { + try { + $bot = $this->getBotFromHeaders($token, $reaction); + } catch (\InvalidArgumentException $e) { + $response = new DataResponse([], $e->getCode()); + if ($e->getCode() === Http::STATUS_UNAUTHORIZED) { + $response->throttle(['action' => 'bot']); + } + return $response; + } + + $room = $this->manager->getRoomByToken($token); + + $actorType = Attendee::ACTOR_BOTS; + $actorId = Attendee::ACTOR_BOT_PREFIX . $bot->getBotServer()->getUrlHash(); + + try { + $this->reactionManager->addReactionMessage( + $room, + $actorType, + $actorId, + $messageId, + $reaction + ); + } catch (NotFoundException) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } catch (ReactionAlreadyExistsException) { + return new DataResponse([], Http::STATUS_OK); + } catch (ReactionNotSupportedException | ReactionOutOfContextException | \Exception) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse([], Http::STATUS_CREATED); + } + + #[BruteForceProtection(action: 'bot')] + #[PublicPage] + public function deleteReaction(string $token, int $messageId, string $reaction): DataResponse { + try { + $bot = $this->getBotFromHeaders($token, $reaction); + } catch (\InvalidArgumentException $e) { + $response = new DataResponse([], $e->getCode()); + if ($e->getCode() === Http::STATUS_UNAUTHORIZED) { + $response->throttle(['action' => 'bot']); + } + return $response; + } + + $room = $this->manager->getRoomByToken($token); + + $actorType = Attendee::ACTOR_BOTS; + $actorId = Attendee::ACTOR_BOT_PREFIX . $bot->getBotServer()->getUrlHash(); + + try { + $this->reactionManager->deleteReactionMessage( + $room, + $actorType, + $actorId, + $messageId, + $reaction + ); + } catch (ReactionNotSupportedException | ReactionOutOfContextException | NotFoundException) { + return new DataResponse([], Http::STATUS_NOT_FOUND); + } catch (\Exception) { + return new DataResponse([], Http::STATUS_BAD_REQUEST); + } + + return new DataResponse([], Http::STATUS_OK); + } + #[NoAdminRequired] #[RequireLoggedInModeratorParticipant] public function listBots(): DataResponse { diff --git a/lib/Controller/ReactionController.php b/lib/Controller/ReactionController.php index add91bec7..5d9bbe531 100644 --- a/lib/Controller/ReactionController.php +++ b/lib/Controller/ReactionController.php @@ -60,7 +60,8 @@ class ReactionController extends AEnvironmentAwareController { try { $this->reactionManager->addReactionMessage( $this->getRoom(), - $this->getParticipant(), + $this->getParticipant()->getAttendee()->getActorType(), + $this->getParticipant()->getAttendee()->getActorId(), $messageId, $reaction ); @@ -85,7 +86,8 @@ class ReactionController extends AEnvironmentAwareController { try { $this->reactionManager->deleteReactionMessage( $this->getRoom(), - $this->getParticipant(), + $this->getParticipant()->getAttendee()->getActorType(), + $this->getParticipant()->getAttendee()->getActorId(), $messageId, $reaction ); diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 28c7796dc..cf4037692 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -3191,6 +3191,7 @@ class FeatureContext implements Context, SnippetAcceptingContext { * @Given /^user "([^"]*)" retrieve reactions "([^"]*)" of message "([^"]*)" in room "([^"]*)" with (\d+)(?: \((v1)\))?$/ */ public function userRetrieveReactionsOfMessageInRoomWith(string $user, string $reaction, string $message, string $identifier, int $statusCode, string $apiVersion = 'v1', ?TableNode $formData = null): void { + $message = str_replace('\n', "\n", $message); $token = self::$identifierToToken[$identifier]; $messageId = self::$textToMessageId[$message]; $this->setCurrentUser($user); @@ -3208,6 +3209,14 @@ class FeatureContext implements Context, SnippetAcceptingContext { foreach ($formData->getHash() as $row) { $reaction = $row['reaction']; unset($row['reaction']); + + if ($row['actorType'] === 'bots') { + $result = preg_match('/BOT\(([^)]+)\)/', $row['actorId'], $matches); + if ($result && isset(self::$botNameToHash[$matches[1]])) { + $row['actorId'] = 'bot-' . self::$botNameToHash[$matches[1]]; + } + } + $expected[$reaction][] = $row; } diff --git a/tests/integration/features/chat/bots.feature b/tests/integration/features/chat/bots.feature index 6a1024c00..89ff60b56 100644 --- a/tests/integration/features/chat/bots.feature +++ b/tests/integration/features/chat/bots.feature @@ -60,6 +60,14 @@ Feature: chat/bots | room | users | participant1 | participant1-displayname | - Task 2\n-Task 3 | [] | | room | users | participant1 | participant1-displayname | * Task 1 | [] | | room | users | participant1 | participant1-displayname | - Before call | [] | + Then user "participant1" retrieve reactions "👍" of message "- Before call" in room "room" with 200 + | actorType | actorId | actorDisplayName | reaction | + Then user "participant1" retrieve reactions "👍" of message "* Task 1" in room "room" with 200 + | actorType | actorId | actorDisplayName | reaction | + | bots | BOT(Call summary) | Call summary (Bot) | 👍 | + Then user "participant1" retrieve reactions "👍" of message "- Task 2\n-Task 3" in room "room" with 200 + | actorType | actorId | actorDisplayName | reaction | + | bots | BOT(Call summary) | Call summary (Bot) | 👍 | # Different states bot # Already enabled |