use std::sync::Arc; use std::time::Duration; use anyhow::{Context, Result}; use axum::routing::{get, post}; use axum::Router; use tokio::sync::Mutex; use tracing::info; mod api; mod db; mod poller; mod types; mod webhook; use types::WebhookTarget; #[tokio::main] async fn main() -> Result<()> { tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .init(); let port: u16 = std::env::var("NOISEBELL_CACHE_PORT") .unwrap_or_else(|_| "3000".into()) .parse() .context("NOISEBELL_CACHE_PORT must be a valid u16")?; let pi_address = std::env::var("NOISEBELL_CACHE_PI_ADDRESS") .context("NOISEBELL_CACHE_PI_ADDRESS is required")?; let pi_api_key = std::env::var("NOISEBELL_CACHE_PI_API_KEY") .context("NOISEBELL_CACHE_PI_API_KEY is required")?; let inbound_api_key = std::env::var("NOISEBELL_CACHE_INBOUND_API_KEY") .context("NOISEBELL_CACHE_INBOUND_API_KEY is required")?; let data_dir = std::env::var("NOISEBELL_CACHE_DATA_DIR").unwrap_or_else(|_| "/var/lib/noisebell-cache".into()); let status_poll_interval_secs: u64 = std::env::var("NOISEBELL_CACHE_STATUS_POLL_INTERVAL_SECS") .unwrap_or_else(|_| "60".into()) .parse() .context("NOISEBELL_CACHE_STATUS_POLL_INTERVAL_SECS must be a valid u64")?; let info_poll_interval_secs: u64 = std::env::var("NOISEBELL_CACHE_INFO_POLL_INTERVAL_SECS") .unwrap_or_else(|_| "300".into()) .parse() .context("NOISEBELL_CACHE_INFO_POLL_INTERVAL_SECS must be a valid u64")?; let offline_threshold: u32 = std::env::var("NOISEBELL_CACHE_OFFLINE_THRESHOLD") .unwrap_or_else(|_| "3".into()) .parse() .context("NOISEBELL_CACHE_OFFLINE_THRESHOLD must be a valid u32")?; let retry_attempts: u32 = std::env::var("NOISEBELL_CACHE_RETRY_ATTEMPTS") .unwrap_or_else(|_| "3".into()) .parse() .context("NOISEBELL_CACHE_RETRY_ATTEMPTS must be a valid u32")?; let retry_base_delay_secs: u64 = std::env::var("NOISEBELL_CACHE_RETRY_BASE_DELAY_SECS") .unwrap_or_else(|_| "1".into()) .parse() .context("NOISEBELL_CACHE_RETRY_BASE_DELAY_SECS must be a valid u64")?; let http_timeout_secs: u64 = std::env::var("NOISEBELL_CACHE_HTTP_TIMEOUT_SECS") .unwrap_or_else(|_| "10".into()) .parse() .context("NOISEBELL_CACHE_HTTP_TIMEOUT_SECS must be a valid u64")?; // Parse outbound webhooks from NOISEBELL_CACHE_WEBHOOK__URL and _SECRET env vars let mut webhooks = Vec::new(); for i in 0.. { let url_key = format!("NOISEBELL_CACHE_WEBHOOK_{i}_URL"); match std::env::var(&url_key) { Ok(url) => { let secret_key = format!("NOISEBELL_CACHE_WEBHOOK_{i}_SECRET"); let secret = std::env::var(&secret_key).ok(); webhooks.push(WebhookTarget { url, secret }); } Err(_) => break, } } info!( port, %pi_address, webhook_count = webhooks.len(), "starting noisebell-cache" ); let db_path = format!("{data_dir}/noisebell.db"); let conn = db::init(&db_path)?; let db = Arc::new(Mutex::new(conn)); let client = reqwest::Client::builder() .timeout(Duration::from_secs(http_timeout_secs)) .build() .context("failed to build HTTP client")?; let poller_config = Arc::new(poller::PollerConfig { pi_address, pi_api_key, status_poll_interval: Duration::from_secs(status_poll_interval_secs), info_poll_interval: Duration::from_secs(info_poll_interval_secs), offline_threshold, retry_attempts, retry_base_delay_secs, http_timeout_secs, webhooks: webhooks.clone(), }); poller::spawn_status_poller(poller_config.clone(), db.clone(), client.clone()); poller::spawn_info_poller(poller_config, db.clone(), client.clone()); let app_state = Arc::new(api::AppState { db, client, inbound_api_key, webhooks, retry_attempts, retry_base_delay_secs, }); let app = Router::new() .route("/webhook", post(api::post_webhook)) .route("/status", get(api::get_status)) .route("/info", get(api::get_info)) .route("/history", get(api::get_history)) .with_state(app_state); let listener = tokio::net::TcpListener::bind(("0.0.0.0", port)) .await .context(format!("failed to bind to 0.0.0.0:{port}"))?; info!(port, "listening"); let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) .context("failed to register SIGTERM handler")?; axum::serve(listener, app) .with_graceful_shutdown(async move { sigterm.recv().await; }) .await .context("server error")?; info!("shutdown complete"); Ok(()) }