1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5use super::{SearchResult, SearchResults};
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct DuckDuckGoResponse {
9 #[serde(rename = "Abstract")]
10 pub abstract_text: String,
11 #[serde(rename = "AbstractText")]
12 pub abstract_text_alt: String,
13 #[serde(rename = "AbstractSource")]
14 pub abstract_source: String,
15 #[serde(rename = "AbstractURL")]
16 pub abstract_url: String,
17 #[serde(rename = "Image")]
18 pub image: String,
19 #[serde(rename = "Heading")]
20 pub heading: String,
21 #[serde(rename = "Answer")]
22 pub answer: String,
23 #[serde(rename = "AnswerType")]
24 pub answer_type: String,
25 #[serde(rename = "Definition")]
26 pub definition: String,
27 #[serde(rename = "DefinitionSource")]
28 pub definition_source: String,
29 #[serde(rename = "DefinitionURL")]
30 pub definition_url: String,
31 #[serde(rename = "RelatedTopics")]
32 pub related_topics: Vec<DuckDuckGoRelatedTopic>,
33 #[serde(rename = "Results")]
34 pub results: Vec<DuckDuckGoResult>,
35 #[serde(rename = "Type")]
36 pub result_type: String,
37 #[serde(rename = "Redirect")]
38 pub redirect: String,
39}
40
41#[derive(Debug, Serialize, Deserialize)]
42pub struct DuckDuckGoRelatedTopic {
43 #[serde(rename = "Result")]
44 pub result: Option<String>,
45 #[serde(rename = "Icon")]
46 pub icon: Option<DuckDuckGoIcon>,
47 #[serde(rename = "FirstURL")]
48 pub first_url: Option<String>,
49 #[serde(rename = "Text")]
50 pub text: Option<String>,
51}
52
53#[derive(Debug, Serialize, Deserialize)]
54pub struct DuckDuckGoResult {
55 #[serde(rename = "Result")]
56 pub result: String,
57 #[serde(rename = "FirstURL")]
58 pub first_url: String,
59 #[serde(rename = "Icon")]
60 pub icon: Option<DuckDuckGoIcon>,
61 #[serde(rename = "Text")]
62 pub text: String,
63}
64
65#[derive(Debug, Serialize, Deserialize)]
66pub struct DuckDuckGoIcon {
67 #[serde(rename = "URL")]
68 pub url: String,
69 #[serde(rename = "Height")]
70 pub height: Option<serde_json::Value>,
71 #[serde(rename = "Width")]
72 pub width: Option<serde_json::Value>,
73}
74
75pub struct DuckDuckGoProvider {
76 pub url: String,
77 pub headers: HashMap<String, String>,
78}
79
80impl DuckDuckGoProvider {
81 pub fn new(url: String, headers: HashMap<String, String>) -> Self {
82 Self { url, headers }
83 }
84
85 pub async fn search(&self, query: &str, count: Option<usize>) -> Result<SearchResults> {
86 let client = reqwest::Client::new();
87
88 let params = vec![
90 ("q", query.to_string()),
91 ("format", "json".to_string()),
92 ("no_redirect", "1".to_string()),
93 ("no_html", "1".to_string()),
94 ("skip_disambig", "1".to_string()),
95 ];
96
97 crate::debug_log!(
98 "DuckDuckGo: Making GET request to {} with params: {:?}",
99 self.url,
100 params
101 );
102
103 let mut request = client.get(&self.url).query(¶ms);
104
105 for (key, value) in &self.headers {
107 request = request.header(key, value);
108 }
109
110 let response = request.send().await?;
111
112 let status = response.status();
113 crate::debug_log!("DuckDuckGo: Received response with status: {}", status);
114
115 if !status.is_success() {
116 let error_text = response.text().await.unwrap_or_default();
117 crate::debug_log!("DuckDuckGo: Error response: {}", error_text);
118 anyhow::bail!(
119 "DuckDuckGo request failed with status {}: {}",
120 status,
121 error_text
122 );
123 }
124
125 let response_text = response.text().await?;
126 crate::debug_log!(
127 "DuckDuckGo: Response body length: {} bytes",
128 response_text.len()
129 );
130
131 let ddg_response: DuckDuckGoResponse = serde_json::from_str(&response_text)
132 .map_err(|e| anyhow::anyhow!("Failed to parse DuckDuckGo response: {}", e))?;
133
134 crate::debug_log!("DuckDuckGo: Successfully parsed response");
135
136 let mut results = Vec::new();
137 let max_results = count.unwrap_or(10);
138
139 if !ddg_response.abstract_text.is_empty() && !ddg_response.abstract_url.is_empty() {
141 let title = if !ddg_response.heading.is_empty() {
142 ddg_response.heading.clone()
143 } else {
144 format!("About {}", query)
145 };
146
147 results.push(SearchResult {
148 title,
149 url: ddg_response.abstract_url.clone(),
150 snippet: ddg_response.abstract_text.clone(),
151 published_date: None,
152 author: Some(ddg_response.abstract_source.clone()),
153 score: None,
154 });
155 }
156
157 if !ddg_response.definition.is_empty() && !ddg_response.definition_url.is_empty() {
159 results.push(SearchResult {
160 title: format!("Definition: {}", query),
161 url: ddg_response.definition_url.clone(),
162 snippet: ddg_response.definition.clone(),
163 published_date: None,
164 author: Some(ddg_response.definition_source.clone()),
165 score: None,
166 });
167 }
168
169 if !ddg_response.answer.is_empty() {
171 results.push(SearchResult {
172 title: format!("Answer: {}", query),
173 url: format!("https://duckduckgo.com/?q={}", urlencoding::encode(query)),
174 snippet: ddg_response.answer.clone(),
175 published_date: None,
176 author: Some("DuckDuckGo".to_string()),
177 score: None,
178 });
179 }
180
181 for result in ddg_response
183 .results
184 .iter()
185 .take(max_results.saturating_sub(results.len()))
186 {
187 if !result.text.is_empty() && !result.first_url.is_empty() {
188 let title = if let Some(dash_pos) = result.text.find(" - ") {
190 result.text[..dash_pos].to_string()
191 } else {
192 result.text.clone()
193 };
194
195 let snippet = if let Some(dash_pos) = result.text.find(" - ") {
196 result.text[dash_pos + 3..].to_string()
197 } else {
198 String::new()
199 };
200
201 results.push(SearchResult {
202 title,
203 url: result.first_url.clone(),
204 snippet,
205 published_date: None,
206 author: None,
207 score: None,
208 });
209 }
210 }
211
212 if results.len() < max_results {
214 for topic in ddg_response
215 .related_topics
216 .iter()
217 .take(max_results.saturating_sub(results.len()))
218 {
219 if let (Some(text), Some(url)) = (&topic.text, &topic.first_url) {
220 if !text.is_empty() && !url.is_empty() {
221 let title = if let Some(dash_pos) = text.find(" - ") {
223 text[..dash_pos].to_string()
224 } else {
225 text.clone()
226 };
227
228 let snippet = if let Some(dash_pos) = text.find(" - ") {
229 text[dash_pos + 3..].to_string()
230 } else {
231 String::new()
232 };
233
234 results.push(SearchResult {
235 title,
236 url: url.clone(),
237 snippet,
238 published_date: None,
239 author: None,
240 score: None,
241 });
242 }
243 }
244 }
245 }
246
247 crate::debug_log!(
248 "DuckDuckGo: Successfully extracted {} results",
249 results.len()
250 );
251
252 Ok(SearchResults {
253 query: query.to_string(),
254 provider: "DuckDuckGo".to_string(),
255 results,
256 total_results: None, search_time_ms: None, })
259 }
260}
261
262pub async fn search(
264 provider_config: &super::SearchProviderConfig,
265 query: &str,
266 count: Option<usize>,
267) -> anyhow::Result<super::SearchResults> {
268 let provider =
269 DuckDuckGoProvider::new(provider_config.url.clone(), provider_config.headers.clone());
270
271 provider.search(query, count).await
272}