use std::sync::Arc;
use std::time::Duration;
use tracing;
use crate::core::config::AppConfig;
use crate::fetch::FetchClient;
use crate::meta::MetadataSearchAdapter;
#[derive(Clone)]
pub struct ServerState {
pub config: Arc<AppConfig>,
pub adapter: Arc<MetadataSearchAdapter>,
pub fetch_client: Option<Arc<FetchClient>>,
}
impl std::fmt::Debug for ServerState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServerState")
.field("mode", &self.config.search.mode)
.field("providers", &self.adapter.provider_ids())
.field("fetch_enabled", &self.config.fetch.enabled)
.finish()
}
}
impl ServerState {
pub fn build(config: AppConfig) -> anyhow::Result<Self> {
config.validate()?;
let config = Arc::new(config);
let enabled: Vec<String> = config
.search
.providers
.iter()
.filter_map(|(id, on)| if *on { Some(id.clone()) } else { None })
.collect();
let global_timeout = Duration::from_millis(config.search.timeout_ms);
let user_agent = Some(config.fetch.user_agent.clone());
let searxng_requested = enabled.iter().any(|id| id == "searxng");
let searxng_base_url = config.search.searxng.base_url.clone();
let searxng_base_url_is_empty = searxng_base_url
.as_deref()
.map(str::is_empty)
.unwrap_or(true);
if searxng_requested && (config.search.searxng.enabled || !searxng_base_url_is_empty) {
if !config.search.searxng.enabled {
tracing::warn!(
"[search].providers.searxng = true but [search].searxng.enabled = false; \
the searxng provider will be skipped"
);
} else if searxng_base_url_is_empty {
tracing::warn!(
"[search].providers.searxng = true but [search].searxng.base_url is empty; \
the searxng provider will be skipped"
);
}
}
let searxng_base_url = if config.search.searxng.enabled {
searxng_base_url
} else {
None
};
let adapter = MetadataSearchAdapter::new(
enabled,
global_timeout,
user_agent,
searxng_base_url,
config.search.sanitize_output,
config.search.default_providers.clone(),
&config.search.api,
)?;
let misconfigured = config.misconfigured_default_providers();
for id in &misconfigured {
tracing::warn!(
provider_id = %id,
"provider listed in [search].default_providers is not enabled; \
it will be silently skipped. Enable it in [search].providers or \
remove it from default_providers."
);
}
if config.search.live.user_agent.is_some() {
tracing::warn!(
"[search].live.user_agent is reserved for future use and is not yet applied. \
The vendored HTML engines use a hard-coded browser-like user agent."
);
}
if config.search.live.respect_robots_txt.is_some_and(|v| v) {
tracing::warn!(
"[search].live.respect_robots_txt is reserved for future use and is not yet applied. \
web_fetch does not consult robots.txt in the current build."
);
}
let fetch_client = if config.fetch.enabled {
let limits = config.fetch_limits();
let ua = config.fetch_user_agent();
match FetchClient::new(limits, ua, config.fetch.sanitize_output) {
Ok(c) => Some(Arc::new(c)),
Err(e) => {
tracing::warn!(error = %e, "failed to build shared fetch client; web_fetch will fail at call time");
None
}
}
} else {
None
};
Ok(Self {
config,
adapter: Arc::new(adapter),
fetch_client,
})
}
pub fn with_adapter(config: AppConfig, adapter: std::sync::Arc<MetadataSearchAdapter>) -> Self {
let config = Arc::new(config);
let fetch_client = if config.fetch.enabled {
let limits = config.fetch_limits();
let ua = config.fetch_user_agent();
FetchClient::new(limits, ua, config.fetch.sanitize_output)
.ok()
.map(Arc::new)
} else {
None
};
Self {
config,
adapter,
fetch_client,
}
}
pub fn fetch_client(&self) -> Option<Arc<FetchClient>> {
self.fetch_client.clone()
}
}