From bc11ead8d8c84cb3cdd4737aa9652a30f709b8bc Mon Sep 17 00:00:00 2001 From: cottongin Date: Tue, 10 Mar 2026 21:51:35 -0400 Subject: [PATCH] feat: add config module with TOML parsing and env var support Made-with: Cursor --- src/config.rs | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 + 2 files changed, 188 insertions(+) create mode 100644 src/config.rs diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..788b5b0 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,186 @@ +use std::path::Path; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct BridgeConfig { + pub irc: IrcConfig, + pub owncast: OwncastConfig, + #[serde(default)] + pub bridge: BridgeSettings, + #[serde(default)] + pub control: ControlConfig, +} + +#[derive(Debug, Deserialize)] +pub struct IrcConfig { + pub server: String, + #[serde(default = "default_irc_port")] + pub port: u16, + #[serde(default)] + pub tls: bool, + #[serde(default = "default_nick")] + pub nick: String, + pub channel: String, +} + +#[derive(Debug, Deserialize)] +pub struct OwncastConfig { + pub url: String, + #[serde(default = "default_webhook_port")] + pub webhook_port: u16, + #[serde(default)] + pub websocket_enabled: bool, + #[serde(default = "default_health_poll_interval")] + pub health_poll_interval_secs: u64, +} + +#[derive(Debug, Deserialize)] +pub struct BridgeSettings { + #[serde(default = "default_irc_prefix")] + pub irc_prefix: String, + #[serde(default = "default_owncast_prefix")] + pub owncast_prefix: String, + #[serde(default)] + pub message_buffer_size: usize, +} + +#[derive(Debug, Deserialize)] +pub struct ControlConfig { + #[serde(default = "default_socket_path")] + pub socket_path: String, +} + +impl BridgeConfig { + pub fn load(path: &Path) -> anyhow::Result { + let contents = std::fs::read_to_string(path)?; + let config: BridgeConfig = toml::from_str(&contents)?; + Ok(config) + } + + pub fn owncast_access_token(&self) -> anyhow::Result { + std::env::var("OWNCAST_ACCESS_TOKEN") + .map_err(|_| anyhow::anyhow!("OWNCAST_ACCESS_TOKEN env var not set")) + } +} + +fn default_irc_port() -> u16 { 6667 } +fn default_nick() -> String { "owncast-bridge".to_string() } +fn default_webhook_port() -> u16 { 9078 } +fn default_health_poll_interval() -> u64 { 30 } +fn default_irc_prefix() -> String { "[IRC]".to_string() } +fn default_owncast_prefix() -> String { "[OC]".to_string() } +fn default_socket_path() -> String { "/tmp/owncast-irc-bridge.sock".to_string() } + +impl Default for BridgeSettings { + fn default() -> Self { + Self { + irc_prefix: default_irc_prefix(), + owncast_prefix: default_owncast_prefix(), + message_buffer_size: 0, + } + } +} + +impl Default for ControlConfig { + fn default() -> Self { + Self { socket_path: default_socket_path() } + } +} + +#[cfg(test)] +impl BridgeConfig { + pub fn default_for_test() -> Self { + Self { + irc: IrcConfig { + server: "localhost".to_string(), + port: 6667, + tls: false, + nick: "test-bot".to_string(), + channel: "#test".to_string(), + }, + owncast: OwncastConfig { + url: "http://localhost:8080".to_string(), + webhook_port: 9078, + websocket_enabled: false, + health_poll_interval_secs: 30, + }, + bridge: BridgeSettings::default(), + control: ControlConfig::default(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_minimal_config() { + let toml_str = r##" +[irc] +server = "irc.example.com" +channel = "#test" + +[owncast] +url = "https://owncast.example.com" +"##; + let config: BridgeConfig = toml::from_str(toml_str).unwrap(); + assert_eq!(config.irc.server, "irc.example.com"); + assert_eq!(config.irc.port, 6667); + assert_eq!(config.irc.tls, false); + assert_eq!(config.irc.nick, "owncast-bridge"); + assert_eq!(config.irc.channel, "#test"); + assert_eq!(config.owncast.url, "https://owncast.example.com"); + assert_eq!(config.owncast.webhook_port, 9078); + assert_eq!(config.owncast.websocket_enabled, false); + assert_eq!(config.owncast.health_poll_interval_secs, 30); + assert_eq!(config.bridge.irc_prefix, "[IRC]"); + assert_eq!(config.bridge.owncast_prefix, "[OC]"); + assert_eq!(config.bridge.message_buffer_size, 0); + assert_eq!(config.control.socket_path, "/tmp/owncast-irc-bridge.sock"); + } + + #[test] + fn test_parse_full_config() { + let toml_str = r##" +[irc] +server = "irc.example.com" +port = 6697 +tls = true +nick = "mybot" +channel = "#mychan" + +[owncast] +url = "https://oc.example.com" +webhook_port = 8888 +websocket_enabled = true +health_poll_interval_secs = 10 + +[bridge] +irc_prefix = "" +owncast_prefix = "" +message_buffer_size = 50 + +[control] +socket_path = "/var/run/bridge.sock" +"##; + let config: BridgeConfig = toml::from_str(toml_str).unwrap(); + assert_eq!(config.irc.port, 6697); + assert!(config.irc.tls); + assert_eq!(config.irc.nick, "mybot"); + assert_eq!(config.owncast.webhook_port, 8888); + assert!(config.owncast.websocket_enabled); + assert_eq!(config.bridge.message_buffer_size, 50); + assert_eq!(config.control.socket_path, "/var/run/bridge.sock"); + } + + #[test] + fn test_access_token_from_env() { + std::env::set_var("OWNCAST_ACCESS_TOKEN", "test-token-123"); + let config = BridgeConfig::default_for_test(); + let token = config.owncast_access_token(); + assert_eq!(token.unwrap(), "test-token-123"); + std::env::remove_var("OWNCAST_ACCESS_TOKEN"); + } +} diff --git a/src/main.rs b/src/main.rs index 75b6503..98a45b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +mod config; + fn main() { println!("owncast-irc-bridge"); }