summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSanskar Soni <sanskarsoni300@gmail.com>2024-06-28 15:18:29 +0530
committerSanskar Soni <sanskarsoni300@gmail.com>2024-06-28 15:21:05 +0530
commite8af04d5579ab0c165de7c45fee6c42a6f368d47 (patch)
tree2e342953bca3b130d7c9a37e58acaccac67888f5
parentef390fef18a10b18099b92b432901ee0de6ab7c5 (diff)
feat(bots): Add events for enabling and disabling bots
Signed-off-by: Sanskar Soni <sanskarsoni300@gmail.com>
-rw-r--r--docs/bots.md94
-rw-r--r--docs/events.md14
-rw-r--r--lib/AppInfo/Application.php4
-rw-r--r--lib/Command/Bot/Remove.php28
-rw-r--r--lib/Command/Bot/Setup.php10
-rw-r--r--lib/Controller/BotController.php12
-rw-r--r--lib/Events/BotDisabledEvent.php26
-rw-r--r--lib/Events/BotEnabledEvent.php26
-rw-r--r--lib/Listener/BotListener.php17
-rw-r--r--lib/Service/BotService.php121
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