use std::fs; use std::path::PathBuf; use crate::log; #[derive(Debug, Clone)] pub struct Config { pub name: String, pub server: String, pub port: u16, pub tls: bool, pub nick: String, pub user: String, pub realname: String, pub channels: Vec, pub command_prefix: String, pub verbose: bool, pub password: Option, pub sasl_user: Option, pub sasl_pass: Option, } impl Default for Config { fn default() -> Config { Config { name: String::new(), server: "irc.tchatou.fr".to_string(), port: 6667, tls: false, nick: "rubot".to_string(), user: "rubot".to_string(), realname: "rubot".to_string(), channels: vec!["#devs".to_string()], command_prefix: "!".to_string(), verbose: true, password: None, sasl_user: None, sasl_pass: None, } } } type Entry = (usize, String, String); pub fn load() -> Vec { let path = config_path(); let content = path.as_ref().and_then(|p| match fs::read_to_string(p) { Ok(s) => { log(&format!("loaded config from {}", p.display())); Some(s) } Err(e) => { log(&format!("could not read config {}: {e}", p.display())); None } }); let (toplevel, sections) = match &content { Some(text) => parse_sections(text), None => (Vec::new(), Vec::new()), }; let mut base = Config::default(); for (line, key, value) in &toplevel { apply_kv(&mut base, key, value, &loc(&path, *line)); } apply_env(&mut base); assemble(base, sections, &path) } pub fn parse_str(content: &str) -> Vec { let (toplevel, sections) = parse_sections(content); let mut base = Config::default(); for (line, key, value) in &toplevel { apply_kv(&mut base, key, value, &format!("line {line}")); } assemble(base, sections, &None) } fn assemble(base: Config, sections: Vec<(String, Vec)>, path: &Option) -> Vec { let mut configs = Vec::new(); if sections.is_empty() { let mut c = base; finalize(&mut c); configs.push(c); } else { for (name, entries) in sections { let mut c = base.clone(); c.name = name; for (line, key, value) in &entries { apply_kv(&mut c, key, value, &loc(path, *line)); } finalize(&mut c); configs.push(c); } } configs } fn finalize(c: &mut Config) { if c.name.is_empty() { c.name = c.server.clone(); } if c.tls && c.port == 6667 { c.port = 6697; } } fn parse_sections(content: &str) -> (Vec, Vec<(String, Vec)>) { let mut toplevel: Vec = Vec::new(); let mut sections: Vec<(String, Vec)> = Vec::new(); for (i, raw) in content.lines().enumerate() { let line = raw.trim(); let lineno = i + 1; if line.is_empty() || line.starts_with('#') || line.starts_with(';') { continue; } if let Some(inner) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) { sections.push((inner.trim().to_string(), Vec::new())); continue; } let Some((key, value)) = line.split_once('=') else { log(&format!("line {lineno}: ignoring line without '='")); continue; }; let entry = (lineno, key.trim().to_ascii_lowercase(), value.trim().to_string()); match sections.last_mut() { Some((_, entries)) => entries.push(entry), None => toplevel.push(entry), } } (toplevel, sections) } fn apply_kv(c: &mut Config, key: &str, value: &str, where_: &str) { match key { "name" => c.name = value.to_string(), "server" => c.server = value.to_string(), "port" => match value.parse() { Ok(p) => c.port = p, Err(_) => log(&format!("{where_}: invalid port '{value}'")), }, "tls" => c.tls = parse_bool(value), "nick" => c.nick = value.to_string(), "user" => c.user = value.to_string(), "realname" => c.realname = value.to_string(), "channels" => c.channels = split_list(value), "prefix" | "command_prefix" => c.command_prefix = value.to_string(), "verbose" => c.verbose = parse_bool(value), "password" => c.password = (!value.is_empty()).then(|| value.to_string()), "sasl_user" => c.sasl_user = (!value.is_empty()).then(|| value.to_string()), "sasl_pass" => c.sasl_pass = (!value.is_empty()).then(|| value.to_string()), other => log(&format!("{where_}: unknown key '{other}'")), } } fn loc(path: &Option, line: usize) -> String { match path { Some(p) => format!("{}:{}", p.display(), line), None => format!("line {line}"), } } fn config_path() -> Option { if let Some(arg) = std::env::args().nth(1) { return Some(PathBuf::from(arg)); } if let Ok(env) = std::env::var("RUSTBOT_CONFIG") { return Some(PathBuf::from(env)); } let default = PathBuf::from("rustbot.conf"); default.exists().then_some(default) } fn apply_env(c: &mut Config) { if let Ok(v) = std::env::var("IRC_SERVER") { c.server = v; } if let Some(p) = std::env::var("IRC_PORT").ok().and_then(|v| v.parse().ok()) { c.port = p; } if let Ok(v) = std::env::var("IRC_TLS") { c.tls = parse_bool(&v); } if let Ok(v) = std::env::var("IRC_NICK") { c.user = v.clone(); c.nick = v; } if let Ok(v) = std::env::var("IRC_REALNAME") { c.realname = v; } if let Ok(v) = std::env::var("IRC_CHANNELS") { c.channels = split_list(&v); } if let Ok(v) = std::env::var("IRC_PREFIX") { c.command_prefix = v; } if let Ok(v) = std::env::var("IRC_VERBOSE") { c.verbose = parse_bool(&v); } if let Ok(v) = std::env::var("IRC_PASSWORD") { c.password = Some(v); } if let Ok(v) = std::env::var("IRC_SASL_USER") { c.sasl_user = Some(v); } if let Ok(v) = std::env::var("IRC_SASL_PASS") { c.sasl_pass = Some(v); } } fn parse_bool(value: &str) -> bool { matches!( value.trim().to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on" ) } fn split_list(value: &str) -> Vec { value .split(|ch: char| ch == ',' || ch.is_whitespace()) .filter(|s| !s.is_empty()) .map(str::to_string) .collect() }