Skip to main content

spider_browser/protocol/
protocol_adapter.rs

1//! Unified protocol adapter — wraps either CDPSession or BiDiSession.
2
3use crate::errors::{Result, SpiderError};
4use crate::events::SpiderEventEmitter;
5use crate::protocol::bidi_session::BiDiSession;
6use crate::protocol::cdp_session::CDPSession;
7use crate::protocol::types::get_key_params;
8use serde_json::{json, Value};
9use std::sync::Arc;
10use tokio::sync::mpsc;
11use tracing::{debug, info};
12
13/// Protocol adapter options.
14pub struct ProtocolAdapterOptions {
15    pub command_timeout_ms: Option<u64>,
16}
17
18/// Determines which protocol to use.
19#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum ProtocolType {
21    Cdp,
22    Bidi,
23    Auto,
24}
25
26/// Unified protocol interface wrapping CDPSession or BiDiSession.
27pub struct ProtocolAdapter {
28    cdp: Option<CDPSession>,
29    bidi: Option<BiDiSession>,
30    protocol: ProtocolType,
31    send_tx: mpsc::UnboundedSender<String>,
32    emitter: SpiderEventEmitter,
33    command_timeout_ms: u64,
34}
35
36impl ProtocolAdapter {
37    pub fn new(
38        send_tx: mpsc::UnboundedSender<String>,
39        emitter: SpiderEventEmitter,
40        browser: &str,
41        opts: Option<ProtocolAdapterOptions>,
42    ) -> Self {
43        let timeout = opts.as_ref()
44            .and_then(|o| o.command_timeout_ms)
45            .unwrap_or(30_000);
46
47        let (cdp, bidi, protocol) = if browser == "auto" {
48            (None, None, ProtocolType::Auto)
49        } else if browser == "firefox" {
50            (None, Some(BiDiSession::new(send_tx.clone(), timeout)), ProtocolType::Bidi)
51        } else {
52            (Some(CDPSession::new(send_tx.clone(), timeout)), None, ProtocolType::Cdp)
53        };
54
55        Self {
56            cdp,
57            bidi,
58            protocol,
59            send_tx,
60            emitter,
61            command_timeout_ms: timeout,
62        }
63    }
64
65    pub fn protocol_type(&self) -> ProtocolType {
66        self.protocol
67    }
68
69    /// Route incoming WebSocket messages to the right session.
70    pub fn route_message(&self, data: &str) {
71        // Check for Spider.* events first
72        if let Ok(msg) = serde_json::from_str::<Value>(data) {
73            if let Some(method) = msg.get("method").and_then(|v| v.as_str()) {
74                if method.starts_with("Spider.") {
75                    self.handle_spider_event(method, msg.get("params").cloned().unwrap_or(json!({})));
76                    return;
77                }
78            }
79        }
80
81        if let Some(ref cdp) = self.cdp {
82            cdp.handle_message(data);
83        } else if let Some(ref bidi) = self.bidi {
84            bidi.handle_message(data);
85        }
86    }
87
88    fn handle_spider_event(&self, method: &str, params: Value) {
89        match method {
90            "Spider.captchaDetected" => {
91                self.emitter.emit("captcha.detected", params);
92            }
93            "Spider.captchaSolving" => {
94                self.emitter.emit("captcha.solving", params);
95            }
96            "Spider.captchaSolved" => {
97                self.emitter.emit("captcha.solved", params);
98            }
99            "Spider.captchaFailed" => {
100                self.emitter.emit("captcha.failed", params);
101            }
102            _ => {
103                debug!("unhandled Spider event: {}", method);
104            }
105        }
106    }
107
108    /// Initialize the protocol session.
109    pub async fn init(&mut self) -> Result<()> {
110        if self.protocol == ProtocolType::Auto {
111            self.auto_detect_and_init().await?;
112            return Ok(());
113        }
114
115        if let Some(ref cdp) = self.cdp {
116            cdp.attach_to_page().await?;
117        } else if let Some(ref bidi) = self.bidi {
118            bidi.get_or_create_context().await?;
119        }
120        Ok(())
121    }
122
123    async fn auto_detect_and_init(&mut self) -> Result<()> {
124        // Try CDP first
125        let cdp = CDPSession::new(self.send_tx.clone(), self.command_timeout_ms);
126        match cdp.attach_to_page().await {
127            Ok(_) => {
128                self.cdp = Some(cdp);
129                self.protocol = ProtocolType::Cdp;
130                info!("auto-detected CDP protocol");
131                return Ok(());
132            }
133            Err(_) => {
134                cdp.destroy();
135            }
136        }
137
138        // Try BiDi
139        let bidi = BiDiSession::new(self.send_tx.clone(), self.command_timeout_ms);
140        bidi.get_or_create_context().await?;
141        self.bidi = Some(bidi);
142        self.protocol = ProtocolType::Bidi;
143        info!("auto-detected BiDi protocol");
144        Ok(())
145    }
146
147    // ------------------------------------------------------------------
148    // Unified interface
149    // ------------------------------------------------------------------
150
151    pub async fn navigate(&self, url: &str) -> Result<()> {
152        if let Some(ref cdp) = self.cdp {
153            cdp.navigate(url).await
154        } else if let Some(ref bidi) = self.bidi {
155            bidi.navigate(url).await
156        } else {
157            Err(SpiderError::Protocol("No protocol session".into()))
158        }
159    }
160
161    pub async fn navigate_fast(&self, url: &str) -> Result<()> {
162        if let Some(ref cdp) = self.cdp {
163            cdp.navigate_fast(url).await
164        } else if let Some(ref bidi) = self.bidi {
165            bidi.navigate(url).await
166        } else {
167            Err(SpiderError::Protocol("No protocol session".into()))
168        }
169    }
170
171    pub async fn navigate_dom(&self, url: &str) -> Result<()> {
172        if let Some(ref cdp) = self.cdp {
173            cdp.navigate_dom(url).await
174        } else if let Some(ref bidi) = self.bidi {
175            bidi.navigate(url).await
176        } else {
177            Err(SpiderError::Protocol("No protocol session".into()))
178        }
179    }
180
181    pub async fn get_html(&self) -> Result<String> {
182        if let Some(ref cdp) = self.cdp {
183            cdp.get_html().await
184        } else if let Some(ref bidi) = self.bidi {
185            bidi.get_html().await
186        } else {
187            Err(SpiderError::Protocol("No protocol session".into()))
188        }
189    }
190
191    pub async fn evaluate(&self, expression: &str) -> Result<Value> {
192        if let Some(ref cdp) = self.cdp {
193            cdp.evaluate(expression).await
194        } else if let Some(ref bidi) = self.bidi {
195            bidi.evaluate(expression).await
196        } else {
197            Err(SpiderError::Protocol("No protocol session".into()))
198        }
199    }
200
201    /// Send a raw page-scoped CDP command and return its `result`. CDP-only.
202    pub async fn send_command(&self, method: &str, params: Value) -> Result<Value> {
203        if let Some(ref cdp) = self.cdp {
204            let resp = cdp.send_to_target(method, params).await?;
205            Ok(resp.get("result").cloned().unwrap_or(Value::Null))
206        } else {
207            Err(SpiderError::Protocol(format!(
208                "{method} is not supported over BiDi"
209            )))
210        }
211    }
212
213    pub async fn capture_screenshot(&self) -> Result<String> {
214        if let Some(ref cdp) = self.cdp {
215            cdp.capture_screenshot().await
216        } else if let Some(ref bidi) = self.bidi {
217            bidi.capture_screenshot().await
218        } else {
219            Err(SpiderError::Protocol("No protocol session".into()))
220        }
221    }
222
223    pub async fn click_point(&self, x: f64, y: f64) -> Result<()> {
224        if let Some(ref cdp) = self.cdp {
225            cdp.click_point(x, y).await
226        } else if let Some(ref bidi) = self.bidi {
227            bidi.click_point(x, y).await
228        } else {
229            Err(SpiderError::Protocol("No protocol session".into()))
230        }
231    }
232
233    pub async fn right_click_point(&self, x: f64, y: f64) -> Result<()> {
234        if let Some(ref cdp) = self.cdp {
235            cdp.right_click_point(x, y).await
236        } else if let Some(ref bidi) = self.bidi {
237            bidi.perform_actions(json!([{
238                "type": "pointer", "id": "mouse",
239                "actions": [
240                    {"type": "pointerMove", "x": x.round() as i64, "y": y.round() as i64},
241                    {"type": "pointerDown", "button": 2},
242                    {"type": "pointerUp", "button": 2},
243                ]
244            }])).await
245        } else {
246            Err(SpiderError::Protocol("No protocol session".into()))
247        }
248    }
249
250    pub async fn double_click_point(&self, x: f64, y: f64) -> Result<()> {
251        if let Some(ref cdp) = self.cdp {
252            cdp.double_click_point(x, y).await
253        } else if let Some(ref bidi) = self.bidi {
254            bidi.perform_actions(json!([{
255                "type": "pointer", "id": "mouse",
256                "actions": [
257                    {"type": "pointerMove", "x": x.round() as i64, "y": y.round() as i64},
258                    {"type": "pointerDown", "button": 0},
259                    {"type": "pointerUp", "button": 0},
260                    {"type": "pointerDown", "button": 0},
261                    {"type": "pointerUp", "button": 0},
262                ]
263            }])).await
264        } else {
265            Err(SpiderError::Protocol("No protocol session".into()))
266        }
267    }
268
269    pub async fn click_hold_point(&self, x: f64, y: f64, hold_ms: u64) -> Result<()> {
270        if let Some(ref cdp) = self.cdp {
271            cdp.click_hold_point(x, y, hold_ms).await
272        } else if let Some(ref bidi) = self.bidi {
273            bidi.perform_actions(json!([{
274                "type": "pointer", "id": "mouse",
275                "actions": [
276                    {"type": "pointerMove", "x": x.round() as i64, "y": y.round() as i64},
277                    {"type": "pointerDown", "button": 0},
278                    {"type": "pause", "duration": hold_ms},
279                    {"type": "pointerUp", "button": 0},
280                ]
281            }])).await
282        } else {
283            Err(SpiderError::Protocol("No protocol session".into()))
284        }
285    }
286
287    pub async fn hover_point(&self, x: f64, y: f64) -> Result<()> {
288        if let Some(ref cdp) = self.cdp {
289            cdp.hover_point(x, y).await
290        } else if let Some(ref bidi) = self.bidi {
291            bidi.perform_actions(json!([{
292                "type": "pointer", "id": "mouse",
293                "actions": [{"type": "pointerMove", "x": x.round() as i64, "y": y.round() as i64}]
294            }])).await
295        } else {
296            Err(SpiderError::Protocol("No protocol session".into()))
297        }
298    }
299
300    pub async fn drag_point(&self, from_x: f64, from_y: f64, to_x: f64, to_y: f64) -> Result<()> {
301        if let Some(ref cdp) = self.cdp {
302            cdp.drag_point(from_x, from_y, to_x, to_y).await
303        } else if let Some(ref bidi) = self.bidi {
304            let steps = 10;
305            let mut actions = vec![
306                json!({"type": "pointerMove", "x": from_x.round() as i64, "y": from_y.round() as i64}),
307                json!({"type": "pointerDown", "button": 0}),
308            ];
309            for i in 1..=steps {
310                let t = i as f64 / steps as f64;
311                actions.push(json!({
312                    "type": "pointerMove",
313                    "x": (from_x + (to_x - from_x) * t).round() as i64,
314                    "y": (from_y + (to_y - from_y) * t).round() as i64,
315                    "duration": 16,
316                }));
317            }
318            actions.push(json!({"type": "pointerUp", "button": 0}));
319            bidi.perform_actions(json!([{"type": "pointer", "id": "mouse", "actions": actions}])).await
320        } else {
321            Err(SpiderError::Protocol("No protocol session".into()))
322        }
323    }
324
325    pub async fn insert_text(&self, text: &str) -> Result<()> {
326        if let Some(ref cdp) = self.cdp {
327            cdp.insert_text(text).await
328        } else if let Some(ref bidi) = self.bidi {
329            bidi.insert_text(text).await
330        } else {
331            Err(SpiderError::Protocol("No protocol session".into()))
332        }
333    }
334
335    pub async fn press_key(&self, key_name: &str) -> Result<()> {
336        let (key, code, key_code) = get_key_params(key_name);
337        if let Some(ref cdp) = self.cdp {
338            cdp.press_key(key, code, key_code).await
339        } else if let Some(ref bidi) = self.bidi {
340            bidi.perform_actions(json!([{
341                "type": "key", "id": "keyboard",
342                "actions": [
343                    {"type": "keyDown", "value": key},
344                    {"type": "keyUp", "value": key},
345                ]
346            }])).await
347        } else {
348            Err(SpiderError::Protocol("No protocol session".into()))
349        }
350    }
351
352    pub async fn key_down(&self, key_name: &str) -> Result<()> {
353        let (key, code, key_code) = get_key_params(key_name);
354        if let Some(ref cdp) = self.cdp {
355            cdp.key_down(key, code, key_code).await
356        } else if let Some(ref bidi) = self.bidi {
357            bidi.perform_actions(json!([{
358                "type": "key", "id": "keyboard",
359                "actions": [{"type": "keyDown", "value": key}]
360            }])).await
361        } else {
362            Err(SpiderError::Protocol("No protocol session".into()))
363        }
364    }
365
366    pub async fn key_up(&self, key_name: &str) -> Result<()> {
367        let (key, code, key_code) = get_key_params(key_name);
368        if let Some(ref cdp) = self.cdp {
369            cdp.key_up(key, code, key_code).await
370        } else if let Some(ref bidi) = self.bidi {
371            bidi.perform_actions(json!([{
372                "type": "key", "id": "keyboard",
373                "actions": [{"type": "keyUp", "value": key}]
374            }])).await
375        } else {
376            Err(SpiderError::Protocol("No protocol session".into()))
377        }
378    }
379
380    pub async fn set_viewport(&self, width: u32, height: u32, dpr: f64, mobile: bool) -> Result<()> {
381        if let Some(ref cdp) = self.cdp {
382            cdp.set_viewport(width, height, dpr, mobile).await
383        } else if let Some(ref bidi) = self.bidi {
384            bidi.evaluate(&format!("window.resizeTo({width}, {height})")).await?;
385            Ok(())
386        } else {
387            Err(SpiderError::Protocol("No protocol session".into()))
388        }
389    }
390
391    pub fn on_protocol_event(&self, method: &str, handler: Arc<dyn Fn(Value) + Send + Sync>) {
392        if let Some(ref cdp) = self.cdp {
393            cdp.on(method, handler);
394        } else if let Some(ref bidi) = self.bidi {
395            bidi.on(method, handler);
396        }
397    }
398
399    pub fn destroy(&self) {
400        if let Some(ref cdp) = self.cdp {
401            cdp.destroy();
402        }
403        if let Some(ref bidi) = self.bidi {
404            bidi.destroy();
405        }
406    }
407}