PageRenderTime 2ms CodeModel.GetById 2ms app.highlight 11ms RepoModel.GetById 1ms app.codeStats 0ms

/ddg.go

http://github.com/whee/ddg
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}