add optional api

This commit is contained in:
mat 2024-06-29 01:06:27 -05:00
parent cb0edfd4fc
commit 24b8bb749c
5 changed files with 64 additions and 23 deletions

View File

@ -1,8 +1,7 @@
# See https://github.com/mat-1/metasearch2/blob/master/config-base.toml and # See src/config.rs for all of the possible options
# https://github.com/mat-1/metasearch2/blob/master/src/config.rs for some of
# the possible options
bind = "0.0.0.0:28019" bind = "0.0.0.0:28019"
api = false
[ui] [ui]
# engine_list_separator = true # engine_list_separator = true

View File

@ -8,6 +8,8 @@ use crate::engines::Engine;
#[derive(Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
pub bind: SocketAddr, pub bind: SocketAddr,
/// Whether the JSON API should be accessible.
pub api: bool,
pub ui: UiConfig, pub ui: UiConfig,
pub image_search: ImageSearchConfig, pub image_search: ImageSearchConfig,
pub engines: EnginesConfig, pub engines: EnginesConfig,
@ -16,6 +18,7 @@ pub struct Config {
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct PartialConfig { pub struct PartialConfig {
pub bind: Option<SocketAddr>, pub bind: Option<SocketAddr>,
pub api: Option<bool>,
pub ui: Option<PartialUiConfig>, pub ui: Option<PartialUiConfig>,
pub image_search: Option<PartialImageSearchConfig>, pub image_search: Option<PartialImageSearchConfig>,
pub engines: Option<PartialEnginesConfig>, pub engines: Option<PartialEnginesConfig>,
@ -24,6 +27,7 @@ pub struct PartialConfig {
impl Config { impl Config {
pub fn overlay(&mut self, partial: PartialConfig) { pub fn overlay(&mut self, partial: PartialConfig) {
self.bind = partial.bind.unwrap_or(self.bind); 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.ui.overlay(partial.ui.unwrap_or_default());
self.image_search self.image_search
.overlay(partial.image_search.unwrap_or_default()); .overlay(partial.image_search.unwrap_or_default());
@ -79,16 +83,16 @@ impl ImageSearchConfig {
#[derive(Debug)] #[derive(Debug)]
pub struct ImageProxyConfig { pub struct ImageProxyConfig {
/// Whether we should proxy remote images through our server. This is mostly
/// a privacy feature.
pub enabled: bool, pub enabled: bool,
/// The maximum size of an image that can be proxied. This is in bytes.
pub max_download_size: u64, pub max_download_size: u64,
} }
#[derive(Deserialize, Debug, Default)] #[derive(Deserialize, Debug, Default)]
pub struct PartialImageProxyConfig { pub struct PartialImageProxyConfig {
/// Whether we should proxy remote images through our server. This is mostly
/// a privacy feature.
pub enabled: Option<bool>, pub enabled: Option<bool>,
/// The maximum size of an image that can be proxied. This is in bytes.
pub max_download_size: Option<u64>, pub max_download_size: Option<u64>,
} }
@ -145,7 +149,9 @@ impl EnginesConfig {
#[derive(Debug)] #[derive(Debug)]
pub struct EngineConfig { pub struct EngineConfig {
pub enabled: bool, pub enabled: bool,
/// The priority of this engine relative to the other engines.
pub weight: f64, pub weight: f64,
/// Per-engine configs. These are parsed at request time.
pub extra: toml::Table, pub extra: toml::Table,
} }
@ -154,10 +160,8 @@ pub struct PartialEngineConfig {
#[serde(default)] #[serde(default)]
pub enabled: Option<bool>, pub enabled: Option<bool>,
/// The priority of this engine relative to the other engines.
#[serde(default)] #[serde(default)]
pub weight: Option<f64>, pub weight: Option<f64>,
/// Per-engine configs. These are parsed at request time.
#[serde(flatten)] #[serde(flatten)]
pub extra: toml::Table, pub extra: toml::Table,
} }
@ -194,6 +198,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
Config { Config {
bind: "0.0.0.0:28019".parse().unwrap(), bind: "0.0.0.0:28019".parse().unwrap(),
api: false,
ui: UiConfig { ui: UiConfig {
show_engine_list_separator: false, show_engine_list_separator: false,
show_version_info: false, show_version_info: false,

View File

@ -1,7 +1,7 @@
#[macro_export] #[macro_export]
macro_rules! engines { macro_rules! engines {
($($engine:ident = $id:expr),* $(,)?) => { ($($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 { pub enum Engine {
$($engine,)* $($engine,)*
} }

View File

@ -12,7 +12,7 @@ use eyre::bail;
use futures::future::join_all; use futures::future::join_all;
use maud::PreEscaped; use maud::PreEscaped;
use reqwest::{header::HeaderMap, RequestBuilder}; use reqwest::{header::HeaderMap, RequestBuilder};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer, Serialize};
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{error, info}; use tracing::{error, info};
@ -204,7 +204,7 @@ impl From<HttpResponse> for reqwest::Response {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct EngineSearchResult { pub struct EngineSearchResult {
pub url: String, pub url: String,
pub title: String, pub title: String,
@ -261,7 +261,7 @@ impl EngineImagesResponse {
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct EngineImageResult { pub struct EngineImageResult {
pub image_url: String, pub image_url: String,
pub page_url: String, pub page_url: String,
@ -602,35 +602,38 @@ pub static CLIENT: LazyLock<reqwest::Client> = LazyLock::new(|| {
.unwrap() .unwrap()
}); });
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct Response { pub struct Response {
pub search_results: Vec<SearchResult<EngineSearchResult>>, pub search_results: Vec<SearchResult<EngineSearchResult>>,
pub featured_snippet: Option<FeaturedSnippet>, pub featured_snippet: Option<FeaturedSnippet>,
pub answer: Option<Answer>, pub answer: Option<Answer>,
pub infobox: Option<Infobox>, pub infobox: Option<Infobox>,
#[serde(skip)]
pub config: Arc<Config>, pub config: Arc<Config>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct ImagesResponse { pub struct ImagesResponse {
pub image_results: Vec<SearchResult<EngineImageResult>>, pub image_results: Vec<SearchResult<EngineImageResult>>,
#[serde(skip)]
pub config: Arc<Config>, pub config: Arc<Config>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum ResponseForTab { pub enum ResponseForTab {
All(Response), All(Response),
Images(ImagesResponse), Images(ImagesResponse),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct SearchResult<R> { pub struct SearchResult<R: Serialize> {
pub result: R, pub result: R,
pub engines: BTreeSet<Engine>, pub engines: BTreeSet<Engine>,
pub score: f64, pub score: f64,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct FeaturedSnippet { pub struct FeaturedSnippet {
pub url: String, pub url: String,
pub title: String, pub title: String,
@ -638,14 +641,16 @@ pub struct FeaturedSnippet {
pub engine: Engine, pub engine: Engine,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct Answer { pub struct Answer {
#[serde(serialize_with = "serialize_markup")]
pub html: PreEscaped<String>, pub html: PreEscaped<String>,
pub engine: Engine, pub engine: Engine,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone, Serialize)]
pub struct Infobox { pub struct Infobox {
#[serde(serialize_with = "serialize_markup")]
pub html: PreEscaped<String>, pub html: PreEscaped<String>,
pub engine: Engine, pub engine: Engine,
} }
@ -654,3 +659,10 @@ pub struct AutocompleteResult {
pub query: String, pub query: String,
pub score: f64, pub score: f64,
} }
fn serialize_markup<S>(markup: &PreEscaped<String>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&markup.0)
}

View File

@ -9,6 +9,7 @@ use axum::{
extract::{ConnectInfo, Query, State}, extract::{ConnectInfo, Query, State},
http::{header, HeaderMap, StatusCode}, http::{header, HeaderMap, StatusCode},
response::IntoResponse, response::IntoResponse,
Json,
}; };
use bytes::Bytes; use bytes::Bytes;
use maud::{html, PreEscaped, DOCTYPE}; use maud::{html, PreEscaped, DOCTYPE};
@ -128,7 +129,7 @@ pub async fn route(
State(config): State<Arc<Config>>, State(config): State<Arc<Config>>,
headers: HeaderMap, headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>, ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> impl IntoResponse { ) -> axum::response::Response {
let query = params let query = params
.get("q") .get("q")
.cloned() .cloned()
@ -144,7 +145,8 @@ pub async fn route(
(header::CONTENT_TYPE, "text/html; charset=utf-8"), (header::CONTENT_TYPE, "text/html; charset=utf-8"),
], ],
Body::from("<a href=\"/\">No query provided, click here to go back to index</a>"), Body::from("<a href=\"/\">No query provided, click here to go back to index</a>"),
); )
.into_response();
} }
let search_tab = params let search_tab = params
@ -176,6 +178,29 @@ pub async fn route(
config: config.clone(), 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! { let s = stream! {
type R = Result<Bytes, eyre::Error>; type R = Result<Bytes, eyre::Error>;
@ -238,11 +263,11 @@ pub async fn route(
let stream = Body::from_stream(s); let stream = Body::from_stream(s);
( (
StatusCode::OK,
[ [
(header::CONTENT_TYPE, "text/html; charset=utf-8"), (header::CONTENT_TYPE, "text/html; charset=utf-8"),
(header::TRANSFER_ENCODING, "chunked"), (header::TRANSFER_ENCODING, "chunked"),
], ],
stream, stream,
) )
.into_response()
} }