use futures_util::{SinkExt, StreamExt};
use serde_json::Value;
use tokio_tungstenite::{connect_async, tungstenite::Message};
use crate::TailFinError;
pub struct CdpTab {
write: futures_util::stream::SplitSink<
tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
>,
Message,
>,
read: futures_util::stream::SplitStream<
tokio_tungstenite::WebSocketStream<
tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>,
>,
>,
msg_id: u64,
}
impl CdpTab {
pub async fn connect(ws_url: &str) -> Result<Self, TailFinError> {
let (ws_stream, _) = connect_async(ws_url)
.await
.map_err(|e| TailFinError::Api(format!("CDP connect failed: {}", e)))?;
let (write, read) = ws_stream.split();
Ok(Self {
write,
read,
msg_id: 1,
})
}
pub async fn eval(&mut self, expression: &str) -> Result<Value, TailFinError> {
let id = self.msg_id;
self.msg_id += 1;
let msg = serde_json::json!({
"id": id,
"method": "Runtime.evaluate",
"params": { "expression": expression, "awaitPromise": true, "returnByValue": true }
});
self.write
.send(Message::Text(msg.to_string().into()))
.await
.map_err(|e| TailFinError::Api(format!("CDP send failed: {}", e)))?;
while let Some(msg) = self.read.next().await {
let msg = msg.map_err(|e| TailFinError::Api(format!("CDP recv failed: {}", e)))?;
if let Message::Text(text) = msg {
if let Ok(resp) = serde_json::from_str::<Value>(&text) {
if resp.get("id").and_then(|v| v.as_u64()) == Some(id) {
if let Some(val) = resp.pointer("/result/result/value") {
return Ok(val.clone());
}
if let Some(val) = resp.pointer("/result/result") {
if val.get("type").and_then(|v| v.as_str()) == Some("string") {
if let Some(s) = val.get("value") {
return Ok(s.clone());
}
}
return Ok(val.clone());
}
return Ok(Value::Null);
}
}
}
}
Err(TailFinError::Api("CDP connection closed".into()))
}
pub async fn get_cookies(&mut self) -> Result<Vec<Value>, TailFinError> {
let result = self
.send_command("Network.getCookies", serde_json::json!({}))
.await?;
Ok(result
.get("cookies")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default())
}
pub async fn get_all_cookies(&mut self) -> Result<Vec<Value>, TailFinError> {
let result = self
.send_command("Network.getAllCookies", serde_json::json!({}))
.await?;
Ok(result
.pointer("/cookies")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default())
}
pub async fn send_command(
&mut self,
method: &str,
params: Value,
) -> Result<Value, TailFinError> {
let id = self.msg_id;
self.msg_id += 1;
let msg = serde_json::json!({ "id": id, "method": method, "params": params });
self.write
.send(Message::Text(msg.to_string().into()))
.await
.map_err(|e| TailFinError::Api(format!("CDP send failed: {}", e)))?;
while let Some(msg) = self.read.next().await {
let msg = msg.map_err(|e| TailFinError::Api(format!("CDP recv failed: {}", e)))?;
if let Message::Text(text) = msg {
if let Ok(resp) = serde_json::from_str::<Value>(&text) {
if resp.get("id").and_then(|v| v.as_u64()) == Some(id) {
return Ok(resp.get("result").cloned().unwrap_or(Value::Null));
}
}
}
}
Err(TailFinError::Api("CDP connection closed".into()))
}
}
pub async fn list_chrome_tabs(chrome_host: &str) -> Result<Vec<Value>, TailFinError> {
let url = format!("http://{}/json/list", chrome_host);
let resp = reqwest::get(&url)
.await
.map_err(|e| TailFinError::Api(format!("Cannot reach Chrome at {}: {}", chrome_host, e)))?;
let tabs: Vec<Value> = resp
.json()
.await
.map_err(|e| TailFinError::Parse(format!("Invalid Chrome tab list: {}", e)))?;
Ok(tabs)
}
pub async fn find_tab_ws_url(
chrome_host: &str,
url_hint: Option<&str>,
) -> Result<String, TailFinError> {
let tabs = list_chrome_tabs(chrome_host).await?;
let page_tabs: Vec<&Value> = tabs
.iter()
.filter(|t| t.get("type").and_then(|v| v.as_str()) == Some("page"))
.collect();
if page_tabs.is_empty() {
return Err(TailFinError::Api(
"No Chrome page tabs found. Is Chrome running with --remote-debugging-port?".into(),
));
}
let tab = if let Some(hint) = url_hint {
page_tabs
.iter()
.find(|t| {
t.get("url")
.and_then(|v| v.as_str())
.map(|u| u.contains(hint))
.unwrap_or(false)
})
.copied()
.unwrap_or(page_tabs[0])
} else {
page_tabs[0]
};
tab.get("webSocketDebuggerUrl")
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| TailFinError::Api("Tab has no WebSocket debugger URL".into()))
}