mod config; mod control; mod events; mod health; mod html; mod irc_format; mod irc_task; mod owncast_api; mod router; mod webhook; mod websocket; 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_api = owncast_api::OwncastApiClient::new( config.owncast.url.clone(), String::new(), ); let ws_display_name = config.owncast.ws_display_name.clone(); let ws_event_tx = event_tx.clone(); let ws_shutdown = shutdown_rx.clone(); Some(tokio::spawn(async move { websocket::run_websocket_task(ws_api, ws_display_name, 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(()) }