Repository navigation
Expand file tree
/
Copy pathchanges.patch
More file actions
224 lines (221 loc) · 7.14 KB
/
Copy pathchanges.patch
File metadata and controls
224 lines (221 loc) · 7.14 KB
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
223
224
diff --git a/README.md b/README.md
index 881b5ea..9c00c8f 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@ Most of the bangs are supported:
| category | engines |
| --- | --- |
-| general | google, google docs, bing, google cse, brave, duckduckgo, duckduckgo web, dogpile, privacywall, oceanhero, swisscows, iseek, s1search, wikipedia |
+| general | google, google docs, bing, google cse, brave, duckduckgo, duckduckgo web, dogpile, qwant, privacywall, oceanhero, swisscows, iseek, s1search, wikipedia |
| images | google images, google cse images, brave images, duckduckgo images, dogpile images |
| videos | google videos, duckduckgo videos, youtube, brave videos, bing videos, dailymotion |
| news | google news, duckduckgo news, reuters, brave news, abc news, bing news, wikinews |
diff --git a/config.toml b/config.toml
index 567f3d4..e509fe2 100644
--- a/config.toml
+++ b/config.toml
@@ -24,6 +24,7 @@ brave = 2
duckduckgo = 2
duckduckgo_web = 1
dogpile = 2
+qwant = 2
privacywall = 1
oceanhero = 2
swisscows = 2
diff --git a/src/engines.c b/src/engines.c
index a481def..e45d1b1 100644
--- a/src/engines.c
+++ b/src/engines.c
@@ -7,10 +7,11 @@ extern struct engine engine_google, engine_google_docs, engine_bing, engine_goog
engine_s1search, engine_wikipedia, engine_google_images, engine_google_cse_images, engine_brave_images,
engine_duckduckgo_images, engine_dogpile_images, engine_google_videos, engine_duckduckgo_videos, engine_youtube,
engine_brave_videos, engine_google_news, engine_duckduckgo_news, engine_reuters, engine_brave_news, engine_abc_news,
- engine_abc_radio, engine_soundcloud, engine_wikicommons_audio, engine_archlinux, engine_mdn, engine_mankier,
- engine_pypi, engine_stackoverflow, engine_askubuntu, engine_superuser, engine_arxiv, engine_europepmc, engine_pdbe,
- engine_pubmed, engine_bing_videos, engine_dailymotion, engine_bing_news, engine_wikinews, engine_genius,
- engine_mixcloud, engine_radio_browser, engine_openalex, engine_crossref, engine_google_scholar;
+ engine_abc_radio, engine_qwant, engine_soundcloud, engine_wikicommons_audio, engine_archlinux, engine_mdn,
+ engine_mankier, engine_pypi, engine_stackoverflow, engine_askubuntu, engine_superuser, engine_arxiv,
+ engine_europepmc, engine_pdbe, engine_pubmed, engine_bing_videos, engine_dailymotion, engine_bing_news,
+ engine_wikinews, engine_genius, engine_mixcloud, engine_radio_browser, engine_openalex, engine_crossref,
+ engine_google_scholar;
static struct engine *available[] = {
&engine_google,
@@ -42,6 +43,7 @@ static struct engine *available[] = {
&engine_brave_news,
&engine_abc_news,
&engine_abc_radio,
+ &engine_qwant,
&engine_soundcloud,
&engine_wikicommons_audio,
&engine_archlinux,
diff --git a/src/fetch.c b/src/fetch.c
index 5e9d534..523ab2f 100644
--- a/src/fetch.c
+++ b/src/fetch.c
@@ -122,7 +122,10 @@ on_header(char *p, size_t size, size_t n, void *arg)
struct fetch *f = arg;
size_t len = size * n, vlen;
- if (len > 7 && !strncasecmp(p, "server:", 7)) {
+ if (len > 11 && !strncasecmp(p, "set-cookie:", 11)) {
+ buf_add(&f->set_cookie, p + 11, len - 11);
+ buf_putc(&f->set_cookie, '\n');
+ } else if (len > 7 && !strncasecmp(p, "server:", 7)) {
p += 7;
len -= 7;
while (len && (*p == ' ' || *p == '\t')) {
@@ -284,6 +287,7 @@ fetch_free(struct fetch *f)
curl_slist_free_all(f->headers);
buf_free(&f->body);
buf_free(&f->cookies);
+ buf_free(&f->set_cookie);
free(f->post);
free(f);
}
diff --git a/src/fetch.h b/src/fetch.h
index d056094..cbf39d0 100644
--- a/src/fetch.h
+++ b/src/fetch.h
@@ -13,6 +13,7 @@ struct fetch {
struct curl_slist *headers;
struct buf body;
struct buf cookies;
+ struct buf set_cookie;
char *post;
char server[64];
long status;
diff --git a/src/engines/qwant.c b/src/engines/qwant.c
new file mode 100644
index 0000000..352a247
--- /dev/null
+++ b/src/engines/qwant.c
@@ -0,0 +1,129 @@
+#include <stdlib.h>
+#include <string.h>
+
+#include "../engine.h"
+
+#define API_URL "https://api.qwant.com/v3/search/web"
+#define SEASON_MS 12000
+#define RETRY_SECS 15
+
+static char *datadome;
+static double datadome_since;
+static int warming;
+
+/* the datadome cookie changes on every response, but only the age of the first one matters */
+static void
+remember(struct fetch *f)
+{
+ const char *v;
+ size_t n;
+
+ if (!(v = find_between(buf_str(&f->set_cookie), "datadome=", ";", &n)) || !n)
+ return;
+ if (!datadome)
+ datadome_since = now_ms();
+ free(datadome);
+ datadome = xmalloc(n + 1);
+ memcpy(datadome, v, n);
+ datadome[n] = 0;
+}
+
+static void
+warm_response(struct fetch *f, void *arg)
+{
+ (void)arg;
+ warming = 0;
+ remember(f);
+ fetch_free(f);
+}
+
+static void
+warm(void)
+{
+ struct fetch *f;
+
+ if (warming)
+ return;
+ warming = 1;
+ f = fetch_new(API_URL
+ "?q=test&count=10&locale=en_US&offset=0&device=desktop&tgp=2&safesearch=1&displayed=true&llm=false");
+ fetch_header(f, "Accept: application/json, text/plain, */*");
+ fetch_timeout(f, 10000);
+ fetch_start(f, warm_response, NULL);
+}
+
+static void
+response(struct job *j, struct fetch *f)
+{
+ yyjson_val *rows, *row, *items, *r;
+ yyjson_doc *doc = NULL;
+ size_t i, n, k, m;
+
+ if (!job_ok(j, f))
+ return;
+ remember(f);
+ if (f->status == 403 && strstr(buf_str(&f->set_cookie), "datadome=") && !strstr(fetch_body(f), "\"status\"")) {
+ job_fail(j, "CAPTCHA", RETRY_SECS);
+ return;
+ }
+ if (!job_http_ok(j, f))
+ return;
+ if (!(doc = job_json(j, f->body.p, f->body.len)) ||
+ strcmp(json_str(yyjson_doc_get_root(doc), "status"), "success") ||
+ !yyjson_is_arr(rows = yyjson_ptr_get(yyjson_doc_get_root(doc), "/data/result/items/mainline"))) {
+ job_fail(j, "parsing error", SUSPEND_NONE);
+ return;
+ }
+ yyjson_arr_foreach (rows, i, n, row) {
+ if (strcmp(json_str(row, "type"), "web"))
+ continue;
+ items = yyjson_obj_get(row, "items");
+ yyjson_arr_foreach (items, k, m, r)
+ add_main(j, json_str(r, "url"), html_to_text(j->a, json_str(r, "title")),
+ html_to_text(j->a, json_str(r, "desc")));
+ }
+ job_done(j);
+}
+
+static void
+search(struct job *j)
+{
+ const char *locale = j->filters->locale->ddg_lang;
+ struct buf q = {0};
+ struct fetch *f;
+
+ if (!datadome) {
+ warm();
+ job_fail(j, "warming up", RETRY_SECS);
+ return;
+ }
+ if (now_ms() - datadome_since < SEASON_MS) {
+ job_fail(j, "warming up", RETRY_SECS);
+ return;
+ }
+ form_add(&q, "q", j->query);
+ form_add(&q, "count", "10");
+ form_add(&q, "locale", strchr(locale, '_') ? locale : "en_US");
+ form_add(&q, "offset", "0");
+ form_add(&q, "device", "desktop");
+ form_add(&q, "tgp", "2");
+ form_add(&q, "safesearch", aprintf(j->a, "%d", j->filters->safesearch));
+ form_add(&q, "displayed", "true");
+ form_add(&q, "llm", "false");
+ f = job_request(j, aprintf(j->a, API_URL "?%s", buf_str(&q)));
+ buf_free(&q);
+ fetch_header(f, "Accept: application/json, text/plain, */*");
+ fetch_cookie(f, "datadome", datadome);
+ job_send(j, f, response);
+}
+
+struct engine engine_qwant = {
+ .name = "qwant",
+ .shortcut = "qw",
+ .website = "www.qwant.com",
+ .safesearch = 1,
+ .bang = "https://www.qwant.com/?q=%s",
+ .category = "general",
+ .init = warm,
+ .search = search,
+};