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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
mod loading;
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
pub use loading::save_ambient_rooms;
|
||||
|
||||
/// 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 {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
Reference in New Issue
Block a user