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 }