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