package tools import ( "context" "fmt" "io" "net/http" "net/url" "regexp" "strings" "time" ) // --- DuckDuckGo Search Provider --- type duckDuckGoSearchProvider struct { maxResults int client *http.Client } func newDuckDuckGoSearchProvider(maxResults int) *duckDuckGoSearchProvider { return &duckDuckGoSearchProvider{ maxResults: normalizeProviderMaxResults(maxResults), client: &http.Client{Timeout: time.Duration(searchTimeoutSeconds) * time.Second}, } } func (p *duckDuckGoSearchProvider) Name() string { return searchProviderDuckDuckGo } func (p *duckDuckGoSearchProvider) Search(ctx context.Context, params searchParams) ([]searchResult, error) { count := clampProviderResultCount(params.Count, p.maxResults) searchURL := fmt.Sprintf("https://html.duckduckgo.com/html/?q=%s", url.QueryEscape(params.Query)) req, err := http.NewRequestWithContext(ctx, "GET", searchURL, nil) if err != nil { return nil, fmt.Errorf("create request: %w", err) } req.Header.Set("User-Agent", webSearchUserAgent) resp, err := p.client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } return extractDDGResults(string(body), count) } var ( ddgLinkRe = regexp.MustCompile(`]*class="[^"]*result__a[^"]*"[^>]*href="([^"]+)"[^>]*>([\s\S]*?)`) ddgSnippetRe = regexp.MustCompile(`([\s\S]*?)`) htmlTagRe = regexp.MustCompile(`<[^>]+>`) ) func extractDDGResults(html string, count int) ([]searchResult, error) { linkMatches := ddgLinkRe.FindAllStringSubmatch(html, count+5) if len(linkMatches) == 0 { return nil, nil } snippetMatches := ddgSnippetRe.FindAllStringSubmatch(html, count+5) var results []searchResult for i := 0; i < len(linkMatches) && i < count; i++ { rawURL := linkMatches[i][1] title := strings.TrimSpace(htmlTagRe.ReplaceAllString(linkMatches[i][2], "")) // DDG wraps URLs with redirect — extract real URL from uddg= param if strings.Contains(rawURL, "uddg=") { if u, err := url.QueryUnescape(rawURL); err == nil { if _, after, ok := strings.Cut(u, "uddg="); ok { extracted := after // uddg value may have trailing ¶ms if ampIdx := strings.Index(extracted, "&"); ampIdx != -1 { extracted = extracted[:ampIdx] } rawURL = extracted } } } desc := "" if i < len(snippetMatches) { desc = strings.TrimSpace(htmlTagRe.ReplaceAllString(snippetMatches[i][1], "")) } results = append(results, searchResult{ Title: title, URL: rawURL, Description: desc, }) } return results, nil }