1use 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
13pub struct ProtocolAdapterOptions {
15 pub command_timeout_ms: Option<u64>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq)]
20pub enum ProtocolType {
21 Cdp,
22 Bidi,
23 Auto,
24}
25
26pub 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 pub fn route_message(&self, data: &str) {
71 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 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 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 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 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 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}