use std::path::PathBuf;
use crate::error::{Error, Result};
pub const BOT_TOKEN_ENV_VAR: &str = "DISCORD_BOT_TOKEN";
pub const CONFIG_SECTION: &str = "discord";
#[derive(Clone, Default)]
pub struct DiscordConfig {
pub application_id: String,
pub public_key: String,
bot_token: Option<String>,
}
impl std::fmt::Debug for DiscordConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DiscordConfig")
.field("application_id", &self.application_id)
.field("public_key", &self.public_key)
.field("bot_token", &self.bot_token.as_ref().map(|_| "<redacted>"))
.finish_non_exhaustive()
}
}
impl DiscordConfig {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn load() -> Result<Self> {
let mut config = Self::new();
if let Some(table) = read_section_table()? {
if let Some(value) = table.get("application_id").and_then(|v| v.as_str()) {
value.clone_into(&mut config.application_id);
}
if let Some(value) = table.get("public_key").and_then(|v| v.as_str()) {
value.clone_into(&mut config.public_key);
}
}
if let Ok(token) = std::env::var(BOT_TOKEN_ENV_VAR)
&& !token.trim().is_empty()
{
config.bot_token = Some(token);
}
Ok(config)
}
pub fn apply_overrides(&mut self, overrides: &Self) {
if !overrides.application_id.is_empty() {
self.application_id.clone_from(&overrides.application_id);
}
if !overrides.public_key.is_empty() {
self.public_key.clone_from(&overrides.public_key);
}
}
#[must_use]
pub fn bot_token(&self) -> Option<&str> {
self.bot_token.as_deref()
}
pub fn validate(&self) -> Result<()> {
if self.application_id.trim().is_empty() {
return Err(Error::Config(
"discord.application_id is missing; set it in autumn.toml [discord]".to_owned(),
));
}
if !self
.application_id
.trim()
.chars()
.all(|c| c.is_ascii_digit())
{
return Err(Error::Config(
"discord.application_id must be a numeric snowflake".to_owned(),
));
}
let key = self.public_key.trim();
if key.len() != 64 || hex::decode(key).is_err() {
return Err(Error::Config(
"discord.public_key must be 64 hex chars (the Ed25519 application public key)"
.to_owned(),
));
}
match &self.bot_token {
Some(token) if !token.trim().is_empty() => Ok(()),
_ => Err(Error::Config(format!(
"discord bot token is missing; set the {BOT_TOKEN_ENV_VAR} environment variable"
))),
}
}
}
fn read_section_table() -> Result<Option<toml::Table>> {
let path = config_file_path();
let Ok(contents) = std::fs::read_to_string(&path) else {
return Ok(None);
};
let table: toml::Table = toml::from_str(&contents)
.map_err(|err| Error::Config(format!("invalid {}: {err}", path.display())))?;
Ok(table
.get(CONFIG_SECTION)
.and_then(|value| value.as_table())
.cloned())
}
fn config_file_path() -> PathBuf {
if let Ok(manifest_dir) = std::env::var("AUTUMN_MANIFEST_DIR") {
let candidate = PathBuf::from(manifest_dir).join("autumn.toml");
if candidate.exists() {
return candidate;
}
}
PathBuf::from("autumn.toml")
}
#[cfg(test)]
mod tests;