PageRenderTime 47ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/ddg.go

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