package main import ( "encoding/json" "fmt" "io" "net/http" "net/url" "sort" "sync" "time" ) // SuggestionSource represents a search suggestion source along with its latency. type SuggestionSource struct { Name string FetchFunc func(string) []string Latency time.Duration mu sync.Mutex } // Initialize suggestion sources with default latency values. var suggestionSources = []SuggestionSource{ { Name: "DuckDuckGo", FetchFunc: fetchDuckDuckGoSuggestions, Latency: 50 * time.Millisecond, }, { Name: "Edge", FetchFunc: fetchEdgeSuggestions, Latency: 50 * time.Millisecond, }, { Name: "Brave", FetchFunc: fetchBraveSuggestions, Latency: 50 * time.Millisecond, }, { Name: "Ecosia", FetchFunc: fetchEcosiaSuggestions, Latency: 50 * time.Millisecond, }, // { // Not working with fetchSuggestionsFromURL func // Name: "Qwant", // FetchFunc: fetchQwantSuggestions, // Latency: 50 * time.Millisecond, // }, { Name: "Startpage", FetchFunc: fetchStartpageSuggestions, Latency: 50 * time.Millisecond, }, { Name: "Yahoo", FetchFunc: fetchYahooSuggestions, Latency: 50 * time.Millisecond, }, // I advise against it, but you can use it if you want to // { // Name: "Google", // FetchFunc: fetchGoogleSuggestions, // Latency: 500 * time.Millisecond, // }, } // Mutex to protect the suggestionSources during sorting. var suggestionsMU sync.Mutex func handleSuggestions(w http.ResponseWriter, r *http.Request) { query := r.URL.Query().Get("q") if query == "" { w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `["",[]]`) return } // Sort the suggestion sources based on their latency. suggestionsMU.Lock() sort.Slice(suggestionSources, func(i, j int) bool { return suggestionSources[i].Latency < suggestionSources[j].Latency }) suggestionsMU.Unlock() var suggestions []string for i := range suggestionSources { source := &suggestionSources[i] start := time.Now() suggestions = source.FetchFunc(query) elapsed := time.Since(start) updateLatency(source, elapsed) if len(suggestions) > 0 { printDebug("Suggestions found using %s", source.Name) break } else { printWarn("%s did not return any suggestions or failed.", source.Name) } } // Trim the suggestions to a maximum of 8 items suggestions = trimSuggestions(suggestions) if len(suggestions) == 0 { printErr("All suggestion services failed. Returning empty response.") } // Return the final suggestions as JSON. w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `["",%s]`, toJSONStringArray(suggestions)) } // trimSuggestions trims the suggestion list to a maximum of 8 suggestions. func trimSuggestions(suggestions []string) []string { if len(suggestions) > 8 { return suggestions[:8] } return suggestions } // updateLatency updates the latency of a suggestion source using an exponential moving average. func updateLatency(source *SuggestionSource, newLatency time.Duration) { source.mu.Lock() defer source.mu.Unlock() const alpha = 0.5 // Smoothing factor. source.Latency = time.Duration(float64(source.Latency)*(1-alpha) + float64(newLatency)*alpha) } func fetchGoogleSuggestions(query string) []string { encodedQuery := url.QueryEscape(query) url := fmt.Sprintf("http://suggestqueries.google.com/complete/search?client=firefox&q=%s", encodedQuery) printDebug("Fetching suggestions from Google: %s", url) return fetchSuggestionsFromURL(url) } func fetchDuckDuckGoSuggestions(query string) []string { encodedQuery := url.QueryEscape(query) url := fmt.Sprintf("https://duckduckgo.com/ac/?q=%s&type=list", encodedQuery) printDebug("Fetching suggestions from DuckDuckGo: %s", url) return fetchSuggestionsFromURL(url) } func fetchEdgeSuggestions(query string) []string { encodedQuery := url.QueryEscape(query) url := fmt.Sprintf("https://api.bing.com/osjson.aspx?query=%s", encodedQuery) printDebug("Fetching suggestions from Edge (Bing): %s", url) return fetchSuggestionsFromURL(url) } func fetchBraveSuggestions(query string) []string { encodedQuery := url.QueryEscape(query) url := fmt.Sprintf("https://search.brave.com/api/suggest?q=%s", encodedQuery) printDebug("Fetching suggestions from Brave: %s", url) return fetchSuggestionsFromURL(url) } func fetchEcosiaSuggestions(query string) []string { encodedQuery := url.QueryEscape(query) url := fmt.Sprintf("https://ac.ecosia.org/?q=%s&type=list", encodedQuery) printDebug("Fetching suggestions from Ecosia: %s", url) return fetchSuggestionsFromURL(url) } // Is this working? func fetchQwantSuggestions(query string) []string { encodedQuery := url.QueryEscape(query) url := fmt.Sprintf("https://api.qwant.com/v3/suggest?q=%s", encodedQuery) printDebug("Fetching suggestions from Qwant: %s", url) return fetchSuggestionsFromURL(url) } func fetchStartpageSuggestions(query string) []string { encodedQuery := url.QueryEscape(query) url := fmt.Sprintf("https://startpage.com/suggestions?q=%s", encodedQuery) printDebug("Fetching suggestions from Startpage: %s", url) return fetchSuggestionsFromURL(url) } func fetchYahooSuggestions(query string) []string { encodedQuery := url.QueryEscape(query) url := fmt.Sprintf("https://search.yahoo.com/sugg/gossip/gossip-us-ura/?output=fxjson&command=%s", encodedQuery) printDebug("Fetching suggestions from Yahoo: %s", url) return fetchSuggestionsFromURL(url) } // func fetchBaiduSuggestions(query string) []string { // encodedQuery := url.QueryEscape(query) // url := fmt.Sprintf("https://suggestion.baidu.com/su?wd=%s", encodedQuery) // printDebug("Fetching suggestions from Baidu: %s", url) // return fetchSuggestionsFromURL(url) // } func fetchSuggestionsFromURL(url string) []string { resp, err := http.Get(url) if err != nil { printWarn("Error fetching suggestions from %s: %v", url, err) return []string{} } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { printWarn("Error reading response body from %s: %v", url, err) return []string{} } // Print the raw HTTP response for debugging fmt.Printf("Raw response from %s:\n%s\n", url, string(body)) // Log the Content-Type for debugging. contentType := resp.Header.Get("Content-Type") printDebug("Response Content-Type from %s: %s", url, contentType) // Check if the body is non-empty. if len(body) == 0 { printWarn("Received empty response body from %s", url) return []string{} } // Attempt to parse the response as JSON regardless of Content-Type. var parsedResponse []interface{} if err := json.Unmarshal(body, &parsedResponse); err != nil { printErr("Error parsing JSON from %s: %v", url, err) printDebug("Response body: %s", string(body)) return []string{} } // Ensure the response structure is as expected. if len(parsedResponse) < 2 { printWarn("Unexpected response format from %v: %v", url, string(body)) return []string{} } suggestions := []string{} if items, ok := parsedResponse[1].([]interface{}); ok { for _, item := range items { if suggestion, ok := item.(string); ok { suggestions = append(suggestions, suggestion) } } } else { printErr("Unexpected suggestions format in response from: %v", url) } return suggestions } // toJSONStringArray converts a slice of strings to a JSON array string. func toJSONStringArray(strings []string) string { result := "" for i, str := range strings { result += fmt.Sprintf(`"%s"`, str) if i < len(strings)-1 { result += "," } } return "[" + result + "]" } // func testSuggestionSources(query string) { // for _, source := range suggestionSources { // fmt.Printf("Testing %s...\n", source.Name) // // Fetch suggestions // suggestions := source.FetchFunc(query) // // If we get results, print them // if len(suggestions) > 0 { // fmt.Printf("Suggestions from %s:\n", source.Name) // for i, suggestion := range suggestions { // fmt.Printf("%d: %s\n", i+1, suggestion) // } // } else { // fmt.Printf("No suggestions from %s.\n", source.Name) // } // // Small separator for clarity // fmt.Println("--------------------------") // } // } // func main() { // query := "test query" // testSuggestionSources(query) // }