This commit is contained in:
parent
facbde55bd
commit
7846f91afe
11 changed files with 209 additions and 54 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue