/ddg.go
Go | 222 lines | 141 code | 32 blank | 49 comment | 29 complexity | 77ba4260bd772e42cd09b20f010154dc MD5 | raw file
1// Copyright 2012, Brian Hetro <whee@smaertness.net> 2// Use of this source code is governed by the ISC license 3// that can be found in the LICENSE file. 4 5// DuckDuckGo Zero-click API client. See https://duckduckgo.com/api.html 6// for information about the API. 7// 8// Example command-line program to show abstracts: 9// 10// package main 11// 12// import ( 13// "fmt" 14// "github.com/whee/ddg" 15// "os" 16// ) 17// 18// func main() { 19// for _, s := range os.Args[1:] { 20// if r, err := ddg.ZeroClick(s); err == nil { 21// fmt.Printf("%s: %s\n", s, r.Abstract) 22// } else { 23// fmt.Printf("Error looking up %s: %v\n", s, err) 24// } 25// } 26// } 27package ddg 28 29import ( 30 "bytes" 31 "encoding/json" 32 "net/http" 33 "net/url" 34 "reflect" 35) 36 37// Response represents the response from a zero-click API request via 38// the ZeroClick function. 39type Response struct { 40 Abstract string // topic summary (can contain HTML, e.g. italics) 41 AbstractText string // topic summary (with no HTML) 42 AbstractSource string // name of Abstract source 43 AbstractURL string // deep link to expanded topic page in AbstractSource 44 Image string // link to image that goes with Abstract 45 Heading string // name of topic that goes with Abstract 46 47 Answer string // instant answer 48 AnswerType string // 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). 49 50 Definition string // dictionary definition (may differ from Abstract) 51 DefinitionSource string // name of Definition source 52 DefinitionURL string // deep link to expanded definition page in DefinitionSource 53 54 RelatedTopics []Result // array of internal links to related topics associated with Abstract 55 RelatedTopicsSections []SectionResults // disambiguation types will populate this 56 57 Results []Result // array of external links associated with Abstract 58 59 Type CategoryType // response category, i.e. A (article), D (disambiguation), C (category), N (name), E (exclusive), or nothing. 60 61 Redirect string // !bang redirect URL 62} 63 64// SectionResults are results grouped by a topic name. 65type SectionResults struct { 66 Name string 67 Topics []Result 68} 69 70type Result struct { 71 Result string // HTML link(s) to external site(s) 72 FirstURL string // first URL in Result 73 Icon Icon // icon associated with FirstURL 74 Text string // text from FirstURL 75} 76 77type Icon struct { 78 URL string // URL of icon 79 Height int `json:"-"` // height of icon (px) 80 Width int `json:"-"` // width of icon (px) 81 82 // The height and width can be "" (string; we treat as 0) or an int. Unmarshal here, then populate the above two. 83 RawHeight interface{} `json:"Height"` 84 RawWidth interface{} `json:"Width"` 85} 86 87// disambiguationResponse is used when Type is Disambiguation. RelatedTopics is a mix of types in this case -- 88// this struct handles Results grouped by topic. 89type disambiguationResponse struct { 90 RelatedTopics []SectionResults 91} 92 93type CategoryType string 94 95const ( 96 Article = "A" 97 Disambiguation = "D" 98 Category = "C" 99 Name = "N" 100 Exclusive = "E" 101 None = "" 102) 103 104// A Client is a DDG Zero-click client. 105type Client struct { 106 // Secure specifies whether HTTPS is used. 107 Secure bool 108 // NoHtml will remove HTML from the response text 109 NoHtml bool `parameter:"no_html"` 110 // SkipDisambiguation will prevent Disambiguation type responses 111 SkipDisambiguation bool `parameter:"skip_disambig"` 112 // NoRedirect skips HTTP redirects (for !bang commands) 113 NoRedirect bool `parameter:"no_redirect"` 114 // BaseURL specifies where to send API requests. If zero-value, 115 // "api.duckduckgo.com" is used. 116 BaseURL string 117} 118 119// ZeroClick queries DuckDuckGo's zero-click API for the specified query 120// and returns the Response. This helper function uses a zero-value Client. 121func ZeroClick(query string) (res Response, err error) { 122 c := &Client{} 123 return c.ZeroClick(query) 124} 125 126// ZeroClick queries DuckDuckGo's zero-click API for the specified query 127// and returns the Response. 128func (c *Client) ZeroClick(query string) (res Response, err error) { 129 v := url.Values{} 130 v.Set("q", query) 131 v.Set("format", "json") 132 133 cE := reflect.ValueOf(c).Elem() 134 typeOfC := cE.Type() 135 for i := 0; i < cE.NumField(); i++ { 136 if tag := typeOfC.Field(i).Tag; tag != "" { 137 if cE.Field(i).Interface().(bool) { 138 v.Set(typeOfC.Field(i).Tag.Get("parameter"), "1") 139 } 140 } 141 } 142 143 var scheme string 144 if c.Secure { 145 scheme = "https" 146 } else { 147 scheme = "http" 148 } 149 150 if c.BaseURL == "" { 151 c.BaseURL = "api.duckduckgo.com" 152 } 153 154 req, err := http.NewRequest("GET", scheme+"://"+c.BaseURL+"/?"+v.Encode(), nil) 155 if err != nil { 156 return 157 } 158 req.Header.Set("User-Agent", "ddg.go/0.7") 159 160 client := &http.Client{} 161 resp, err := client.Do(req) 162 if err != nil { 163 return 164 } 165 defer resp.Body.Close() 166 167 b := new(bytes.Buffer) 168 _, err = b.ReadFrom(resp.Body) 169 if err != nil { 170 return 171 } 172 173 err = json.Unmarshal(b.Bytes(), &res) 174 if err != nil { 175 return 176 } 177 178 handleInterfaces(&res) 179 180 if res.Type == Disambiguation { 181 handleDisambiguation(&res, b.Bytes()) 182 } 183 184 return 185} 186 187// handleInterfaces cleans up incoming data that can be of multiple types; for example, the 188// icon width and height are either a float64 or string, but we want to treat them as int. 189func handleInterfaces(response *Response) { 190 for _, result := range response.Results { 191 if height, ok := result.Icon.RawHeight.(float64); ok { 192 result.Icon.Height = int(height) 193 } 194 if width, ok := result.Icon.RawWidth.(float64); ok { 195 result.Icon.Width = int(width) 196 } 197 } 198} 199 200// handleDisambiguation performs a second pass on the response to populate RelatedTopics and 201// RelatedTopicsSections properly. 202func handleDisambiguation(response *Response, data []byte) { 203 // First, clean up RelatedTopics. The grouped topic results ended up as zero-valued Results, 204 // and those are useless. 205 var cleanRelated []Result 206 for _, r := range response.RelatedTopics { 207 if r.Result != "" { 208 cleanRelated = append(cleanRelated, r) 209 } 210 } 211 response.RelatedTopics = cleanRelated 212 213 // We could handle this like handleInterfaces, but json's Unmarshal will do the work for us. 214 dResponse := &disambiguationResponse{} 215 if err := json.Unmarshal(data, dResponse); err == nil { 216 for _, r := range dResponse.RelatedTopics { 217 if r.Name != "" { 218 response.RelatedTopicsSections = append(response.RelatedTopicsSections, r) 219 } 220 } 221 } 222}