sparrow/tools/
web_search.rs1use std::process::Command;
4
5pub fn web_search(query: &str, max_results: usize) -> anyhow::Result<Vec<SearchResult>> {
7 let output = Command::new("curl")
9 .args([
10 "-s",
11 "-A", "sparrow/0.5",
12 "--max-time", "10",
13 &format!("https://api.duckduckgo.com/?q={}&format=json&no_html=1",
14 urlencoding(query)),
15 ])
16 .output()?;
17
18 if output.status.success() {
19 let json: serde_json::Value = serde_json::from_slice(&output.stdout)?;
20 return parse_duckduckgo(&json, max_results);
21 }
22
23 if let Ok(output) = Command::new("curl")
25 .args(["-s", "--max-time", "10",
26 &format!("https://searx.be/search?q={}&format=json", urlencoding(query))])
27 .output()
28 {
29 if output.status.success() {
30 let json: serde_json::Value = serde_json::from_slice(&output.stdout)?;
31 return parse_searx(&json, max_results);
32 }
33 }
34
35 Ok(vec![SearchResult {
37 title: format!("Search: {}", query),
38 url: format!("https://duckduckgo.com/?q={}", urlencoding(query)),
39 snippet: "No results. Open the URL in your browser to search manually.".into(),
40 }])
41}
42
43#[derive(Debug, Clone, serde::Serialize)]
44pub struct SearchResult {
45 pub title: String,
46 pub url: String,
47 pub snippet: String,
48}
49
50fn parse_duckduckgo(json: &serde_json::Value, max: usize) -> anyhow::Result<Vec<SearchResult>> {
51 let mut results = Vec::new();
52 if let Some(items) = json.get("RelatedTopics").and_then(|t| t.as_array()) {
53 for item in items.iter().take(max) {
54 if let (Some(text), Some(url)) = (item.get("Text").and_then(|t| t.as_str()),
55 item.get("FirstURL").and_then(|u| u.as_str())) {
56 results.push(SearchResult {
57 title: text.chars().take(80).collect(),
58 url: url.to_string(),
59 snippet: text.to_string(),
60 });
61 }
62 }
63 }
64 Ok(results)
65}
66
67fn parse_searx(json: &serde_json::Value, max: usize) -> anyhow::Result<Vec<SearchResult>> {
68 let mut results = Vec::new();
69 if let Some(items) = json.get("results").and_then(|r| r.as_array()) {
70 for item in items.iter().take(max) {
71 results.push(SearchResult {
72 title: item.get("title").and_then(|t| t.as_str()).unwrap_or("").to_string(),
73 url: item.get("url").and_then(|u| u.as_str()).unwrap_or("").to_string(),
74 snippet: item.get("content").or(item.get("snippet"))
75 .and_then(|s| s.as_str()).unwrap_or("").to_string(),
76 });
77 }
78 }
79 Ok(results)
80}
81
82fn urlencoding(s: &str) -> String {
83 s.replace(' ', "+")
84 .replace('&', "%26")
85 .replace('=', "%3D")
86 .replace('#', "%23")
87}