1use crate::browser::builder::BrowserBuilder;
2use crate::dom::{DomTree, FormInfo, InteractiveElement, LinkInfo, PageObservation, SearchResults};
3use crate::google::{
4 GenericGoogleResult, GoogleAutocompleteResult, GoogleEndpoints, GoogleParser,
5 GoogleSearchResult,
6};
7use crate::js::context::JsRuntime;
8use crate::network::client::{FetchResult, NetworkClient};
9use crate::network::fingerprint::DeviceProfile;
10use anyhow::{Context, Result};
11use serde::{Deserialize, Serialize};
12#[derive(Debug, Serialize, Deserialize)]
13pub struct NavigationReport {
14 pub status: u16,
15 pub requested_url: String,
16 pub final_url: String,
17 pub page_title: String,
18 pub is_captcha_detected: bool,
19 pub html_bytes: usize,
20}
21
22pub struct BrowserTab {
23 network: NetworkClient,
24 dom: Option<DomTree>,
25 js: JsRuntime,
26 pub current_url: Option<String>,
27}
28
29impl BrowserTab {
30 pub fn new() -> Result<Self> {
31 Self::with_profile(DeviceProfile::ChromeWindows)
32 }
33
34 pub fn builder() -> BrowserBuilder {
35 BrowserBuilder::new()
36 }
37
38 pub fn with_profile(profile: DeviceProfile) -> Result<Self> {
39 let network = NetworkClient::with_profile(profile)?;
40 Self::from_network(network)
41 }
42
43 pub fn from_network(network: NetworkClient) -> Result<Self> {
44 let js = JsRuntime::with_fingerprint(&network.fingerprint)?;
45 Ok(Self {
46 network,
47 dom: None,
48 js,
49 current_url: None,
50 })
51 }
52
53 pub fn profile(&self) -> DeviceProfile {
54 self.network.profile
55 }
56
57 pub fn set_profile(&mut self, profile: DeviceProfile) -> Result<()> {
58 self.network.set_profile(profile)?;
59 self.js = JsRuntime::with_fingerprint(&self.network.fingerprint)?;
60 Ok(())
61 }
62
63 pub async fn navigate(&mut self, url: &str) -> Result<NavigationReport> {
64 let fetch_result: FetchResult = self.network.fetch(url).await?;
65 let dom = DomTree::parse(&fetch_result.html)?;
66
67 let search_results = dom.parse_google_search_results();
68 let page_title = search_results.page_title.clone();
69 let html_bytes = fetch_result.html.len();
70
71 let _ = self
72 .js
73 .update_page_state(&fetch_result.final_url, &page_title);
74
75 self.dom = Some(dom);
76 self.current_url = Some(fetch_result.final_url.clone());
77
78 Ok(NavigationReport {
79 status: fetch_result.status,
80 requested_url: url.to_string(),
81 final_url: fetch_result.final_url,
82 page_title,
83 is_captcha_detected: fetch_result.is_captcha_detected,
84 html_bytes,
85 })
86 }
87
88 pub async fn search(&mut self, query: &str) -> Result<NavigationReport> {
90 self.search_google(query, None).await
91 }
92
93 pub async fn search_google(
95 &mut self,
96 query: &str,
97 mode: Option<&str>,
98 ) -> Result<NavigationReport> {
99 let encoded: String = query
100 .chars()
101 .map(|c| match c {
102 ' ' => "+".to_string(),
103 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' | '~' => c.to_string(),
104 _ => format!("%{:02X}", c as u8),
105 })
106 .collect();
107
108 let url = match mode {
109 Some("ai") | Some("udm50") => {
110 format!("https://www.google.com/search?q={}&udm=50", encoded)
111 }
112 Some("web") | Some("udm14") => {
113 format!("https://www.google.com/search?q={}&udm=14", encoded)
114 }
115 Some("images") | Some("udm2") => {
116 format!("https://www.google.com/search?q={}&udm=2", encoded)
117 }
118 Some("news") => format!("https://www.google.com/search?q={}&tbm=nws", encoded),
119 _ => format!("https://www.google.com/search?q={}", encoded),
120 };
121 self.navigate(&url).await
122 }
123
124 pub async fn google_search(&mut self, query: &str) -> Result<GoogleSearchResult> {
125 let url = GoogleEndpoints::search(query);
126 let nav = self.navigate(&url).await?;
127 let html = self
128 .dom
129 .as_ref()
130 .map(|d| d.raw_content.clone())
131 .unwrap_or_default();
132 Ok(GoogleParser::parse_search_results(&html, &nav.final_url))
133 }
134
135 pub async fn google_web_search(&mut self, query: &str) -> Result<GoogleSearchResult> {
136 let url = GoogleEndpoints::web_search(query);
137 let nav = self.navigate(&url).await?;
138 let html = self
139 .dom
140 .as_ref()
141 .map(|d| d.raw_content.clone())
142 .unwrap_or_default();
143 Ok(GoogleParser::parse_search_results(&html, &nav.final_url))
144 }
145
146 pub async fn google_image_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
147 let url = GoogleEndpoints::image_search(query);
148 let nav = self.navigate(&url).await?;
149 let html = self
150 .dom
151 .as_ref()
152 .map(|d| d.raw_content.clone())
153 .unwrap_or_default();
154 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
155 }
156
157 pub async fn google_video_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
158 let url = GoogleEndpoints::video_search(query);
159 let nav = self.navigate(&url).await?;
160 let html = self
161 .dom
162 .as_ref()
163 .map(|d| d.raw_content.clone())
164 .unwrap_or_default();
165 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
166 }
167
168 pub async fn google_short_video_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
169 let url = GoogleEndpoints::short_video_search(query);
170 let nav = self.navigate(&url).await?;
171 let html = self
172 .dom
173 .as_ref()
174 .map(|d| d.raw_content.clone())
175 .unwrap_or_default();
176 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
177 }
178
179 pub async fn google_news_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
180 let url = GoogleEndpoints::news_search(query);
181 let nav = self.navigate(&url).await?;
182 let html = self
183 .dom
184 .as_ref()
185 .map(|d| d.raw_content.clone())
186 .unwrap_or_default();
187 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
188 }
189
190 pub async fn google_forum_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
191 let url = GoogleEndpoints::forum_search(query);
192 let nav = self.navigate(&url).await?;
193 let html = self
194 .dom
195 .as_ref()
196 .map(|d| d.raw_content.clone())
197 .unwrap_or_default();
198 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
199 }
200
201 pub async fn google_shopping_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
202 let url = GoogleEndpoints::shopping_search(query);
203 let nav = self.navigate(&url).await?;
204 let html = self
205 .dom
206 .as_ref()
207 .map(|d| d.raw_content.clone())
208 .unwrap_or_default();
209 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
210 }
211
212 pub async fn google_product_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
213 let url = GoogleEndpoints::product_search(query);
214 let nav = self.navigate(&url).await?;
215 let html = self
216 .dom
217 .as_ref()
218 .map(|d| d.raw_content.clone())
219 .unwrap_or_default();
220 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
221 }
222
223 pub async fn google_books_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
224 let url = GoogleEndpoints::books_search(query);
225 let nav = self.navigate(&url).await?;
226 let html = self
227 .dom
228 .as_ref()
229 .map(|d| d.raw_content.clone())
230 .unwrap_or_default();
231 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
232 }
233
234 pub async fn google_autocomplete(&mut self, query: &str) -> Result<GoogleAutocompleteResult> {
235 let url = GoogleEndpoints::autocomplete(query);
236 let fetch_result = self.network.fetch(&url).await?;
237 Ok(GoogleParser::parse_autocomplete(&fetch_result.html))
238 }
239
240 pub async fn google_ai_overview(&mut self, query: &str) -> Result<GenericGoogleResult> {
241 let url = GoogleEndpoints::ai_overview(query);
242 let nav = self.navigate(&url).await?;
243 let html = self
244 .dom
245 .as_ref()
246 .map(|d| d.raw_content.clone())
247 .unwrap_or_default();
248 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
249 }
250
251 pub async fn google_ai_mode(&mut self, query: &str) -> Result<GenericGoogleResult> {
252 let url = GoogleEndpoints::ai_mode(query);
253 let nav = self.navigate(&url).await?;
254 let html = self
255 .dom
256 .as_ref()
257 .map(|d| d.raw_content.clone())
258 .unwrap_or_default();
259 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
260 }
261
262 pub async fn google_scholar_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
263 let url = GoogleEndpoints::scholar_search(query);
264 let nav = self.navigate(&url).await?;
265 let html = self
266 .dom
267 .as_ref()
268 .map(|d| d.raw_content.clone())
269 .unwrap_or_default();
270 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
271 }
272
273 pub async fn google_patents_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
274 let url = GoogleEndpoints::patents_search(query);
275 let nav = self.navigate(&url).await?;
276 let html = self
277 .dom
278 .as_ref()
279 .map(|d| d.raw_content.clone())
280 .unwrap_or_default();
281 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
282 }
283
284 pub async fn google_maps_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
285 let url = GoogleEndpoints::maps_search(query);
286 let nav = self.navigate(&url).await?;
287 let html = self
288 .dom
289 .as_ref()
290 .map(|d| d.raw_content.clone())
291 .unwrap_or_default();
292 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
293 }
294
295 pub async fn google_finance_quote(&mut self, ticker: &str) -> Result<GenericGoogleResult> {
296 let url = GoogleEndpoints::finance_quote(ticker);
297 let nav = self.navigate(&url).await?;
298 let html = self
299 .dom
300 .as_ref()
301 .map(|d| d.raw_content.clone())
302 .unwrap_or_default();
303 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
304 }
305
306 pub async fn google_trends_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
307 let url = GoogleEndpoints::trends_search(query);
308 let nav = self.navigate(&url).await?;
309 let html = self
310 .dom
311 .as_ref()
312 .map(|d| d.raw_content.clone())
313 .unwrap_or_default();
314 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
315 }
316
317 pub async fn google_flights_search(
318 &mut self,
319 origin: &str,
320 dest: &str,
321 ) -> Result<GenericGoogleResult> {
322 let url = GoogleEndpoints::flights_search(origin, dest);
323 let nav = self.navigate(&url).await?;
324 let html = self
325 .dom
326 .as_ref()
327 .map(|d| d.raw_content.clone())
328 .unwrap_or_default();
329 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
330 }
331
332 pub async fn google_hotels_search(&mut self, location: &str) -> Result<GenericGoogleResult> {
333 let url = GoogleEndpoints::hotels_search(location);
334 let nav = self.navigate(&url).await?;
335 let html = self
336 .dom
337 .as_ref()
338 .map(|d| d.raw_content.clone())
339 .unwrap_or_default();
340 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
341 }
342
343 pub async fn google_travel_explore(
344 &mut self,
345 destination: &str,
346 ) -> Result<GenericGoogleResult> {
347 let url = GoogleEndpoints::travel_explore(destination);
348 let nav = self.navigate(&url).await?;
349 let html = self
350 .dom
351 .as_ref()
352 .map(|d| d.raw_content.clone())
353 .unwrap_or_default();
354 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
355 }
356
357 pub async fn youtube_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
358 let url = GoogleEndpoints::youtube_search(query);
359 let nav = self.navigate(&url).await?;
360 let html = self
361 .dom
362 .as_ref()
363 .map(|d| d.raw_content.clone())
364 .unwrap_or_default();
365 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
366 }
367
368 pub async fn youtube_shorts_search(&mut self, query: &str) -> Result<GenericGoogleResult> {
369 let url = GoogleEndpoints::youtube_shorts_search(query);
370 let nav = self.navigate(&url).await?;
371 let html = self
372 .dom
373 .as_ref()
374 .map(|d| d.raw_content.clone())
375 .unwrap_or_default();
376 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
377 }
378
379 pub async fn youtube_video(&mut self, video_id: &str) -> Result<GenericGoogleResult> {
380 let url = GoogleEndpoints::youtube_video(video_id);
381 let nav = self.navigate(&url).await?;
382 let html = self
383 .dom
384 .as_ref()
385 .map(|d| d.raw_content.clone())
386 .unwrap_or_default();
387 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
388 }
389
390 pub async fn youtube_channel(&mut self, channel: &str) -> Result<GenericGoogleResult> {
391 let url = GoogleEndpoints::youtube_channel(channel);
392 let nav = self.navigate(&url).await?;
393 let html = self
394 .dom
395 .as_ref()
396 .map(|d| d.raw_content.clone())
397 .unwrap_or_default();
398 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
399 }
400
401 pub async fn youtube_playlist(&mut self, playlist_id: &str) -> Result<GenericGoogleResult> {
402 let url = GoogleEndpoints::youtube_playlist(playlist_id);
403 let nav = self.navigate(&url).await?;
404 let html = self
405 .dom
406 .as_ref()
407 .map(|d| d.raw_content.clone())
408 .unwrap_or_default();
409 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
410 }
411
412 pub async fn google_lens_visual_matches(
413 &mut self,
414 image_url: &str,
415 ) -> Result<GenericGoogleResult> {
416 let url = GoogleEndpoints::lens_visual_matches(image_url);
417 let nav = self.navigate(&url).await?;
418 let html = self
419 .dom
420 .as_ref()
421 .map(|d| d.raw_content.clone())
422 .unwrap_or_default();
423 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
424 }
425
426 pub async fn google_lens_exact_matches(
427 &mut self,
428 image_url: &str,
429 ) -> Result<GenericGoogleResult> {
430 let url = GoogleEndpoints::lens_exact_matches(image_url);
431 let nav = self.navigate(&url).await?;
432 let html = self
433 .dom
434 .as_ref()
435 .map(|d| d.raw_content.clone())
436 .unwrap_or_default();
437 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
438 }
439
440 pub async fn google_lens_products(&mut self, image_url: &str) -> Result<GenericGoogleResult> {
441 let url = GoogleEndpoints::lens_products(image_url);
442 let nav = self.navigate(&url).await?;
443 let html = self
444 .dom
445 .as_ref()
446 .map(|d| d.raw_content.clone())
447 .unwrap_or_default();
448 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
449 }
450
451 pub async fn google_lens_about_image(
452 &mut self,
453 image_url: &str,
454 ) -> Result<GenericGoogleResult> {
455 let url = GoogleEndpoints::lens_about_image(image_url);
456 let nav = self.navigate(&url).await?;
457 let html = self
458 .dom
459 .as_ref()
460 .map(|d| d.raw_content.clone())
461 .unwrap_or_default();
462 Ok(GoogleParser::parse_generic(&html, &nav.final_url))
463 }
464
465 pub fn google_capabilities(&self) -> Vec<&'static str> {
466 vec![
467 "google_search",
468 "google_web_search",
469 "google_image_search",
470 "google_video_search",
471 "google_short_video_search",
472 "google_news_search",
473 "google_forum_search",
474 "google_shopping_search",
475 "google_product_search",
476 "google_books_search",
477 "google_autocomplete",
478 "google_ai_overview",
479 "google_ai_mode",
480 "google_scholar_search",
481 "google_patents_search",
482 "google_maps_search",
483 "google_finance_quote",
484 "google_trends_search",
485 "google_flights_search",
486 "google_hotels_search",
487 "google_travel_explore",
488 "youtube_search",
489 "youtube_shorts_search",
490 "youtube_video",
491 "youtube_channel",
492 "youtube_playlist",
493 "google_lens_visual_matches",
494 "google_lens_exact_matches",
495 "google_lens_products",
496 "google_lens_about_image",
497 "google_capabilities",
498 ]
499 }
500
501 pub fn set_content(&mut self, html: &str, url: Option<&str>) -> Result<NavigationReport> {
502 let dom = DomTree::parse(html)?;
503 let search_results = dom.parse_google_search_results();
504 let page_title = search_results.page_title.clone();
505 let final_url = url.unwrap_or("about:blank").to_string();
506 let html_bytes = html.len();
507
508 let _ = self.js.update_page_state(&final_url, &page_title);
509
510 self.dom = Some(dom);
511 self.current_url = Some(final_url.clone());
512
513 Ok(NavigationReport {
514 status: 200,
515 requested_url: final_url.clone(),
516 final_url,
517 page_title,
518 is_captcha_detected: search_results.is_captcha_detected,
519 html_bytes,
520 })
521 }
522
523 pub fn observe(&self) -> Option<PageObservation> {
524 let dom = self.dom.as_ref()?;
525 let url = self.current_url.clone().unwrap_or_default();
526 let results = dom.parse_google_search_results();
527 let elements = dom.extract_interactive_elements(Some(&url));
528
529 let mut tree_lines = Vec::new();
530 for el in &elements {
531 tree_lines.push(el.to_agent_string());
532 }
533
534 let agent_tree_text = tree_lines.join("\n");
535 let content_summary_markdown = dom.extract_markdown(None, Some(&url));
536
537 Some(PageObservation {
538 url,
539 title: results.page_title,
540 is_captcha_detected: results.is_captcha_detected,
541 interactive_elements: elements,
542 agent_tree_text,
543 content_summary_markdown,
544 })
545 }
546
547 pub fn evaluate_js(&mut self, code: &str) -> Result<String> {
548 self.js.evaluate(code)
549 }
550
551 pub fn extract_dom(&self, selector: Option<&str>) -> Option<String> {
552 self.dom.as_ref().and_then(|d| d.extract(selector))
553 }
554
555 pub fn extract_markdown(&self, selector: Option<&str>) -> Option<String> {
556 self.dom
557 .as_ref()
558 .map(|d| d.extract_markdown(selector, self.current_url.as_deref()))
559 }
560
561 pub fn extract_interactive_elements(&self) -> Vec<InteractiveElement> {
562 self.dom
563 .as_ref()
564 .map(|d| d.extract_interactive_elements(self.current_url.as_deref()))
565 .unwrap_or_default()
566 }
567
568 pub fn extract_links(&self) -> Vec<LinkInfo> {
569 self.dom
570 .as_ref()
571 .map(|d| d.extract_links(self.current_url.as_deref()))
572 .unwrap_or_default()
573 }
574
575 pub fn extract_forms(&self) -> Vec<FormInfo> {
576 self.dom
577 .as_ref()
578 .map(|d| d.extract_forms())
579 .unwrap_or_default()
580 }
581
582 pub fn extract_search_results(&self) -> Option<SearchResults> {
583 self.dom.as_ref().map(|d| d.parse_google_search_results())
584 }
585
586 pub async fn screenshot_async(&self) -> Option<crate::dom::ScreenshotResult> {
587 let dom = self.dom.as_ref()?;
588 let url = self.current_url.as_deref().unwrap_or("about:blank");
589 let results = dom.parse_google_search_results();
590 Some(
591 dom.screenshot_async(url, &results.page_title, self.current_url.as_deref())
592 .await,
593 )
594 }
595
596 pub fn screenshot(&self) -> Option<crate::dom::ScreenshotResult> {
597 let dom = self.dom.as_ref()?;
598 let url = self.current_url.as_deref().unwrap_or("about:blank");
599 let results = dom.parse_google_search_results();
600 Some(dom.screenshot(url, &results.page_title, self.current_url.as_deref()))
601 }
602
603 pub fn screenshot_svg(&self) -> Option<String> {
604 self.screenshot().map(|s| s.svg)
605 }
606
607 pub fn screenshot_layout(&self) -> Option<String> {
608 self.screenshot().map(|s| s.layout_wireframe)
609 }
610
611 pub async fn act_click(&mut self, target: &str) -> Result<Option<NavigationReport>> {
612 if let Ok(idx) = target.parse::<usize>() {
614 let elements = self.extract_interactive_elements();
615 if let Some(el) = elements.iter().find(|e| e.index == idx) {
616 if !el.href.is_empty() {
617 let report = self.navigate(&el.href).await?;
618 return Ok(Some(report));
619 }
620 return self.click(&el.selector).await;
621 }
622 }
623
624 self.click(target).await
625 }
626
627 pub async fn act_type(&mut self, target: &str, text: &str) -> Result<String> {
628 if let Ok(idx) = target.parse::<usize>() {
630 let elements = self.extract_interactive_elements();
631 if let Some(el) = elements.iter().find(|e| e.index == idx) {
632 return self.type_text(&el.selector, text);
633 }
634 }
635
636 self.type_text(target, text)
637 }
638
639 pub async fn click(&mut self, selector_or_text: &str) -> Result<Option<NavigationReport>> {
640 let links = self.extract_links();
641
642 if let Some(link) = links.iter().find(|l| {
644 l.text.eq_ignore_ascii_case(selector_or_text)
645 || l.href.contains(selector_or_text)
646 || l.text
647 .to_lowercase()
648 .contains(&selector_or_text.to_lowercase())
649 }) {
650 let report = self.navigate(&link.href).await?;
651 return Ok(Some(report));
652 }
653
654 let sel_json = serde_json::to_string(selector_or_text)
656 .unwrap_or_else(|_| format!("\"{}\"", selector_or_text));
657 let js_code = format!(
658 "var el = document.querySelector({}); if (el) {{ try {{ el.click(); }} catch(e){{}} true; }} else {{ false; }}",
659 sel_json
660 );
661 let _ = self.evaluate_js(&js_code);
662
663 Ok(None)
664 }
665
666 pub fn type_text(&mut self, selector: &str, text: &str) -> Result<String> {
667 let sel_json =
668 serde_json::to_string(selector).unwrap_or_else(|_| format!("\"{}\"", selector));
669 let text_json = serde_json::to_string(text).unwrap_or_else(|_| format!("\"{}\"", text));
670 let js_code = format!(
671 r#"var el = document.querySelector({sel});
672 if (el) {{
673 if ('value' in el) {{
674 el.value = {txt};
675 }} else {{
676 el.innerText = {txt};
677 el.textContent = {txt};
678 }}
679 try {{
680 el.dispatchEvent(new Event('input', {{ bubbles: true }}));
681 el.dispatchEvent(new Event('change', {{ bubbles: true }}));
682 }} catch(e) {{}}
683 'updated';
684 }} else {{
685 'not_found';
686 }}"#,
687 sel = sel_json,
688 txt = text_json
689 );
690 self.evaluate_js(&js_code)
691 .context("Failed to evaluate type action")
692 }
693}