diff --git a/config-default.toml b/config-default.toml index 9e57e15..133c979 100644 --- a/config-default.toml +++ b/config-default.toml @@ -1,8 +1,7 @@ -# See https://github.com/mat-1/metasearch2/blob/master/config-base.toml and -# https://github.com/mat-1/metasearch2/blob/master/src/config.rs for some of -# the possible options +# See src/config.rs for all of the possible options bind = "0.0.0.0:28019" +api = false [ui] # engine_list_separator = true diff --git a/src/config.rs b/src/config.rs index 187c22f..c8a3341 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,6 +8,8 @@ use crate::engines::Engine; #[derive(Debug)] pub struct Config { pub bind: SocketAddr, + /// Whether the JSON API should be accessible. + pub api: bool, pub ui: UiConfig, pub image_search: ImageSearchConfig, pub engines: EnginesConfig, @@ -16,6 +18,7 @@ pub struct Config { #[derive(Deserialize, Debug)] pub struct PartialConfig { pub bind: Option, + pub api: Option, pub ui: Option, pub image_search: Option, pub engines: Option, @@ -24,6 +27,7 @@ pub struct PartialConfig { impl Config { pub fn overlay(&mut self, partial: PartialConfig) { self.bind = partial.bind.unwrap_or(self.bind); + self.api = partial.api.unwrap_or(self.api); self.ui.overlay(partial.ui.unwrap_or_default()); self.image_search .overlay(partial.image_search.unwrap_or_default()); @@ -79,16 +83,16 @@ impl ImageSearchConfig { #[derive(Debug)] pub struct ImageProxyConfig { + /// Whether we should proxy remote images through our server. This is mostly + /// a privacy feature. pub enabled: bool, + /// The maximum size of an image that can be proxied. This is in bytes. pub max_download_size: u64, } #[derive(Deserialize, Debug, Default)] pub struct PartialImageProxyConfig { - /// Whether we should proxy remote images through our server. This is mostly - /// a privacy feature. pub enabled: Option, - /// The maximum size of an image that can be proxied. This is in bytes. pub max_download_size: Option, } @@ -145,7 +149,9 @@ impl EnginesConfig { #[derive(Debug)] pub struct EngineConfig { pub enabled: bool, + /// The priority of this engine relative to the other engines. pub weight: f64, + /// Per-engine configs. These are parsed at request time. pub extra: toml::Table, } @@ -154,10 +160,8 @@ pub struct PartialEngineConfig { #[serde(default)] pub enabled: Option, - /// The priority of this engine relative to the other engines. #[serde(default)] pub weight: Option, - /// Per-engine configs. These are parsed at request time. #[serde(flatten)] pub extra: toml::Table, } @@ -194,6 +198,7 @@ impl Default for Config { fn default() -> Self { Config { bind: "0.0.0.0:28019".parse().unwrap(), + api: false, ui: UiConfig { show_engine_list_separator: false, show_version_info: false, diff --git a/src/engines/macros.rs b/src/engines/macros.rs index 6fbad1c..a8a468f 100644 --- a/src/engines/macros.rs +++ b/src/engines/macros.rs @@ -1,7 +1,7 @@ #[macro_export] macro_rules! engines { ($($engine:ident = $id:expr),* $(,)?) => { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)] pub enum Engine { $($engine,)* } diff --git a/src/engines/mod.rs b/src/engines/mod.rs index 3e8cb72..4a3ebe3 100644 --- a/src/engines/mod.rs +++ b/src/engines/mod.rs @@ -12,7 +12,7 @@ use eyre::bail; use futures::future::join_all; use maud::PreEscaped; use reqwest::{header::HeaderMap, RequestBuilder}; -use serde::{Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer, Serialize}; use tokio::sync::mpsc; use tracing::{error, info}; @@ -204,7 +204,7 @@ impl From for reqwest::Response { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct EngineSearchResult { pub url: String, pub title: String, @@ -261,7 +261,7 @@ impl EngineImagesResponse { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct EngineImageResult { pub image_url: String, pub page_url: String, @@ -602,35 +602,38 @@ pub static CLIENT: LazyLock = LazyLock::new(|| { .unwrap() }); -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct Response { pub search_results: Vec>, pub featured_snippet: Option, pub answer: Option, pub infobox: Option, + #[serde(skip)] pub config: Arc, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct ImagesResponse { pub image_results: Vec>, + #[serde(skip)] pub config: Arc, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] +#[serde(untagged)] pub enum ResponseForTab { All(Response), Images(ImagesResponse), } -#[derive(Debug, Clone)] -pub struct SearchResult { +#[derive(Debug, Clone, Serialize)] +pub struct SearchResult { pub result: R, pub engines: BTreeSet, pub score: f64, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct FeaturedSnippet { pub url: String, pub title: String, @@ -638,14 +641,16 @@ pub struct FeaturedSnippet { pub engine: Engine, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct Answer { + #[serde(serialize_with = "serialize_markup")] pub html: PreEscaped, pub engine: Engine, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize)] pub struct Infobox { + #[serde(serialize_with = "serialize_markup")] pub html: PreEscaped, pub engine: Engine, } @@ -654,3 +659,10 @@ pub struct AutocompleteResult { pub query: String, pub score: f64, } + +fn serialize_markup(markup: &PreEscaped, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&markup.0) +} diff --git a/src/web/search.rs b/src/web/search.rs index bf3c31d..20e8acb 100644 --- a/src/web/search.rs +++ b/src/web/search.rs @@ -9,6 +9,7 @@ use axum::{ extract::{ConnectInfo, Query, State}, http::{header, HeaderMap, StatusCode}, response::IntoResponse, + Json, }; use bytes::Bytes; use maud::{html, PreEscaped, DOCTYPE}; @@ -128,7 +129,7 @@ pub async fn route( State(config): State>, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, -) -> impl IntoResponse { +) -> axum::response::Response { let query = params .get("q") .cloned() @@ -144,7 +145,8 @@ pub async fn route( (header::CONTENT_TYPE, "text/html; charset=utf-8"), ], Body::from("No query provided, click here to go back to index"), - ); + ) + .into_response(); } let search_tab = params @@ -176,6 +178,29 @@ pub async fn route( config: config.clone(), }; + let trying_to_use_api = + query.request_headers.get("accept") == Some(&"application/json".to_string()); + if trying_to_use_api { + if !config.api { + return (StatusCode::FORBIDDEN, "API access is disabled").into_response(); + } + + let (progress_tx, mut progress_rx) = tokio::sync::mpsc::unbounded_channel(); + let search_future = tokio::spawn(async move { engines::search(&query, progress_tx).await }); + if let Err(e) = search_future.await { + return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(); + } + + let mut results = Vec::new(); + while let Some(progress_update) = progress_rx.recv().await { + if let ProgressUpdateData::Response(r) = progress_update.data { + results.push(r); + } + } + + return Json(results).into_response(); + } + let s = stream! { type R = Result; @@ -238,11 +263,11 @@ pub async fn route( let stream = Body::from_stream(s); ( - StatusCode::OK, [ (header::CONTENT_TYPE, "text/html; charset=utf-8"), (header::TRANSFER_ENCODING, "chunked"), ], stream, ) + .into_response() }