//! HTTP client. //! //! dle.rae.es answers 403 to short or missing user agents, so requests to it //! identify as a desktop browser. use std::time::Duration; use anyhow::{Context, Result}; use ureq::Agent; /// Browser user agent, for dle.rae.es, which rejects clients that do not /// identify as one. pub const BROWSER_UA: &str = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 \ (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"; /// Honest identification for APIs that want to know who is calling, as the /// Wikimedia policy asks. pub const CLIENT_UA: &str = concat!( "drae-cli/", env!("CARGO_PKG_VERSION"), " (terminal dictionary)" ); pub struct Http { agent: Agent, } impl Http { pub fn new(timeout_secs: u64) -> Self { let config = Agent::config_builder() .timeout_global(Some(Duration::from_secs(timeout_secs))) // A 404 is an answer we want to read, not a network error. .http_status_as_error(false) .build(); Http { agent: config.new_agent(), } } /// Returns the status code and the body. pub fn get(&self, url: &str, user_agent: &str) -> Result<(u16, String)> { let mut res = self .agent .get(url) .header("User-Agent", user_agent) .header("Accept-Language", "es-ES,es;q=0.9") .header("Accept", "text/html,application/json;q=0.9,*/*;q=0.8") .call() .with_context(|| format!("no se pudo conectar con {url}"))?; let status = res.status().as_u16(); let body = res .body_mut() .read_to_string() .with_context(|| format!("respuesta ilegible de {url}"))?; Ok((status, body)) } } /// Encodes a word for a URL path. The RAE needs `%C3%B1` for ñ; other safe /// characters pass through unchanged. pub fn percent_encode(s: &str) -> String { let mut out = String::with_capacity(s.len()); for b in s.as_bytes() { match b { 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}")), } } out } #[cfg(test)] mod tests { use super::*; #[test] fn encodes_spanish_characters() { assert_eq!(percent_encode("año"), "a%C3%B1o"); assert_eq!(percent_encode("café"), "caf%C3%A9"); assert_eq!(percent_encode("perro"), "perro"); assert_eq!(percent_encode("dar de sí"), "dar%20de%20s%C3%AD"); } }