use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use navi_notifier_core::RuleConfig;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
pub general: General,
pub github: GitHubConfig,
pub gitlab: GitLabConfig,
pub gitea: GiteaConfig,
pub slack: SlackConfig,
pub discord: DiscordConfig,
pub email: EmailConfig,
pub rules: RuleConfig,
pub routes: Vec<RouteConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct General {
pub poll_interval_secs: u64,
pub log_level: String,
pub utc_offset_minutes: i32,
}
impl Default for General {
fn default() -> Self {
Self {
poll_interval_secs: 60,
log_level: "info".into(),
utc_offset_minutes: 0,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GitHubConfig {
pub enabled: bool,
pub token_env: String,
pub token: Option<String>,
pub api_base: Option<String>,
}
impl Default for GitHubConfig {
fn default() -> Self {
Self {
enabled: true,
token_env: "NAVI_GITHUB_TOKEN".into(),
token: None,
api_base: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GitLabConfig {
pub enabled: bool,
pub token_env: String,
pub token: Option<String>,
pub api_base: Option<String>,
}
impl Default for GitLabConfig {
fn default() -> Self {
Self {
enabled: false,
token_env: "NAVI_GITLAB_TOKEN".into(),
token: None,
api_base: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GiteaConfig {
pub enabled: bool,
pub token_env: String,
pub token: Option<String>,
pub api_base: Option<String>,
}
impl Default for GiteaConfig {
fn default() -> Self {
Self {
enabled: false,
token_env: "NAVI_GITEA_TOKEN".into(),
token: None,
api_base: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SlackConfig {
pub enabled: bool,
pub token_env: String,
pub token: Option<String>,
pub dm_to: String,
}
impl Default for SlackConfig {
fn default() -> Self {
Self {
enabled: true,
token_env: "NAVI_SLACK_TOKEN".into(),
token: None,
dm_to: "self".into(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DiscordConfig {
pub enabled: bool,
pub token_env: String,
pub token: Option<String>,
pub dm_to: String,
}
impl Default for DiscordConfig {
fn default() -> Self {
Self {
enabled: false,
token_env: "NAVI_DISCORD_TOKEN".into(),
token: None,
dm_to: String::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct EmailConfig {
pub enabled: bool,
pub smtp_host: String,
pub smtp_port: u16,
pub tls: String,
pub username: Option<String>,
pub password_env: String,
pub password: Option<String>,
pub from: String,
pub to: String,
}
impl Default for EmailConfig {
fn default() -> Self {
Self {
enabled: false,
smtp_host: String::new(),
smtp_port: 587,
tls: "starttls".into(),
username: None,
password_env: "NAVI_EMAIL_PASSWORD".into(),
password: None,
from: String::new(),
to: String::new(),
}
}
}
impl EmailConfig {
pub fn resolve_password(&self) -> Option<String> {
if let Some(p) = self.password.as_deref().filter(|p| !p.is_empty()) {
return Some(p.to_string());
}
std::env::var(&self.password_env)
.ok()
.filter(|v| !v.is_empty())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RouteConfig {
pub source: String,
#[serde(alias = "notifier")]
pub destination: String,
}
impl GitHubConfig {
pub fn resolve_token(&self) -> Result<String> {
resolve_secret("github", self.token.as_deref(), &self.token_env)
}
}
impl GitLabConfig {
pub fn resolve_token(&self) -> Result<String> {
resolve_secret("gitlab", self.token.as_deref(), &self.token_env)
}
}
impl GiteaConfig {
pub fn resolve_token(&self) -> Result<String> {
resolve_secret("gitea", self.token.as_deref(), &self.token_env)
}
}
impl SlackConfig {
pub fn resolve_token(&self) -> Result<String> {
resolve_secret("slack", self.token.as_deref(), &self.token_env)
}
}
impl DiscordConfig {
pub fn resolve_token(&self) -> Option<String> {
if let Some(t) = self.token.as_deref().filter(|t| !t.is_empty()) {
return Some(t.to_string());
}
std::env::var(&self.token_env)
.ok()
.filter(|v| !v.is_empty())
}
}
fn resolve_secret(what: &str, inline: Option<&str>, env_var: &str) -> Result<String> {
if let Some(tok) = inline.filter(|t| !t.is_empty()) {
return Ok(tok.to_string());
}
let val = std::env::var(env_var).map_err(|_| {
anyhow!("{what} token not found: set env var `{env_var}` (or the inline `token` field)")
})?;
if val.is_empty() {
return Err(anyhow!("{what} token env var `{env_var}` is empty"));
}
Ok(val)
}
impl Config {
pub fn load(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("reading config at {}", path.display()))?;
let cfg: Config = toml::from_str(&text)
.with_context(|| format!("parsing config at {}", path.display()))?;
Ok(cfg)
}
pub fn engine_routes(&self) -> Vec<navi_notifier_core::Route> {
self.routes
.iter()
.map(|r| navi_notifier_core::Route {
source: r.source.clone(),
destination: r.destination.clone(),
})
.collect()
}
}
pub fn resolve_config_path(explicit: Option<PathBuf>) -> Result<PathBuf> {
if let Some(p) = explicit {
return Ok(p);
}
let dirs = directories::ProjectDirs::from("dev", "navi", "navi")
.ok_or_else(|| anyhow!("could not determine a config directory for this platform"))?;
Ok(dirs.config_dir().join("config.toml"))
}
pub fn resolve_state_path() -> Result<PathBuf> {
let dirs = directories::ProjectDirs::from("dev", "navi", "navi")
.ok_or_else(|| anyhow!("could not determine a data directory for this platform"))?;
Ok(dirs.data_dir().join("navi.sqlite3"))
}