diff options
author | Sanskar Soni <sanskarsoni300@gmail.com> | 2024-06-28 15:18:29 +0530 |
---|---|---|
committer | Sanskar Soni <sanskarsoni300@gmail.com> | 2024-06-28 15:21:05 +0530 |
commit | e8af04d5579ab0c165de7c45fee6c42a6f368d47 (patch) | |
tree | 2e342953bca3b130d7c9a37e58acaccac67888f5 | |
parent | ef390fef18a10b18099b92b432901ee0de6ab7c5 (diff) |
feat(bots): Add events for enabling and disabling bots
Signed-off-by: Sanskar Soni <sanskarsoni300@gmail.com>
-rw-r--r-- | docs/bots.md | 94 | ||||
-rw-r--r-- | docs/events.md | 14 | ||||
-rw-r--r-- | lib/AppInfo/Application.php | 4 | ||||
-rw-r--r-- | lib/Command/Bot/Remove.php | 28 | ||||
-rw-r--r-- | lib/Command/Bot/Setup.php | 10 | ||||
-rw-r--r-- | lib/Controller/BotController.php | 12 | ||||
-rw-r--r-- | lib/Events/BotDisabledEvent.php | 26 | ||||
-rw-r--r-- | lib/Events/BotEnabledEvent.php | 26 | ||||
-rw-r--r-- | lib/Listener/BotListener.php | 17 | ||||
-rw-r--r-- | lib/Service/BotService.php | 121 |
10 files changed, 306 insertions, 46 deletions
diff --git a/docs/bots.md b/docs/bots.md index 42613636b..b5817a488 100644 --- a/docs/bots.md +++ b/docs/bots.md @@ -13,7 +13,7 @@ Webhook based bots are available with the Nextcloud 27.1 compatible Nextcloud Ta --- -## Receiving chat messages +## Signing and Verifying Requests Messages are signed using the shared secret that is specified when installing a bot on the server. Create a HMAC with SHA256 over the `RANDOM` header and the request body using the shared secret. @@ -29,6 +29,10 @@ if (!hash_equals($digest, strtolower($_SERVER['HTTP_X_NEXTCLOUD_TALK_SIGNATURE'] } ``` +## Receiving chat messages + +Bot receives all the chat messages following the same signature/verification method. + ### Headers | Header | Content type | Description | @@ -79,6 +83,92 @@ The content format follows the [Activity Streams 2.0 Vocabulary](https://www.w3. | target.id | The token of the conversation in which the message was posted. It can be used to react or reply to the given message. | | target.name | The name of the conversation in which the message was posted. | +## Bot added in a chat + +When the bot is added to a chat, the server sends a request to the bot, informing it of the event. The same signature/verification method is applied. + +### Headers + +| Header | Content type | Description | +|-----------------------------------|---------------------|------------------------------------------------------| +| `HTTP_X_NEXTCLOUD_TALK_SIGNATURE` | `[a-f0-9]{64}` | SHA265 signature of the body | +| `HTTP_X_NEXTCLOUD_TALK_RANDOM` | `[A-Za-z0-9+\]{64}` | Random string used when signing the body | +| `HTTP_X_NEXTCLOUD_TALK_BACKEND` | URI | Base URL of the Nextcloud server sending the message | + +### Content + +The content format follows the [Activity Streams 2.0 Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/). + +#### Sample request + +```json +{ + "type": "Join", + "actor": { + "type": "Application", + "id": "bots/bot-a78f46c5c203141b247554e180e1aa3553d282c6", + "name": "Bot123" + }, + "target": { + "type": "Collection", + "id": "n3xtc10ud", + "name": "world" + } +} +``` + +#### Explanation + +| Path | Description | +|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| actor.id | Bot's [actor type](constants.md#actor-types-of-chat-messages) followed by the `/` slash character and a bot's unique sha1 identifier with `bot-` prefix. | +| actor.name | The display name of the bot. | +| target.id | The token of the conversation in which the bot was added. | +| target.name | The name of the conversation in which the bot was added. | + +## Bot removed from a chat + +When the bot is removed from a chat, the server sends a request to the bot, informing it of the event. The same signature/verification method is applied. + +### Headers + +| Header | Content type | Description | +|-----------------------------------|---------------------|------------------------------------------------------| +| `HTTP_X_NEXTCLOUD_TALK_SIGNATURE` | `[a-f0-9]{64}` | SHA265 signature of the body | +| `HTTP_X_NEXTCLOUD_TALK_RANDOM` | `[A-Za-z0-9+\]{64}` | Random string used when signing the body | +| `HTTP_X_NEXTCLOUD_TALK_BACKEND` | URI | Base URL of the Nextcloud server sending the message | + +### Content + +The content format follows the [Activity Streams 2.0 Vocabulary](https://www.w3.org/TR/activitystreams-vocabulary/). + +#### Sample request + +```json +{ + "type": "Leave", + "actor": { + "type": "Application", + "id": "bots/bot-a78f46c5c203141b247554e180e1aa3553d282c6", + "name": "Bot123" + }, + "target": { + "type": "Collection", + "id": "n3xtc10ud", + "name": "world" + } +} +``` + +#### Explanation + +| Path | Description | +|------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| actor.id | Bot's [actor type](constants.md#actor-types-of-chat-messages) followed by the `/` slash character and a bot's unique sha1 identifier with `bot-` prefix. | +| actor.name | The display name of the bot. | +| target.id | The token of the conversation from which the bot was removed. | +| target.name | The name of the conversation from which the bot was removed. | + ## Sending a chat message Bots can also send message. On the sending process the same signature/verification method is applied. @@ -143,7 +233,7 @@ Bots can also react to a message. The same signature/verification method is appl ## Delete a reaction -Bots can also remove their previous reaction from amessage. The same signature/verification method is applied. +Bots can also remove their previous reaction from a message. The same signature/verification method is applied. * Required capability: `bots-v1` * Method: `DELETE` diff --git a/docs/events.md b/docs/events.md index f71b82fce..93d902c6e 100644 --- a/docs/events.md +++ b/docs/events.md @@ -176,6 +176,20 @@ listen to the `OCA\Talk\Events\SystemMessagesMultipleSentEvent` event instead. * After event: *Not available* * Since: 18.0.0 +### Bot enabled + +Sends a request to the bot server, informing it was added in a chat. + +* Event: `OCA\Talk\Events\BotEnabledEvent` +* Since: 20.0.0 + +### Bot disabled + +Sends a request to the bot server, informing it was removed from a chat. + +* Event: `OCA\Talk\Events\BotDisabledEvent` +* Since: 20.0.0 + ## Inbound events to invoke Talk ### Bot install diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 9dc983047..c0bac6371 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -43,6 +43,8 @@ use OCA\Talk\Events\BeforeRoomDeletedEvent; use OCA\Talk\Events\BeforeRoomsFetchEvent; use OCA\Talk\Events\BeforeSessionLeftRoomEvent; use OCA\Talk\Events\BeforeUserJoinedRoomEvent; +use OCA\Talk\Events\BotDisabledEvent; +use OCA\Talk\Events\BotEnabledEvent; use OCA\Talk\Events\BotInstallEvent; use OCA\Talk\Events\BotUninstallEvent; use OCA\Talk\Events\CallEndedForEveryoneEvent; @@ -174,6 +176,8 @@ class Application extends App implements IBootstrap { $context->registerEventListener(SessionLeftRoomEvent::class, ActivityListener::class, -100); // Bot listeners + $context->registerEventListener(BotDisabledEvent::class, BotListener::class); + $context->registerEventListener(BotEnabledEvent::class, BotListener::class); $context->registerEventListener(BotInstallEvent::class, BotListener::class); $context->registerEventListener(BotUninstallEvent::class, BotListener::class); $context->registerEventListener(ChatMessageSentEvent::class, BotListener::class); diff --git a/lib/Command/Bot/Remove.php b/lib/Command/Bot/Remove.php index e822cf6b8..137ba49e3 100644 --- a/lib/Command/Bot/Remove.php +++ b/lib/Command/Bot/Remove.php @@ -9,7 +9,13 @@ declare(strict_types=1); namespace OCA\Talk\Command\Bot; use OC\Core\Command\Base; +use OCA\Talk\Events\BotDisabledEvent; +use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Manager; use OCA\Talk\Model\BotConversationMapper; +use OCA\Talk\Model\BotServerMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\EventDispatcher\IEventDispatcher; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -17,6 +23,9 @@ use Symfony\Component\Console\Output\OutputInterface; class Remove extends Base { public function __construct( private BotConversationMapper $botConversationMapper, + private BotServerMapper $botServerMapper, + private IEventDispatcher $dispatcher, + private Manager $roomManager, ) { parent::__construct(); } @@ -43,9 +52,26 @@ class Remove extends Base { $botId = (int) $input->getArgument('bot-id'); $tokens = $input->getArgument('token'); - $this->botConversationMapper->deleteByBotIdAndTokens($botId, $tokens); + try { + $botServer = $this->botServerMapper->findById($botId); + } catch (DoesNotExistException) { + $output->writeln('<error>Bot could not be found by id: ' . $botId . '</error>'); + return 1; + } + $this->botConversationMapper->deleteByBotIdAndTokens($botId, $tokens); $output->writeln('<info>Remove bot from given conversations</info>'); + + foreach ($tokens as $token) { + try { + $room = $this->roomManager->getRoomByToken($token); + } catch(RoomNotFoundException) { + continue; + } + $event = new BotDisabledEvent($room, $botServer); + $this->dispatcher->dispatchTyped($event); + } + return 0; } } diff --git a/lib/Command/Bot/Setup.php b/lib/Command/Bot/Setup.php index d07f70187..bb66955f5 100644 --- a/lib/Command/Bot/Setup.php +++ b/lib/Command/Bot/Setup.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace OCA\Talk\Command\Bot; use OC\Core\Command\Base; +use OCA\Talk\Events\BotEnabledEvent; use OCA\Talk\Exceptions\RoomNotFoundException; use OCA\Talk\Manager; use OCA\Talk\Model\Bot; @@ -17,6 +18,7 @@ use OCA\Talk\Model\BotConversationMapper; use OCA\Talk\Model\BotServerMapper; use OCP\AppFramework\Db\DoesNotExistException; use OCP\DB\Exception; +use OCP\EventDispatcher\IEventDispatcher; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -26,6 +28,7 @@ class Setup extends Base { private Manager $roomManager, private BotServerMapper $botServerMapper, private BotConversationMapper $botConversationMapper, + private IEventDispatcher $dispatcher, ) { parent::__construct(); } @@ -53,7 +56,7 @@ class Setup extends Base { $tokens = $input->getArgument('token'); try { - $this->botServerMapper->findById($botId); + $botServer = $this->botServerMapper->findById($botId); } catch (DoesNotExistException) { $output->writeln('<error>Bot could not be found by id: ' . $botId . '</error>'); return 1; @@ -67,10 +70,12 @@ class Setup extends Base { if ($room->isFederatedConversation()) { $output->writeln('<error>Federated conversations can not have bots: ' . $token . '</error>'); $returnCode = 2; + continue; } } catch (RoomNotFoundException) { $output->writeln('<error>Conversation could not be found by token: ' . $token . '</error>'); $returnCode = 2; + continue; } $bot = new BotConversation(); @@ -81,6 +86,9 @@ class Setup extends Base { try { $this->botConversationMapper->insert($bot); $output->writeln('<info>Successfully set up for conversation ' . $token . '</info>'); + + $event = new BotEnabledEvent($room, $botServer); + $this->dispatcher->dispatchTyped($event); } catch (\Exception $e) { if ($e instanceof Exception && $e->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { $output->writeln('<error>Bot is already set up for the conversation ' . $token . '</error>'); diff --git a/lib/Controller/BotController.php b/lib/Controller/BotController.php index 1f934f464..4d53d8f88 100644 --- a/lib/Controller/BotController.php +++ b/lib/Controller/BotController.php @@ -11,6 +11,8 @@ namespace OCA\Talk\Controller; use OCA\Talk\Chat\ChatManager; use OCA\Talk\Chat\ReactionManager; +use OCA\Talk\Events\BotDisabledEvent; +use OCA\Talk\Events\BotEnabledEvent; use OCA\Talk\Exceptions\ReactionAlreadyExistsException; use OCA\Talk\Exceptions\ReactionNotSupportedException; use OCA\Talk\Exceptions\ReactionOutOfContextException; @@ -37,6 +39,7 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Comments\MessageTooLongException; use OCP\Comments\NotFoundException; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IRequest; use Psr\Log\LoggerInterface; @@ -58,6 +61,7 @@ class BotController extends AEnvironmentAwareController { protected Manager $manager, protected ReactionManager $reactionManager, protected LoggerInterface $logger, + private IEventDispatcher $dispatcher, ) { parent::__construct($appName, $request); } @@ -370,6 +374,10 @@ class BotController extends AEnvironmentAwareController { $conversationBot->setState(Bot::STATE_ENABLED); $this->botConversationMapper->insert($conversationBot); + + $event = new BotEnabledEvent($this->room, $bot); + $this->dispatcher->dispatchTyped($event); + return new DataResponse($this->formatBot($bot, true), Http::STATUS_CREATED); } @@ -400,6 +408,10 @@ class BotController extends AEnvironmentAwareController { } $this->botConversationMapper->deleteByBotIdAndTokens($botId, [$this->room->getToken()]); + + $event = new BotDisabledEvent($this->room, $bot); + $this->dispatcher->dispatchTyped($event); + return new DataResponse($this->formatBot($bot, false), Http::STATUS_OK); } diff --git a/lib/Events/BotDisabledEvent.php b/lib/Events/BotDisabledEvent.php new file mode 100644 index 000000000..69e973ef1 --- /dev/null +++ b/lib/Events/BotDisabledEvent.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Talk\Events; + +use OCA\Talk\Model\BotServer; +use OCA\Talk\Room; + +class BotDisabledEvent extends ARoomEvent { + + public function __construct( + Room $room, + protected BotServer $botServer, + ) { + parent::__construct($room); + } + + public function getBotServer(): BotServer { + return $this->botServer; + } +} diff --git a/lib/Events/BotEnabledEvent.php b/lib/Events/BotEnabledEvent.php new file mode 100644 index 000000000..af0a3012b --- /dev/null +++ b/lib/Events/BotEnabledEvent.php @@ -0,0 +1,26 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Talk\Events; + +use OCA\Talk\Model\BotServer; +use OCA\Talk\Room; + +class BotEnabledEvent extends ARoomEvent { + + public function __construct( + Room $room, + protected BotServer $botServer, + ) { + parent::__construct($room); + } + + public function getBotServer(): BotServer { + return $this->botServer; + } +} diff --git a/lib/Listener/BotListener.php b/lib/Listener/BotListener.php index 2ebd3e477..d08c64a4f 100644 --- a/lib/Listener/BotListener.php +++ b/lib/Listener/BotListener.php @@ -10,6 +10,8 @@ declare(strict_types=1); namespace OCA\Talk\Listener; use OCA\Talk\Chat\MessageParser; +use OCA\Talk\Events\BotDisabledEvent; +use OCA\Talk\Events\BotEnabledEvent; use OCA\Talk\Events\BotInstallEvent; use OCA\Talk\Events\BotUninstallEvent; use OCA\Talk\Events\ChatMessageSentEvent; @@ -47,17 +49,24 @@ class BotListener implements IEventListener { return; } - /** @var BotService $service */ - $service = Server::get(BotService::class); + if ($event instanceof BotEnabledEvent) { + $this->botService->afterBotEnabled($event); + return; + } + if ($event instanceof BotDisabledEvent) { + $this->botService->afterBotDisabled($event); + return; + } + /** @var MessageParser $messageParser */ $messageParser = Server::get(MessageParser::class); if ($event instanceof ChatMessageSentEvent) { - $service->afterChatMessageSent($event, $messageParser); + $this->botService->afterChatMessageSent($event, $messageParser); return; } if ($event instanceof SystemMessageSentEvent) { - $service->afterSystemMessageSent($event, $messageParser); + $this->botService->afterSystemMessageSent($event, $messageParser); } } diff --git a/lib/Service/BotSer |