#![cfg_attr(not(any(feature = "cdp", feature = "camoufox")), allow(dead_code))]
use serde_json::Value;
#[derive(Debug, Clone, Default)]
pub struct RequestData {
pub headers: Vec<(String, String)>,
pub post_data: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ResponseData {
pub status: u16,
pub status_text: String,
pub headers: Vec<(String, String)>,
pub body: String,
pub body_base64: String,
}
#[derive(Debug, Clone)]
pub struct DataPacket {
pub url: String,
pub method: String,
pub resource_type: String,
pub request: RequestData,
pub response: ResponseData,
}
impl DataPacket {
pub fn path(&self) -> &str {
self.url.split('?').next().unwrap_or(&self.url)
}
pub fn url_has(&self, needle: &str) -> bool {
self.url.contains(needle)
}
pub fn query(&self, key: &str) -> Option<String> {
let q = self.url.split_once('?')?.1;
q.split('&').find_map(|kv| {
let (k, v) = kv.split_once('=').unwrap_or((kv, ""));
(k == key).then(|| v.to_string())
})
}
pub fn queries(&self) -> Vec<(String, String)> {
let Some((_, q)) = self.url.split_once('?') else {
return Vec::new();
};
q.split('&')
.filter(|s| !s.is_empty())
.map(|kv| {
let (k, v) = kv.split_once('=').unwrap_or((kv, ""));
(k.to_string(), v.to_string())
})
.collect()
}
pub fn json(&self) -> Option<Value> {
serde_json::from_str(&self.response.body).ok()
}
}
#[derive(Debug, Clone, Default)]
pub struct ListenFilter {
pub url_keywords: Vec<String>,
pub xhr_only: bool,
}
impl ListenFilter {
pub(crate) fn matches(&self, url: &str, resource_type: &str) -> bool {
self.url_matches(url) && self.type_matches(resource_type)
}
fn url_matches(&self, url: &str) -> bool {
self.url_keywords.is_empty() || self.url_keywords.iter().any(|k| url.contains(k))
}
fn type_matches(&self, resource_type: &str) -> bool {
if !self.xhr_only {
return true;
}
let t = resource_type.to_ascii_lowercase();
t.contains("xhr") || t.contains("fetch") || t.contains("xmlhttprequest")
}
}
#[derive(Debug, Clone, Default)]
pub struct ResumeOptions {
pub url: Option<String>,
pub method: Option<String>,
pub headers: Option<Vec<(String, String)>>,
pub post_data: Option<String>,
}
impl ResumeOptions {
pub fn new() -> Self {
Self::default()
}
pub fn url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
pub fn method(mut self, method: impl Into<String>) -> Self {
self.method = Some(method.into());
self
}
pub fn headers(mut self, headers: Vec<(String, String)>) -> Self {
self.headers = Some(headers);
self
}
pub fn post_data(mut self, post_data: impl Into<String>) -> Self {
self.post_data = Some(post_data.into());
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn filter_matches() {
let f = ListenFilter {
url_keywords: vec!["/api/".into()],
xhr_only: false,
};
assert!(f.matches("https://x.com/api/v1", "fetch"));
assert!(!f.matches("https://x.com/static.js", "fetch"));
let all = ListenFilter::default();
assert!(all.matches("anything", "document"));
}
#[test]
fn datapacket_url_and_json_helpers() {
let p = DataPacket {
url: "https://x.com/api/detail/?aweme_id=123&a_bogus=ZZ%2F1".into(),
method: "GET".into(),
resource_type: "xhr".into(),
request: RequestData::default(),
response: ResponseData {
body: r#"{"aweme_list":[{"aweme_id":"a1"},{"aweme_id":"a2"}]}"#.into(),
..Default::default()
},
};
assert_eq!(p.path(), "https://x.com/api/detail/");
assert!(p.url_has("aweme_id="));
assert_eq!(p.query("aweme_id").as_deref(), Some("123"));
assert_eq!(p.query("a_bogus").as_deref(), Some("ZZ%2F1")); assert_eq!(p.query("missing"), None);
assert_eq!(p.queries().len(), 2);
let j = p.json().unwrap();
assert_eq!(j["aweme_list"][1]["aweme_id"], "a2");
}
#[test]
fn resume_options_builder() {
let o = ResumeOptions::new()
.url("https://x.com")
.method("POST")
.headers(vec![("X-A".into(), "1".into())])
.post_data("body");
assert_eq!(o.url.as_deref(), Some("https://x.com"));
assert_eq!(o.method.as_deref(), Some("POST"));
assert_eq!(o.headers.unwrap()[0].0, "X-A");
assert_eq!(o.post_data.as_deref(), Some("body"));
}
}