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