2026-03-10 21:51:35 -04:00
|
|
|
mod config;
|
2026-03-10 21:58:38 -04:00
|
|
|
mod control;
|
2026-03-10 21:52:17 -04:00
|
|
|
mod events;
|
2026-03-10 21:55:13 -04:00
|
|
|
mod health;
|
2026-03-10 21:53:26 -04:00
|
|
|
mod html;
|
2026-03-10 21:56:48 -04:00
|
|
|
mod irc_task;
|
2026-03-10 21:54:25 -04:00
|
|
|
mod owncast_api;
|
2026-03-10 21:59:54 -04:00
|
|
|
mod router;
|
2026-03-10 21:56:12 -04:00
|
|
|
mod webhook;
|
2026-03-10 21:57:35 -04:00
|
|
|
mod websocket;
|
2026-03-10 21:51:35 -04:00
|
|
|
|
2026-03-10 22:00:54 -04:00
|
|
|
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(())
|
2026-03-10 21:49:42 -04:00
|
|
|
}
|