diff --git a/src/bin/poc-daemon/modules/irc.rs b/src/bin/poc-daemon/modules/irc.rs index a93030c..7d7b727 100644 --- a/src/bin/poc-daemon/modules/irc.rs +++ b/src/bin/poc-daemon/modules/irc.rs @@ -23,6 +23,8 @@ use tracing::{error, info, warn}; const MAX_LOG_LINES: usize = 200; const RECONNECT_BASE_SECS: u64 = 5; const RECONNECT_MAX_SECS: u64 = 300; +const PING_INTERVAL_SECS: u64 = 120; +const PING_TIMEOUT_SECS: u64 = 30; /// Parsed IRC message. struct IrcMessage { @@ -208,8 +210,14 @@ async fn connection_loop( } } + // Reset backoff if we had a working connection (registered + // successfully before disconnecting) + let was_connected = state.borrow().connected; state.borrow_mut().connected = false; state.borrow_mut().writer = None; + if was_connected { + backoff = RECONNECT_BASE_SECS; + } // Persist current channel list to config { @@ -274,11 +282,39 @@ async fn register_and_read( } let mut buf = Vec::new(); + let mut ping_sent = false; + let mut deadline = tokio::time::Instant::now() + + std::time::Duration::from_secs(PING_INTERVAL_SECS); loop { buf.clear(); - let n = reader.read_until(b'\n', &mut buf).await?; + + let read_result = tokio::select! { + result = reader.read_until(b'\n', &mut buf) => result, + _ = tokio::time::sleep_until(deadline) => { + if ping_sent { + return Err(io::Error::new( + io::ErrorKind::TimedOut, + "ping timeout — no response from server", + )); + } + info!("irc: no data for {}s, sending PING", PING_INTERVAL_SECS); + state.borrow_mut().send_raw("PING :keepalive").await?; + ping_sent = true; + deadline = tokio::time::Instant::now() + + std::time::Duration::from_secs(PING_TIMEOUT_SECS); + continue; + } + }; + + let n = read_result?; if n == 0 { break; } + + // Any data from server resets the ping timer + ping_sent = false; + deadline = tokio::time::Instant::now() + + std::time::Duration::from_secs(PING_INTERVAL_SECS); + // IRC is not guaranteed UTF-8 — lossy conversion handles Latin-1 etc. let line = String::from_utf8_lossy(&buf).trim_end().to_string(); if line.is_empty() { continue; } @@ -471,6 +507,15 @@ pub async fn handle_command( return Err("usage: irc send ".into()); } let target = &args[0]; + if target.starts_with('#') { + let s = state.borrow(); + if !s.channels.iter().any(|c| c == target) { + return Err(format!( + "not in channel {target} (joined: {})", + s.channels.join(", ") + )); + } + } let msg = args[1..].join(" "); let nick = state.borrow().config.nick.clone(); state