|
|
|
|
|
|
|
use super::user_agent::random_user_agent; |
|
use crate::handler::paths::{file_path, FileType}; |
|
use crate::models::{ |
|
aggregation_models::{EngineErrorInfo, SearchResult, SearchResults}, |
|
engine_models::{EngineError, EngineHandler}, |
|
}; |
|
use error_stack::Report; |
|
use rand::Rng; |
|
use regex::Regex; |
|
use std::{ |
|
collections::HashMap, |
|
io::{BufReader, Read}, |
|
time::Duration, |
|
}; |
|
use std::{fs::File, io::BufRead}; |
|
use tokio::task::JoinHandle; |
|
|
|
|
|
type FutureVec = Vec<JoinHandle<Result<HashMap<String, SearchResult>, Report<EngineError>>>>; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub async fn aggregate( |
|
query: &str, |
|
page: u32, |
|
random_delay: bool, |
|
debug: bool, |
|
upstream_search_engines: &[EngineHandler], |
|
request_timeout: u8, |
|
safe_search: u8, |
|
) -> Result<SearchResults, Box<dyn std::error::Error>> { |
|
let user_agent: &str = random_user_agent(); |
|
|
|
|
|
if random_delay || !debug { |
|
let mut rng = rand::thread_rng(); |
|
let delay_secs = rng.gen_range(1..10); |
|
tokio::time::sleep(Duration::from_secs(delay_secs)).await; |
|
} |
|
|
|
let mut names: Vec<&str> = Vec::with_capacity(0); |
|
|
|
|
|
let mut tasks: FutureVec = FutureVec::new(); |
|
|
|
for engine_handler in upstream_search_engines { |
|
let (name, search_engine) = engine_handler.to_owned().into_name_engine(); |
|
names.push(name); |
|
let query: String = query.to_owned(); |
|
tasks.push(tokio::spawn(async move { |
|
search_engine |
|
.results( |
|
&query, |
|
page, |
|
user_agent.clone(), |
|
request_timeout, |
|
safe_search, |
|
) |
|
.await |
|
})); |
|
} |
|
|
|
|
|
let mut responses = Vec::with_capacity(tasks.len()); |
|
|
|
for task in tasks { |
|
if let Ok(result) = task.await { |
|
responses.push(result) |
|
} |
|
} |
|
|
|
|
|
let mut result_map: HashMap<String, SearchResult> = HashMap::new(); |
|
let mut engine_errors_info: Vec<EngineErrorInfo> = Vec::new(); |
|
|
|
let mut handle_error = |error: &Report<EngineError>, engine_name: &'static str| { |
|
log::error!("Engine Error: {:?}", error); |
|
engine_errors_info.push(EngineErrorInfo::new( |
|
error.downcast_ref::<EngineError>().unwrap(), |
|
engine_name, |
|
)); |
|
}; |
|
|
|
for _ in 0..responses.len() { |
|
let response = responses.pop().unwrap(); |
|
let engine = names.pop().unwrap(); |
|
|
|
if result_map.is_empty() { |
|
match response { |
|
Ok(results) => { |
|
result_map = results.clone(); |
|
} |
|
Err(error) => { |
|
handle_error(&error, engine); |
|
} |
|
} |
|
continue; |
|
} |
|
|
|
match response { |
|
Ok(result) => { |
|
result.into_iter().for_each(|(key, value)| { |
|
result_map |
|
.entry(key) |
|
.and_modify(|result| { |
|
result.add_engines(engine); |
|
}) |
|
.or_insert_with(|| -> SearchResult { value }); |
|
}); |
|
} |
|
Err(error) => { |
|
handle_error(&error, engine); |
|
} |
|
} |
|
} |
|
|
|
if safe_search >= 3 { |
|
let mut blacklist_map: HashMap<String, SearchResult> = HashMap::new(); |
|
filter_with_lists( |
|
&mut result_map, |
|
&mut blacklist_map, |
|
file_path(FileType::BlockList)?, |
|
)?; |
|
|
|
filter_with_lists( |
|
&mut blacklist_map, |
|
&mut result_map, |
|
file_path(FileType::AllowList)?, |
|
)?; |
|
|
|
drop(blacklist_map); |
|
} |
|
|
|
let results: Vec<SearchResult> = result_map.into_values().collect(); |
|
|
|
Ok(SearchResults::new(results, query, &engine_errors_info)) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pub fn filter_with_lists( |
|
map_to_be_filtered: &mut HashMap<String, SearchResult>, |
|
resultant_map: &mut HashMap<String, SearchResult>, |
|
file_path: &str, |
|
) -> Result<(), Box<dyn std::error::Error>> { |
|
let mut reader = BufReader::new(File::open(file_path)?); |
|
|
|
for line in reader.by_ref().lines() { |
|
let re = Regex::new(line?.trim())?; |
|
|
|
|
|
for (url, search_result) in map_to_be_filtered.clone().into_iter() { |
|
if re.is_match(&url.to_lowercase()) |
|
|| re.is_match(&search_result.title.to_lowercase()) |
|
|| re.is_match(&search_result.description.to_lowercase()) |
|
{ |
|
|
|
resultant_map.insert( |
|
url.to_owned(), |
|
map_to_be_filtered.remove(&url.to_owned()).unwrap(), |
|
); |
|
} |
|
} |
|
} |
|
|
|
Ok(()) |
|
} |
|
|
|
#[cfg(test)] |
|
mod tests { |
|
use super::*; |
|
use smallvec::smallvec; |
|
use std::collections::HashMap; |
|
use std::io::Write; |
|
use tempfile::NamedTempFile; |
|
|
|
#[test] |
|
fn test_filter_with_lists() -> Result<(), Box<dyn std::error::Error>> { |
|
|
|
let mut map_to_be_filtered = HashMap::new(); |
|
map_to_be_filtered.insert( |
|
"https://www.example.com".to_owned(), |
|
SearchResult { |
|
title: "Example Domain".to_owned(), |
|
url: "https://www.example.com".to_owned(), |
|
description: "This domain is for use in illustrative examples in documents." |
|
.to_owned(), |
|
engine: smallvec!["Google".to_owned(), "Bing".to_owned()], |
|
}, |
|
); |
|
map_to_be_filtered.insert( |
|
"https://www.rust-lang.org/".to_owned(), |
|
SearchResult { |
|
title: "Rust Programming Language".to_owned(), |
|
url: "https://www.rust-lang.org/".to_owned(), |
|
description: "A systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.".to_owned(), |
|
engine: smallvec!["Google".to_owned(), "DuckDuckGo".to_owned()], |
|
}, |
|
); |
|
|
|
|
|
let mut file = NamedTempFile::new()?; |
|
writeln!(file, "example")?; |
|
writeln!(file, "rust")?; |
|
file.flush()?; |
|
|
|
let mut resultant_map = HashMap::new(); |
|
filter_with_lists( |
|
&mut map_to_be_filtered, |
|
&mut resultant_map, |
|
file.path().to_str().unwrap(), |
|
)?; |
|
|
|
assert_eq!(resultant_map.len(), 2); |
|
assert!(resultant_map.contains_key("https://www.example.com")); |
|
assert!(resultant_map.contains_key("https://www.rust-lang.org/")); |
|
assert_eq!(map_to_be_filtered.len(), 0); |
|
|
|
Ok(()) |
|
} |
|
|
|
#[test] |
|
fn test_filter_with_lists_wildcard() -> Result<(), Box<dyn std::error::Error>> { |
|
let mut map_to_be_filtered = HashMap::new(); |
|
map_to_be_filtered.insert( |
|
"https://www.example.com".to_owned(), |
|
SearchResult { |
|
title: "Example Domain".to_owned(), |
|
url: "https://www.example.com".to_owned(), |
|
description: "This domain is for use in illustrative examples in documents." |
|
.to_owned(), |
|
engine: smallvec!["Google".to_owned(), "Bing".to_owned()], |
|
}, |
|
); |
|
map_to_be_filtered.insert( |
|
"https://www.rust-lang.org/".to_owned(), |
|
SearchResult { |
|
title: "Rust Programming Language".to_owned(), |
|
url: "https://www.rust-lang.org/".to_owned(), |
|
description: "A systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety.".to_owned(), |
|
engine: smallvec!["Google".to_owned(), "DuckDuckGo".to_owned()], |
|
}, |
|
); |
|
|
|
|
|
let mut file = NamedTempFile::new()?; |
|
writeln!(file, "ex.*le")?; |
|
file.flush()?; |
|
|
|
let mut resultant_map = HashMap::new(); |
|
|
|
filter_with_lists( |
|
&mut map_to_be_filtered, |
|
&mut resultant_map, |
|
file.path().to_str().unwrap(), |
|
)?; |
|
|
|
assert_eq!(resultant_map.len(), 1); |
|
assert!(resultant_map.contains_key("https://www.example.com")); |
|
assert_eq!(map_to_be_filtered.len(), 1); |
|
assert!(map_to_be_filtered.contains_key("https://www.rust-lang.org/")); |
|
|
|
Ok(()) |
|
} |
|
|
|
#[test] |
|
fn test_filter_with_lists_file_not_found() { |
|
let mut map_to_be_filtered = HashMap::new(); |
|
|
|
let mut resultant_map = HashMap::new(); |
|
|
|
|
|
let result = filter_with_lists( |
|
&mut map_to_be_filtered, |
|
&mut resultant_map, |
|
"non-existent-file.txt", |
|
); |
|
|
|
assert!(result.is_err()); |
|
} |
|
|
|
#[test] |
|
fn test_filter_with_lists_invalid_regex() { |
|
let mut map_to_be_filtered = HashMap::new(); |
|
map_to_be_filtered.insert( |
|
"https://www.example.com".to_owned(), |
|
SearchResult { |
|
title: "Example Domain".to_owned(), |
|
url: "https://www.example.com".to_owned(), |
|
description: "This domain is for use in illustrative examples in documents." |
|
.to_owned(), |
|
engine: smallvec!["Google".to_owned(), "Bing".to_owned()], |
|
}, |
|
); |
|
|
|
let mut resultant_map = HashMap::new(); |
|
|
|
|
|
let mut file = NamedTempFile::new().unwrap(); |
|
writeln!(file, "example(").unwrap(); |
|
file.flush().unwrap(); |
|
|
|
let result = filter_with_lists( |
|
&mut map_to_be_filtered, |
|
&mut resultant_map, |
|
file.path().to_str().unwrap(), |
|
); |
|
|
|
assert!(result.is_err()); |
|
} |
|
} |
|
|