use crate::util::{formatting::truncate, web::shorten_url}; use anyhow::{bail, Context, Error, Result}; use futures::try_join; use irc::client::prelude::*; use macros::privmsg; use reqwest::{get, Url}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug)] struct WaResponse { queryresult: QueryResult, } #[derive(Serialize, Deserialize, Debug)] struct QueryResult { pods: Vec, } #[derive(Serialize, Deserialize, Debug)] struct Pod { title: String, id: String, primary: Option, subpods: Vec, } #[derive(Serialize, Deserialize, Debug)] struct SubPod { plaintext: String, } /// Reduces all 'pod' plaintexts to a single string. /// Same as gonzobot does it. fn to_single_string(wa_res: WaResponse) -> String { wa_res .queryresult .pods .iter() .filter(|it| it.id.to_lowercase() != "input" && it.primary.is_some()) .map(|pod| { let subpod_texts = pod .subpods .iter() .map(|subpod| subpod.plaintext.clone()) .collect::>() .join(", "); format!("{}: {}", &pod.title, subpod_texts) }) .collect::>() .join(" - ") } fn get_url(query_str: &str, api_key: Option<&str>, base_url: Option<&str>) -> String { let wa_url = "http://api.wolframalpha.com"; let api_url = format!( "{}/v2/query?input={}&appid={}&output=json", base_url.unwrap_or(wa_url), query_str, api_key.unwrap_or("XXX"), // Allow tests to run without a key ); api_url } async fn send_wa_req(url: &str) -> Result { let body = get(Url::parse(url).context("Failed to parse url")?) .await .context("Failed to make request")? .text() .await .context("failed to get request response text")?; Ok(body) } async fn handle_wa_req(url: &str) -> Result { let res_body = send_wa_req(url).await?; let parsed = serde_json::from_str(&res_body)?; Ok(parsed) } /// Sends a request to the Wolfram Alpha API, returns a plain text response. #[tracing::instrument] async fn wa_query( query_str: &str, api_key: Option<&str>, base_url: Option<&str>, ) -> Result { let user_url = format!("http://www.wolframalpha.com/input/?i={}", query_str); let user_url_shortened_fut = shorten_url(&user_url); let url = get_url(query_str, api_key, base_url); let wa_res_fut = handle_wa_req(&url); // Can't just (foo.await, bar.await), smh // https://rust-lang.github.io/async-book/06_multiple_futures/02_join.html let (wa_res, user_url_shortened) = try_join!(wa_res_fut, user_url_shortened_fut)?; Ok(format!( "{} - {}", truncate(&to_single_string(wa_res), 250), // Same length as in gonzobot &user_url_shortened, )) } pub async fn wa(bot: &crate::Bot, msg: Message) -> Result<()> { privmsg!(msg, { let input = text.chars().as_str().splitn(2, " ").collect::>(); if input.len() != 2 { bail!("Empty input for WA query"); } let content = input[1].trim(); bot.send_privmsg( msg.response_target() .context("failed to get response target")?, &wa_query(content, Some(&bot.config.settings.wa_api_key), None).await?, )?; }) } #[cfg(test)] mod tests { use super::wa_query; use anyhow::{Error, Result}; use mockito::{self, Matcher}; #[tokio::test] async fn test_query_result_json_parsing() -> Result<(), Error> { let body = include_str!("../../tests/resources/wolfram_alpha_api_response.json"); let _m = mockito::mock("GET", Matcher::Any) // Trimmed down version of a full WA response: .with_body(body) .create(); mockito::start(); let res = wa_query("5/10", None, Some(&mockito::server_url())).await?; let res_without_link = res.rsplitn(2, "-").collect::>()[1..].join(" "); assert_eq!( res_without_link.trim(), "Exact result: 1/2 - Decimal form: 0.5" ); Ok(()) } }