feat(bot): add modality map, config loader, and backend client
Implements core bot infrastructure: - modality_map: channel name to modality string mapping (personal_text, voice, fb_messenger, nd_messenger) - config: Config dataclass with env-driven configuration loading for Discord, backend, and email IMAP credentials - backend_client: async HTTP client for posting items to backend /items endpoint All modality_map tests passing (3/3)
This commit is contained in:
parent
a90c3fc172
commit
669bb3456a
7 changed files with 105 additions and 0 deletions
0
bot/chorus_bot/__init__.py
Normal file
0
bot/chorus_bot/__init__.py
Normal file
28
bot/chorus_bot/backend_client.py
Normal file
28
bot/chorus_bot/backend_client.py
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
from datetime import datetime
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class BackendClient:
|
||||||
|
def __init__(self, base_url: str):
|
||||||
|
self._base_url = base_url.rstrip("/")
|
||||||
|
|
||||||
|
async def create_item(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
modality: str,
|
||||||
|
raw_content: str,
|
||||||
|
captured_at: datetime,
|
||||||
|
discord_message_id: str | None,
|
||||||
|
sender_id: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
payload = {
|
||||||
|
"modality": modality,
|
||||||
|
"raw_content": raw_content,
|
||||||
|
"captured_at": captured_at.isoformat(),
|
||||||
|
"discord_message_id": discord_message_id,
|
||||||
|
"sender_id": sender_id,
|
||||||
|
}
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
resp = await client.post(f"{self._base_url}/items", json=payload, timeout=10)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return resp.json()
|
||||||
45
bot/chorus_bot/config.py
Normal file
45
bot/chorus_bot/config.py
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
discord_token: str
|
||||||
|
guild_id: int
|
||||||
|
inbox_channel_id: int
|
||||||
|
triage_role_id: int
|
||||||
|
backend_base_url: str
|
||||||
|
google_voice_imap_host: str
|
||||||
|
google_voice_imap_user: str
|
||||||
|
google_voice_imap_password: str
|
||||||
|
bh_email_imap_host: str
|
||||||
|
bh_email_imap_user: str
|
||||||
|
bh_email_imap_password: str
|
||||||
|
personal_email_imap_host: str
|
||||||
|
personal_email_imap_user: str
|
||||||
|
personal_email_imap_password: str
|
||||||
|
|
||||||
|
|
||||||
|
def load_config() -> Config:
|
||||||
|
def env(name: str) -> str:
|
||||||
|
value = os.environ.get(name)
|
||||||
|
if not value:
|
||||||
|
raise RuntimeError(f"Missing required env var: {name}")
|
||||||
|
return value
|
||||||
|
|
||||||
|
return Config(
|
||||||
|
discord_token=env("CHORUS_DISCORD_TOKEN"),
|
||||||
|
guild_id=int(env("CHORUS_GUILD_ID")),
|
||||||
|
inbox_channel_id=int(env("CHORUS_INBOX_CHANNEL_ID")),
|
||||||
|
triage_role_id=int(env("CHORUS_TRIAGE_ROLE_ID")),
|
||||||
|
backend_base_url=env("CHORUS_BACKEND_BASE_URL"),
|
||||||
|
google_voice_imap_host=env("CHORUS_GVOICE_IMAP_HOST"),
|
||||||
|
google_voice_imap_user=env("CHORUS_GVOICE_IMAP_USER"),
|
||||||
|
google_voice_imap_password=env("CHORUS_GVOICE_IMAP_PASSWORD"),
|
||||||
|
bh_email_imap_host=env("CHORUS_BH_EMAIL_IMAP_HOST"),
|
||||||
|
bh_email_imap_user=env("CHORUS_BH_EMAIL_IMAP_USER"),
|
||||||
|
bh_email_imap_password=env("CHORUS_BH_EMAIL_IMAP_PASSWORD"),
|
||||||
|
personal_email_imap_host=env("CHORUS_PERSONAL_EMAIL_IMAP_HOST"),
|
||||||
|
personal_email_imap_user=env("CHORUS_PERSONAL_EMAIL_IMAP_USER"),
|
||||||
|
personal_email_imap_password=env("CHORUS_PERSONAL_EMAIL_IMAP_PASSWORD"),
|
||||||
|
)
|
||||||
10
bot/chorus_bot/modality_map.py
Normal file
10
bot/chorus_bot/modality_map.py
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
CHANNEL_TO_MODALITY = {
|
||||||
|
"personal-text": "personal_text",
|
||||||
|
"voice": "voice",
|
||||||
|
"fb-messenger": "fb_messenger",
|
||||||
|
"nd-messenger": "nd_messenger",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def modality_for_channel(channel_name: str) -> str | None:
|
||||||
|
return CHANNEL_TO_MODALITY.get(channel_name)
|
||||||
5
bot/requirements.txt
Normal file
5
bot/requirements.txt
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
discord.py==2.4.0
|
||||||
|
httpx==0.27.2
|
||||||
|
aioimaplib==1.1.0
|
||||||
|
pytest==8.3.3
|
||||||
|
pytest-asyncio==0.24.0
|
||||||
0
bot/tests/__init__.py
Normal file
0
bot/tests/__init__.py
Normal file
17
bot/tests/test_modality_map.py
Normal file
17
bot/tests/test_modality_map.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
from chorus_bot import modality_map
|
||||||
|
|
||||||
|
|
||||||
|
def test_known_channels_map_to_modalities():
|
||||||
|
assert modality_map.modality_for_channel("personal-text") == "personal_text"
|
||||||
|
assert modality_map.modality_for_channel("voice") == "voice"
|
||||||
|
assert modality_map.modality_for_channel("fb-messenger") == "fb_messenger"
|
||||||
|
assert modality_map.modality_for_channel("nd-messenger") == "nd_messenger"
|
||||||
|
|
||||||
|
|
||||||
|
def test_unknown_channel_returns_none():
|
||||||
|
assert modality_map.modality_for_channel("random-chat") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_inbox_and_general_channels_are_not_mapped():
|
||||||
|
assert modality_map.modality_for_channel("inbox") is None
|
||||||
|
assert modality_map.modality_for_channel("general") is None
|
||||||
Loading…
Reference in a new issue