gopher.c - frontends - front-ends for some sites (experiment) (DIR) Log (DIR) Files (DIR) Refs (DIR) README (DIR) 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-dl"); 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, port); 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->game->name, 20, ' ') != -1) 172 OUTTEXT(buf); 173 } else { 174 printf("%20s", ""); 175 } 176 OUT(" "); 177 } 178 179 if (utf8pad(buf, sizeof(buf), stream->user_name, 20, ' ') != -1) 180 OUTTEXT(buf); 181 OUT(" "); 182 183 if (stream->language[0]) 184 snprintf(title, sizeof(title), "[%s] %s", stream->language, stream->title); 185 if (utf8pad(buf, sizeof(buf), title, 50, ' ') != -1) 186 OUTTEXT(buf); 187 else { 188 if (utf8pad(buf, sizeof(buf), stream->title, 50, ' ') != -1) 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\">\n"); 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, ' ') != -1) 270 OUTLINK(buf); 271 OUT(" "); 272 if (utf8pad(buf, sizeof(buf), video->title, 50, ' ') != -1) 273 OUTLINK(buf); 274 OUT(" "); 275 if (utf8pad(buf, sizeof(buf), video->duration, 10, ' ') != -1) 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) == -1) 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) == -1) 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 search 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 }