gopher.c - frontends - front-ends for some sites (experiment) | |
Log | |
Files | |
Refs | |
README | |
LICENSE | |
--- | |
gopher.c (9547B) | |
--- | |
1 #include <sys/socket.h> | |
2 #include <sys/types.h> | |
3 | |
4 #include <ctype.h> | |
5 #include <errno.h> | |
6 #include <locale.h> | |
7 #include <netdb.h> | |
8 #include <stdarg.h> | |
9 #include <stdio.h> | |
10 #include <stdlib.h> | |
11 #include <string.h> | |
12 #include <time.h> | |
13 #include <unistd.h> | |
14 #include <wchar.h> | |
15 | |
16 #include "https.h" | |
17 #include "json.h" | |
18 #include "twitch.h" | |
19 #include "util.h" | |
20 | |
21 #define OUT(s) (fputs((s), stdout)) | |
22 #define OUTTEXT(s) gophertext(stdout, s, strlen(s)) | |
23 #define OUTLINK(s) gophertext(stdout, s, strlen(s)) | |
24 | |
25 extern char **environ; | |
26 | |
27 static const char *baserel = "/twitch.cgi"; | |
28 static const char *host = "127.0.0.1", *port = "70"; | |
29 | |
30 /* page variables */ | |
31 static char *title = "", *pagetitle = ""; | |
32 | |
33 void | |
34 line(int _type, const char *username, const char *selector) | |
35 { | |
36 putchar(_type); | |
37 OUTTEXT(username); | |
38 putchar('\t'); | |
39 OUTLINK(selector); | |
40 printf("\t%s\t%s\r\n", host, port); | |
41 } | |
42 | |
43 void | |
44 error(const char *s) | |
45 { | |
46 line('3', s, ""); | |
47 } | |
48 | |
49 void | |
50 info(const char *s) | |
51 { | |
52 line('i', s, ""); | |
53 } | |
54 | |
55 void | |
56 dir(const char *username, const char *selector) | |
57 { | |
58 line('1', username, selector); | |
59 } | |
60 | |
61 void | |
62 html(const char *username, const char *selector) | |
63 { | |
64 line('h', username, selector); | |
65 } | |
66 | |
67 void | |
68 page(int _type, const char *username, const char *page) | |
69 { | |
70 putchar(_type); | |
71 OUTTEXT(username); | |
72 putchar('\t'); | |
73 printf("%s?p=%s", baserel, page); | |
74 printf("\t%s\t%s\r\n", host, port); | |
75 } | |
76 | |
77 static int | |
78 gamecmp_name(const void *v1, const void *v2) | |
79 { | |
80 struct game *g1 = (struct game *)v1; | |
81 struct game *g2 = (struct game *)v2; | |
82 | |
83 return strcmp(g1->name, g2->name); | |
84 } | |
85 | |
86 void | |
87 header(void) | |
88 { | |
89 putchar('i'); | |
90 if (title[0]) { | |
91 OUTTEXT(title); | |
92 OUT(" - "); | |
93 } | |
94 if (pagetitle[0]) { | |
95 OUTTEXT(pagetitle); | |
96 OUT(" - "); | |
97 } | |
98 printf("Twitch.tv\t%s\t%s\t%s\r\n", "", host, port); | |
99 info("---"); | |
100 page('1', "Featured", "featured"); | |
101 page('1', "Games", "games"); | |
102 page('1', "VODS", "vods"); | |
103 dir("Source-code", "/git/frontends"); | |
104 page('1', "Links", "links"); | |
105 info("---"); | |
106 } | |
107 | |
108 void | |
109 footer(void) | |
110 { | |
111 printf(".\r\n"); | |
112 } | |
113 | |
114 void | |
115 render_links(void) | |
116 { | |
117 header(); | |
118 info(""); | |
119 html("mpv player", "URL:https://mpv.io/installation/"); | |
120 html("youtube-dl", "URL:https://github.com/ytdl-org/youtube-d… | |
121 html("VLC", "URL:https://www.videolan.org/"); | |
122 html("Twitch.tv API", "URL:https://dev.twitch.tv/docs"); | |
123 footer(); | |
124 } | |
125 | |
126 void | |
127 render_games_top(struct games_response *r) | |
128 { | |
129 struct game *game; | |
130 size_t i; | |
131 | |
132 header(); | |
133 info("Name"); | |
134 for (i = 0; i < r->nitems; i++) { | |
135 game = &(r->data[i]); | |
136 | |
137 putchar('1'); | |
138 OUTTEXT(game->name); | |
139 printf("\t%s?p=streams&game_id=", baserel); | |
140 OUTLINK(game->id); | |
141 printf("\t%s\t%s\r\n", host, port); | |
142 } | |
143 footer(); | |
144 } | |
145 | |
146 void | |
147 render_streams(struct streams_response *r, const char *game_id) | |
148 { | |
149 struct stream *stream; | |
150 char buf[256], title[256]; | |
151 size_t i; | |
152 | |
153 header(); | |
154 if (!game_id[0]) | |
155 printf("i%-20s %-20s %-50s %7s\t%s\t%s\t%s\r\n", | |
156 "Game", "Name", "Title", "Viewers", "", host, po… | |
157 else | |
158 printf("i%-20s %-50s %7s\t%s\t%s\t%s\r\n", | |
159 "Name", "Title", "Viewers", "", host, port); | |
160 | |
161 for (i = 0; i < r->nitems; i++) { | |
162 stream = &(r->data[i]); | |
163 | |
164 if (stream->user) | |
165 putchar('h'); | |
166 else | |
167 putchar('i'); | |
168 | |
169 if (!game_id[0]) { | |
170 if (stream->game) { | |
171 if (utf8pad(buf, sizeof(buf), stream->ga… | |
172 OUTTEXT(buf); | |
173 } else { | |
174 printf("%20s", ""); | |
175 } | |
176 OUT(" "); | |
177 } | |
178 | |
179 if (utf8pad(buf, sizeof(buf), stream->user_name, 20, ' '… | |
180 OUTTEXT(buf); | |
181 OUT(" "); | |
182 | |
183 if (stream->language[0]) | |
184 snprintf(title, sizeof(title), "[%s] %s", stream… | |
185 if (utf8pad(buf, sizeof(buf), title, 50, ' ') !=… | |
186 OUTTEXT(buf); | |
187 else { | |
188 if (utf8pad(buf, sizeof(buf), stream->title, 50,… | |
189 OUTTEXT(buf); | |
190 } | |
191 | |
192 printf(" %7lld\t", stream->viewer_count); | |
193 | |
194 if (stream->user) { | |
195 OUT("URL:https://www.twitch.tv/"); | |
196 OUTLINK(stream->user->login); | |
197 } | |
198 printf("\t%s\t%s\r\n", host, port); | |
199 } | |
200 footer(); | |
201 } | |
202 | |
203 void | |
204 render_videos_atom(struct videos_response *r, const char *login) | |
205 { | |
206 struct video *video; | |
207 size_t i; | |
208 | |
209 OUT("<feed xmlns=\"http://www.w3.org/2005/Atom\" xml:lang=\"en\"… | |
210 for (i = 0; i < r->nitems; i++) { | |
211 video = &(r->data[i]); | |
212 | |
213 OUT("<entry>\n"); | |
214 OUT("\t<title type=\"text\">"); | |
215 xmlencode(video->title); | |
216 OUT("</title>\n"); | |
217 OUT("\t<link rel=\"alternate\" type=\"text/html\" href=\… | |
218 xmlencode(video->url); | |
219 OUT("\" />\n"); | |
220 OUT("\t<id>"); | |
221 xmlencode(video->url); | |
222 OUT("</id>\n"); | |
223 OUT("\t<updated>"); | |
224 xmlencode(video->created_at); | |
225 OUT("</updated>\n"); | |
226 OUT("\t<published>"); | |
227 xmlencode(video->created_at); | |
228 OUT("</published>\n"); | |
229 OUT("</entry>\n"); | |
230 } | |
231 OUT("</feed>\n"); | |
232 } | |
233 | |
234 void | |
235 render_videos(struct videos_response *r, const char *login) | |
236 { | |
237 struct video *video; | |
238 char buf[256]; | |
239 size_t i; | |
240 | |
241 header(); | |
242 | |
243 page('7', "Submit Twitch login name to list VODs", "vods"); | |
244 info(""); | |
245 | |
246 /* no results or no user_id parameter: quick exit */ | |
247 if (r == NULL) { | |
248 footer(); | |
249 return; | |
250 } | |
251 | |
252 /* link to Atom format (text). */ | |
253 if (login[0]) { | |
254 OUT("0Atom feed for "); | |
255 OUTLINK(login); | |
256 printf("\t%s?p=vods&format=atom&login=", baserel); | |
257 OUTLINK(login); | |
258 printf("\t%s\t%s\r\n", host, port); | |
259 info(""); | |
260 } | |
261 | |
262 printf("i%-20s %-50s %-10s %7s\t%s\t%s\t%s\r\n", | |
263 "Created", "Title", "Duration", "Views", "", host, port); | |
264 | |
265 for (i = 0; i < r->nitems; i++) { | |
266 video = &(r->data[i]); | |
267 | |
268 putchar('h'); | |
269 if (utf8pad(buf, sizeof(buf), video->created_at, 20, ' '… | |
270 OUTLINK(buf); | |
271 OUT(" "); | |
272 if (utf8pad(buf, sizeof(buf), video->title, 50, ' ') != … | |
273 OUTLINK(buf); | |
274 OUT(" "); | |
275 if (utf8pad(buf, sizeof(buf), video->duration, 10, ' ') … | |
276 OUTLINK(buf); | |
277 | |
278 printf(" %7lld\t", video->view_count); | |
279 OUT("URL:"); | |
280 OUTLINK(video->url); | |
281 printf("\t%s\t%s\r\n", host, port); | |
282 } | |
283 footer(); | |
284 } | |
285 | |
286 void | |
287 handle_streams(void) | |
288 { | |
289 struct streams_response *r; | |
290 struct users_response *ru = NULL; | |
291 struct games_response *rg = NULL; | |
292 char game_id[32] = ""; | |
293 char *p, *querystring; | |
294 | |
295 pagetitle = "Streams"; | |
296 | |
297 /* parse "game_id" parameter */ | |
298 if ((querystring = getenv("QUERY_STRING"))) { | |
299 if ((p = getparam(querystring, "game_id"))) { | |
300 if (decodeparam(game_id, sizeof(game_id), p) == … | |
301 game_id[0] = '\0'; | |
302 } | |
303 } | |
304 | |
305 if (game_id[0]) | |
306 r = twitch_streams_bygame(game_id); | |
307 else | |
308 r = twitch_streams(); | |
309 | |
310 if (r == NULL) | |
311 return; | |
312 | |
313 /* find detailed games data with streams */ | |
314 if (!game_id[0]) | |
315 rg = twitch_streams_games(r); | |
316 | |
317 /* find detailed user data with streams */ | |
318 ru = twitch_streams_users(r); | |
319 | |
320 if (pledge("stdio", NULL) == -1) | |
321 exit(1); | |
322 | |
323 render_streams(r, game_id); | |
324 | |
325 free(r); | |
326 free(rg); | |
327 free(ru); | |
328 } | |
329 | |
330 void | |
331 handle_videos(void) | |
332 { | |
333 struct videos_response *r = NULL; | |
334 struct users_response *ru = NULL; | |
335 char user_id[32] = "", login[64] = "", format[6] = ""; | |
336 char *p, *querystring; | |
337 | |
338 pagetitle = "Videos"; | |
339 | |
340 /* parse "user_id" or "login" parameter */ | |
341 if ((querystring = getenv("QUERY_STRING"))) { | |
342 if ((p = getparam(querystring, "user_id"))) { | |
343 if (decodeparam(user_id, sizeof(user_id), p) == … | |
344 user_id[0] = '\0'; | |
345 } | |
346 if ((p = getparam(querystring, "login"))) { | |
347 if (decodeparam(login, sizeof(login), p) == -1) | |
348 login[0] = '\0'; | |
349 } | |
350 if ((p = getparam(querystring, "format"))) { | |
351 if (decodeparam(format, sizeof(format), p) == -1) | |
352 format[0] = '\0'; | |
353 } | |
354 } | |
355 | |
356 /* login: if not set as query string parameter then use gopher s… | |
357 parameter */ | |
358 if (login[0] == '\0') { | |
359 if (!(p = getenv("X_GOPHER_SEARCH"))) /* geomyidae */ | |
360 p = getenv("SEARCHREQUEST"); /* gophernicus */ | |
361 if (p && decodeparam(login, sizeof(login), p) == -1) | |
362 login[0] = '\0'; | |
363 } | |
364 | |
365 /* no parameter given, show form */ | |
366 if (!user_id[0] && !login[0]) { | |
367 if (pledge("stdio", NULL) == -1) | |
368 exit(1); | |
369 | |
370 render_videos(r, ""); | |
371 return; | |
372 } | |
373 | |
374 if (user_id[0]) { | |
375 r = twitch_videos_byuserid(user_id); | |
376 } else { | |
377 ru = twitch_users_bylogin(login); | |
378 if (ru && ru->nitems > 0) | |
379 r = twitch_videos_byuserid(ru->data[0].id); | |
380 } | |
381 | |
382 if (pledge("stdio", NULL) == -1) | |
383 exit(1); | |
384 | |
385 if (r && r->nitems > 0) | |
386 title = r->data[0].user_name; | |
387 | |
388 if (!strcmp(format, "atom")) | |
389 render_videos_atom(r, login); | |
390 else | |
391 render_videos(r, login); | |
392 | |
393 free(ru); | |
394 free(r); | |
395 } | |
396 | |
397 void | |
398 handle_games_top(void) | |
399 { | |
400 struct games_response *r; | |
401 | |
402 pagetitle = "Top 100 games"; | |
403 | |
404 if (!(r = twitch_games_top())) | |
405 return; | |
406 | |
407 if (pledge("stdio", NULL) == -1) | |
408 exit(1); | |
409 | |
410 /* sort by name alphabetically, NOTE: the results are the top 100 | |
411 sorted by viewcount. View counts are not visible in the new | |
412 Helix API data). */ | |
413 qsort(r->data, r->nitems, sizeof(r->data[0]), gamecmp_name); | |
414 | |
415 render_games_top(r); | |
416 | |
417 free(r); | |
418 } | |
419 | |
420 void | |
421 handle_links(void) | |
422 { | |
423 if (pledge("stdio", NULL) == -1) | |
424 exit(1); | |
425 | |
426 pagetitle = "Links"; | |
427 | |
428 render_links(); | |
429 } | |
430 | |
431 int | |
432 main(void) | |
433 { | |
434 char *p, path[256] = "", *querystring; | |
435 | |
436 setlocale(LC_CTYPE, ""); | |
437 | |
438 if (pledge("stdio dns inet rpath unveil", NULL) == -1 || | |
439 unveil(TLS_CA_CERT_FILE, "r") == -1 || | |
440 unveil(NULL, NULL) == -1) { | |
441 exit(1); | |
442 } | |
443 | |
444 if ((p = getenv("SERVER_NAME"))) | |
445 host = p; | |
446 if ((p = getenv("SERVER_PORT"))) | |
447 port = p; | |
448 | |
449 if (!(querystring = getenv("QUERY_STRING"))) | |
450 querystring = ""; | |
451 | |
452 if ((p = getparam(querystring, "p"))) { | |
453 if (decodeparam(path, sizeof(path), p) == -1) | |
454 path[0] = '\0'; | |
455 } | |
456 | |
457 if (!strcmp(path, "") || | |
458 !strcmp(path, "featured") || | |
459 !strcmp(path, "streams")) { | |
460 /* featured / by game id */ | |
461 handle_streams(); | |
462 } else if (!strcmp(path, "topgames") || | |
463 !strcmp(path, "games")) { | |
464 handle_games_top(); | |
465 } else if (!strcmp(path, "videos") || | |
466 !strcmp(path, "vods")) { | |
467 handle_videos(); | |
468 } else if (!strcmp(path, "links")) { | |
469 handle_links(); | |
470 } else { | |
471 error("Not Found"); | |
472 printf(".\r\n"); | |
473 exit(1); | |
474 } | |
475 | |
476 return 0; | |
477 } |