cgi.c - frontends - front-ends for some sites (experiment)
(DIR) Log
(DIR) Files
(DIR) Refs
(DIR) README
(DIR) 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; charset=UTF-8\" />\n"
53 " <meta http-equiv=\"Content-Language\" content=\"en\" />\n"
54 " <link href=\"twitch.css\" rel=\"stylesheet\" type=\"text/css\" />\n"
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/\">Source-code</a> | \n"
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>\n"
101 "<li><a href=\"https://github.com/ytdl-org/youtube-dl\">youtube-dl</a></li>\n"
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></li>\n"
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\">\n");
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=\"\" placeholder=\"Login name...\" autofocus=\"autofocus\" />\n");
253 OUT("\t<input type=\"submit\" name=\"list\" value=\"List vods\" />\n");
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) == -1)
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) == -1)
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 }