Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bots): Add events for enabling and disabling bots #12551

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 92 additions & 2 deletions docs/bots.md
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved
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,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 |
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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`
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
28 changes: 27 additions & 1 deletion lib/Command/Bot/Remove.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,23 @@
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;

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 +52,26 @@ 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) {
try {
$room = $this->roomManager->getRoomByToken($token);
} catch(RoomNotFoundException) {
continue;
}
$event = new BotDisabledEvent($room, $botServer);
$this->dispatcher->dispatchTyped($event);
}

return 0;
}
}
10 changes: 9 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 All @@ -67,10 +70,12 @@ protected function execute(InputInterface $input, OutputInterface $output): int
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();
Expand All @@ -81,6 +86,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);
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved
$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 {
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved

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 {
nickvergessen marked this conversation as resolved.
Show resolved Hide resolved

public function __construct(
Room $room,
protected BotServer $botServer,
) {
parent::__construct($room);
}

public function getBotServer(): BotServer {
return $this->botServer;
}
}
Loading
Loading