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
# 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

View File

@ -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<SocketAddr>,
pub api: Option<bool>,
pub ui: Option<PartialUiConfig>,
pub image_search: Option<PartialImageSearchConfig>,
pub engines: Option<PartialEnginesConfig>,
@ -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<bool>,
/// The maximum size of an image that can be proxied. This is in bytes.
pub max_download_size: Option<u64>,
}
@ -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<bool>,
/// The priority of this engine relative to the other engines.
#[serde(default)]
pub weight: Option<f64>,
/// 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,

View File

@ -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,)*
}

View File

@ -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<HttpResponse> 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<reqwest::Client> = LazyLock::new(|| {
.unwrap()
});
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct Response {
pub search_results: Vec<SearchResult<EngineSearchResult>>,
pub featured_snippet: Option<FeaturedSnippet>,
pub answer: Option<Answer>,
pub infobox: Option<Infobox>,
#[serde(skip)]
pub config: Arc<Config>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct ImagesResponse {
pub image_results: Vec<SearchResult<EngineImageResult>>,
#[serde(skip)]
pub config: Arc<Config>,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub enum ResponseForTab {
All(Response),
Images(ImagesResponse),
}
#[derive(Debug, Clone)]
pub struct SearchResult<R> {
#[derive(Debug, Clone, Serialize)]
pub struct SearchResult<R: Serialize> {
pub result: R,
pub engines: BTreeSet<Engine>,
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<String>,
pub engine: Engine,
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Serialize)]
pub struct Infobox {
#[serde(serialize_with = "serialize_markup")]
pub html: PreEscaped<String>,
pub engine: Engine,
}
@ -654,3 +659,10 @@ pub struct AutocompleteResult {
pub query: String,
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},
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<Arc<Config>>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> 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("<a href=\"/\">No query provided, click here to go back to index</a>"),
);
)
.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<Bytes, eyre::Error>;
@ -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()
}