Harden bot: line cap, send throttle, panic isolation, verbose flag, weather cache, tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jean Chevronnet 2026-07-29 18:46:10 +00:00
parent d9e279d8ed
commit 0094465ca9
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
7 changed files with 165 additions and 10 deletions

View file

@ -1,20 +1,27 @@
use std::io::{self, BufRead, BufReader, Read, Write};
use std::net::TcpStream;
use std::time::{Duration, Instant};
use native_tls::TlsConnector;
use super::Message;
const MAX_LINE: usize = 500;
const SEND_THROTTLE: Duration = Duration::from_millis(300);
trait Stream: Read + Write {}
impl<T: Read + Write> Stream for T {}
pub struct Connection {
inner: BufReader<Box<dyn Stream>>,
log_prefix: String,
verbose: bool,
throttle: Duration,
last_send: Option<Instant>,
}
impl Connection {
pub fn connect(host: &str, port: u16, use_tls: bool) -> io::Result<Connection> {
pub fn connect(host: &str, port: u16, use_tls: bool, verbose: bool) -> io::Result<Connection> {
let tcp = TcpStream::connect((host, port))?;
let stream: Box<dyn Stream> = if use_tls {
let connector =
@ -26,7 +33,24 @@ impl Connection {
} else {
Box::new(tcp)
};
Ok(Connection { inner: BufReader::new(stream), log_prefix: String::new() })
Ok(Connection {
inner: BufReader::new(stream),
log_prefix: String::new(),
verbose,
throttle: SEND_THROTTLE,
last_send: None,
})
}
pub fn from_stream<S: Read + Write + 'static>(stream: S) -> Connection {
let boxed: Box<dyn Stream> = Box::new(stream);
Connection {
inner: BufReader::new(boxed),
log_prefix: String::new(),
verbose: false,
throttle: Duration::ZERO,
last_send: None,
}
}
pub fn set_label(&mut self, label: &str) {
@ -47,7 +71,9 @@ impl Connection {
if trimmed.is_empty() {
continue;
}
eprintln!("{}<< {trimmed}", self.log_prefix);
if self.verbose {
eprintln!("{}<< {trimmed}", self.log_prefix);
}
if let Some(msg) = Message::parse(trimmed) {
return Ok(Some(msg));
}
@ -55,8 +81,20 @@ impl Connection {
}
pub fn send_raw(&mut self, line: &str) -> io::Result<()> {
if !self.throttle.is_zero() {
if let Some(last) = self.last_send {
let elapsed = last.elapsed();
if elapsed < self.throttle {
std::thread::sleep(self.throttle - elapsed);
}
}
self.last_send = Some(Instant::now());
}
let clean = line.replace(['\r', '\n'], " ");
eprintln!("{}>> {clean}", self.log_prefix);
let clean = truncate_to(&clean, MAX_LINE);
if self.verbose {
eprintln!("{}>> {clean}", self.log_prefix);
}
let writer = self.inner.get_mut();
write!(writer, "{clean}\r\n")?;
writer.flush()
@ -110,3 +148,14 @@ impl Connection {
self.send_raw(&format!("AUTHENTICATE {payload}"))
}
}
fn truncate_to(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
let mut end = max;
while end > 0 && !s.is_char_boundary(end) {
end -= 1;
}
&s[..end]
}