From 6a9bbf0d82e23e858ee7795b4f3998d60de1db88 Mon Sep 17 00:00:00 2001 From: cottongin Date: Tue, 10 Mar 2026 21:56:48 -0400 Subject: [PATCH] feat: add IRC task with auto-reconnect and backoff Made-with: Cursor --- src/irc_task.rs | 99 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 1 + 2 files changed, 100 insertions(+) create mode 100644 src/irc_task.rs diff --git a/src/irc_task.rs b/src/irc_task.rs new file mode 100644 index 0000000..0821507 --- /dev/null +++ b/src/irc_task.rs @@ -0,0 +1,99 @@ +use std::time::Duration; + +use futures_util::StreamExt; +use irc::client::prelude::*; +use tokio::sync::mpsc; +use tracing::{error, info, warn}; + +use crate::config::IrcConfig; +use crate::events::{BridgeEvent, Source}; + +pub async fn run_irc_task( + config: IrcConfig, + event_tx: mpsc::Sender, + mut outbound_rx: mpsc::Receiver, + mut shutdown: tokio::sync::watch::Receiver, +) { + let mut backoff = Duration::from_secs(1); + let max_backoff = Duration::from_secs(60); + + loop { + info!(server = %config.server, channel = %config.channel, "Connecting to IRC"); + + match connect_and_run(&config, &event_tx, &mut outbound_rx, &mut shutdown).await { + Ok(()) => { + info!("IRC task exiting cleanly"); + return; + } + Err(e) => { + error!(error = %e, "IRC connection error"); + info!(backoff_secs = backoff.as_secs(), "Reconnecting after backoff"); + + tokio::select! { + _ = tokio::time::sleep(backoff) => {}, + _ = shutdown.changed() => return, + } + + backoff = (backoff * 2).min(max_backoff); + } + } + } +} + +async fn connect_and_run( + config: &IrcConfig, + event_tx: &mpsc::Sender, + outbound_rx: &mut mpsc::Receiver, + shutdown: &mut tokio::sync::watch::Receiver, +) -> anyhow::Result<()> { + let irc_config = Config { + nickname: Some(config.nick.clone()), + server: Some(config.server.clone()), + port: Some(config.port), + use_tls: Some(config.tls), + channels: vec![config.channel.clone()], + ..Config::default() + }; + + let mut client = Client::from_config(irc_config).await?; + client.identify()?; + + let mut stream = client.stream()?; + let sender = client.sender(); + + info!("IRC connected, waiting for messages"); + + loop { + tokio::select! { + msg = stream.next() => { + match msg { + Some(Ok(message)) => { + if let Command::PRIVMSG(ref target, ref text) = message.command { + if target == &config.channel { + let nick = message.source_nickname().unwrap_or("unknown").to_string(); + let event = BridgeEvent::ChatMessage { + source: Source::Irc, + username: nick, + body: text.clone(), + id: None, + }; + if event_tx.send(event).await.is_err() { + return Ok(()); + } + } + } + } + Some(Err(e)) => return Err(e.into()), + None => return Err(anyhow::anyhow!("IRC stream ended")), + } + } + Some(text) = outbound_rx.recv() => { + sender.send_privmsg(&config.channel, &text)?; + } + _ = shutdown.changed() => { + let _ = sender.send_quit("Bridge shutting down"); + return Ok(()); + } + } + } +} diff --git a/src/main.rs b/src/main.rs index f8a999c..8c3ed5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod config; mod events; mod health; mod html; +mod irc_task; mod owncast_api; mod webhook;