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 }