use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
use anyhow::{anyhow, Context};
use night_fury_core::BrowserSession;
use night_fury_daemon_core::{protocol::Response, SessionDriver};
use serde_json::Value;
use tail_fin_cli_core::browser_session;
use tail_fin_core::Site;
static SITE_REGISTRY: OnceLock<HashMap<&'static str, Arc<dyn Site>>> = OnceLock::new();
pub fn register_sites(sites: Vec<Arc<dyn Site>>) {
let map: HashMap<&'static str, Arc<dyn Site>> =
sites.into_iter().map(|s| (s.id(), s)).collect();
let _ = SITE_REGISTRY.set(map);
}
pub fn lookup_site(id: &str) -> Option<Arc<dyn Site>> {
SITE_REGISTRY.get()?.get(id).cloned()
}
pub fn registered_site_ids() -> Vec<&'static str> {
SITE_REGISTRY
.get()
.map(|m| m.keys().copied().collect())
.unwrap_or_default()
}
pub struct TailFinSession {
pub session: BrowserSession,
pub site: Arc<dyn Site>,
pub host: String,
}
impl SessionDriver for TailFinSession {
async fn launch(params: Value) -> anyhow::Result<Self> {
let site_id = params
.get("site")
.and_then(|v| v.as_str())
.ok_or_else(|| anyhow!("launch: missing 'site'"))?;
let site = lookup_site(site_id).ok_or_else(|| {
let known = registered_site_ids().join(", ");
anyhow!(
"unknown site '{site_id}' (registered: {known}) — was it registered via register_sites?"
)
})?;
let host = params
.get("host")
.and_then(|v| v.as_str())
.unwrap_or(crate::cli::DEFAULT_CDP_HOST)
.to_string();
if host == "auto" {
return Err(anyhow!(
"host='auto' (stealth launch) is not supported in MVP; use --connect"
));
}
let session = browser_session(&host, false)
.await
.with_context(|| format!("connect to chrome at {host}"))?;
Ok(Self {
session,
site,
host,
})
}
async fn handle(&self, id: &str, cmd: &str, params: &Value) -> (Response, bool) {
let resp =
match crate::handlers::dispatch(self.site.as_ref(), &self.session, id, cmd, params)
.await
{
Some(r) => r,
None => Response::err(
id,
format!("cmd '{cmd}' does not belong to site '{}'", self.site.id()),
),
};
(resp, false)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct FakeSite;
#[async_trait::async_trait]
impl Site for FakeSite {
fn id(&self) -> &'static str {
"fake"
}
fn display_name(&self) -> &'static str {
"Fake"
}
fn cookie_domain_patterns(&self) -> &'static [&'static str] {
&["*.fake.test"]
}
fn refresh_url(&self) -> &'static str {
"https://fake.test/"
}
async fn validate(
&self,
_session: &BrowserSession,
) -> Result<tail_fin_core::SessionStatus, tail_fin_core::SiteError> {
Ok(tail_fin_core::SessionStatus::Valid)
}
}
#[test]
fn register_and_lookup() {
register_sites(vec![Arc::new(FakeSite)]);
let s = lookup_site("fake").expect("fake site registered");
assert_eq!(s.id(), "fake");
assert!(registered_site_ids().contains(&"fake"));
assert!(lookup_site("definitely-not-a-real-site").is_none());
}
}