227 lines
6.6 KiB
Rust
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()
|
|
}
|