diff --git a/README.md b/README.md index fd075c1..c1cfeed 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ It is built on [csp](https://github.com/point72/csp), [csp-gateway](https://gith `csp-bot` makes it easy to build extensible command-driven bots, and has some key features: - connect to multiple backend chat platforms from the same instance +- supports Slack, Symphony, Discord, and Telegram - register custom commands across backends - create scheduled commands - create asynchronous commands diff --git a/csp_bot/config/backend/discord.yaml b/csp_bot/config/backend/discord.yaml new file mode 100644 index 0000000..6821576 --- /dev/null +++ b/csp_bot/config/backend/discord.yaml @@ -0,0 +1,7 @@ +# @package modules.bot.config +discord: + _target_: csp_bot.DiscordConfig + bot_name: ${bot_name} + config: + _target_: chatom.discord.DiscordConfig + token: ${oc.env:DISCORD_TOKEN} diff --git a/csp_bot/config/backend/slack.yaml b/csp_bot/config/backend/slack.yaml new file mode 100644 index 0000000..34e1e92 --- /dev/null +++ b/csp_bot/config/backend/slack.yaml @@ -0,0 +1,8 @@ +# @package modules.bot.config +slack: + _target_: csp_bot.SlackConfig + bot_name: ${bot_name} + config: + _target_: chatom.slack.SlackConfig + bot_token: ${oc.env:SLACK_BOT_TOKEN} + app_token: ${oc.env:SLACK_APP_TOKEN} diff --git a/csp_bot/config/backend/symphony.yaml b/csp_bot/config/backend/symphony.yaml new file mode 100644 index 0000000..115d6b1 --- /dev/null +++ b/csp_bot/config/backend/symphony.yaml @@ -0,0 +1,7 @@ +# @package modules.bot.config +symphony: + _target_: csp_bot.SymphonyConfig + bot_name: ${bot_name} + config: + _target_: chatom.symphony.SymphonyConfig + bot_private_key_content: ${oc.env:SYMPHONY_BOT_KEY} diff --git a/csp_bot/config/backend/telegram.yaml b/csp_bot/config/backend/telegram.yaml new file mode 100644 index 0000000..1e40f02 --- /dev/null +++ b/csp_bot/config/backend/telegram.yaml @@ -0,0 +1,7 @@ +# @package modules.bot.config +telegram: + _target_: csp_bot.TelegramConfig + bot_name: ${bot_name} + config: + _target_: chatom.telegram.TelegramConfig + bot_token: ${oc.env:TELEGRAM_BOT_TOKEN} diff --git a/csp_bot/config/gateway/all.yaml b/csp_bot/config/gateway/all.yaml index 6a5d4dc..34e1988 100644 --- a/csp_bot/config/gateway/all.yaml +++ b/csp_bot/config/gateway/all.yaml @@ -1,51 +1,13 @@ # @package _global_ defaults: - - /modules + - bot + - /backend: + - slack + - discord + - symphony + - telegram - _self_ -discord_bot_name: ??? -discord_token: ??? -slack_bot_name: ??? -slack_app_token: ??? -slack_bot_token: ??? -symphony_bot_name: ??? -symphony_cert: ??? -symphony_key: ??? -telegram_bot_name: ??? -telegram_bot_token: ??? - -modules: - bot: - _target_: csp_bot.Bot - config: - _target_: csp_bot.BotConfig - discord_config: - _target_: csp_bot.DiscordConfig - bot_name: ${discord_bot_name} - adapter_config: - _target_: csp_bot.DiscordAdapterConfig - token: ${discord_token} - slack_config: - _target_: csp_bot.SlackConfig - bot_name: ${slack_bot_name} - adapter_config: - _target_: csp_bot.SlackAdapterConfig - app_token: ${slack_app_token} - bot_token: ${slack_bot_token} - symphony_config: - _target_: csp_bot.SymphonyConfig - bot_name: ${symphony_bot_name} - adapter_config: - _target_: csp_bot.SymphonyAdapterConfig - cert_string: ${symphony_cert} - key_string: ${symphony_key} - telegram_config: - _target_: csp_bot.TelegramConfig - bot_name: ${telegram_bot_name} - adapter_config: - _target_: csp_bot.TelegramAdapterConfig - bot_token: ${telegram_bot_token} - hydra: job: - name: csp-bot--discord[${discord_bot_name}]-slack[${slack_bot_name}]-symphony[${symphony_bot_name}]-telegram[${telegram_bot_name}] + name: csp-bot-all diff --git a/csp_bot/config/gateway/bot.yaml b/csp_bot/config/gateway/bot.yaml new file mode 100644 index 0000000..ac3f8d6 --- /dev/null +++ b/csp_bot/config/gateway/bot.yaml @@ -0,0 +1,16 @@ +# @package _global_ +defaults: + - /modules + - _self_ + +bot_name: CSP Bot + +modules: + bot: + _target_: csp_bot.Bot + config: + _target_: csp_bot.BotConfig + +hydra: + job: + name: csp-bot diff --git a/csp_bot/config/gateway/discord.yaml b/csp_bot/config/gateway/discord.yaml index 02e2a9f..21f0e21 100644 --- a/csp_bot/config/gateway/discord.yaml +++ b/csp_bot/config/gateway/discord.yaml @@ -1,23 +1,10 @@ # @package _global_ defaults: - - /modules + - bot + - /backend: + - discord - _self_ -bot_name: ??? -token: ??? - -modules: - bot: - _target_: csp_bot.Bot - config: - _target_: csp_bot.BotConfig - discord_config: - _target_: csp_bot.DiscordConfig - bot_name: ${bot_name} - adapter_config: - _target_: csp_bot.DiscordAdapterConfig - token: ${token} - hydra: job: - name: csp-bot-discord[${bot_name}] + name: csp-bot-discord diff --git a/csp_bot/config/gateway/mixed.yaml b/csp_bot/config/gateway/mixed.yaml index 2bf1a01..93a9b55 100644 --- a/csp_bot/config/gateway/mixed.yaml +++ b/csp_bot/config/gateway/mixed.yaml @@ -1,33 +1,11 @@ # @package _global_ defaults: - - /modules + - bot + - /backend: + - slack + - discord - _self_ -discord_bot_name: ??? -discord_token: ??? -slack_bot_name: ??? -slack_app_token: ??? -slack_bot_token: ??? - -modules: - bot: - _target_: csp_bot.Bot - config: - _target_: csp_bot.BotConfig - discord_config: - _target_: csp_bot.DiscordConfig - bot_name: ${discord_bot_name} - adapter_config: - _target_: csp_bot.DiscordAdapterConfig - token: ${discord_token} - slack_config: - _target_: csp_bot.SlackConfig - bot_name: ${slack_bot_name} - adapter_config: - _target_: csp_bot.SlackAdapterConfig - app_token: ${slack_app_token} - bot_token: ${slack_bot_token} - hydra: job: - name: csp-bot--discord[${discord_bot_name}]-slack[${slack_bot_name}] + name: csp-bot-mixed diff --git a/csp_bot/config/gateway/slack.yaml b/csp_bot/config/gateway/slack.yaml index c087cf7..91d8c3a 100644 --- a/csp_bot/config/gateway/slack.yaml +++ b/csp_bot/config/gateway/slack.yaml @@ -1,25 +1,10 @@ # @package _global_ defaults: - - /modules + - bot + - /backend: + - slack - _self_ -bot_name: ??? -app_token: ??? -bot_token: ??? - -modules: - bot: - _target_: csp_bot.Bot - config: - _target_: csp_bot.BotConfig - slack_config: - _target_: csp_bot.SlackConfig - bot_name: ${bot_name} - adapter_config: - _target_: csp_bot.SlackAdapterConfig - app_token: ${app_token} - bot_token: ${bot_token} - hydra: job: - name: csp-bot-slack[${bot_name}] + name: csp-bot-slack diff --git a/csp_bot/config/gateway/symphony.yaml b/csp_bot/config/gateway/symphony.yaml index 90970e4..f3714df 100644 --- a/csp_bot/config/gateway/symphony.yaml +++ b/csp_bot/config/gateway/symphony.yaml @@ -1,25 +1,10 @@ # @package _global_ defaults: - - /modules + - bot + - /backend: + - symphony - _self_ -bot_name: ??? -cert: ??? -key: ??? - -modules: - bot: - _target_: csp_bot.Bot - config: - _target_: csp_bot.BotConfig - symphony_config: - _target_: csp_bot.SymphonyConfig - bot_name: ${bot_name} - adapter_config: - _target_: csp_bot.SymphonyAdapterConfig - cert_string: ${cert} - key_string: ${key} - hydra: job: - name: csp-bot-symphony[${bot_name}] + name: csp-bot-symphony diff --git a/csp_bot/config/gateway/telegram.yaml b/csp_bot/config/gateway/telegram.yaml index aec17c0..36402de 100644 --- a/csp_bot/config/gateway/telegram.yaml +++ b/csp_bot/config/gateway/telegram.yaml @@ -1,23 +1,10 @@ # @package _global_ defaults: - - /modules + - bot + - /backend: + - telegram - _self_ -bot_name: ??? -bot_token: ??? - -modules: - bot: - _target_: csp_bot.Bot - config: - _target_: csp_bot.BotConfig - telegram_config: - _target_: csp_bot.TelegramConfig - bot_name: ${bot_name} - adapter_config: - _target_: csp_bot.TelegramAdapterConfig - bot_token: ${bot_token} - hydra: job: - name: csp-bot-telegram[${bot_name}] + name: csp-bot-telegram diff --git a/csp_bot/tests/test_config.py b/csp_bot/tests/test_config.py index 880e40f..cf9b96b 100644 --- a/csp_bot/tests/test_config.py +++ b/csp_bot/tests/test_config.py @@ -1,7 +1,7 @@ """Tests for BotConfig and related configuration classes.""" from csp_bot import BotConfig -from csp_bot.bot_config import SlackConfig, SymphonyConfig +from csp_bot.bot_config import SlackConfig, SymphonyConfig, TelegramConfig class TestBotConfig: @@ -13,6 +13,7 @@ def test_empty_config(self): assert config.discord is None assert config.slack is None assert config.symphony is None + assert config.telegram is None def test_config_with_slack(self): """Test creating a BotConfig with Slack backend.""" @@ -23,6 +24,17 @@ def test_config_with_slack(self): assert config.slack is not None assert "general" in config.slack.channels + def test_config_with_multiple_backends(self): + """Test creating a BotConfig with several backends at once.""" + config = BotConfig( + slack=SlackConfig(bot_name="Bot"), + telegram=TelegramConfig(bot_name="Bot"), + ) + assert config.slack is not None + assert config.telegram is not None + assert config.discord is None + assert config.symphony is None + class TestBackendConfig: """Tests for backend configuration classes.""" diff --git a/docs/wiki/Backends.md b/docs/wiki/Backends.md new file mode 100644 index 0000000..1a23854 --- /dev/null +++ b/docs/wiki/Backends.md @@ -0,0 +1,65 @@ +`csp-bot` connects to chat platforms through `csp` adapters, wrapped by [`chatom`](https://github.com/Point72/chatom) for a unified message, user, and channel model. +This page is the reference for which backends exist, how to authenticate them, and what each one can do. + +## Supported backends + +| Backend | Adapter | Support | +| :------- | :------------------------------------------------------------------------------------------ | :-------- | +| Slack | [point72/csp-adapter-slack](https://github.com/point72/csp-adapter-slack) | Official | +| Symphony | [point72/csp-adapter-symphony](https://github.com/point72/csp-adapter-symphony) | Official | +| Discord | [csp-community/csp-adapter-discord](https://github.com/csp-community/csp-adapter-discord) | Community | +| Telegram | [csp-community/csp-adapter-telegram](https://github.com/csp-community/csp-adapter-telegram) | Community | + +The adapters are optional dependencies — install the ones you need: + +```bash +pip install csp-adapter-slack csp-adapter-telegram +``` + +## Credentials + +Each backend reads its credentials from environment variables, matching the names used by the built-in `backend` configs. + +| Backend | Environment variables | `chatom` config field | +| :------- | :----------------------------------- | :------------------------ | +| Slack | `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN` | `bot_token`, `app_token` | +| Discord | `DISCORD_TOKEN` | `token` | +| Symphony | `SYMPHONY_BOT_KEY` | `bot_private_key_content` | +| Telegram | `TELEGRAM_BOT_TOKEN` | `bot_token` | + +To set a field other than the default (for example, a Symphony key on disk rather than in the environment), override it in your own config: + +```yaml +# @package modules.bot.config +symphony: + config: + bot_private_key_path: /path/to/bot-key.pem + bot_certificate_path: /path/to/bot-cert.pem +``` + +For platform-specific setup of tokens and bot accounts, follow the adapter guides: + +- [Slack Adapter setup](https://github.com/Point72/csp-adapter-slack/wiki/Setup) +- [Symphony Adapter setup](https://github.com/Point72/csp-adapter-symphony/wiki/Setup) +- [Discord Adapter setup](https://github.com/csp-community/csp-adapter-discord/wiki/Setup) +- [Telegram Adapter](https://github.com/csp-community/csp-adapter-telegram/wiki) + +## Feature matrix + +| Backend | Public Room | Private Room | User-initiated IM | Threads | Reactions | Attachments | +| :------- | :---------- | :----------- | :---------------- | :------ | :-------- | :---------- | +| Slack | X | X | X | X | X | | +| Symphony | X | X | X | | | | +| Discord | X | X | X | X | X | | +| Telegram | X | X | X | | | | + +## Response formatting + +Response formatting varies between backends, and built-in commands render their output in the right dialect for each one. + +| Backend | Format | Tables | Images | +| :------- | :--------------- | :----- | :----- | +| Slack | Minimal Markdown | No | Yes | +| Symphony | Custom HTML | Yes | Yes | +| Discord | Minimal Markdown | No | Yes | +| Telegram | HTML | No | Yes | diff --git a/docs/wiki/Commands.md b/docs/wiki/Commands.md new file mode 100644 index 0000000..d59bcd7 --- /dev/null +++ b/docs/wiki/Commands.md @@ -0,0 +1,26 @@ +`csp-bot` ships a small set of built-in commands and makes it easy to add your own. + +Because chat platforms each have their own command registration and syntax, `csp-bot` adopts an IRC-like `/`-initiated command syntax. +On most platforms you interact with the bot by tagging it and then giving a command: + +```raw +@CSP Bot /help +``` + +Most commands accept their own arguments, and many can be redirected to another channel by appending `/channel `. + +## Built-in commands + +| Name | Command | Description | +| :------- | :---------- | :------------------------------------------------------------- | +| Help | `/help` | Get help with bot commands. Syntax: `/help [command]` | +| Echo | `/echo` | Echo a message. Syntax: `/echo [/channel ]` | +| Schedule | `/schedule` | Schedule a command to run later or on a cron schedule | +| Status | `/status` | Display system status. Syntax: `/status [/channel ]` | + +Which commands a bot exposes is controlled by configuration; see [Configuration](Configuration). + +## More commands + +Community-supported commands live in [csp-community/csp-bot-commands](https://github.com/csp-community/csp-bot-commands). +To write your own, see [Writing Commands](Writing-Commands). diff --git a/docs/wiki/Configuration.md b/docs/wiki/Configuration.md index 8241afb..d637cbb 100644 --- a/docs/wiki/Configuration.md +++ b/docs/wiki/Configuration.md @@ -1,10 +1,58 @@ -Configuration for `csp-bot` classes is driven by [`ccflow`](https://github.com/Point72/ccflow). +Configuration for `csp-bot` is driven by [`ccflow`](https://github.com/Point72/ccflow). `ccflow` leverages [Pydantic](https://docs.pydantic.dev/latest/) for type validation, and combines it with [Hydra](https://hydra.cc/) / [OmegaConf](https://omegaconf.readthedocs.io/en/2.3_branch/) for config-driven initialization. The [`ccflow` examples](https://github.com/Point72/ccflow/wiki/First-Steps) provide a nice overview of its functionality. -In the context of `csp-bot`, this means that we can control commands, backends, and their respective configuration, all from a convenient and extensible set of yaml files. +In `csp-bot`, this means commands, backends, and their settings are all controlled from a small set of composable yaml files. -Here is an example of a yaml-based configuration for the standard Slack-based chatbot: +## Two config groups + +`csp-bot` exposes two Hydra config groups: + +- `gateway` — assembles the bot module and the `csp-gateway` server around it. +- `backend` — one fragment per chat platform (`slack`, `discord`, `symphony`, `telegram`), each contributing its configuration to the bot. + +The `backend` group is what makes running against any mix of platforms easy. +Because each fragment writes to a different field of the bot config, you can select **any combination** of them at once. + +## Selecting backends + +The bare `bot` gateway has no backends of its own. +Add backends by listing them from the `backend` group: + +```bash +# One backend +csp-bot-start +gateway=bot +backend='[slack]' + +# Any combination — no dedicated config required +csp-bot-start +gateway=bot +backend='[slack,telegram]' +csp-bot-start +gateway=bot +backend='[slack,discord,symphony,telegram]' +``` + +Each backend reads its credentials from environment variables, so nothing else is required on the command line. +See [Backends](Backends) for the full list of variables. + +## Pre-canned gateways + +For the common cases, ready-made `gateway` configs select the backends for you: + +| Gateway | Backends | +| :------------------ | :------------------------------------ | +| `+gateway=slack` | Slack | +| `+gateway=discord` | Discord | +| `+gateway=symphony` | Symphony | +| `+gateway=telegram` | Telegram | +| `+gateway=mixed` | Slack + Discord | +| `+gateway=all` | Slack + Discord + Symphony + Telegram | +| `+gateway=bot` | none — add your own with `+backend` | + +```bash +csp-bot-start +gateway=all +``` + +## Using a local config file + +Selecting backends from the command line is convenient, but a small yaml file is easier to version and share. +Hydra unions configs, so a local file only needs to declare which gateway it builds on: **example/bot/slack.yaml** @@ -15,70 +63,69 @@ defaults: - _self_ bot_name: CSP Bot -app_token: .slack_app_token -bot_token: .slack_bot_token +# Tokens are read from the environment: SLACK_BOT_TOKEN, SLACK_APP_TOKEN # csp-bot-start --config-dir=example +bot=slack ``` -This configuration: +To pick your own mix of backends in a file, build on the bare `bot` gateway and list them: -- Inherits from a builtin configuration called `slack` -- Sets the bot name to `CSP Bot` -- Expects `app_token` and `bot_token` files called `.slack_app_token` and `.slack_bot_token`, respectively +**example/bot/custom.yaml** -> [!NOTE] -> -> We didn't need to create a local yaml file, but its convenient to do so. -> Instead, we could have used the [hydra override syntax](https://hydra.cc/docs/advanced/override_grammar/basic/) to provide these from the command line: -> `csp-bot-start +gateway=slack +bot_name="CSP Bot" +app_token=.slack_app_token +bot_token=.slack_bot_token` +```yaml +# @package _global_ +defaults: + - /gateway: bot + - /backend: + - slack + - telegram + - _self_ + +bot_name: CSP Bot +``` + +Run either with: -Hydra allows for easy unioning of configs, which is what we do here. -To see the full picture, let's take a look at the builtin configuration `slack` +```bash +csp-bot-start --config-dir=example +bot=slack +csp-bot-start --config-dir=example +bot=custom +``` + +## What a gateway config expands to + +A `gateway` config is built from the bare `bot` skeleton plus the selected `backend` fragments. +The `bot` gateway defines the bot module and an empty `BotConfig`: ```yaml # @package _global_ defaults: + - /modules - _self_ -bot_name: ??? -app_token: ??? -bot_token: ??? +bot_name: CSP Bot modules: bot: _target_: csp_bot.Bot config: _target_: csp_bot.BotConfig - slack_config: - _target_: csp_bot.SlackConfig - bot_name: ${bot_name} - adapter_config: - _target_: csp_bot.SlackAdapterConfig - app_token: ${app_token} - bot_token: ${bot_token} - -hydra: - job: - name: csp-bot-slack[${bot_name}] ``` -The `???` show that these fields should be overridden in other configs, which is exactly what we do in our `example/bot/slack.yaml`. -We also see that the bot instance is configured with a `SlackConfig`. +Each `backend` fragment fills in one field of that `BotConfig`. +For example, the `slack` fragment: -For each backend supported, this is just a wrapper around the backend adapter's configuration: - -- [Discord Adapter Config](https://github.com/csp-community/csp-adapter-discord/wiki/Setup) -- [Slack Adapter Config](https://github.com/Point72/csp-adapter-slack/wiki/Setup) -- [Symphony Adapter Config](https://github.com/Point72/csp-adapter-symphony/wiki/Setup) - -By default, we provide a few builtinn configurations +```yaml +# @package modules.bot.config +slack: + _target_: csp_bot.SlackConfig + bot_name: ${bot_name} + config: + _target_: chatom.slack.SlackConfig + bot_token: ${oc.env:SLACK_BOT_TOKEN} + app_token: ${oc.env:SLACK_APP_TOKEN} +``` -- Slack (Hydra: `/gateway: slack`) -- Symphony (Hydra: `/gateway: symphony`) -- Discord: (Hydra: `/gateway: discord`) -- Slack+Discord (Hydra: `/gateway: mixed`) -- Slack+Symphony+Discord (Hydra: `/gateway: all`) +Selecting `+backend='[slack,telegram]'` merges the `slack` and `telegram` fragments into the same `BotConfig`, leaving the unused backends unset. +The per-platform `config` block is the corresponding [`chatom`](https://github.com/Point72/chatom) backend config, so any field that backend supports can be set here. -You can extend these configs, or use them as the basis for your own custom config. -All can be found in-source in [csp_bot/config/gateway](https://github.com/Point72/csp-bot/tree/main/csp_bot/config/gateway). +All of these configs live in-source under [csp_bot/config](https://github.com/Point72/csp-bot/tree/main/csp_bot/config); copy or extend them as the basis for your own. diff --git a/docs/wiki/Installation.md b/docs/wiki/Installation.md index 68bd68e..a06cce1 100644 --- a/docs/wiki/Installation.md +++ b/docs/wiki/Installation.md @@ -14,6 +14,20 @@ pip install csp-bot conda install csp-bot --channel conda-forge ``` +## Backend adapters + +`csp-bot` connects to each chat platform through a separate adapter package. +Install the ones you need: + +```bash +pip install csp-adapter-slack # Slack +pip install csp-adapter-discord # Discord +pip install csp-adapter-symphony # Symphony +pip install csp-adapter-telegram # Telegram +``` + +See [Backends](Backends) for credentials and per-platform setup. + ## Source installation For other platforms and for development installations, [build `csp-bot` from source](Build-from-Source). diff --git a/docs/wiki/Overview.md b/docs/wiki/Overview.md index fd0a170..6818457 100644 --- a/docs/wiki/Overview.md +++ b/docs/wiki/Overview.md @@ -1,147 +1,53 @@ -`csp-bot` is a framework for building chat applications. +`csp-bot` is a framework for building command-driven chat bots that run against one or more chat platforms at once. -It is is composed of two major components: +It is composed of two major components: - Engine: [csp](https://github.com/point72/csp) and [csp-gateway](https://github.com/point72/csp-gateway), a streaming, complex event processor core and corresponding application framework - Configuration: [ccflow](https://github.com/point72/ccflow), a [Pydantic](https://docs.pydantic.dev/latest/)/[Hydra](https://hydra.cc) based extensible, composeable dependency injection and configuration framework -## Table of Contents +Messages, users, and channels are unified across platforms by [chatom](https://github.com/Point72/chatom), so a single bot and its commands work the same on Slack, Symphony, Discord, and Telegram. -- [Table of Contents](#table-of-contents) -- [Running the bot](#running-the-bot) -- [Available Commands](#available-commands) -- [Writing commands](#writing-commands) -- [Backend Feature Matrix](#backend-feature-matrix) -- [Response Formatting](#response-formatting) +## Running your first bot -## Running the bot - -Let's start with a simple Slack bot. -Before we can run the `csp-bot` framework, we need to setup a bot on Slack. -We can follow the documentation on [csp-adapter-slack](https://github.com/point72/csp-adapter-slack/wiki/Setup) using bot name "CSP Bot". - -At the end of this setup, we should have two tokens: an "app token" and a "bot token". -Put these in files `.slack_app_token` and `.slack_bot_token`, respectively. +Let's run a Slack bot. +First, follow the [csp-adapter-slack setup](https://github.com/point72/csp-adapter-slack/wiki/Setup) to create a bot named "CSP Bot". +That gives you two tokens: an *app token* and a *bot token*. > [!WARNING] > -> Be sure to avoid committing any tokens for any backends in public repos. +> Be sure to avoid committing tokens for any backend into a repository. -Now we can run our bot. -`csp-bot` uses `ccflow` and `hydra` for configuration, so this syntax might look a little different from CLIs you are used to. -See [Configuration](Configuration) for more information. +Provide the tokens through the environment and start the bot: ```bash -csp-bot-start +gateway=slack +bot_name="CSP Bot" +app_token=.slack_app_token +bot_token=.slack_bot_token -``` - -Now you can message your bot, and by default it should reply with a help menu of all available commands: - -Message to bot with a response of all commands +export SLACK_BOT_TOKEN=xoxb-... +export SLACK_APP_TOKEN=xapp-... -## Available Commands - -One of the main goals of `csp-bot` is to allow for extremely easy extension. -The framework comes with a small number of example commands, but makes it easy to extend. -Because backend chat platforms use their own custom command registration and syntax, we adopt an IRC-like `/`-initiated command syntax. -On most platforms, this means to interact with the bot means to tag the bot and provide a command after: - -```raw -@CSP Bot /help +csp-bot-start +gateway=slack ``` -| Name | Command | Description | -| :------- | :---------- | :---------------------------------------------------------------------------- | -| Help | `/help` | Displays a list of all commands and their help text | -| Echo | `/echo` | Echos text | -| Schedule | `/schedule` | Schedules a command to be run repeatedly, as an example of command scheduling | - -Commands can have their own extra arguments, and most commands can be redirected to another channel by appending `/channel `. -Community supported commands can be found at [csp-community/csp-bot-commands](https://github.com/csp-community/csp-bot-commands) - -## Writing commands - -Commands have a simple Python structure. -Here is an example command that replies "hello" to the initiating user. +`csp-bot` uses `ccflow` and `hydra` for configuration, so the command line is composed of config selections rather than flags. +`+gateway=slack` selects the pre-canned Slack gateway; see [Configuration](Configuration) for the full picture. -```python -from typing import Type -from csp_bot.structs import BotCommand, Message -from csp_bot.commands import ReplyToOtherCommand, BaseCommandModel, BaseCommand, mention_user +Message your bot, and by default it replies with a help menu of all available commands: +Message to bot with a response of all commands -class HelloCommand(ReplyToOtherCommand): - def command(self) -> str: - return "hello" - - def name(self) -> str: - return "Hello" - - def help(self) -> str: - return "Say hello to the tagged users. Syntax: /hello [user tags]" - - def execute(self, command: BotCommand) -> Message: - author = mention_user(command.source.id, command.backend) - target = [mention_user(user.id, command.backend) for user in command.targets] - message = f"{author} says hello to {' '.join(target)}!" - return Message( - msg=message, - channel=command.channel, - backend=command.backend, - ) - - -class HelloCommandModel(BaseCommandModel): - command: Type[BaseCommand] = HelloCommand -``` - -To select commands, including custom commands, follow the documentation in [Configuration](Configuration). -For example, with a Slack bot and the above command in `hello.py`: - -**my_bot_config/bot/slack_with_hello.yaml** - -```yaml -# @package _global_ -defaults: - - /gateway: slack - - _self_ - -bot_name: CSP Bot -app_token: .slack_app_token -bot_token: .slack_bot_token - -gateway: - _target_: csp_bot.Gateway - modules: - - /modules/bot - commands: - # Selecting a builtin command - - /commands/help - # Adding a custom command/s - - _target_: hello.HelloCommandModel -``` +## Running against multiple backends -And run with: +The same bot can connect to several platforms simultaneously. +Add backends by listing them — no per-combination config required: ```bash -csp-bot-start --config-dir=my_bot_config +bot=slack_with_hello +csp-bot-start +gateway=bot +backend='[slack,telegram]' ``` -## Backend Feature Matrix - -| Backend | Repo | Official / Community Supported | Public Room | Private Room | User-iniated IM | Non user-initiated IM | Threads | Reactions | Attachments | -| :------- | :---------------------------------------------------------------------------------------- | :----------------------------- | :---------- | :----------- | :-------------- | :-------------------- | :------ | :-------- | :---------- | -| Slack | [point72/csp-adapter-slack](https://github.com/point72/csp-adapter-slack) | Official | X | X | X | | X | X | | -| Symphony | [point72/csp-adapter-symphony](https://github.com/point72/csp-adapter-symphony) | Official | X | X | X | | | | | -| Discord | [csp-community/csp-adapter-discord](https://github.com/csp-community/csp-adapter-discord) | Community | X | X | X | | X | X | | - -## Response Formatting +See [Configuration](Configuration) for pre-canned combinations and how to capture your selection in a file. -Response formatting varies wildly between backends, and it can be quite difficult to produce nicely-formatted results. -We are working on a convenience layer to make this easier. +## Where to go next -| Backend | Format | Tables | Images | -| :------- | :--------------- | :----- | :----- | -| Slack | Minimal Markdown | No | Yes | -| Symphony | Custom HTML | Yes | Yes | -| Discord | Minimal Markdown | No | Yes | +- [Installation](Installation) — install `csp-bot` and the backend adapters you need. +- [Configuration](Configuration) — select backends and customize the gateway. +- [Backends](Backends) — supported platforms, credentials, and feature support. +- [Commands](Commands) — the built-in commands. +- [Writing Commands](Writing-Commands) — add your own commands. diff --git a/docs/wiki/Writing-Commands.md b/docs/wiki/Writing-Commands.md new file mode 100644 index 0000000..011489a --- /dev/null +++ b/docs/wiki/Writing-Commands.md @@ -0,0 +1,91 @@ +Commands are the unit of behaviour in `csp-bot`. +This guide walks through writing a custom command and wiring it into a running bot. + +## Write the command + +A command subclasses one of the reply base classes and implements four methods: `command()`, `name()`, `help()`, and `execute()`. +The `execute()` method receives a `BotCommand` and returns a `chatom` `Message` (or `None` for no reply). + +This example greets the users tagged in the message: + +```python +from typing import Optional, Type + +from chatom import Message + +from csp_bot.commands import BaseCommand, BaseCommandModel, ReplyToOtherCommand +from csp_bot.structs import BotCommand +from csp_bot.utils import mention_users + + +class HelloCommand(ReplyToOtherCommand): + def command(self) -> str: + return "hello" + + def name(self) -> str: + return "Hello" + + def help(self) -> str: + return "Say hello to the tagged users. Syntax: /hello [user tags]" + + def execute(self, command: BotCommand) -> Optional[Message]: + if not command.targets: + return None + mentions = mention_users( + [t.to_chatom_user() for t in command.targets], + command.backend, + ) + return Message( + content=f"Hello {mentions}!", + channel=command.channel, + metadata={"backend": command.backend}, + ) + + +class HelloCommandModel(BaseCommandModel): + command: Type[BaseCommand] = HelloCommand +``` + +`mention_users()` renders each tagged user in the right format for the backend the message came from, so the same command works across every platform. + +## Register the command + +Commands are selected by configuration. +Put `hello.py` next to a bot config and list the command alongside the built-ins: + +**my_bot/bot/slack.yaml** + +```yaml +# @package _global_ +defaults: + - /gateway: slack + - _self_ + +bot_name: CSP Bot + +gateway: + commands: + - /commands/help + - /commands/echo + - _target_: hello.HelloCommandModel +``` + +Then start the bot, pointing Hydra at your config directory so it can import `hello.py`: + +```bash +csp-bot-start --config-dir=my_bot +bot=slack +``` + +Tagging the bot with `/hello @someone` now replies with a greeting. + +## Choosing a base class + +The reply base class fixes how many users a command may tag and who gets mentioned: + +| Base class | Tags | Mentions | +| :--------------------- | :--------- | :----------------- | +| `ReplyCommand` | none | replies in-channel | +| `ReplyToAuthorCommand` | one | the message author | +| `ReplyToOtherCommand` | one | the tagged user | +| `ReplyToAllCommand` | any number | all tagged users | +| `NoResponseCommand` | none | no reply | diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index ed42932..b72aa0e 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -13,6 +13,9 @@ Notes for editors: - [Installation](Installation) - [Overview](Overview) - [Configuration](Configuration) +- [Backends](Backends) +- [Commands](Commands) +- [Writing Commands](Writing-Commands) - [Advanced](Advanced) **For Developers** diff --git a/example/bot/all.yaml b/example/bot/all.yaml index 2414eab..c684697 100644 --- a/example/bot/all.yaml +++ b/example/bot/all.yaml @@ -1,15 +1,10 @@ # @package _global_ defaults: - - /gateway: mixed + - /gateway: all - _self_ -discord_bot_name: CSP Bot -discord_token: .discord_token -slack_bot_name: CSP Bot -slack_app_token: .slack_app_token -slack_bot_token: .slack_bot_token -symphony_bot_name: CSP Bot -symphony_cert: .symphony_cert -symphony_key: .symphony_key +bot_name: CSP Bot +# Tokens are read from the environment: +# SLACK_BOT_TOKEN, SLACK_APP_TOKEN, DISCORD_TOKEN, SYMPHONY_BOT_KEY, TELEGRAM_BOT_TOKEN # csp-bot-start --config-dir=example +bot=all diff --git a/example/bot/custom.yaml b/example/bot/custom.yaml new file mode 100644 index 0000000..d8b8626 --- /dev/null +++ b/example/bot/custom.yaml @@ -0,0 +1,18 @@ +# @package _global_ +# +# Select any combination of backends without a dedicated gateway config by +# starting from the bare `bot` gateway and listing the backends you want. +defaults: + - /gateway: bot + - /backend: + - slack + - telegram + - _self_ + +bot_name: CSP Bot + +# Tokens are read from the environment: SLACK_BOT_TOKEN, SLACK_APP_TOKEN, TELEGRAM_BOT_TOKEN +# csp-bot-start --config-dir=example +bot=custom +# +# The same result can be achieved entirely from the command line: +# csp-bot-start +gateway=bot +backend='[slack,telegram]' +bot_name='CSP Bot' diff --git a/example/bot/discord.yaml b/example/bot/discord.yaml index 702f23d..545b708 100644 --- a/example/bot/discord.yaml +++ b/example/bot/discord.yaml @@ -4,6 +4,6 @@ defaults: - _self_ bot_name: CSP Bot -token: .discord_token +# Tokens are read from the environment: DISCORD_TOKEN # csp-bot-start --config-dir=example +bot=discord diff --git a/example/bot/mixed.yaml b/example/bot/mixed.yaml index 92689ae..2700ccf 100644 --- a/example/bot/mixed.yaml +++ b/example/bot/mixed.yaml @@ -3,10 +3,7 @@ defaults: - /gateway: mixed - _self_ -discord_bot_name: CSP Bot -discord_token: .discord_token -slack_bot_name: CSP Bot -slack_app_token: .slack_app_token -slack_bot_token: .slack_bot_token +bot_name: CSP Bot +# Tokens are read from the environment: SLACK_BOT_TOKEN, SLACK_APP_TOKEN, DISCORD_TOKEN # csp-bot-start --config-dir=example +bot=mixed diff --git a/example/bot/slack.yaml b/example/bot/slack.yaml index 7a7c562..8ab760e 100644 --- a/example/bot/slack.yaml +++ b/example/bot/slack.yaml @@ -4,15 +4,6 @@ defaults: - _self_ bot_name: CSP Bot -app_token: .slack_app_token -bot_token: .slack_bot_token - -gateway: - _target_: csp_bot.Gateway - modules: - - /modules/bot - commands: - - /commands/help - - _target_: hello.HelloCommandModel +# Tokens are read from the environment: SLACK_BOT_TOKEN, SLACK_APP_TOKEN # csp-bot-start --config-dir=example +bot=slack diff --git a/example/bot/symphony.yaml b/example/bot/symphony.yaml index bf9aa64..1903f54 100644 --- a/example/bot/symphony.yaml +++ b/example/bot/symphony.yaml @@ -4,7 +4,6 @@ defaults: - _self_ bot_name: CSP Bot -cert: .symphony_cert -key: .symphony_key +# Tokens are read from the environment: SYMPHONY_BOT_KEY # csp-bot-start --config-dir=example +bot=symphony diff --git a/example/bot/telegram.yaml b/example/bot/telegram.yaml new file mode 100644 index 0000000..ede3c94 --- /dev/null +++ b/example/bot/telegram.yaml @@ -0,0 +1,9 @@ +# @package _global_ +defaults: + - /gateway: telegram + - _self_ + +bot_name: CSP Bot + +# Tokens are read from the environment: TELEGRAM_BOT_TOKEN +# csp-bot-start --config-dir=example +bot=telegram