use async_trait::async_trait;
use super::super::headers::browser_headers;
use super::super::utils;
use super::super::{
detect_query_language, SearchError, SearchOptions, SearchResult, WebSearchAdapter,
};
pub struct BingAdapter {
client: reqwest::Client,
}
impl BingAdapter {
pub fn new() -> Result<Self, SearchError> {
let client = super::super::shared_search_client()?;
Ok(Self { client })
}
}
#[async_trait]
impl WebSearchAdapter for BingAdapter {
fn name(&self) -> &'static str {
"bing"
}
async fn search(
&self,
query: &str,
options: &SearchOptions,
) -> Result<Vec<SearchResult>, SearchError> {
let max = options.max_results.unwrap_or(5);
let lang = detect_query_language(query);
let setmkt = match lang {
"zh" => "zh-CN",
_ => "en-US",
};
let url = format!(
"https://www.bing.com/search?q={}&count={}&setmkt={}",
urlencoding::encode(query),
max * 2,
setmkt
);
let mut headers = browser_headers();
let accept_lang = match lang {
"zh" => "zh-CN,zh;q=0.9,en;q=0.8",
_ => "en-US,en;q=0.9",
};
if let Ok(val) = reqwest::header::HeaderValue::from_str(accept_lang) {
headers.insert(reqwest::header::ACCEPT_LANGUAGE, val);
}
let resp = self
.client
.get(&url)
.headers(headers)
.send()
.await
.map_err(|e| SearchError::Http(e.to_string()))?;
if !resp.status().is_success() {
return Err(SearchError::Http(format!("HTTP {}", resp.status())));
}
let html = resp
.text()
.await
.map_err(|e| SearchError::Http(e.to_string()))?;
let mut results = parse_bing_html(&html, max);
for r in &mut results {
r.url = resolve_bing_url(&r.url).unwrap_or_else(|| r.url.clone());
}
results = super::super::filter_by_domains(
results,
&options.allowed_domains,
&options.blocked_domains,
);
results.truncate(max);
Ok(results)
}
}
pub fn parse_bing_html(html: &str, limit: usize) -> Vec<SearchResult> {
let mut results = Vec::new();
for chunk in html.split("class=\"b_algo\"") {
if results.len() >= limit {
break;
}
let href = extract_bing_href(chunk);
let title = utils::extract_text_between(chunk, "<h2>", "</h2>")
.or_else(|| utils::extract_text_between(chunk, "<a", "</a>"))
.unwrap_or_else(|| "untitled".into());
let clean_title = utils::strip_html_tags(&title);
let snippet = extract_p_text(chunk)
.map(|s| utils::strip_html_tags(&s).trim().to_string())
.filter(|s| !s.is_empty());
if let Some(url) = href {
if !clean_title.trim().is_empty() && !url.is_empty() {
results.push(SearchResult {
title: clean_title.trim().to_string(),
url,
snippet,
});
}
}
}
results
}
fn extract_bing_href(chunk: &str) -> Option<String> {
if let Some(h2_start) = chunk.find("<h2") {
let h2_rest = &chunk[h2_start..];
if let Some(a_start) = h2_rest.find("<a") {
let a_rest = &h2_rest[a_start..];
if let Some(href_start) = a_rest.find("href=\"") {
let rest = &a_rest[href_start + 6..];
if let Some(end) = rest.find('"') {
let href = &rest[..end];
if href.starts_with("http") {
return Some(href.to_string());
}
}
}
}
}
if let Some(start) = chunk.find("href=\"http") {
let rest = &chunk[start + 6..];
if let Some(end) = rest.find('"') {
return Some(rest[..end].to_string());
}
}
None
}
fn extract_p_text(chunk: &str) -> Option<String> {
let start = chunk.find("<p")? + 2;
let rest = &chunk[start..];
let gt = rest.find('>')? + 1;
let rest = &rest[gt..];
let end = rest.find("</p>")?;
Some(rest[..end].to_string())
}
pub fn resolve_bing_url(raw_url: &str) -> Option<String> {
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
if !raw_url.contains("bing.com") && raw_url.starts_with("http") {
return Some(raw_url.to_string());
}
let u_match = raw_url.split("u=").nth(1)?;
let encoded = u_match.split('&').next()?;
if encoded.len() < 3 {
return None;
}
let prefix = &encoded[..2];
let b64 = &encoded[2..];
let decoded = URL_SAFE_NO_PAD.decode(b64).ok()?;
let decoded_str = String::from_utf8(decoded).ok()?;
match prefix {
"a1" if decoded_str.starts_with("http") => Some(decoded_str),
"a0" if decoded_str.starts_with("http") => Some(decoded_str),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
#[test]
fn resolve_direct_url_passes_through() {
let url = "https://example.com/page";
assert_eq!(resolve_bing_url(url), Some(url.to_string()));
}
#[test]
fn resolve_bing_redirect_https() {
let encoded = URL_SAFE_NO_PAD.encode(b"https://example.com");
let redirect = format!("https://www.bing.com/ck/a?...&u=a1{}", encoded);
assert_eq!(
resolve_bing_url(&redirect),
Some("https://example.com".to_string())
);
}
#[test]
fn resolve_bing_redirect_http() {
let encoded = URL_SAFE_NO_PAD.encode(b"http://example.org");
let redirect = format!("https://www.bing.com/ck/a?u=a0{}", encoded);
assert_eq!(
resolve_bing_url(&redirect),
Some("http://example.org".to_string())
);
}
#[test]
fn resolve_invalid_bing_redirect_returns_none() {
let redirect = "https://www.bing.com/ck/a?u=xxinvalid";
assert_eq!(resolve_bing_url(redirect), None);
}
}