Skip to content

Commit 62d72c1

Browse files
authored
Merge pull request #77 from Point72/tkp/composable-backends
Address backend config review feedback
2 parents a388841 + 289b202 commit 62d72c1

9 files changed

Lines changed: 102 additions & 38 deletions

File tree

csp_bot/commands/echo.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def execute(self, command: BotCommand) -> Optional[Message]:
3636

3737
# Add mentions for any tagged users
3838
if command.targets:
39-
mentions = mention_users([t.to_chatom_user() for t in command.targets], command.backend)
39+
mentions = mention_users(list(command.targets), command.backend)
4040
if mentions:
4141
content = f"{content} {mentions}".strip()
4242

csp_bot/config/backend/discord.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ discord:
55
config:
66
_target_: chatom.discord.DiscordConfig
77
token: ${oc.env:DISCORD_TOKEN}
8+
# message_content is a privileged intent and must also be enabled in the
9+
# Discord Developer Portal for the bot to read command text in guilds.
10+
intents:
11+
- guilds
12+
- messages
13+
- message_content

csp_bot/config/backend/symphony.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,6 @@ symphony:
44
bot_name: ${bot_name}
55
config:
66
_target_: chatom.symphony.SymphonyConfig
7-
bot_private_key_content: ${oc.env:SYMPHONY_BOT_KEY}
7+
host: ${oc.env:SYMPHONY_HOST}
8+
bot_username: ${oc.env:SYMPHONY_BOT_USERNAME}
9+
bot_certificate_path: ${oc.env:SYMPHONY_CERT_PATH}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Tests that the packaged gateway/backend config groups compose correctly."""
2+
3+
import os
4+
5+
import pytest
6+
from hydra import compose, initialize_config_dir
7+
from omegaconf import OmegaConf
8+
9+
import csp_bot.config
10+
11+
CONFIG_DIR = os.path.dirname(csp_bot.config.__file__)
12+
13+
GATEWAY_BACKENDS = {
14+
"slack": {"slack"},
15+
"discord": {"discord"},
16+
"symphony": {"symphony"},
17+
"telegram": {"telegram"},
18+
"mixed": {"slack", "discord"},
19+
"all": {"slack", "discord", "symphony", "telegram"},
20+
}
21+
22+
23+
@pytest.fixture
24+
def backend_env(monkeypatch):
25+
"""Provide dummy values for every backend's credential variables."""
26+
for name in (
27+
"SLACK_BOT_TOKEN",
28+
"SLACK_APP_TOKEN",
29+
"DISCORD_TOKEN",
30+
"SYMPHONY_HOST",
31+
"SYMPHONY_BOT_USERNAME",
32+
"SYMPHONY_CERT_PATH",
33+
"TELEGRAM_BOT_TOKEN",
34+
):
35+
monkeypatch.setenv(name, "dummy")
36+
37+
38+
def _compose(overrides):
39+
with initialize_config_dir(config_dir=CONFIG_DIR, version_base=None):
40+
cfg = compose(config_name="conf", overrides=overrides)
41+
return OmegaConf.to_container(cfg.modules.bot.config, resolve=True)
42+
43+
44+
@pytest.mark.parametrize("gateway,expected", GATEWAY_BACKENDS.items())
45+
def test_precanned_gateway_composes(gateway, expected, backend_env):
46+
"""Each pre-canned gateway resolves to its expected backend set."""
47+
config = _compose([f"+gateway={gateway}"])
48+
backends = {k for k in config if k != "_target_"}
49+
assert backends == expected
50+
51+
52+
def test_bare_bot_has_no_backends(backend_env):
53+
"""The bare `bot` gateway selects no backends on its own."""
54+
config = _compose(["+gateway=bot"])
55+
assert {k for k in config if k != "_target_"} == set()
56+
57+
58+
def test_ad_hoc_backend_selection(backend_env):
59+
"""Any combination can be assembled from the bare gateway."""
60+
config = _compose(["+gateway=bot", "+backend=[slack,telegram]"])
61+
assert {k for k in config if k != "_target_"} == {"slack", "telegram"}

csp_bot/tests/test_echo.py

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -70,19 +70,11 @@ def test_execute_with_no_args_returns_none(self):
7070
assert result is None
7171

7272
def test_execute_with_targets(self):
73-
"""Test executing echo with targets.
74-
75-
Note: The echo command calls to_chatom_user() on targets, but the
76-
targets are already chatom.User objects. For this test we use a mock.
77-
"""
78-
from unittest.mock import Mock
79-
73+
"""Test executing echo with targets."""
8074
cmd = EchoCommand()
8175
channel = Channel(id="ch1", name="test-channel")
8276

83-
# Create a mock target that has the to_chatom_user method
84-
mock_target = Mock()
85-
mock_target.to_chatom_user.return_value = User(id="u123", name="testuser")
77+
target = User(id="u123", name="testuser")
8678

8779
bot_cmd = BotCommand(
8880
backend="slack",
@@ -91,7 +83,7 @@ def test_execute_with_targets(self):
9183
channel_id=channel.id,
9284
channel_name=channel.name,
9385
source=User(id="u1", name="sender"),
94-
targets=(mock_target,),
86+
targets=(target,),
9587
variant=CommandVariant.REPLY_TO_OTHER,
9688
message=None,
9789
)
@@ -104,19 +96,11 @@ def test_execute_with_targets(self):
10496
assert "<@u123>" in result.content or "testuser" in result.content
10597

10698
def test_execute_with_only_targets(self):
107-
"""Test executing echo with only targets (no args).
108-
109-
Note: The echo command calls to_chatom_user() on targets, but the
110-
targets are already chatom.User objects. For this test we use a mock.
111-
"""
112-
from unittest.mock import Mock
113-
99+
"""Test executing echo with only targets (no args)."""
114100
cmd = EchoCommand()
115101
channel = Channel(id="ch1", name="test-channel")
116102

117-
# Create a mock target that has the to_chatom_user method
118-
mock_target = Mock()
119-
mock_target.to_chatom_user.return_value = User(id="u123", name="testuser")
103+
target = User(id="u123", name="testuser")
120104

121105
bot_cmd = BotCommand(
122106
backend="slack",
@@ -125,7 +109,7 @@ def test_execute_with_only_targets(self):
125109
channel_id=channel.id,
126110
channel_name=channel.name,
127111
source=User(id="u1", name="sender"),
128-
targets=(mock_target,),
112+
targets=(target,),
129113
variant=CommandVariant.REPLY_TO_OTHER,
130114
message=None,
131115
)

docs/wiki/Backends.md

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,26 @@ pip install csp-adapter-slack csp-adapter-telegram
2020

2121
Each backend reads its credentials from environment variables, matching the names used by the built-in `backend` configs.
2222

23-
| Backend | Environment variables | `chatom` config field |
24-
| :------- | :----------------------------------- | :------------------------ |
25-
| Slack | `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN` | `bot_token`, `app_token` |
26-
| Discord | `DISCORD_TOKEN` | `token` |
27-
| Symphony | `SYMPHONY_BOT_KEY` | `bot_private_key_content` |
28-
| Telegram | `TELEGRAM_BOT_TOKEN` | `bot_token` |
23+
| Backend | Environment variables | `chatom` config field |
24+
| :------- | :------------------------------------------------------------- | :--------------------------------------------- |
25+
| Slack | `SLACK_BOT_TOKEN`, `SLACK_APP_TOKEN` | `bot_token`, `app_token` |
26+
| Discord | `DISCORD_TOKEN` | `token` |
27+
| Symphony | `SYMPHONY_HOST`, `SYMPHONY_BOT_USERNAME`, `SYMPHONY_CERT_PATH` | `host`, `bot_username`, `bot_certificate_path` |
28+
| Telegram | `TELEGRAM_BOT_TOKEN` | `bot_token` |
2929

30-
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:
30+
Symphony needs a host, a bot username, and either a certificate or an RSA private key.
31+
The built-in preset uses certificate authentication with a combined certificate/key `.pem` file on disk (`bot_certificate_path`); a path keeps long-lived key material out of the process environment.
32+
33+
Discord's `message_content` is a [privileged intent](https://discord.com/developers/docs/topics/gateway#privileged-intents).
34+
The built-in preset requests it, but it must also be enabled for the bot in the Discord Developer Portal, or the bot will not receive command text in guild channels.
35+
36+
To set a field other than the defaults — for example, to authenticate Symphony with an RSA key instead of a certificate — override it in your own config:
3137

3238
```yaml
3339
# @package modules.bot.config
3440
symphony:
3541
config:
3642
bot_private_key_path: /path/to/bot-key.pem
37-
bot_certificate_path: /path/to/bot-cert.pem
3843
```
3944
4045
For platform-specific setup of tokens and bot accounts, follow the adapter guides:

docs/wiki/Writing-Commands.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class HelloCommand(ReplyToOtherCommand):
3232
if not command.targets:
3333
return None
3434
mentions = mention_users(
35-
[t.to_chatom_user() for t in command.targets],
35+
list(command.targets),
3636
command.backend,
3737
)
3838
return Message(
@@ -51,7 +51,9 @@ class HelloCommandModel(BaseCommandModel):
5151
## Register the command
5252

5353
Commands are selected by configuration.
54-
Put `hello.py` next to a bot config and list the command alongside the built-ins:
54+
`csp-bot` imports each command by its `_target_`, so `hello.py` must be importable on the `PYTHONPATH` (for example, run from the directory that contains it, or ship it as part of an installed package).
55+
56+
List your command alongside the built-ins:
5557

5658
**my_bot/bot/slack.yaml**
5759

@@ -67,13 +69,16 @@ gateway:
6769
commands:
6870
- /commands/help
6971
- /commands/echo
72+
- /commands/schedule
73+
- /commands/status
7074
- _target_: hello.HelloCommandModel
7175
```
7276
73-
Then start the bot, pointing Hydra at your config directory so it can import `hello.py`:
77+
`gateway.commands` replaces the default command list, so include the built-ins you want to keep.
78+
Then start the bot, adding the config directory to both Hydra's search path and Python's import path:
7479

7580
```bash
76-
csp-bot-start --config-dir=my_bot +bot=slack
81+
PYTHONPATH=my_bot/bot csp-bot-start --config-dir=my_bot +bot=slack
7782
```
7883

7984
Tagging the bot with `/hello @someone` now replies with a greeting.

example/bot/all.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ defaults:
66
bot_name: CSP Bot
77

88
# Tokens are read from the environment:
9-
# SLACK_BOT_TOKEN, SLACK_APP_TOKEN, DISCORD_TOKEN, SYMPHONY_BOT_KEY, TELEGRAM_BOT_TOKEN
9+
# SLACK_BOT_TOKEN, SLACK_APP_TOKEN, DISCORD_TOKEN,
10+
# SYMPHONY_HOST, SYMPHONY_BOT_USERNAME, SYMPHONY_CERT_PATH, TELEGRAM_BOT_TOKEN
1011
# csp-bot-start --config-dir=example +bot=all

example/bot/symphony.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ defaults:
55

66
bot_name: CSP Bot
77

8-
# Tokens are read from the environment: SYMPHONY_BOT_KEY
8+
# Credentials are read from the environment: SYMPHONY_HOST, SYMPHONY_BOT_USERNAME, SYMPHONY_CERT_PATH
99
# csp-bot-start --config-dir=example +bot=symphony

0 commit comments

Comments
 (0)