refactor: split chat/transport/matrix/config.rs into mod.rs + loading.rs
The 1260-line config.rs is split into: - mod.rs: BotConfig struct + small impl + default helpers + tests (1047 lines) - loading.rs: BotConfig::load + save_ambient_rooms (223 lines) Tests stay co-located. No behaviour change. All 41 matrix::config tests pass; full suite green.
This commit is contained in:
@@ -0,0 +1,223 @@
|
|||||||
|
//! BotConfig::load implementation and the save_ambient_rooms helper.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use super::BotConfig;
|
||||||
|
|
||||||
|
impl BotConfig {
|
||||||
|
/// Load bot configuration from `.huskies/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(".huskies").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" {
|
||||||
|
if config.whatsapp_provider == "twilio" {
|
||||||
|
// Validate Twilio-specific fields.
|
||||||
|
if config
|
||||||
|
.twilio_account_sid
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|s| s.is_empty())
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"[bot] bot.toml: whatsapp_provider=\"twilio\" requires \
|
||||||
|
twilio_account_sid"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if config
|
||||||
|
.twilio_auth_token
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|s| s.is_empty())
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"[bot] bot.toml: whatsapp_provider=\"twilio\" requires \
|
||||||
|
twilio_auth_token"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if config
|
||||||
|
.twilio_whatsapp_number
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|s| s.is_empty())
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"[bot] bot.toml: whatsapp_provider=\"twilio\" requires \
|
||||||
|
twilio_whatsapp_number"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Validate Meta (default) WhatsApp 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.transport == "discord" {
|
||||||
|
// Validate Discord-specific fields.
|
||||||
|
if config
|
||||||
|
.discord_bot_token
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|s| s.is_empty())
|
||||||
|
{
|
||||||
|
eprintln!(
|
||||||
|
"[bot] bot.toml: transport=\"discord\" requires \
|
||||||
|
discord_bot_token"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if config.discord_channel_ids.is_empty() {
|
||||||
|
eprintln!(
|
||||||
|
"[bot] bot.toml: transport=\"discord\" requires \
|
||||||
|
at least one discord_channel_ids entry"
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Default transport is Matrix — validate Matrix-specific fields.
|
||||||
|
if config.homeserver.as_ref().is_none_or(|s| s.is_empty()) {
|
||||||
|
eprintln!("[bot] bot.toml: transport=\"matrix\" requires homeserver");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if config.username.as_ref().is_none_or(|s| s.is_empty()) {
|
||||||
|
eprintln!("[bot] bot.toml: transport=\"matrix\" requires username");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if config.password.as_ref().is_none_or(|s| s.is_empty()) {
|
||||||
|
eprintln!("[bot] bot.toml: transport=\"matrix\" requires password");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
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(".huskies").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}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+2
-215
@@ -184,224 +184,11 @@ fn default_whatsapp_provider() -> String {
|
|||||||
"meta".to_string()
|
"meta".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BotConfig {
|
|
||||||
/// Load bot configuration from `.huskies/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(".huskies").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" {
|
mod loading;
|
||||||
if config.whatsapp_provider == "twilio" {
|
|
||||||
// Validate Twilio-specific fields.
|
|
||||||
if config
|
|
||||||
.twilio_account_sid
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|s| s.is_empty())
|
|
||||||
{
|
|
||||||
eprintln!(
|
|
||||||
"[bot] bot.toml: whatsapp_provider=\"twilio\" requires \
|
|
||||||
twilio_account_sid"
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if config
|
|
||||||
.twilio_auth_token
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|s| s.is_empty())
|
|
||||||
{
|
|
||||||
eprintln!(
|
|
||||||
"[bot] bot.toml: whatsapp_provider=\"twilio\" requires \
|
|
||||||
twilio_auth_token"
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if config
|
|
||||||
.twilio_whatsapp_number
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|s| s.is_empty())
|
|
||||||
{
|
|
||||||
eprintln!(
|
|
||||||
"[bot] bot.toml: whatsapp_provider=\"twilio\" requires \
|
|
||||||
twilio_whatsapp_number"
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Validate Meta (default) WhatsApp 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.transport == "discord" {
|
|
||||||
// Validate Discord-specific fields.
|
|
||||||
if config
|
|
||||||
.discord_bot_token
|
|
||||||
.as_ref()
|
|
||||||
.is_none_or(|s| s.is_empty())
|
|
||||||
{
|
|
||||||
eprintln!(
|
|
||||||
"[bot] bot.toml: transport=\"discord\" requires \
|
|
||||||
discord_bot_token"
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if config.discord_channel_ids.is_empty() {
|
|
||||||
eprintln!(
|
|
||||||
"[bot] bot.toml: transport=\"discord\" requires \
|
|
||||||
at least one discord_channel_ids entry"
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Default transport is Matrix — validate Matrix-specific fields.
|
|
||||||
if config.homeserver.as_ref().is_none_or(|s| s.is_empty()) {
|
|
||||||
eprintln!("[bot] bot.toml: transport=\"matrix\" requires homeserver");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if config.username.as_ref().is_none_or(|s| s.is_empty()) {
|
|
||||||
eprintln!("[bot] bot.toml: transport=\"matrix\" requires username");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if config.password.as_ref().is_none_or(|s| s.is_empty()) {
|
|
||||||
eprintln!("[bot] bot.toml: transport=\"matrix\" requires password");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
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`
|
pub use loading::save_ambient_rooms;
|
||||||
/// 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(".huskies").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 {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
Reference in New Issue
Block a user