rubot/src/config.rs

227 lines
6.6 KiB
Rust

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<String>,
pub command_prefix: String,
pub verbose: bool,
pub password: Option<String>,
pub sasl_user: Option<String>,
pub sasl_pass: Option<String>,
}
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<Config> {
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<Config> {
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<Entry>)>, path: &Option<PathBuf>) -> Vec<Config> {
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<Entry>, Vec<(String, Vec<Entry>)>) {
let mut toplevel: Vec<Entry> = Vec::new();
let mut sections: Vec<(String, Vec<Entry>)> = 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<PathBuf>, line: usize) -> String {
match path {
Some(p) => format!("{}:{}", p.display(), line),
None => format!("line {line}"),
}
}
fn config_path() -> Option<PathBuf> {
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<String> {
value
.split(|ch: char| ch == ',' || ch.is_whitespace())
.filter(|s| !s.is_empty())
.map(str::to_string)
.collect()
}