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