├── .travis.yml ├── AUTHORS.md ├── .gitignore ├── LICENSE ├── goduckgo ├── duckduck_test.go └── duckduck.go ├── CONTRIBUTING.md ├── README.md └── main.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.1 5 | - 1.2 6 | - 1.3 7 | - 1.4.1 8 | 9 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | * [Aleksandar Janicijevic](https://github.com/ajanicij) - 2 | * [Fernando Álvarez](https://github.com/fern4lvarez) - 3 | * [Brian Tomlinson](https://github.com/darthlukan) - 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Aleksandar Janicijevic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /goduckgo/duckduck_test.go: -------------------------------------------------------------------------------- 1 | package goduckgo 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestQuery(t *testing.T) { 13 | query := "New York City" 14 | expectedUrl := "https://api.duckduckgo.com/?q=New+York+City&format=json&pretty=1" 15 | expectedBody := `{ 16 | "Heading" : "New York City" 17 | }` 18 | expectedMessage := &Message{} 19 | expectedMessage.Heading = "New York City" 20 | 21 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 22 | w.Header().Set("Content-Type", "application/json") 23 | fmt.Fprintln(w, expectedBody) 24 | })) 25 | defer ts.Close() 26 | 27 | if url := EncodeUrl(query); url != expectedUrl { 28 | t.Errorf("Got %s, want %s", url, expectedUrl) 29 | } 30 | 31 | body, err := Do(ts.URL) 32 | if err != nil { 33 | t.Errorf("Got %v, want %v", err, nil) 34 | } 35 | 36 | if trimmedBody := strings.TrimSpace(string(body)); trimmedBody != expectedBody { 37 | t.Errorf("Got %s, want %s", trimmedBody, expectedBody) 38 | } 39 | 40 | message := &Message{} 41 | if err = message.Decode(body); err != nil { 42 | t.Errorf("Got %v, want %v", err, nil) 43 | } 44 | 45 | if !reflect.DeepEqual(message, expectedMessage) { 46 | t.Errorf("Got %v, want %v", message, expectedMessage) 47 | } 48 | } 49 | 50 | func TestEncodeUrl(t *testing.T) { 51 | query := "!gi New York City" 52 | expectedUrl := "https://api.duckduckgo.com/?q=%21gi+New+York+City&format=json&pretty=1&no_redirect=1" 53 | 54 | url := EncodeUrl(query) 55 | if url != expectedUrl { 56 | t.Errorf("Got %s, want %s", url, expectedUrl) 57 | } 58 | 59 | query = "New York City" 60 | expectedUrl = "https://api.duckduckgo.com/?q=New+York+City&format=json&pretty=1" 61 | url = EncodeUrl(query) 62 | if url != expectedUrl { 63 | t.Errorf("Got %s, want %s", url, expectedUrl) 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing to GoDuckGo 2 | ======================== 3 | 4 | If you love Go and DuckDuckGo, then this is 5 | your place. We're waiting for your Pull Request! 6 | 7 | Getting Started 8 | --------------- 9 | 10 | Before you can do anything, you first need a [GitHub account](https://github.com/signup/free). 11 | This is required because we use GitHub to handle all incoming *Pull Requests* (code modifications) 12 | and *Issues* (bug reports) which cannot be made without a GitHub account. 13 | 14 | Submitting a **Bug** or **Suggestion** 15 | -------------------------------------- 16 | 17 | - Firstly, please make sure the bug is related to the **GoDuckGo** package. If this bug 18 | is about the DuckDuckGo API, or the relevancy of the search results, please visit DuckDuckGo's 19 | feedback page at . 20 | 21 | - Check the **GoDuckGo** [issues](https://github.com/ajanicij/goduckgo/issues) to see if 22 | an issue already exists for the given bug or suggestion: 23 | - If one doesn't exist, create a GitHub issue in the **GoDuckGo** repository: 24 | - Clearly describe the bug/improvement, including steps to reproduce when it is a bug. 25 | - If one already exists, please add any additional comments you have regarding the matter. 26 | 27 | If you're submitting a **pull request** (bugfix/addition): 28 | - Fork the **GoDuckGo** repository on GitHub. 29 | 30 | Making Changes 31 | -------------- 32 | 33 | - Before making any changes, make sure your [Go environment](http://golang.org/doc/install) is setup. 34 | - Run `gofmt` and `go vet` commands to clean up your code. 35 | - Make sure your commits are of a reasonable size. They shouldn't be too big. 36 | - Make sure your commit messages effectively explain what changes have been made. 37 | 38 | ```shell 39 | main.go: Handle error when Icon is empty 40 | ``` 41 | 42 | is much better than: 43 | 44 | ```shell 45 | annoying error for empty icon is fixed now 46 | ``` 47 | 48 | - Make sure you have added the necessary tests for your changes. 49 | - Make sure your change doesn't affect backwards compatibility. 50 | 51 | Submitting Changes 52 | ------------------ 53 | 54 | 1. Commit your changes. 55 | 56 | ```shell 57 | git commit -am "My first commit!" 58 | ``` 59 | 60 | 2. Get your commit history [how you like it](http://book.git-scm.com/4_interactive_rebasing.html). 61 | 62 | ```shell 63 | git rebase -i origin/master 64 | ``` 65 | 66 | or 67 | 68 | ```shell 69 | git pull --rebase origin/master 70 | ``` 71 | 72 | 3. Push your forked repository back to GitHub. 73 | 74 | ```shell 75 | git push 76 | ``` 77 | 78 | 4. Add your info to the [AUTHORS.md page](https://github.com/ajanicij/goduckgo/blob/master/AUTHORS.md). 79 | 80 | 5. Go into GitHub and submit a [pull request!](http://help.github.com/send-pull-requests/) to the 81 | **GoDuckGo** repository. 82 | 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | goduckgo [![Build Status](https://travis-ci.org/ajanicij/goduckgo.svg?branch=master)](https://travis-ci.org/ajanicij/goduckgo)[![GoDoc](https://godoc.org/github.com/ajanicij/goduckgo/goduckgo?status.png)](http://godoc.org/github.com/ajanicij/goduckgo/goduckgo) 2 | ======== 3 | 4 | Go package for DuckDuckGo Instant Answer API. 5 | 6 | [DuckDuckGo](http://duckduckgo.com) is a search engine that: 7 | 8 | * Emphasizes privacy 9 | * Does not record user information 10 | * Breaks out of the filter bubble 11 | 12 | The Instant Answer API is described [here](http://duckduckgo.com/api.html). For 13 | example, the URL for querying about New York City is 14 | 15 | [http://api.duckduckgo.com/?q=New+York+City&format=json&pretty=1](http://api.duckduckgo.com/?q=New+York+City&format=json&pretty=1) 16 | 17 | The previous query causes DuckDuckGo to return the result in JSON format. 18 | 19 | Function goduckgo.Query declared as 20 | 21 | ``` 22 | func Query(query string) (*Message, error) 23 | ``` 24 | 25 | generates the URL, sends it to DuckDuckGo, receives the result, unmarshals from 26 | JSON format to Message structure and returns a pointer to the structure. 27 | 28 | Installation 29 | ------------ 30 | 31 | ``` 32 | go get -u github.com/ajanicij/goduckgo 33 | ``` 34 | 35 | Usage 36 | ----- 37 | 38 | Look at the source for the command-line utility, `main.go`. It imports 39 | package `github.com/ajanicij/goduckgo/goduckgo`, generates the query in the variable 40 | `query` (for example, "New York City") and passes it to function 41 | `goduckgo.Query`. That function returns two values: `*Message` and `error`. 42 | 43 | Command-line utility 44 | -------------------- 45 | 46 | The source code of the command-line utility is `main.go`. It builds `goduckgo` 47 | command. Its usage is: 48 | 49 | `goduckgo [{flags}] ` 50 | 51 | Flags determine which fields we will see in the result. 52 | For example, if we want to search for "New York City," we can issue command 53 | 54 | `goduckgo -All New York City` 55 | 56 | Flag -All tells the command that we want all fields. 57 | 58 | Command 59 | 60 | `goduckgo -help` 61 | 62 | (or `goduckgo` without any flags) will give us a help string that lists 63 | all available options: 64 | 65 | ``` 66 | Usage of ./goduckgo: 67 | -Abstract=false: Abstract 68 | -AbstractSource=false: Abstract Source 69 | -AbstractText=false: Abstract Text 70 | -AbstractURL=false: Abstract URL 71 | -All=false: All Fields 72 | -Answer=false: Answer 73 | -AnswerType=false: Answer Type 74 | -Definition=false: Definition 75 | -DefinitionSource=false: Definition Source 76 | -DefinitionURL=false: Definition URL 77 | -Heading=false: Heading 78 | -Image=false: Image 79 | -Redirect=false: Redirect 80 | -RelatedTopics=false: Related Topics 81 | -Results=false: Results 82 | -Type=false: Type 83 | ``` 84 | 85 | For example, query 86 | 87 | `goduckgo -Abstract DuckDuckgo` 88 | 89 | produces the following: 90 | 91 | ``` 92 | Abstract: DuckDuckGo is an Internet search engine that emphasizes protecting searchers' 93 | privacy and avoiding the "filter bubble" of personalized search results. DuckDuckGo 94 | distinguishes itself from other search engines by not profiling its users and by deliberately 95 | showing all users the same search results for a given search term. DuckDuckGo also 96 | emphasizes getting information from the best sources rather than the most sources, 97 | generating its search results from key crowdsourced sites such as Wikipedia and from 98 | partnerships with other search engines like Yandex, Yahoo!, Bing, Wolfram Alpha and Yummly. 99 | ``` 100 | 101 | License 102 | ------- 103 | `goduckgo` is MIT licensed, see [here](https://github.com/ajanicij/goduckgo/blob/master/LICENSE). 104 | 105 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "strings" 8 | 9 | "github.com/ajanicij/goduckgo/goduckgo" 10 | ) 11 | 12 | func main() { 13 | // Read flags from command line 14 | fl_definition := flag.Bool("Definition", false, "Definition") 15 | fl_definition_source := flag.Bool("DefinitionSource", false, "Definition Source") 16 | fl_heading := flag.Bool("Heading", false, "Heading") 17 | fl_abstract_text := flag.Bool("AbstractText", false, "Abstract Text") 18 | fl_abstract := flag.Bool("Abstract", false, "Abstract") 19 | fl_abstract_source := flag.Bool("AbstractSource", false, "Abstract Source") 20 | fl_image := flag.Bool("Image", false, "Image") 21 | fl_type := flag.Bool("Type", false, "Type") 22 | fl_answer_type := flag.Bool("AnswerType", false, "Answer Type") 23 | fl_redirect := flag.Bool("Redirect", false, "Redirect") 24 | fl_definition_url := flag.Bool("DefinitionURL", false, "Definition URL") 25 | fl_answer := flag.Bool("Answer", false, "Answer") 26 | fl_abstract_url := flag.Bool("AbstractURL", false, "Abstract URL") 27 | fl_results := flag.Bool("Results", false, "Results") 28 | fl_related_topics := flag.Bool("RelatedTopics", false, "Related Topics") 29 | fl_all := flag.Bool("All", false, "All Fields") 30 | 31 | if len(os.Args) == 1 { 32 | flag.PrintDefaults() 33 | os.Exit(0) 34 | } 35 | 36 | flag.Parse() 37 | 38 | if len(flag.Args()) < 1 { 39 | fmt.Println("Usage: simplequery [{flags}] ") 40 | os.Exit(0) 41 | } 42 | query := strings.Join(flag.Args(), " ") 43 | 44 | message, err := goduckgo.Query(query) 45 | CheckError(err) 46 | 47 | if *fl_all || *fl_definition { 48 | fmt.Println("Definition:", message.Definition) 49 | } 50 | if *fl_all || *fl_definition_source { 51 | fmt.Println("Definition Source:", message.DefinitionSource) 52 | } 53 | if *fl_all || *fl_heading { 54 | fmt.Println("Heading:", message.Heading) 55 | } 56 | if *fl_all || *fl_abstract_text { 57 | fmt.Println("Abstract Text:", message.AbstractText) 58 | } 59 | if *fl_all || *fl_abstract { 60 | fmt.Println("Abstract:", message.Abstract) 61 | } 62 | if *fl_all || *fl_abstract_source { 63 | fmt.Println("Abstract Source:", message.AbstractSource) 64 | } 65 | if *fl_all || *fl_image { 66 | fmt.Println("Image:", message.Image) 67 | } 68 | if *fl_all || *fl_type { 69 | fmt.Println("Type:", TypeDefinition(message.Type)) 70 | } 71 | if *fl_all || *fl_answer_type { 72 | fmt.Println("Answer Type:", message.AnswerType) 73 | } 74 | if *fl_all || *fl_redirect { 75 | fmt.Println("Redirect:", message.Redirect) 76 | } 77 | if *fl_all || *fl_definition_url { 78 | fmt.Println("Definition URL:", message.DefinitionURL) 79 | } 80 | if *fl_all || *fl_answer { 81 | fmt.Println("Answer:", message.Answer) 82 | } 83 | if *fl_all || *fl_abstract_url { 84 | fmt.Println("Abstract URL:", message.AbstractURL) 85 | } 86 | if *fl_all || *fl_results { 87 | if message.Results != nil && len(message.Results) != 0 { 88 | for _, result := range message.Results { 89 | fmt.Println("Result") 90 | result.Show(" ") 91 | } 92 | } 93 | } 94 | if *fl_all || *fl_related_topics { 95 | if message.RelatedTopics != nil && len(message.RelatedTopics) != 0 { 96 | for _, topic := range message.RelatedTopics { 97 | fmt.Println("Related Topic") 98 | topic.Show(" ") 99 | } 100 | } 101 | } 102 | } 103 | 104 | func TypeDefinition(d string) string { 105 | switch d { 106 | case "A": 107 | return "Article" 108 | case "D": 109 | return "Disambiguation" 110 | case "C": 111 | return "Category" 112 | case "N": 113 | return "Name" 114 | case "E": 115 | return "Exclusive" 116 | } 117 | return "Unknown" 118 | } 119 | 120 | func CheckError(e error) { 121 | if e != nil { 122 | fmt.Println(e.Error()) 123 | os.Exit(-1) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /goduckgo/duckduck.go: -------------------------------------------------------------------------------- 1 | // Package goduckgo provides the functionality for using 2 | // DuckDuckGo API. For the description of the API, visit 3 | // http://duckduckgo.com/api.html. 4 | package goduckgo 5 | 6 | import ( 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "net/http" 12 | "net/url" 13 | "os" 14 | "strings" 15 | ) 16 | 17 | var baseUrl = "https://api.duckduckgo.com/?q=%s&format=json&pretty=1%s" 18 | 19 | // Message is a structure containing all the information returned by 20 | // DDG for a query. 21 | // Abstract: topic summary (can contain HTML, e.g. italics) 22 | // AbstractText: topic summary (with no HTML) 23 | // AbstractSource: name of Abstract source 24 | // AbstractURL: deep link to expanded topic page in AbstractSource 25 | // Image: link to image that goes with Abstract 26 | // Heading: name of topic that goes with Abstract 27 | // Answer: instant answer 28 | // AnswerType: type of Answer, e.g. calc, color, digest, info, ip, iploc, phone, pw, rand, regexp, unicode, upc, or zip (see goodies & tech pages for examples). 29 | // Definition: dictionary definition (may differ from Abstract) 30 | // DefinitionSource: name of Definition source 31 | // DefinitionURL: deep link to expanded definition page in DefinitionSource 32 | // RelatedTopics: array of internal links to related topics associated with Abstract 33 | // Results: array of external links associated with Abstract 34 | // Type: response category, i.e. A (article), D (disambiguation), C (category), N (name), E (exclusive), or nothing. 35 | // Redirect: !bang redirect URL 36 | type Message struct { 37 | Definition string 38 | DefinitionSource string 39 | Heading string 40 | AbstractText string 41 | Abstract string 42 | AbstractSource string 43 | Image string 44 | Type string 45 | AnswerType string 46 | Redirect string 47 | DefinitionURL string 48 | Answer string 49 | AbstractURL string 50 | Results Results 51 | RelatedTopics RelatedTopics 52 | } 53 | 54 | // Decode a message given a HTTP response body 55 | func (message *Message) Decode(body []byte) error { 56 | if err := json.Unmarshal(body, message); err != nil { 57 | return err 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // Show Result as standard output 64 | func (result *Result) Show(prefix string) { 65 | result.fshow(os.Stdout, prefix) 66 | } 67 | 68 | func (result *Result) fshow(w io.Writer, prefix string) { 69 | fmt.Fprintln(w, prefix, "Result:", result.Result) 70 | if !result.Icon.IsEmpty() { 71 | fmt.Fprintln(w, prefix, "Icon:") 72 | result.Icon.fshow(w, prefix+prefix) 73 | } 74 | fmt.Fprintln(w, prefix, "First URL:", result.FirstURL) 75 | fmt.Fprintln(w, prefix, "Text:", result.Text) 76 | } 77 | 78 | type Results []Result 79 | 80 | type RelatedTopics []RelatedTopic 81 | 82 | // Result is an external link associated with Abstract 83 | // Result: HTML link(s) to external site(s) 84 | // FirstURL: first URL in Result 85 | // Icon: icon associated with FirstURL 86 | // Text: text from FirstURL 87 | type Result RelatedTopic 88 | 89 | // RelatedTopic is a internal link to related topics associated with Abstract 90 | // Result: HTML link to a related topic 91 | // FirstURL: first URL in Result 92 | // Icon: icon associated with related topic 93 | // Text: text from first URL 94 | type RelatedTopic struct { 95 | Result string 96 | Icon Icon 97 | FirstURL string 98 | Text string 99 | } 100 | 101 | // Show RelatedTopic as standard output 102 | func (topic *RelatedTopic) Show(prefix string) { 103 | topic.fshow(os.Stdout, prefix) 104 | } 105 | 106 | func (topic *RelatedTopic) fshow(w io.Writer, prefix string) { 107 | fmt.Fprintln(w, prefix, "Result:", topic.Result) 108 | if !topic.Icon.IsEmpty() { 109 | fmt.Fprintln(w, prefix, "Icon:") 110 | topic.Icon.fshow(w, prefix+prefix) 111 | } 112 | fmt.Fprintln(w, prefix, "First URL:", topic.FirstURL) 113 | fmt.Fprintln(w, prefix, "Text:", topic.Text) 114 | } 115 | 116 | // Icon associated with related topics 117 | // URL: URL of icon 118 | // Height: height of icon (px) 119 | // Width: width of icon (px) 120 | type Icon struct { 121 | URL string 122 | Height interface{} // can be string or number ("16" or 16) 123 | Width interface{} // can be string or number ("16" or 16) 124 | } 125 | 126 | // IsEmpty if all Icon fields are empty 127 | func (icon *Icon) IsEmpty() bool { 128 | return icon.URL == "" && 129 | icon.Height == "" && 130 | icon.Width == "" 131 | } 132 | 133 | // Show Show as standard output 134 | func (icon *Icon) Show(prefix string) { 135 | icon.fshow(os.Stdout, prefix) 136 | } 137 | 138 | func (icon *Icon) fshow(w io.Writer, prefix string) { 139 | fmt.Fprintln(w, prefix, "URL:", icon.URL) 140 | fmt.Fprintln(w, prefix, "Height:", icon.Height) 141 | fmt.Fprintln(w, prefix, "Width:", icon.Width) 142 | } 143 | 144 | // EncodeUrl given a text query 145 | func EncodeUrl(query string) string { 146 | queryEnc := url.QueryEscape(query) 147 | if strings.HasPrefix(query, "!") { 148 | return fmt.Sprintf(baseUrl, queryEnc, "&no_redirect=1") 149 | } 150 | return fmt.Sprintf(baseUrl, queryEnc, "") 151 | } 152 | 153 | // Do the HTTP requests against API and handle errors 154 | func Do(url string) ([]byte, error) { 155 | resp, err := http.Get(url) 156 | if err != nil { 157 | return nil, err 158 | } 159 | 160 | body, err := ioutil.ReadAll(resp.Body) 161 | if err != nil { 162 | return nil, err 163 | } 164 | 165 | return body, nil 166 | } 167 | 168 | // Query the API given a text query, returns a Message 169 | func Query(query string) (*Message, error) { 170 | ddgUrl := EncodeUrl(query) 171 | 172 | body, err := Do(ddgUrl) 173 | if err != nil { 174 | return nil, err 175 | } 176 | 177 | message := &Message{} 178 | if err = message.Decode(body); err != nil { 179 | return nil, err 180 | } 181 | 182 | return message, nil 183 | } 184 | --------------------------------------------------------------------------------