diff --git a/src/config.rs b/src/config.rs index 788b5b0..5649f18 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,7 +12,7 @@ pub struct BridgeConfig { pub control: ControlConfig, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] pub struct IrcConfig { pub server: String, #[serde(default = "default_irc_port")] diff --git a/src/main.rs b/src/main.rs index 260c885..fa16771 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,128 @@ mod router; mod webhook; mod websocket; -fn main() { - println!("owncast-irc-bridge"); +use std::path::PathBuf; +use std::time::Instant; + +use clap::Parser; +use tokio::sync::{mpsc, watch}; +use tracing::info; + +#[derive(Parser)] +#[command(name = "owncast-irc-bridge")] +#[command(about = "Bidirectional chat bridge between Owncast and IRC")] +struct Cli { + /// Path to config file + #[arg(short, long, default_value = "config.toml")] + config: PathBuf, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let cli = Cli::parse(); + let config = config::BridgeConfig::load(&cli.config)?; + let access_token = config.owncast_access_token()?; + + info!("Starting owncast-irc-bridge"); + + let start_time = Instant::now(); + let (shutdown_tx, shutdown_rx) = watch::channel(false); + + let (event_tx, event_rx) = mpsc::channel(256); + let (irc_outbound_tx, irc_outbound_rx) = mpsc::channel(256); + let (state_tx, state_rx) = mpsc::channel(32); + let (control_tx, control_rx) = mpsc::channel(32); + + let api_client = owncast_api::OwncastApiClient::new( + config.owncast.url.clone(), + access_token, + ); + + let irc_config = config.irc.clone(); + let irc_event_tx = event_tx.clone(); + let irc_shutdown = shutdown_rx.clone(); + let _irc_handle = tokio::spawn(async move { + irc_task::run_irc_task(irc_config, irc_event_tx, irc_outbound_rx, irc_shutdown).await; + }); + + let webhook_port = config.owncast.webhook_port; + let webhook_event_tx = event_tx.clone(); + let _webhook_handle = tokio::spawn(async move { + if let Err(e) = webhook::run_webhook_server(webhook_port, webhook_event_tx).await { + tracing::error!(error = %e, "Webhook server failed"); + } + }); + + let _ws_handle = if config.owncast.websocket_enabled { + let ws_url = config.owncast.url.clone(); + let ws_event_tx = event_tx.clone(); + let ws_shutdown = shutdown_rx.clone(); + Some(tokio::spawn(async move { + websocket::run_websocket_task(ws_url, ws_event_tx, ws_shutdown).await; + })) + } else { + None + }; + + let health_api = owncast_api::OwncastApiClient::new( + config.owncast.url.clone(), + String::new(), + ); + let health_interval = std::time::Duration::from_secs(config.owncast.health_poll_interval_secs); + let health_shutdown = shutdown_rx.clone(); + let _health_handle = tokio::spawn(async move { + health::run_health_poller(&health_api, state_tx, health_interval, health_shutdown).await; + }); + + let control_socket_path = config.control.socket_path.clone(); + let _control_handle = tokio::spawn(async move { + if let Err(e) = control::run_control_socket(&control_socket_path, control_tx).await { + tracing::error!(error = %e, "Control socket failed"); + } + }); + + let sig_shutdown_tx = shutdown_tx.clone(); + tokio::spawn(async move { + let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("Failed to register SIGTERM handler"); + let mut sighup = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::hangup()) + .expect("Failed to register SIGHUP handler"); + + tokio::select! { + _ = tokio::signal::ctrl_c() => { + info!("SIGINT received, shutting down"); + let _ = sig_shutdown_tx.send(true); + } + _ = sigterm.recv() => { + info!("SIGTERM received, shutting down"); + let _ = sig_shutdown_tx.send(true); + } + _ = sighup.recv() => { + info!("SIGHUP received (reconnect not yet wired)"); + } + } + }); + + router::run_router( + config.bridge, + config.owncast.url, + api_client, + event_rx, + irc_outbound_tx, + state_rx, + control_rx, + shutdown_tx, + start_time, + ) + .await; + + info!("Bridge shutting down"); + Ok(()) }