use night_fury_core::BrowserSession;
use serde_json::Value;
use crate::error::TailFinError;
pub async fn navigate_and_eval(
session: &BrowserSession,
url: &str,
wait_secs: u64,
js: &str,
) -> Result<Value, TailFinError> {
session.navigate(url).await?;
if let Err(e) = session.wait_for_network_idle(15000, 1000).await {
eprintln!("warning: wait_for_network_idle failed: {}", e);
}
tokio::time::sleep(std::time::Duration::from_secs(wait_secs)).await;
Ok(session.eval(js).await?)
}
pub async fn scroll_to_load(
session: &BrowserSession,
count: usize,
delay_ms: u64,
) -> Result<(), TailFinError> {
for _ in 0..count {
session
.eval("window.scrollBy(0, window.innerHeight)")
.await?;
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
}
Ok(())
}
pub async fn mask_webdriver(session: &BrowserSession) -> Result<(), TailFinError> {
session
.eval(
r#"Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
if (!window.chrome) { window.chrome = { runtime: {} }; }
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en', 'zh-TW', 'zh'] });"#,
)
.await?;
Ok(())
}
pub async fn ensure_on_domain(
session: &BrowserSession,
domains: &[&str],
) -> Result<(), TailFinError> {
let current_url = session.get_url().await?;
if !domains.iter().any(|d| current_url.contains(d)) {
session.navigate(&format!("https://{}", domains[0])).await?;
if let Err(e) = session.wait_for_network_idle(15000, 1000).await {
eprintln!("warning: wait_for_network_idle failed: {}", e);
}
}
Ok(())
}
pub async fn browser_origin_fetch(
session: &BrowserSession,
path: &str,
headers: Option<&Value>,
) -> Result<Value, TailFinError> {
let path_escaped = serde_json::to_string(path).unwrap_or_default();
let headers_json = headers
.map(|h| serde_json::to_string(h).unwrap_or_default())
.unwrap_or_else(|| r#"{"Accept":"application/json"}"#.to_string());
let js = format!(
r#"(async () => {{
try {{
const resp = await fetch(window.location.origin + {path_escaped}, {{
credentials: "include",
headers: {headers_json}
}});
if (!resp.ok) return {{ __error: true, status: resp.status, statusText: resp.statusText }};
return await resp.json();
}} catch (e) {{
return {{ __error: true, status: 0, statusText: e.toString() }};
}}
}})()"#
);
let result = session.eval(&js).await?;
if result
.get("__error")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
let status = result.get("status").and_then(|v| v.as_u64()).unwrap_or(0);
let text = result
.get("statusText")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
return Err(TailFinError::Http {
status: status as u16,
status_text: text.to_string(),
body: String::new(),
});
}
Ok(result)
}
pub async fn page_fetch(
session: &BrowserSession,
url: &str,
method: &str,
headers: &Value,
) -> Result<Value, TailFinError> {
page_fetch_with_body(session, url, method, headers, None).await
}
pub async fn page_fetch_text_with_body(
session: &BrowserSession,
url: &str,
method: &str,
headers: &Value,
body: Option<&Value>,
) -> Result<String, TailFinError> {
let url_escaped = serde_json::to_string(url).unwrap_or_default();
let method_escaped = serde_json::to_string(method).unwrap_or_default();
let headers_json = serde_json::to_string(headers).unwrap_or_default();
let body_part = match body {
Some(b) => {
let body_json = serde_json::to_string(b).unwrap_or_default();
format!(r#", body: JSON.stringify({})"#, body_json)
}
None => String::new(),
};
let js = format!(
r#"
(async () => {{
try {{
const resp = await fetch({url_escaped}, {{
method: {method_escaped},
headers: {headers_json},
credentials: "include"{body_part}
}});
if (!resp.ok) {{
let body = "";
try {{ body = await resp.text(); }} catch (_) {{}}
return {{ __error: true, status: resp.status, statusText: resp.statusText, body: body.slice(0, 800) }};
}}
const text = await resp.text();
return {{ __text: text }};
}} catch (e) {{
return {{ __error: true, status: 0, statusText: e.toString() }};
}}
}})()
"#
);
let result = session.eval(&js).await?;
if result
.get("__error")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
let status = result.get("status").and_then(|v| v.as_u64()).unwrap_or(0);
let text = result
.get("statusText")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let body = result.get("body").and_then(|v| v.as_str()).unwrap_or("");
return Err(TailFinError::Http {
status: status as u16,
status_text: text.to_string(),
body: body.to_string(),
});
}
result
.get("__text")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| TailFinError::Parse("page_fetch_text: missing __text".into()))
}
pub async fn page_fetch_with_body(
session: &BrowserSession,
url: &str,
method: &str,
headers: &Value,
body: Option<&Value>,
) -> Result<Value, TailFinError> {
let url_escaped = serde_json::to_string(url).unwrap_or_default();
let method_escaped = serde_json::to_string(method).unwrap_or_default();
let headers_json = serde_json::to_string(headers).unwrap_or_default();
let body_part = match body {
Some(b) => {
let body_json = serde_json::to_string(b).unwrap_or_default();
format!(r#", body: JSON.stringify({})"#, body_json)
}
None => String::new(),
};
let js = format!(
r#"
(async () => {{
try {{
const resp = await fetch({url_escaped}, {{
method: {method_escaped},
headers: {headers_json},
credentials: "include"{body_part}
}});
if (!resp.ok) {{
return {{ __error: true, status: resp.status, statusText: resp.statusText }};
}}
try {{
return await resp.json();
}} catch (parseErr) {{
return {{ __error: true, status: resp.status, statusText: "Response is not valid JSON: " + parseErr.toString() }};
}}
}} catch (e) {{
return {{ __error: true, status: 0, statusText: e.toString() }};
}}
}})()
"#
);
let result = session.eval(&js).await?;
if result
.get("__error")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
let status = result.get("status").and_then(|v| v.as_u64()).unwrap_or(0);
let text = result
.get("statusText")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
return Err(TailFinError::Http {
status: status as u16,
status_text: text.to_string(),
body: String::new(),
});
}
Ok(result)
}