feat(config): fail fast on misspelled env values

A typo in EDIT_MESSAGE_TTL_SECONDS / WEBHOOK_PORT etc. used to silently
fall back to a default, so the bot ran with different behavior than the
operator intended (or failed much later on a bare .expect). Unparseable
values now log a loud warning naming the variable; invalid webhook
settings still surface as a hard .expect in webhook mode.
This commit is contained in:
2026-08-13 22:26:23 +08:00
parent 246fc989f0
commit e21643063e
+55 -21
View File
@@ -23,32 +23,66 @@ pub struct Config {
impl Config { impl Config {
pub fn load() -> Config { pub fn load() -> Config {
let admin_ids = env::var("BOT_ADMIN") // Fail-fast helpers: a misspelled value must not silently fall back
.ok() // to a default and run with different behavior than the operator
.map(|s| { // intended — log a loud warning naming the variable instead.
s.split(',') fn parse_u64(name: &str, default: u64) -> u64 {
.filter_map(|part| part.trim().parse::<i64>().ok()) match env::var(name) {
.collect() Ok(v) => v.parse::<u64>().unwrap_or_else(|_| {
}) log::warn!("invalid {name}={v:?}; using default {default}");
.unwrap_or_default(); default
}),
Err(_) => default,
}
}
let edit_message_ttl = env::var("EDIT_MESSAGE_TTL_SECONDS") let admin_ids = match env::var("BOT_ADMIN") {
.ok() Ok(s) => {
.and_then(|s| s.parse::<u64>().ok()) let (ids, bad): (Vec<_>, Vec<_>) = s
.map(Duration::from_secs) .split(',')
.unwrap_or(Duration::from_secs(86400)); .map(str::trim)
.filter(|part| !part.is_empty())
.partition(|part| part.parse::<i64>().is_ok());
if !bad.is_empty() {
log::warn!("BOT_ADMIN: ignoring non-numeric ids: {bad:?}");
}
ids.into_iter().filter_map(|p| p.parse::<i64>().ok()).collect()
}
Err(_) => Vec::new(),
};
let link_cache_ttl = env::var("LINK_CACHE_TTL_SECONDS") let edit_message_ttl = Duration::from_secs(parse_u64(
.ok() "EDIT_MESSAGE_TTL_SECONDS",
.and_then(|s| s.parse::<u64>().ok()) 24 * 3600,
.map(Duration::from_secs) ));
.unwrap_or(Duration::from_secs(7 * 24 * 3600)); let link_cache_ttl = Duration::from_secs(parse_u64(
"LINK_CACHE_TTL_SECONDS",
7 * 24 * 3600,
));
let webhook_enabled = env::var("WEBHOOK") let webhook_enabled = env::var("WEBHOOK")
.is_ok_and(|v| matches!(v.to_lowercase().as_str(), "true" | "yes" | "1")); .is_ok_and(|v| matches!(v.to_lowercase().as_str(), "true" | "yes" | "1"));
let webhook_url = env::var("WEBHOOK_URL").ok().and_then(|s| s.parse().ok()); // The webhook settings are consumed by `.expect()` in main when
let webhook_listen = env::var("WEBHOOK_LISTEN").ok().and_then(|s| s.parse().ok()); // WEBHOOK=true, so an unparseable value fails fast at startup with a
let webhook_port = env::var("WEBHOOK_PORT").ok().and_then(|s| s.parse().ok()); // clear message; still log here for the WEBHOOK=false case.
let webhook_url = env::var("WEBHOOK_URL").ok().and_then(|s| {
s.parse::<url::Url>().ok().or_else(|| {
log::warn!("invalid WEBHOOK_URL={s:?}");
None
})
});
let webhook_listen = env::var("WEBHOOK_LISTEN").ok().and_then(|s| {
s.parse::<IpAddr>().ok().or_else(|| {
log::warn!("invalid WEBHOOK_LISTEN={s:?}");
None
})
});
let webhook_port = env::var("WEBHOOK_PORT").ok().and_then(|s| {
s.parse::<u16>().ok().or_else(|| {
log::warn!("invalid WEBHOOK_PORT={s:?}");
None
})
});
// Empty strings count as unset (e.g. `-e WEBHOOK_CERT=` to disable a // Empty strings count as unset (e.g. `-e WEBHOOK_CERT=` to disable a
// value that would otherwise come from `.env`). // value that would otherwise come from `.env`).
let webhook_cert = env::var("WEBHOOK_CERT").ok().filter(|s| !s.is_empty()); let webhook_cert = env::var("WEBHOOK_CERT").ok().filter(|s| !s.is_empty());