Format with rustfmt
All checks were successful
ci / check (push) Successful in 44s

This commit is contained in:
Jean Chevronnet 2026-07-29 19:17:16 +00:00
parent facbde55bd
commit 7846f91afe
No known key found for this signature in database
GPG key ID: 439666D63A9477E4
11 changed files with 209 additions and 54 deletions

View file

@ -14,8 +14,16 @@ use crate::bot::module::{Action, Command, CommandSpec, Module};
const CACHE_TTL: Duration = Duration::from_secs(600);
const COMMANDS: &[CommandSpec] = &[
CommandSpec { name: "add", usage: "add <location>", about: "save your weather location" },
CommandSpec { name: "w", usage: "w [location]", about: "weather for your saved location" },
CommandSpec {
name: "add",
usage: "add <location>",
about: "save your weather location",
},
CommandSpec {
name: "w",
usage: "w [location]",
about: "weather for your saved location",
},
];
pub struct Weather {
@ -35,7 +43,11 @@ impl Weather {
}
pub fn with_path(path: PathBuf) -> Weather {
Weather { store: Store::load(&path), path, cache: HashMap::new() }
Weather {
store: Store::load(&path),
path,
cache: HashMap::new(),
}
}
pub fn location_of(&self, nick: &str) -> Option<&str> {
@ -56,15 +68,27 @@ impl Module for Weather {
match cmd.name {
"add" => {
if cmd.args.is_empty() {
return vec![Action::reply(format!("usage: {}add <location>", cmd.prefix))];
return vec![Action::reply(format!(
"usage: {}add <location>",
cmd.prefix
))];
}
let loc = cmd.args.join(" ");
self.store.set(cmd.sender, &loc);
if let Err(e) = self.store.save(&self.path) {
crate::log(&format!("weather: could not save {}: {e}", self.path.display()));
return vec![Action::reply(format!("{}: could not save location", cmd.sender))];
crate::log(&format!(
"weather: could not save {}: {e}",
self.path.display()
));
return vec![Action::reply(format!(
"{}: could not save location",
cmd.sender
))];
}
vec![Action::reply(format!("{}: saved {loc} — try {}w", cmd.sender, cmd.prefix))]
vec![Action::reply(format!(
"{}: saved {loc} — try {}w",
cmd.sender, cmd.prefix
))]
}
"w" => {
let loc = if !cmd.args.is_empty() {
@ -104,7 +128,9 @@ impl Store {
fn load(path: &Path) -> Store {
match fs::read_to_string(path) {
Ok(s) => Store { map: parse_db(&s) },
Err(_) => Store { map: BTreeMap::new() },
Err(_) => Store {
map: BTreeMap::new(),
},
}
}
@ -130,7 +156,11 @@ fn fetch_weather(location: &str) -> Result<String, String> {
}
match https_get("wttr.in", &path) {
Ok(body) => {
let raw = body.lines().map(str::trim).find(|l| !l.is_empty()).unwrap_or("");
let raw = body
.lines()
.map(str::trim)
.find(|l| !l.is_empty())
.unwrap_or("");
if !raw.is_empty() {
return Ok(format_weather(raw, location));
}
@ -174,32 +204,52 @@ struct HttpErr {
fn https_get(host: &str, path: &str) -> Result<String, HttpErr> {
let addr = (host, 443)
.to_socket_addrs()
.map_err(|e| HttpErr { msg: e.to_string(), retry: false })?
.map_err(|e| HttpErr {
msg: e.to_string(),
retry: false,
})?
.next()
.ok_or(HttpErr { msg: "dns lookup failed".to_string(), retry: false })?;
let stream = TcpStream::connect_timeout(&addr, Duration::from_secs(4))
.map_err(|e| HttpErr { retry: retryable_io(&e), msg: e.to_string() })?;
.ok_or(HttpErr {
msg: "dns lookup failed".to_string(),
retry: false,
})?;
let stream =
TcpStream::connect_timeout(&addr, Duration::from_secs(4)).map_err(|e| HttpErr {
retry: retryable_io(&e),
msg: e.to_string(),
})?;
stream.set_read_timeout(Some(Duration::from_secs(5))).ok();
stream.set_write_timeout(Some(Duration::from_secs(5))).ok();
let connector = TlsConnector::new().map_err(|e| HttpErr { msg: e.to_string(), retry: false })?;
let mut tls = connector
.connect(host, stream)
.map_err(|e| HttpErr { msg: e.to_string(), retry: true })?;
let connector = TlsConnector::new().map_err(|e| HttpErr {
msg: e.to_string(),
retry: false,
})?;
let mut tls = connector.connect(host, stream).map_err(|e| HttpErr {
msg: e.to_string(),
retry: true,
})?;
let req = format!(
"GET {path} HTTP/1.0\r\nHost: {host}\r\nUser-Agent: curl/rubot\r\nAccept: text/plain\r\nConnection: close\r\n\r\n"
);
tls.write_all(req.as_bytes())
.map_err(|e| HttpErr { retry: retryable_io(&e), msg: e.to_string() })?;
tls.write_all(req.as_bytes()).map_err(|e| HttpErr {
retry: retryable_io(&e),
msg: e.to_string(),
})?;
let mut buf = Vec::new();
tls.read_to_end(&mut buf)
.map_err(|e| HttpErr { retry: retryable_io(&e), msg: e.to_string() })?;
tls.read_to_end(&mut buf).map_err(|e| HttpErr {
retry: retryable_io(&e),
msg: e.to_string(),
})?;
let text = String::from_utf8_lossy(&buf);
let Some((head, body)) = text.split_once("\r\n\r\n") else {
return Err(HttpErr { msg: "malformed response".to_string(), retry: true });
return Err(HttpErr {
msg: "malformed response".to_string(),
retry: true,
});
};
let code = head
.lines()
@ -210,13 +260,18 @@ fn https_get(host: &str, path: &str) -> Result<String, HttpErr> {
.and_then(|c| c.parse::<u16>().ok())
.unwrap_or(0);
if !(200..300).contains(&code) {
return Err(HttpErr { retry: code == 429 || code >= 500, msg: format!("http {code}") });
return Err(HttpErr {
retry: code == 429 || code >= 500,
msg: format!("http {code}"),
});
}
Ok(body.to_string())
}
fn retryable_io(e: &io::Error) -> bool {
use io::ErrorKind::{BrokenPipe, ConnectionAborted, ConnectionRefused, ConnectionReset, UnexpectedEof};
use io::ErrorKind::{
BrokenPipe, ConnectionAborted, ConnectionRefused, ConnectionReset, UnexpectedEof,
};
matches!(
e.kind(),
ConnectionReset | ConnectionAborted | ConnectionRefused | BrokenPipe | UnexpectedEof
@ -227,7 +282,9 @@ fn url_encode(s: &str) -> String {
let mut out = String::new();
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => out.push(b as char),
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' => {
out.push(b as char)
}
_ => out.push_str(&format!("%{b:02X}")),
}
}
@ -259,13 +316,17 @@ pub fn parse_db(s: &str) -> BTreeMap<String, String> {
}
_ => break,
}
let Some(key) = parse_string(&mut chars) else { break };
let Some(key) = parse_string(&mut chars) else {
break;
};
skip_ws(&mut chars);
if chars.next() != Some(':') {
break;
}
skip_ws(&mut chars);
let Some(value) = parse_string(&mut chars) else { break };
let Some(value) = parse_string(&mut chars) else {
break;
};
map.insert(key, value);
}
map