add calc answer
This commit is contained in:
parent
2b2771c132
commit
c656e65614
8
Cargo.lock
generated
8
Cargo.lock
generated
@ -299,6 +299,12 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fend-core"
|
||||
version = "1.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e470ea3be6ce980f4d7f6cc08a6084e7715f2b052eeb1f123f2d4d8fb1d35de1"
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@ -792,8 +798,10 @@ dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"eyre",
|
||||
"fend-core",
|
||||
"futures",
|
||||
"html-escape",
|
||||
"rand",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"scraper",
|
||||
|
@ -12,8 +12,10 @@ axum = { version = "0.7.2", features = ["http2"] }
|
||||
base64 = "0.21.5"
|
||||
bytes = "1.5.0"
|
||||
eyre = "0.6.11"
|
||||
fend-core = "1.3.3"
|
||||
futures = "0.3.29"
|
||||
html-escape = "0.2.13"
|
||||
rand = "0.8.5"
|
||||
regex = "1.10.2"
|
||||
reqwest = { version = "0.11.23", default-features = false, features = [
|
||||
"rustls-tls",
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod calc;
|
||||
pub mod ip;
|
||||
pub mod useragent;
|
||||
|
||||
|
74
src/engines/answer/calc.rs
Normal file
74
src/engines/answer/calc.rs
Normal file
@ -0,0 +1,74 @@
|
||||
use crate::engines::{EngineResponse, SearchQuery};
|
||||
|
||||
pub fn request(_client: &reqwest::Client, query: &SearchQuery) -> EngineResponse {
|
||||
let query = query.query.as_str();
|
||||
|
||||
let Some(result_html) = evaluate(query, true) else {
|
||||
return EngineResponse::new();
|
||||
};
|
||||
|
||||
EngineResponse::answer_html(format!(
|
||||
r#"<p class="answer-calc-query">{query} =</p>
|
||||
<h3><b>{result_html}</b></h3>"#,
|
||||
query = html_escape::encode_text(query),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn request_autocomplete(_client: &reqwest::Client, query: &str) -> Vec<String> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
if let Some(result) = evaluate(query, false) {
|
||||
results.push(format!("{query} = {result}"));
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
fn evaluate(query: &str, html: bool) -> Option<String> {
|
||||
// at least 3 characters and not one of the short constants
|
||||
if query.len() < 3 && !matches!(query.to_lowercase().as_str(), "pi" | "e" | "c") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut context = fend_core::Context::new();
|
||||
|
||||
// make lowercase f and c work
|
||||
context.define_custom_unit_v1("f", "f", "°F", &fend_core::CustomUnitAttribute::Alias);
|
||||
context.define_custom_unit_v1("c", "c", "°C", &fend_core::CustomUnitAttribute::Alias);
|
||||
// make random work
|
||||
context.set_random_u32_fn(|| rand::random::<u32>());
|
||||
if html {
|
||||
// this makes it generate slightly nicer outputs for some queries like 2d6
|
||||
context.set_output_mode_terminal();
|
||||
}
|
||||
|
||||
let Ok(result) = fend_core::evaluate(query, &mut context) else {
|
||||
return None;
|
||||
};
|
||||
let main_result = result.get_main_result();
|
||||
if main_result == query {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !html {
|
||||
return Some(main_result.to_string());
|
||||
}
|
||||
|
||||
let mut result_html = String::new();
|
||||
for span in result.get_main_result_spans() {
|
||||
let class = match span.kind() {
|
||||
fend_core::SpanKind::Number | fend_core::SpanKind::Boolean => "answer-calc-constant",
|
||||
_ => "",
|
||||
};
|
||||
if !class.is_empty() {
|
||||
result_html.push_str(&format!(
|
||||
r#"<span class="{class}">{text}</span>"#,
|
||||
text = html_escape::encode_text(span.string())
|
||||
));
|
||||
} else {
|
||||
result_html.push_str(&html_escape::encode_text(span.string()));
|
||||
}
|
||||
}
|
||||
|
||||
return Some(result_html);
|
||||
}
|
@ -9,5 +9,5 @@ pub fn request(_client: &reqwest::Client, query: &SearchQuery) -> EngineResponse
|
||||
|
||||
let ip = &query.ip;
|
||||
|
||||
EngineResponse::answer_html(format!("Your IP address is <b>{ip}</b>"))
|
||||
EngineResponse::answer_html(format!(r#"<h3><b>{ip}</b></h3>"#))
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ use crate::engines::{EngineResponse, SearchQuery};
|
||||
use super::regex;
|
||||
|
||||
pub fn request(_client: &reqwest::Client, query: &SearchQuery) -> EngineResponse {
|
||||
if !regex!("^what('s|s| is) my (user ?agent|ua)|ua|user ?agent$")
|
||||
if !regex!("^(what('s|s| is) my (user ?agent|ua)|ua|user ?agent)$")
|
||||
.is_match(&query.query.to_lowercase())
|
||||
{
|
||||
return EngineResponse::new();
|
||||
@ -12,7 +12,7 @@ pub fn request(_client: &reqwest::Client, query: &SearchQuery) -> EngineResponse
|
||||
let user_agent = query.request_headers.get("user-agent");
|
||||
|
||||
EngineResponse::answer_html(if let Some(user_agent) = user_agent {
|
||||
format!("Your user agent is <b>{user_agent}</b>")
|
||||
format!("<h3><b>{user_agent}</b></h3>")
|
||||
} else {
|
||||
format!("You don't have a user agent")
|
||||
})
|
||||
|
@ -8,8 +8,6 @@ use std::{
|
||||
use futures::future::join_all;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use self::search::{bing, brave, google};
|
||||
|
||||
pub mod answer;
|
||||
pub mod search;
|
||||
|
||||
@ -22,6 +20,7 @@ pub enum Engine {
|
||||
// answer
|
||||
Useragent,
|
||||
Ip,
|
||||
Calc,
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
@ -32,6 +31,7 @@ impl Engine {
|
||||
Engine::Brave,
|
||||
Engine::Useragent,
|
||||
Engine::Ip,
|
||||
Engine::Calc,
|
||||
]
|
||||
}
|
||||
|
||||
@ -42,6 +42,7 @@ impl Engine {
|
||||
Engine::Brave => "brave",
|
||||
Engine::Useragent => "useragent",
|
||||
Engine::Ip => "ip",
|
||||
Engine::Calc => "calc",
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,19 +57,20 @@ impl Engine {
|
||||
|
||||
pub fn request(&self, client: &reqwest::Client, query: &SearchQuery) -> RequestResponse {
|
||||
match self {
|
||||
Engine::Google => google::request(client, query).into(),
|
||||
Engine::Bing => bing::request(client, query).into(),
|
||||
Engine::Google => search::google::request(client, query).into(),
|
||||
Engine::Bing => search::bing::request(client, query).into(),
|
||||
Engine::Brave => search::brave::request(client, query).into(),
|
||||
Engine::Useragent => answer::useragent::request(client, query).into(),
|
||||
Engine::Ip => answer::ip::request(client, query).into(),
|
||||
Engine::Calc => answer::calc::request(client, query).into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_response(&self, body: &str) -> eyre::Result<EngineResponse> {
|
||||
match self {
|
||||
Engine::Google => google::parse_response(body),
|
||||
Engine::Bing => bing::parse_response(body),
|
||||
Engine::Brave => brave::parse_response(body),
|
||||
Engine::Google => search::google::parse_response(body),
|
||||
Engine::Bing => search::bing::parse_response(body),
|
||||
Engine::Brave => search::brave::parse_response(body),
|
||||
_ => eyre::bail!("engine {self:?} can't parse response"),
|
||||
}
|
||||
}
|
||||
@ -77,17 +79,18 @@ impl Engine {
|
||||
&self,
|
||||
client: &reqwest::Client,
|
||||
query: &str,
|
||||
) -> Option<reqwest::RequestBuilder> {
|
||||
) -> Option<RequestAutocompleteResponse> {
|
||||
match self {
|
||||
Engine::Google => Some(google::request_autocomplete(client, query)),
|
||||
Engine::Google => Some(search::google::request_autocomplete(client, query).into()),
|
||||
Engine::Calc => Some(answer::calc::request_autocomplete(client, query).into()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_autocomplete_response(&self, body: &str) -> eyre::Result<Vec<String>> {
|
||||
match self {
|
||||
Engine::Google => google::parse_autocomplete_response(body),
|
||||
_ => Ok(Vec::new()),
|
||||
Engine::Google => search::google::parse_autocomplete_response(body),
|
||||
_ => eyre::bail!("engine {self:?} can't parse autocomplete response"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -110,7 +113,6 @@ pub enum RequestResponse {
|
||||
Http(reqwest::RequestBuilder),
|
||||
Instant(EngineResponse),
|
||||
}
|
||||
|
||||
impl From<reqwest::RequestBuilder> for RequestResponse {
|
||||
fn from(req: reqwest::RequestBuilder) -> Self {
|
||||
Self::Http(req)
|
||||
@ -122,6 +124,21 @@ impl From<EngineResponse> for RequestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RequestAutocompleteResponse {
|
||||
Http(reqwest::RequestBuilder),
|
||||
Instant(Vec<String>),
|
||||
}
|
||||
impl From<reqwest::RequestBuilder> for RequestAutocompleteResponse {
|
||||
fn from(req: reqwest::RequestBuilder) -> Self {
|
||||
Self::Http(req)
|
||||
}
|
||||
}
|
||||
impl From<Vec<String>> for RequestAutocompleteResponse {
|
||||
fn from(res: Vec<String>) -> Self {
|
||||
Self::Instant(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EngineSearchResult {
|
||||
pub url: String,
|
||||
@ -258,9 +275,14 @@ pub async fn autocomplete_with_client_and_engines(
|
||||
for engine in engines {
|
||||
if let Some(request) = engine.request_autocomplete(client, query) {
|
||||
requests.push(async {
|
||||
let res = request.send().await?;
|
||||
let body = res.text().await?;
|
||||
let response = engine.parse_autocomplete_response(&body)?;
|
||||
let response = match request {
|
||||
RequestAutocompleteResponse::Http(request) => {
|
||||
let res = request.send().await?;
|
||||
let body = res.text().await?;
|
||||
engine.parse_autocomplete_response(&body)?
|
||||
}
|
||||
RequestAutocompleteResponse::Instant(response) => response,
|
||||
};
|
||||
Ok((*engine, response))
|
||||
});
|
||||
}
|
||||
|
@ -27,7 +27,9 @@ pub fn parse_response(body: &str) -> eyre::Result<EngineResponse> {
|
||||
parse_html_response_with_opts(
|
||||
body,
|
||||
ParseOpts::new()
|
||||
.result("div.g, div.xpd")
|
||||
// xpd is weird, some results have it but it's usually used for ads?
|
||||
// the :first-child filters out the ads though since for ads the first child is always a span
|
||||
.result("div.g > div, div.xpd > div:first-child")
|
||||
.title("h3")
|
||||
.href("a[href]")
|
||||
.description("div[data-sncf], div[style='-webkit-line-clamp:2']")
|
||||
|
@ -6,7 +6,10 @@ pub fn normalize_url(url: &str) -> eyre::Result<String> {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let mut url = Url::parse(url)?;
|
||||
let Ok(mut url) = Url::parse(url) else {
|
||||
eprintln!("failed to parse url: {url}");
|
||||
return Ok(url.to_string());
|
||||
};
|
||||
|
||||
// make sure the scheme is https
|
||||
if url.scheme() == "http" {
|
||||
@ -22,11 +25,12 @@ pub fn normalize_url(url: &str) -> eyre::Result<String> {
|
||||
url.set_path(path);
|
||||
}
|
||||
|
||||
// remove ref_src tracking param
|
||||
// remove tracking params
|
||||
let query_pairs = url.query_pairs().into_owned();
|
||||
let mut new_query_pairs = Vec::new();
|
||||
const TRACKING_PARAMS: &[&str] = &["ref_src", "_sm_au_"];
|
||||
for (key, value) in query_pairs {
|
||||
if key != "ref_src" {
|
||||
if !TRACKING_PARAMS.contains(&key.as_str()) {
|
||||
new_query_pairs.push((key, value));
|
||||
}
|
||||
}
|
||||
|
@ -144,6 +144,12 @@ pub(super) fn parse_html_response_with_opts(
|
||||
let url = normalize_url(&url)?;
|
||||
let description = description_query_method.call(&result)?;
|
||||
|
||||
// this can happen on google if you search "roll d6"
|
||||
let is_empty = description.is_empty() && title.is_empty();
|
||||
if is_empty {
|
||||
continue;
|
||||
}
|
||||
|
||||
search_results.push(EngineSearchResult {
|
||||
url,
|
||||
title,
|
||||
@ -162,7 +168,7 @@ pub(super) fn parse_html_response_with_opts(
|
||||
let description = featured_snippet_description_query_method.call(&featured_snippet)?;
|
||||
|
||||
// this can happen on google if you search "what's my user agent"
|
||||
let is_empty = description.is_empty() && title.is_empty() && url.is_empty();
|
||||
let is_empty = description.is_empty() && title.is_empty();
|
||||
if is_empty {
|
||||
None
|
||||
} else {
|
||||
|
@ -9,7 +9,9 @@ searchInputEl.insertAdjacentElement("afterend", datalistEl);
|
||||
searchInputEl.addEventListener("input", async (e) => {
|
||||
const value = e.target.value;
|
||||
|
||||
const res = await fetch(`/autocomplete?q=${value}`).then((res) => res.json());
|
||||
const res = await fetch(`/autocomplete?q=${encodeURIComponent(value)}`).then(
|
||||
(res) => res.json()
|
||||
);
|
||||
const options = res[1];
|
||||
|
||||
datalistEl.innerHTML = "";
|
||||
|
@ -97,13 +97,6 @@ h1 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* answer */
|
||||
.answer {
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #234;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
/* progress update */
|
||||
.progress-updates {
|
||||
margin-bottom: 1rem;
|
||||
@ -122,3 +115,25 @@ h1 {
|
||||
color: #7fd962;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* answer */
|
||||
.answer {
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #234;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.answer h3 {
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
/* styles for specific answers */
|
||||
.answer-calc-query {
|
||||
margin: 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.answer-calc-constant {
|
||||
color: #d2a6ff;
|
||||
white-space: pre;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user