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",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fend-core"
|
||||||
|
version = "1.3.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e470ea3be6ce980f4d7f6cc08a6084e7715f2b052eeb1f123f2d4d8fb1d35de1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fnv"
|
name = "fnv"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@ -792,8 +798,10 @@ dependencies = [
|
|||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"eyre",
|
"eyre",
|
||||||
|
"fend-core",
|
||||||
"futures",
|
"futures",
|
||||||
"html-escape",
|
"html-escape",
|
||||||
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"scraper",
|
"scraper",
|
||||||
|
@ -12,8 +12,10 @@ axum = { version = "0.7.2", features = ["http2"] }
|
|||||||
base64 = "0.21.5"
|
base64 = "0.21.5"
|
||||||
bytes = "1.5.0"
|
bytes = "1.5.0"
|
||||||
eyre = "0.6.11"
|
eyre = "0.6.11"
|
||||||
|
fend-core = "1.3.3"
|
||||||
futures = "0.3.29"
|
futures = "0.3.29"
|
||||||
html-escape = "0.2.13"
|
html-escape = "0.2.13"
|
||||||
|
rand = "0.8.5"
|
||||||
regex = "1.10.2"
|
regex = "1.10.2"
|
||||||
reqwest = { version = "0.11.23", default-features = false, features = [
|
reqwest = { version = "0.11.23", default-features = false, features = [
|
||||||
"rustls-tls",
|
"rustls-tls",
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod calc;
|
||||||
pub mod ip;
|
pub mod ip;
|
||||||
pub mod useragent;
|
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;
|
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;
|
use super::regex;
|
||||||
|
|
||||||
pub fn request(_client: &reqwest::Client, query: &SearchQuery) -> EngineResponse {
|
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())
|
.is_match(&query.query.to_lowercase())
|
||||||
{
|
{
|
||||||
return EngineResponse::new();
|
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");
|
let user_agent = query.request_headers.get("user-agent");
|
||||||
|
|
||||||
EngineResponse::answer_html(if let Some(user_agent) = 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 {
|
} else {
|
||||||
format!("You don't have a user agent")
|
format!("You don't have a user agent")
|
||||||
})
|
})
|
||||||
|
@ -8,8 +8,6 @@ use std::{
|
|||||||
use futures::future::join_all;
|
use futures::future::join_all;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use self::search::{bing, brave, google};
|
|
||||||
|
|
||||||
pub mod answer;
|
pub mod answer;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
|
|
||||||
@ -22,6 +20,7 @@ pub enum Engine {
|
|||||||
// answer
|
// answer
|
||||||
Useragent,
|
Useragent,
|
||||||
Ip,
|
Ip,
|
||||||
|
Calc,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
@ -32,6 +31,7 @@ impl Engine {
|
|||||||
Engine::Brave,
|
Engine::Brave,
|
||||||
Engine::Useragent,
|
Engine::Useragent,
|
||||||
Engine::Ip,
|
Engine::Ip,
|
||||||
|
Engine::Calc,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,6 +42,7 @@ impl Engine {
|
|||||||
Engine::Brave => "brave",
|
Engine::Brave => "brave",
|
||||||
Engine::Useragent => "useragent",
|
Engine::Useragent => "useragent",
|
||||||
Engine::Ip => "ip",
|
Engine::Ip => "ip",
|
||||||
|
Engine::Calc => "calc",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,19 +57,20 @@ impl Engine {
|
|||||||
|
|
||||||
pub fn request(&self, client: &reqwest::Client, query: &SearchQuery) -> RequestResponse {
|
pub fn request(&self, client: &reqwest::Client, query: &SearchQuery) -> RequestResponse {
|
||||||
match self {
|
match self {
|
||||||
Engine::Google => google::request(client, query).into(),
|
Engine::Google => search::google::request(client, query).into(),
|
||||||
Engine::Bing => bing::request(client, query).into(),
|
Engine::Bing => search::bing::request(client, query).into(),
|
||||||
Engine::Brave => search::brave::request(client, query).into(),
|
Engine::Brave => search::brave::request(client, query).into(),
|
||||||
Engine::Useragent => answer::useragent::request(client, query).into(),
|
Engine::Useragent => answer::useragent::request(client, query).into(),
|
||||||
Engine::Ip => answer::ip::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> {
|
pub fn parse_response(&self, body: &str) -> eyre::Result<EngineResponse> {
|
||||||
match self {
|
match self {
|
||||||
Engine::Google => google::parse_response(body),
|
Engine::Google => search::google::parse_response(body),
|
||||||
Engine::Bing => bing::parse_response(body),
|
Engine::Bing => search::bing::parse_response(body),
|
||||||
Engine::Brave => brave::parse_response(body),
|
Engine::Brave => search::brave::parse_response(body),
|
||||||
_ => eyre::bail!("engine {self:?} can't parse response"),
|
_ => eyre::bail!("engine {self:?} can't parse response"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -77,17 +79,18 @@ impl Engine {
|
|||||||
&self,
|
&self,
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
query: &str,
|
query: &str,
|
||||||
) -> Option<reqwest::RequestBuilder> {
|
) -> Option<RequestAutocompleteResponse> {
|
||||||
match self {
|
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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_autocomplete_response(&self, body: &str) -> eyre::Result<Vec<String>> {
|
pub fn parse_autocomplete_response(&self, body: &str) -> eyre::Result<Vec<String>> {
|
||||||
match self {
|
match self {
|
||||||
Engine::Google => google::parse_autocomplete_response(body),
|
Engine::Google => search::google::parse_autocomplete_response(body),
|
||||||
_ => Ok(Vec::new()),
|
_ => eyre::bail!("engine {self:?} can't parse autocomplete response"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,7 +113,6 @@ pub enum RequestResponse {
|
|||||||
Http(reqwest::RequestBuilder),
|
Http(reqwest::RequestBuilder),
|
||||||
Instant(EngineResponse),
|
Instant(EngineResponse),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<reqwest::RequestBuilder> for RequestResponse {
|
impl From<reqwest::RequestBuilder> for RequestResponse {
|
||||||
fn from(req: reqwest::RequestBuilder) -> Self {
|
fn from(req: reqwest::RequestBuilder) -> Self {
|
||||||
Self::Http(req)
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct EngineSearchResult {
|
pub struct EngineSearchResult {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@ -258,9 +275,14 @@ pub async fn autocomplete_with_client_and_engines(
|
|||||||
for engine in engines {
|
for engine in engines {
|
||||||
if let Some(request) = engine.request_autocomplete(client, query) {
|
if let Some(request) = engine.request_autocomplete(client, query) {
|
||||||
requests.push(async {
|
requests.push(async {
|
||||||
|
let response = match request {
|
||||||
|
RequestAutocompleteResponse::Http(request) => {
|
||||||
let res = request.send().await?;
|
let res = request.send().await?;
|
||||||
let body = res.text().await?;
|
let body = res.text().await?;
|
||||||
let response = engine.parse_autocomplete_response(&body)?;
|
engine.parse_autocomplete_response(&body)?
|
||||||
|
}
|
||||||
|
RequestAutocompleteResponse::Instant(response) => response,
|
||||||
|
};
|
||||||
Ok((*engine, response))
|
Ok((*engine, response))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,9 @@ pub fn parse_response(body: &str) -> eyre::Result<EngineResponse> {
|
|||||||
parse_html_response_with_opts(
|
parse_html_response_with_opts(
|
||||||
body,
|
body,
|
||||||
ParseOpts::new()
|
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")
|
.title("h3")
|
||||||
.href("a[href]")
|
.href("a[href]")
|
||||||
.description("div[data-sncf], div[style='-webkit-line-clamp:2']")
|
.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());
|
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
|
// make sure the scheme is https
|
||||||
if url.scheme() == "http" {
|
if url.scheme() == "http" {
|
||||||
@ -22,11 +25,12 @@ pub fn normalize_url(url: &str) -> eyre::Result<String> {
|
|||||||
url.set_path(path);
|
url.set_path(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove ref_src tracking param
|
// remove tracking params
|
||||||
let query_pairs = url.query_pairs().into_owned();
|
let query_pairs = url.query_pairs().into_owned();
|
||||||
let mut new_query_pairs = Vec::new();
|
let mut new_query_pairs = Vec::new();
|
||||||
|
const TRACKING_PARAMS: &[&str] = &["ref_src", "_sm_au_"];
|
||||||
for (key, value) in query_pairs {
|
for (key, value) in query_pairs {
|
||||||
if key != "ref_src" {
|
if !TRACKING_PARAMS.contains(&key.as_str()) {
|
||||||
new_query_pairs.push((key, value));
|
new_query_pairs.push((key, value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,6 +144,12 @@ pub(super) fn parse_html_response_with_opts(
|
|||||||
let url = normalize_url(&url)?;
|
let url = normalize_url(&url)?;
|
||||||
let description = description_query_method.call(&result)?;
|
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 {
|
search_results.push(EngineSearchResult {
|
||||||
url,
|
url,
|
||||||
title,
|
title,
|
||||||
@ -162,7 +168,7 @@ pub(super) fn parse_html_response_with_opts(
|
|||||||
let description = featured_snippet_description_query_method.call(&featured_snippet)?;
|
let description = featured_snippet_description_query_method.call(&featured_snippet)?;
|
||||||
|
|
||||||
// this can happen on google if you search "what's my user agent"
|
// 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 {
|
if is_empty {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
@ -9,7 +9,9 @@ searchInputEl.insertAdjacentElement("afterend", datalistEl);
|
|||||||
searchInputEl.addEventListener("input", async (e) => {
|
searchInputEl.addEventListener("input", async (e) => {
|
||||||
const value = e.target.value;
|
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];
|
const options = res[1];
|
||||||
|
|
||||||
datalistEl.innerHTML = "";
|
datalistEl.innerHTML = "";
|
||||||
|
@ -97,13 +97,6 @@ h1 {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* answer */
|
|
||||||
.answer {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
border: 1px solid #234;
|
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* progress update */
|
/* progress update */
|
||||||
.progress-updates {
|
.progress-updates {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
@ -122,3 +115,25 @@ h1 {
|
|||||||
color: #7fd962;
|
color: #7fd962;
|
||||||
font-weight: bold;
|
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