Files
owncast-IRC-bridge/src/main.rs
cottongin 65471fb9fc feat: strip IRC formatting codes from messages sent to Owncast
Add irc_format module that removes mIRC control codes (bold, color,
italic, underline, reverse, strikethrough, monospace, reset) before
forwarding to Owncast. Color codes with fg/bg digit params are
consumed correctly. Multi-byte UTF-8 (emoji, accented chars, CJK)
is preserved.

Made-with: Cursor
2026-03-12 14:07:01 -04:00

142 lines
4.4 KiB
Rust

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(())
}