An ATProtocol powered blogging engine.
1use serde::{Deserialize, Serialize};
2use std::time::Duration;
3
4use crate::errors::{BlahgError, Result};
5
6/// Application configuration loaded from environment variables.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Config {
9 /// HTTP server configuration
10 pub http: HttpConfig,
11 /// Base URL for external links and content
12 pub external_base: String,
13 /// Certificate bundles for TLS verification
14 pub certificate_bundles: Vec<String>,
15 /// User agent string for HTTP requests
16 pub user_agent: String,
17 /// Hostname for PLC directory lookups
18 pub plc_hostname: String,
19 /// DNS nameservers for resolution
20 pub dns_nameservers: Vec<std::net::IpAddr>,
21 /// HTTP client timeout duration
22 pub http_client_timeout: Duration,
23 /// Author name for the blog
24 pub author: String,
25 /// Storage path for attachments
26 pub attachment_storage: String,
27 /// Database connection URL
28 pub database_url: String,
29 /// Path to jetstream cursor file
30 pub jetstream_cursor_path: Option<String>,
31 /// Whether to enable jetstream event consumption
32 pub enable_jetstream: bool,
33 /// Syntect theme for syntax highlighting
34 pub syntect_theme: String,
35}
36
37/// HTTP server configuration options.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct HttpConfig {
40 /// Port to bind the HTTP server to
41 pub port: u16,
42 /// Path to static assets
43 pub static_path: String,
44 /// Path to template files
45 pub templates_path: String,
46}
47
48impl Default for Config {
49 fn default() -> Self {
50 Self {
51 http: HttpConfig::default(),
52 external_base: "http://localhost:8080".to_string(),
53 certificate_bundles: Vec::new(),
54 user_agent: format!(
55 "Blahg/{} (+https://tangled.sh/@smokesignal.events/blahg)",
56 env!("CARGO_PKG_VERSION")
57 ),
58 plc_hostname: "plc.directory".to_string(),
59 dns_nameservers: Vec::new(),
60 http_client_timeout: Duration::from_secs(10),
61 author: String::new(),
62 attachment_storage: "./attachments".to_string(),
63 database_url: "sqlite://blahg.db".to_string(),
64 jetstream_cursor_path: None,
65 enable_jetstream: true,
66 syntect_theme: std::env::var("SYNTECT_THEME").unwrap_or_else(|_| "base16-ocean.dark".to_string()),
67 }
68 }
69}
70
71impl Default for HttpConfig {
72 fn default() -> Self {
73 Self {
74 port: 8080,
75 static_path: format!("{}/static", env!("CARGO_MANIFEST_DIR")),
76 templates_path: format!("{}/templates", env!("CARGO_MANIFEST_DIR")),
77 }
78 }
79}
80
81impl Config {
82 /// Create configuration from environment variables.
83 pub fn from_env() -> Result<Self> {
84 let mut config = Self::default();
85
86 if let Ok(port) = std::env::var("HTTP_PORT") {
87 config.http.port = port
88 .parse()
89 .map_err(|_| BlahgError::ConfigHttpPortInvalid { port: port.clone() })?;
90 }
91
92 if let Ok(static_path) = std::env::var("HTTP_STATIC_PATH") {
93 config.http.static_path = static_path;
94 }
95
96 if let Ok(templates_path) = std::env::var("HTTP_TEMPLATES_PATH") {
97 config.http.templates_path = templates_path;
98 }
99
100 if let Ok(external_base) = std::env::var("EXTERNAL_BASE") {
101 config.external_base = external_base;
102 }
103
104 if let Ok(cert_bundles) = std::env::var("CERTIFICATE_BUNDLES") {
105 config.certificate_bundles = cert_bundles
106 .split(';')
107 .map(|s| s.trim().to_string())
108 .filter(|s| !s.is_empty())
109 .collect();
110 }
111
112 if let Ok(user_agent) = std::env::var("USER_AGENT") {
113 config.user_agent = user_agent;
114 }
115
116 if let Ok(plc_hostname) = std::env::var("PLC_HOSTNAME") {
117 config.plc_hostname = plc_hostname;
118 }
119
120 if let Ok(dns_nameservers) = std::env::var("DNS_NAMESERVERS") {
121 config.dns_nameservers = dns_nameservers
122 .split(',')
123 .map(|s| s.trim())
124 .filter(|s| !s.is_empty())
125 .map(|s| {
126 s.parse::<std::net::IpAddr>()
127 .map_err(|e| BlahgError::NameserverParsingFailed(s.to_string(), e))
128 })
129 .collect::<Result<Vec<std::net::IpAddr>>>()?;
130 }
131
132 if let Ok(timeout) = std::env::var("HTTP_CLIENT_TIMEOUT") {
133 config.http_client_timeout = duration_str::parse(&timeout).map_err(|e| {
134 BlahgError::ConfigHttpTimeoutInvalid {
135 details: e.to_string(),
136 }
137 })?;
138 }
139
140 if let Ok(author) = std::env::var("AUTHOR") {
141 config.author = author;
142 }
143
144 if let Ok(attachment_storage) = std::env::var("ATTACHMENT_STORAGE") {
145 config.attachment_storage = attachment_storage;
146 }
147
148 if let Ok(database_url) = std::env::var("DATABASE_URL") {
149 config.database_url = database_url;
150 }
151
152 if let Ok(jetstream_cursor_path) = std::env::var("JETSTREAM_CURSOR_PATH") {
153 config.jetstream_cursor_path = Some(jetstream_cursor_path);
154 }
155
156 if let Ok(enable_jetstream) = std::env::var("ENABLE_JETSTREAM") {
157 config.enable_jetstream = enable_jetstream.parse().unwrap_or(true);
158 }
159
160 if let Ok(syntect_theme) = std::env::var("SYNTECT_THEME") {
161 config.syntect_theme = syntect_theme;
162 }
163
164 Ok(config)
165 }
166}