Skip to content

Commit

Permalink
feat(bots): Add events for enabling and disabling bots
Browse files Browse the repository at this point in the history
Signed-off-by: Sanskar Soni <sanskarsoni300@gmail.com>
  • Loading branch information
sanskar-soni-9 committed Jun 21, 2024
1 parent 38bfcad commit 61928d5
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 47 deletions.
90 changes: 88 additions & 2 deletions docs/bots.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -29,6 +29,53 @@ if (!hash_equals($digest, strtolower($_SERVER['HTTP_X_NEXTCLOUD_TALK_SIGNATURE']
}
```

## 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](https://github.com/nextcloud/spreed/blob/main/docs/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 |
Expand All @@ -41,6 +88,45 @@ if (!hash_equals($digest, strtolower($_SERVER['HTTP_X_NEXTCLOUD_TALK_SIGNATURE']

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](https://github.com/nextcloud/spreed/blob/main/docs/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 removed. |
| target.name | The name of the conversation in which the bot was removed. |

## Receiving chat messages

Bot receives all the chat messages following the same signature/verification method.

### 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 |

#### Sample chat message

```json
Expand Down Expand Up @@ -143,7 +229,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`
Expand Down
14 changes: 14 additions & 0 deletions docs/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
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;
Expand Down Expand Up @@ -174,6 +176,8 @@ public function register(IRegistrationContext $context): void {
$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);
Expand Down
22 changes: 21 additions & 1 deletion lib/Command/Bot/Remove.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,22 @@
namespace OCA\Talk\Command\Bot;

use OC\Core\Command\Base;
use OCA\Talk\Events\BotDisabledEvent;
use OCA\Talk\Manager;
use OCA\Talk\Model\BotServerMapper;
use OCA\Talk\Model\BotConversationMapper;
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;

class Remove extends Base {
public function __construct(
private BotConversationMapper $botConversationMapper,
private BotServerMapper $botServerMapper,
private IEventDispatcher $dispatcher,
private Manager $roomManager,
) {
parent::__construct();
}
Expand All @@ -43,9 +51,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$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) {
$event = new BotDisabledEvent($this->roomManager->getRoomByToken($token), $botServer);
$this->dispatcher->dispatchTyped($event);
}

return 0;
}
}
8 changes: 7 additions & 1 deletion lib/Command/Bot/Setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;
Expand All @@ -17,6 +18,7 @@
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;
Expand All @@ -26,6 +28,7 @@ public function __construct(
private Manager $roomManager,
private BotServerMapper $botServerMapper,
private BotConversationMapper $botConversationMapper,
private IEventDispatcher $dispatcher,
) {
parent::__construct();
}
Expand Down Expand Up @@ -53,7 +56,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$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;
Expand Down Expand Up @@ -81,6 +84,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int
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>');
Expand Down
12 changes: 12 additions & 0 deletions lib/Controller/BotController.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

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;
Expand All @@ -37,6 +39,7 @@
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Comments\MessageTooLongException;
use OCP\Comments\NotFoundException;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\IRequest;
use Psr\Log\LoggerInterface;

Expand All @@ -58,6 +61,7 @@ public function __construct(
protected Manager $manager,
protected ReactionManager $reactionManager,
protected LoggerInterface $logger,
private IEventDispatcher $dispatcher,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -370,6 +374,10 @@ public function enableBot(int $botId): DataResponse {
$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);
}

Expand Down Expand Up @@ -400,6 +408,10 @@ public function disableBot(int $botId): DataResponse {
}

$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);
}

Expand Down
26 changes: 26 additions & 0 deletions lib/Events/BotDisabledEvent.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
26 changes: 26 additions & 0 deletions lib/Events/BotEnabledEvent.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
17 changes: 13 additions & 4 deletions lib/Listener/BotListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
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;
Expand Down Expand Up @@ -47,17 +49,24 @@ public function handle(Event $event): void {
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);
}
}

Expand Down
Loading

0 comments on commit 61928d5

Please sign in to comment.