Restore codebase deleted by bad auto-commit e4227cf
Commit e4227cf (a story creation auto-commit) erroneously deleted 175
files from master's tree, likely due to a race condition between
concurrent git operations. This commit re-adds all files from the
working directory.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
815
server/src/matrix/config.rs
Normal file
815
server/src/matrix/config.rs
Normal file
@@ -0,0 +1,815 @@
|
||||
use serde::Deserialize;
|
||||
use std::path::Path;
|
||||
|
||||
fn default_history_size() -> usize {
|
||||
20
|
||||
}
|
||||
|
||||
fn default_permission_timeout_secs() -> u64 {
|
||||
120
|
||||
}
|
||||
|
||||
/// Configuration for the Matrix bot, read from `.storkit/bot.toml`.
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub struct BotConfig {
|
||||
/// Matrix homeserver URL, e.g. `https://matrix.example.com`
|
||||
pub homeserver: String,
|
||||
/// Bot user ID, e.g. `@storykit:example.com`
|
||||
pub username: String,
|
||||
/// Bot password
|
||||
pub password: String,
|
||||
/// Matrix room IDs to join, e.g. `["!roomid:example.com"]`.
|
||||
/// Use an array for multiple rooms; a single string is accepted via the
|
||||
/// deprecated `room_id` key for backwards compatibility.
|
||||
#[serde(default)]
|
||||
pub room_ids: Vec<String>,
|
||||
/// Deprecated: use `room_ids` (list) instead. Still accepted so existing
|
||||
/// `bot.toml` files continue to work without modification.
|
||||
#[serde(default)]
|
||||
pub room_id: Option<String>,
|
||||
/// Set to `true` to enable the bot (default: false)
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
/// Matrix user IDs allowed to interact with the bot.
|
||||
/// If empty or omitted, the bot ignores ALL messages (fail-closed).
|
||||
#[serde(default)]
|
||||
pub allowed_users: Vec<String>,
|
||||
/// Maximum number of conversation turns (user + assistant pairs) to keep
|
||||
/// per room. When the history exceeds this limit the oldest messages are
|
||||
/// dropped. Defaults to 20.
|
||||
#[serde(default = "default_history_size")]
|
||||
pub history_size: usize,
|
||||
/// Timeout in seconds for permission prompts surfaced to the Matrix room.
|
||||
/// If the user does not respond within this window the permission is denied
|
||||
/// (fail-closed). Defaults to 120 seconds.
|
||||
#[serde(default = "default_permission_timeout_secs")]
|
||||
pub permission_timeout_secs: u64,
|
||||
/// Previously used to select an Anthropic model. Now ignored — the bot
|
||||
/// uses Claude Code which manages its own model selection. Kept for
|
||||
/// backwards compatibility so existing bot.toml files still parse.
|
||||
#[allow(dead_code)]
|
||||
pub model: Option<String>,
|
||||
/// Display name the bot uses to identify itself in conversations.
|
||||
/// If unset, the bot falls back to "Assistant".
|
||||
#[serde(default)]
|
||||
pub display_name: Option<String>,
|
||||
/// Room IDs where ambient mode is active (bot responds to all messages).
|
||||
/// Updated at runtime when the user toggles ambient mode — do not edit
|
||||
/// manually while the bot is running.
|
||||
#[serde(default)]
|
||||
pub ambient_rooms: Vec<String>,
|
||||
/// Chat transport to use: `"matrix"` (default) or `"whatsapp"`.
|
||||
///
|
||||
/// Selects which [`ChatTransport`] implementation the bot uses for
|
||||
/// sending and editing messages. Currently only read during bot
|
||||
/// startup to select the transport; the field is kept for config
|
||||
/// round-tripping.
|
||||
#[serde(default = "default_transport")]
|
||||
pub transport: String,
|
||||
|
||||
// ── WhatsApp Business API fields ─────────────────────────────────
|
||||
// These are only required when `transport = "whatsapp"`.
|
||||
|
||||
/// WhatsApp Business phone number ID from the Meta dashboard.
|
||||
#[serde(default)]
|
||||
pub whatsapp_phone_number_id: Option<String>,
|
||||
/// Long-lived access token for the WhatsApp Business API.
|
||||
#[serde(default)]
|
||||
pub whatsapp_access_token: Option<String>,
|
||||
/// Verify token used in the webhook handshake (you choose this value
|
||||
/// and configure it in the Meta webhook settings).
|
||||
#[serde(default)]
|
||||
pub whatsapp_verify_token: Option<String>,
|
||||
/// Name of the approved Meta message template used for pipeline
|
||||
/// notifications when the 24-hour messaging window has expired.
|
||||
///
|
||||
/// The template must be registered in the Meta Business Manager before
|
||||
/// use. Defaults to `"pipeline_notification"`.
|
||||
#[serde(default)]
|
||||
pub whatsapp_notification_template: Option<String>,
|
||||
|
||||
// ── Slack Bot API fields ─────────────────────────────────────────
|
||||
// These are only required when `transport = "slack"`.
|
||||
|
||||
/// Slack Bot User OAuth Token (starts with `xoxb-`).
|
||||
#[serde(default)]
|
||||
pub slack_bot_token: Option<String>,
|
||||
/// Slack Signing Secret used to verify incoming webhook requests.
|
||||
#[serde(default)]
|
||||
pub slack_signing_secret: Option<String>,
|
||||
/// Slack channel IDs the bot should listen in.
|
||||
#[serde(default)]
|
||||
pub slack_channel_ids: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_transport() -> String {
|
||||
"matrix".to_string()
|
||||
}
|
||||
|
||||
impl BotConfig {
|
||||
/// Load bot configuration from `.storkit/bot.toml`.
|
||||
///
|
||||
/// Returns `None` if the file does not exist, fails to parse, has
|
||||
/// `enabled = false`, or specifies no room IDs.
|
||||
pub fn load(project_root: &Path) -> Option<Self> {
|
||||
let path = project_root.join(".storkit").join("bot.toml");
|
||||
if !path.exists() {
|
||||
return None;
|
||||
}
|
||||
let content = std::fs::read_to_string(&path)
|
||||
.map_err(|e| eprintln!("[matrix-bot] Failed to read bot.toml: {e}"))
|
||||
.ok()?;
|
||||
let mut config: BotConfig = toml::from_str(&content)
|
||||
.map_err(|e| eprintln!("[matrix-bot] Invalid bot.toml: {e}"))
|
||||
.ok()?;
|
||||
if !config.enabled {
|
||||
return None;
|
||||
}
|
||||
// Merge deprecated `room_id` (single string) into `room_ids` (list).
|
||||
if let Some(single) = config.room_id.take()
|
||||
&& !config.room_ids.contains(&single)
|
||||
{
|
||||
config.room_ids.push(single);
|
||||
}
|
||||
|
||||
if config.transport == "whatsapp" {
|
||||
// Validate WhatsApp-specific fields.
|
||||
if config.whatsapp_phone_number_id.as_ref().is_none_or(|s| s.is_empty()) {
|
||||
eprintln!(
|
||||
"[bot] bot.toml: transport=\"whatsapp\" requires \
|
||||
whatsapp_phone_number_id"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if config.whatsapp_access_token.as_ref().is_none_or(|s| s.is_empty()) {
|
||||
eprintln!(
|
||||
"[bot] bot.toml: transport=\"whatsapp\" requires \
|
||||
whatsapp_access_token"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if config.whatsapp_verify_token.as_ref().is_none_or(|s| s.is_empty()) {
|
||||
eprintln!(
|
||||
"[bot] bot.toml: transport=\"whatsapp\" requires \
|
||||
whatsapp_verify_token"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
} else if config.transport == "slack" {
|
||||
// Validate Slack-specific fields.
|
||||
if config.slack_bot_token.as_ref().is_none_or(|s| s.is_empty()) {
|
||||
eprintln!(
|
||||
"[bot] bot.toml: transport=\"slack\" requires \
|
||||
slack_bot_token"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if config.slack_signing_secret.as_ref().is_none_or(|s| s.is_empty()) {
|
||||
eprintln!(
|
||||
"[bot] bot.toml: transport=\"slack\" requires \
|
||||
slack_signing_secret"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
if config.slack_channel_ids.is_empty() {
|
||||
eprintln!(
|
||||
"[bot] bot.toml: transport=\"slack\" requires \
|
||||
at least one slack_channel_ids entry"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
} else if config.room_ids.is_empty() {
|
||||
eprintln!(
|
||||
"[matrix-bot] bot.toml has no room_ids configured — \
|
||||
add `room_ids = [\"!roomid:example.com\"]` to bot.toml"
|
||||
);
|
||||
return None;
|
||||
}
|
||||
Some(config)
|
||||
}
|
||||
|
||||
/// Returns all configured room IDs as a flat list. Combines `room_ids`
|
||||
/// and (after loading) any merged `room_id` value.
|
||||
pub fn effective_room_ids(&self) -> &[String] {
|
||||
&self.room_ids
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist the current set of ambient room IDs back to `bot.toml`.
|
||||
///
|
||||
/// Reads the existing file as a TOML document, updates the `ambient_rooms`
|
||||
/// array, and writes the result back. Errors are logged but not propagated
|
||||
/// so a persistence failure never interrupts the bot's message handling.
|
||||
pub fn save_ambient_rooms(project_root: &Path, room_ids: &[String]) {
|
||||
let path = project_root.join(".storkit").join("bot.toml");
|
||||
let content = match std::fs::read_to_string(&path) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
eprintln!("[matrix-bot] save_ambient_rooms: failed to read bot.toml: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut doc: toml::Value = match toml::from_str(&content) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
eprintln!("[matrix-bot] save_ambient_rooms: failed to parse bot.toml: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if let toml::Value::Table(ref mut t) = doc {
|
||||
let arr = toml::Value::Array(
|
||||
room_ids
|
||||
.iter()
|
||||
.map(|s| toml::Value::String(s.clone()))
|
||||
.collect(),
|
||||
);
|
||||
t.insert("ambient_rooms".to_string(), arr);
|
||||
}
|
||||
match toml::to_string_pretty(&doc) {
|
||||
Ok(new_content) => {
|
||||
if let Err(e) = std::fs::write(&path, new_content) {
|
||||
eprintln!("[matrix-bot] save_ambient_rooms: failed to write bot.toml: {e}");
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("[matrix-bot] save_ambient_rooms: failed to serialise bot.toml: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn load_returns_none_when_file_missing() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let result = BotConfig::load(tmp.path());
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_returns_none_when_disabled() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = BotConfig::load(tmp.path());
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_returns_config_when_enabled_with_room_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com", "!def:example.com"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = BotConfig::load(tmp.path());
|
||||
assert!(result.is_some());
|
||||
let config = result.unwrap();
|
||||
assert_eq!(config.homeserver, "https://matrix.example.com");
|
||||
assert_eq!(config.username, "@bot:example.com");
|
||||
assert_eq!(
|
||||
config.effective_room_ids(),
|
||||
&["!abc:example.com", "!def:example.com"]
|
||||
);
|
||||
assert!(config.model.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_merges_deprecated_room_id_into_room_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
// Old-style single room_id key — should still work.
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_id = "!abc:example.com"
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.effective_room_ids(), &["!abc:example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_returns_none_when_no_room_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let result = BotConfig::load(tmp.path());
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_returns_none_when_toml_invalid() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(sk.join("bot.toml"), "not valid toml {{{").unwrap();
|
||||
let result = BotConfig::load(tmp.path());
|
||||
assert!(result.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_respects_optional_model() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
model = "claude-sonnet-4-6"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.model.as_deref(), Some("claude-sonnet-4-6"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_uses_default_history_size() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.history_size, 20);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_respects_custom_history_size() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
history_size = 50
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.history_size, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_reads_display_name() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
display_name = "Timmy"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.display_name.as_deref(), Some("Timmy"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_display_name_defaults_to_none_when_absent() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert!(config.display_name.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_uses_default_permission_timeout() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.permission_timeout_secs, 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_respects_custom_permission_timeout() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
permission_timeout_secs = 60
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.permission_timeout_secs, 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_ignores_legacy_require_verified_devices_key() {
|
||||
// Old bot.toml files that still have `require_verified_devices = true`
|
||||
// must parse successfully — the field is simply ignored now that
|
||||
// verification is always enforced unconditionally.
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
require_verified_devices = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
// Should still load successfully despite the unknown field.
|
||||
let config = BotConfig::load(tmp.path());
|
||||
assert!(
|
||||
config.is_some(),
|
||||
"bot.toml with legacy require_verified_devices key must still load"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_reads_ambient_rooms() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
ambient_rooms = ["!abc:example.com"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.ambient_rooms, vec!["!abc:example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_ambient_rooms_defaults_to_empty_when_absent() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert!(config.ambient_rooms.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_ambient_rooms_persists_to_bot_toml() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
save_ambient_rooms(tmp.path(), &["!abc:example.com".to_string()]);
|
||||
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.ambient_rooms, vec!["!abc:example.com"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_ambient_rooms_clears_when_empty() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
ambient_rooms = ["!abc:example.com"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
save_ambient_rooms(tmp.path(), &[]);
|
||||
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert!(config.ambient_rooms.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_transport_defaults_to_matrix() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.transport, "matrix");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_transport_reads_custom_value() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
room_ids = ["!abc:example.com"]
|
||||
enabled = true
|
||||
transport = "whatsapp"
|
||||
whatsapp_phone_number_id = "123456"
|
||||
whatsapp_access_token = "EAAtoken"
|
||||
whatsapp_verify_token = "my-verify"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.transport, "whatsapp");
|
||||
assert_eq!(
|
||||
config.whatsapp_phone_number_id.as_deref(),
|
||||
Some("123456")
|
||||
);
|
||||
assert_eq!(
|
||||
config.whatsapp_access_token.as_deref(),
|
||||
Some("EAAtoken")
|
||||
);
|
||||
assert_eq!(
|
||||
config.whatsapp_verify_token.as_deref(),
|
||||
Some("my-verify")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_whatsapp_returns_none_when_missing_phone_number_id() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
enabled = true
|
||||
transport = "whatsapp"
|
||||
whatsapp_access_token = "EAAtoken"
|
||||
whatsapp_verify_token = "my-verify"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(BotConfig::load(tmp.path()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_whatsapp_returns_none_when_missing_access_token() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
enabled = true
|
||||
transport = "whatsapp"
|
||||
whatsapp_phone_number_id = "123456"
|
||||
whatsapp_verify_token = "my-verify"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(BotConfig::load(tmp.path()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_whatsapp_returns_none_when_missing_verify_token() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
enabled = true
|
||||
transport = "whatsapp"
|
||||
whatsapp_phone_number_id = "123456"
|
||||
whatsapp_access_token = "EAAtoken"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(BotConfig::load(tmp.path()).is_none());
|
||||
}
|
||||
|
||||
// ── Slack config tests ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn load_slack_transport_reads_config() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
enabled = true
|
||||
transport = "slack"
|
||||
slack_bot_token = "xoxb-123"
|
||||
slack_signing_secret = "secret123"
|
||||
slack_channel_ids = ["C01ABCDEF"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let config = BotConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(config.transport, "slack");
|
||||
assert_eq!(config.slack_bot_token.as_deref(), Some("xoxb-123"));
|
||||
assert_eq!(config.slack_signing_secret.as_deref(), Some("secret123"));
|
||||
assert_eq!(config.slack_channel_ids, vec!["C01ABCDEF"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_slack_returns_none_when_missing_bot_token() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
enabled = true
|
||||
transport = "slack"
|
||||
slack_signing_secret = "secret123"
|
||||
slack_channel_ids = ["C01ABCDEF"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(BotConfig::load(tmp.path()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_slack_returns_none_when_missing_signing_secret() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
enabled = true
|
||||
transport = "slack"
|
||||
slack_bot_token = "xoxb-123"
|
||||
slack_channel_ids = ["C01ABCDEF"]
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(BotConfig::load(tmp.path()).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_slack_returns_none_when_missing_channel_ids() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let sk = tmp.path().join(".storkit");
|
||||
fs::create_dir_all(&sk).unwrap();
|
||||
fs::write(
|
||||
sk.join("bot.toml"),
|
||||
r#"
|
||||
homeserver = "https://matrix.example.com"
|
||||
username = "@bot:example.com"
|
||||
password = "secret"
|
||||
enabled = true
|
||||
transport = "slack"
|
||||
slack_bot_token = "xoxb-123"
|
||||
slack_signing_secret = "secret123"
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(BotConfig::load(tmp.path()).is_none());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user