make UI tips context-dependent
[cycon.git] / state.c
blob4f484f16716f4980ec6e5bd88e616a95cb0c8665
1 /*
2 * Copyright (c) 2019, De Rais <derais@cock.li>
4 * Permission to use, copy, modify, and/or distribute this software for
5 * any purpose with or without fee is hereby granted, provided that the
6 * above copyright notice and this permission notice appear in all
7 * copies.
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
10 * WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
11 * WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
12 * AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL
13 * DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR
14 * PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
15 * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
16 * PERFORMANCE OF THIS SOFTWARE.
18 #include <stdio.h>
19 #include <stdlib.h>
20 #include <string.h>
21 #include <time.h>
23 #include <yajl/yajl_tree.h>
25 #include "cycon.h"
26 #include "macros.h"
28 #define xmin(a, b) ((a) < (b) ? (a) : (b))
30 /* For state while in all the yajl callbacks */
31 struct parse_state {
32 /* */
33 int depth;
34 struct cytube_message *m;
37 /* Various paths for accessing JSON */
38 const char *changemedia_id_path[] = { "id", 0 };
39 const char *changemedia_title_path[] = { "title", 0 };
40 const char *changemedia_runtime_path[] = { "duration", 0 };
41 const char *changemedia_seconds_path[] = { "seconds", 0 };
42 const char *changemedia_type_path[] = { "type", 0 };
43 const char *changemedia_currenttime_path[] = { "currentTime", 0 };
44 const char *changemedia_paused_path[] = { "paused", 0 };
45 const char *chatmsg_msg_path[] = { "msg", 0 };
46 const char *chatmsg_username_path[] = { "username", 0 };
47 const char *movemedia_from_path[] = { "from", 0 };
48 const char *movemedia_after_path[] = { "after", 0 };
49 const char *playlist_id_path[] = { "media", "id", 0 };
50 const char *playlist_runtime_path[] = { "media", "duration", 0 };
51 const char *playlist_title_path[] = { "media", "title", 0 };
52 const char *playlist_type_path[] = { "media", "type", 0 };
53 const char *playlist_uid_path[] = { "uid", 0 };
54 const char *queue_id_path[] = { "item", "media", "id", 0 };
55 const char *queue_runtime_path[] = { "item", "media", "duration", 0 };
56 const char *queue_title_path[] = { "item", "media", "title", 0 };
57 const char *queue_type_path[] = { "item", "media", "type", 0 };
58 const char *queue_uid_path[] = { "item", "uid", 0 };
59 const char *queue_after_path[] = { "after", 0 };
61 /* Fw decl */
62 static char * make_uri(const char *type, const char *id);
64 /* Add a message to the end of s's messages, maybe rolling over */
65 #define CHAT_LOG_LEN 70
66 static void
67 append_chat_entry(struct state volatile *s, const char *username, const
68 char *msg, int special)
70 char *duptext = 0;
71 size_t mlen = msg ? strlen(msg) : 0;
73 if (s->chat_log.msgs_len >= CHAT_LOG_LEN) {
74 /* Just slide them over */
75 char *old_user = s->chat_log.msgs[0].user;
76 char *old_text = s->chat_log.msgs[0].text;
77 char *new_text = 0;
79 memmove(s->chat_log.msgs, s->chat_log.msgs + 1, (CHAT_LOG_LEN -
80 1) *
81 sizeof(*s->chat_log.msgs));
82 s->chat_log.msgs[CHAT_LOG_LEN - 1] = (struct chat_msg) { /* */
83 .user = strdup(username), .text = strdup(msg),
84 .special = special
86 duptext = s->chat_log.msgs[CHAT_LOG_LEN - 1].text;
87 mlen = new_text ? strlen(new_text) : 0;
88 free(old_user);
89 free(old_text);
90 } else {
91 size_t new_msgs_len = xmin(s->chat_log.msgs_len + 1,
92 CHAT_LOG_LEN);
93 struct chat_msg *new_msgs = calloc(sizeof(*new_msgs),
94 new_msgs_len);
95 struct chat_msg *old_msgs = s->chat_log.msgs;
96 size_t k = new_msgs_len - 1;
98 if (!new_msgs) {
99 goto done;
102 new_msgs[k] = (struct chat_msg) { /* */
103 .user = strdup(username), .text = strdup(msg),
104 .special = special
106 duptext = new_msgs[k].text;
107 memcpy(new_msgs, old_msgs, s->chat_log.msgs_len *
108 sizeof(*s->chat_log.msgs));
109 s->chat_log.msgs = new_msgs;
110 s->chat_log.msgs_len++;
111 free(old_msgs);
114 for (size_t j = 0; duptext &&
115 j < mlen; ++j) {
116 if ((unsigned char) duptext[j] < 0x20) {
117 duptext[j] = ' ';
121 done:
123 return;
126 /* Make sure a certain entry is in s's playlist */
127 static void
128 ensure_playlist_entry(struct state volatile *s, const char *id, const
129 char *title, const char *runtime, const char *type, int
130 maybe_after_uid, int
131 maybe_uid, int *out_uid)
133 char *uri = make_uri(type, id);
134 int last_uid = -1;
135 int uid = maybe_uid;
136 size_t where_this_entry_is = (size_t) -1;
137 size_t where_prev_entry_is = (size_t) -1;
138 char *duptitle = 0;
141 * What we'd like to do is use the UIDs to insert into the
142 * playlist. However, we might not have a UID. In that case,
143 * fall back to the uri.
145 if (uid < 0) {
147 * This is bogus -- they don't give us a UID, but
148 * they might move it around later based on the
149 * UID, etc. Fixing this properly takes way too
150 * much computation, so let's just give this a fake
151 * UID. If the playlist actually matters, the next
152 * playlist ping will pick it up correctly within
153 * 2 minutes. If the playlist doesn't matter, then
154 * it's probably hidden and we'll just get a list
155 * of currently-playing stuff.
157 for (size_t k = 0; k < s->playlist.entries_len; ++k) {
158 struct playlist_entry *e = &s->playlist.entries[k];
160 last_uid = e->uid;
162 if (!strcmp(e->uri, uri)) {
163 uid = e->uid;
164 break;
168 if (uid < 0) {
169 uid = last_uid + 10;
175 * Now, just try and stick this thing in place. If there's
176 * already an entry with this UID, we need to updated it
177 * and move it to the right place (if maybe_after_uid is
178 * set)
180 for (size_t k = 0; k < s->playlist.entries_len; ++k) {
181 struct playlist_entry *e = &s->playlist.entries[k];
183 if (e->uid == uid) {
184 where_this_entry_is = k;
187 if (e->uid == maybe_after_uid) {
188 where_prev_entry_is = k;
193 if (where_prev_entry_is == (size_t) -1 &&
194 s->playlist.entries_len > 0 &&
195 where_this_entry_is != 0) {
196 where_prev_entry_is = s->playlist.entries_len - 1;
199 if (where_this_entry_is == (size_t) -1) {
201 /* We have no spot for this entry. So insert it */
202 void *newmem = 0;
203 size_t new_size = (s->playlist.entries_len + 1) *
204 sizeof *(s->playlist.entries);
205 struct playlist_entry *new_e = 0;
207 if (s->playlist.entries_len > ((size_t) -1) >> 2) {
208 ERROR_MESSAGE("overflow");
209 goto done;
212 if (!(newmem = realloc(s->playlist.entries, new_size))) {
213 PERROR_MESSAGE("realloc");
214 goto done;
217 s->playlist.entries = newmem;
218 s->playlist.entries_len = s->playlist.entries_len + 1;
219 new_e = &(s->playlist.entries[s->playlist.entries_len - 1]);
220 new_e->title = strdup(title);
221 duptitle = new_e->title;
222 new_e->title_width = strwidth(title);
223 new_e->runtime = strdup(runtime);
224 new_e->uri = strdup(uri);
225 new_e->uid = uid;
226 where_this_entry_is = s->playlist.entries_len - 1;
227 } else {
229 * We actually have a spot for this entry. Just
230 * overwrite it.
232 struct playlist_entry *e =
233 &s->playlist.entries[where_this_entry_is];
235 free(e->title);
236 e->title = strdup(title);
237 duptitle = e->title;
238 e->title_width = strwidth(title);
239 free(e->runtime);
240 e->runtime = strdup(runtime);
241 free(e->uri);
242 e->uri = strdup(uri);
243 e->uid = uid;
247 * At this point, s->p.e[where_this_entry_is] actually holds
248 * the data we want. The only problem imight be that it
249 * might not be directly after where_prev_entry_is.
251 if (maybe_after_uid < 0 ||
252 where_prev_entry_is == (size_t) -1 ||
253 where_prev_entry_is + 1 == where_this_entry_is) {
254 goto done;
257 /* Now we actually need to rearrange things, sadly. */
258 if (where_prev_entry_is + 1 < where_this_entry_is) {
259 struct playlist_entry temp_e = (struct playlist_entry) { 0 };
260 size_t to_move_num = where_this_entry_is -
261 (where_prev_entry_is + 1);
262 size_t to_move_len = to_move_num * sizeof *s->playlist.entries;
264 memcpy(&temp_e, &(s->playlist.entries[where_this_entry_is]),
265 sizeof temp_e);
266 memmove(&(s->playlist.entries[where_prev_entry_is + 2]),
267 &(s->playlist.entries[where_prev_entry_is + 1]),
268 to_move_len);
269 memcpy(&(s->playlist.entries[where_prev_entry_is + 1]), &temp_e,
270 sizeof temp_e);
271 } else {
272 struct playlist_entry temp_e = (struct playlist_entry) { 0 };
273 size_t to_move_num = where_prev_entry_is - where_this_entry_is;
274 size_t to_move_len = to_move_num * sizeof *s->playlist.entries;
276 memcpy(&temp_e, &(s->playlist.entries[where_this_entry_is]),
277 sizeof temp_e);
278 memmove(&(s->playlist.entries[where_this_entry_is]),
279 &(s->playlist.entries[where_this_entry_is + 1]),
280 to_move_len);
281 memcpy(&(s->playlist.entries[where_prev_entry_is]), &temp_e,
282 sizeof temp_e);
285 done:
286 if (duptitle) {
287 for (size_t k = 0; duptitle[k]; ++k) {
288 if ((unsigned char) duptitle[k] < 0x20) {
289 duptitle[k] = ' ';
294 *out_uid = uid;
295 free(uri);
297 return;
300 /* Free all memory associated with a chat_log */
301 static void
302 clean_chat_log(struct chat_log volatile *c)
304 for (size_t k = 0; k < c->msgs_len; ++k) {
305 free(c->msgs[k].user);
306 free(c->msgs[k].text);
309 free(c->msgs);
310 c->msgs = 0;
311 c->msgs_len = 0;
314 /* Free all memory associated with a playlist */
315 static void
316 clean_playlist(struct playlist volatile *p)
318 for (size_t k = 0; k < p->entries_len; ++k) {
319 struct playlist_entry *e = &p->entries[k];
321 free(e->title);
322 free(e->runtime);
323 free(e->uri);
326 free(p->entries);
327 p->entries = 0;
328 p->entries_len = 0;
331 /* yajl_tree_get, returning strings */
332 static char *
333 get_string_from(yajl_val root, const char **path)
335 yajl_val v = yajl_tree_get(root, path, yajl_t_string);
337 return YAJL_GET_STRING(v);
340 /* yajl_tree_get, returning ints */
341 static int
342 get_integer_from(yajl_val root, const char **path)
344 yajl_val v = yajl_tree_get(root, path, yajl_t_number);
346 if (YAJL_IS_INTEGER(v)) {
347 return YAJL_GET_INTEGER(v);
350 return 0;
353 /* yajl_tree_get, returning floating-point */
354 static float
355 get_double_from(yajl_val root, const char **path)
357 yajl_val v = yajl_tree_get(root, path, yajl_t_number);
359 if (YAJL_IS_DOUBLE(v)) {
360 return YAJL_GET_DOUBLE(v);
363 return 0;
366 /* "yt", "9NACc7DBRh0" -> {an actual URI} */
367 static char *
368 make_uri(const char *type, const char *id)
370 if (!strcmp(type, "yt")) {
371 return aprintf("https://youtube.com/watch?v=%s", id);
372 } else if (!strcmp(type, "vi")) {
373 return aprintf("https://vimeo.com/%s", id);
374 } else if (!strcmp(type, "dm")) {
375 return aprintf("https://dailymotion.com/video/%s", id);
376 } else if (!strcmp(type, "sc")) {
377 return strdup(id);
378 } else if (!strcmp(type, "li")) {
379 return aprintf("https://livestream.com/%s", id);
380 } else if (!strcmp(type, "tw")) {
381 return aprintf("https://twitch.tv/%s", id);
382 } else if (!strcmp(type, "rt")) {
383 return strdup(id);
384 } else if (!strcmp(type, "im")) {
385 return aprintf("https://imgur.com/a/%s", id);
386 } else if (!strcmp(type, "us")) {
387 return aprintf("https://ustream.tv/channel/%s", id);
388 } else if (!strcmp(type, "gd")) {
389 return aprintf("https://docs.google.com/file/d/%s", id);
390 } else if (!strcmp(type, "fi")) {
391 return strdup(id);
392 } else if (!strcmp(type, "hb")) {
393 return aprintf("https://www.smashcast.tv/%s", id);
394 } else if (!strcmp(type, "hl")) {
395 return strdup(id);
396 } else if (!strcmp(type, "sb")) {
397 return aprintf("https://www.streamable.com/%s", id);
398 } else if (!strcmp(type, "tc")) {
399 return aprintf("https://clips.twitch.tv/%s", id);
400 } else if (!strcmp(type, "cm")) {
401 return strdup(id);
404 return strdup("");
407 /* Make valgrind happy */
408 void
409 state_clean(struct state volatile *s)
412 * Note this function only affects the "application state"
413 * and "connection info" parts of state. The UI stuff is
414 * handled in ui.c.
416 clean_playlist(&s->playlist);
417 clean_chat_log(&s->chat_log);
418 free(s->socket_host);
419 free(s->sid);
420 *s = (struct state) { 0 };
423 /* Handle a message. */
425 state_handle(struct state volatile *s, const char *msg, size_t len)
428 * The boilerplate for passing errors back out of the LWS
429 * thread isn't worth it, so just do nothing on parse errors.
431 int ret = -1;
432 char eb[1024] = { 0 };
433 char *null_terminated = 0;
434 const char *command = 0;
435 yajl_val tree = 0;
438 * YAJL has some stupid memory leaks when seeing invalid
439 * JSON. Pull request #168 is one, there's another in string
440 * handling, etc. Let's try to block a few of them.
442 if (msg[len - 1] != ']') {
443 goto done;
446 if (!(null_terminated = malloc(len + 1))) {
447 goto done;
450 memcpy(null_terminated, msg, len);
451 null_terminated[len] = 0;
452 tree = yajl_tree_parse(null_terminated, eb, 1024);
454 if ((ret = -!!eb[0])) {
455 goto done;
458 if (!tree ||
459 !YAJL_IS_ARRAY(tree) ||
460 YAJL_GET_ARRAY(tree)->len < 1 ||
461 !((command = YAJL_GET_STRING(YAJL_GET_ARRAY(tree)->values[0])))) {
462 goto done;
465 if (!strcmp(command, "setPermissions")) {
466 if (!s->playlist.entries_len) {
467 s->must_ask_for_playlist = 1;
469 } else if (!strcmp(command, "changeMedia") &&
470 YAJL_GET_ARRAY(tree)->len >= 2) {
471 s->dirty_top = 1;
472 yajl_val e = YAJL_GET_ARRAY(tree)->values[1];
473 char *id = get_string_from(e, changemedia_id_path);
474 char *title = get_string_from(e, changemedia_title_path);
475 char *runtime = get_string_from(e, changemedia_runtime_path);
476 char *type = get_string_from(e, changemedia_type_path);
477 int runtime_s = get_integer_from(e, changemedia_seconds_path);
478 double currenttime = get_double_from(e,
479 changemedia_currenttime_path);
480 char *paused = get_string_from(e, changemedia_paused_path);
482 s->playlist.paused = (paused &&
483 !strcmp(paused, "true"));
484 s->playlist.current_playtime = (int) currenttime;
485 s->playlist.current_length = runtime_s;
486 s->playlist.zero_clocktime = time(0);
487 s->playlist.zero_playtime = s->playlist.current_playtime;
488 int uid = 0;
490 ensure_playlist_entry(s, id, title, runtime, type, -1, -1,
491 &uid);
492 s->playlist.current_playing_uid = uid;
493 s->dirty_playlist = 1;
494 } else if (!strcmp(command, "moveMedia") &&
495 YAJL_GET_ARRAY(tree)->len >= 2) {
496 yajl_val e = YAJL_GET_ARRAY(tree)->values[1];
497 char *from = get_string_from(e, movemedia_from_path);
498 char *after = get_string_from(e, movemedia_after_path);
500 /* TODO: handle this */
501 LOG("moveMedia: \u00ab%s\u00bb", null_terminated);
502 LOG("from = \u00ab%s\u00bb", from);
503 LOG("after = \u00ab%s\u00bb", after);
504 } else if (!strcmp(command, "playlist") &&
505 YAJL_GET_ARRAY(tree)->len >= 2 &&
506 YAJL_IS_ARRAY(YAJL_GET_ARRAY(tree)->values[1])) {
507 int last_uid = -1;
508 size_t these_len = YAJL_GET_ARRAY(YAJL_GET_ARRAY(
509 tree)->values[1])->len;
511 for (size_t k = 0; k < these_len; ++k) {
512 yajl_val e = YAJL_GET_ARRAY(YAJL_GET_ARRAY(
513 tree)->values[1])->
514 values[k];
515 char *id = get_string_from(e, playlist_id_path);
516 char *title = get_string_from(e, playlist_title_path);
517 char *runtime = get_string_from(e,
518 playlist_runtime_path);
519 char *type = get_string_from(e, playlist_type_path);
520 int uid = get_integer_from(e, playlist_uid_path);
522 ensure_playlist_entry(s, id, title, runtime, type,
523 last_uid, uid, &uid);
524 last_uid = uid;
527 s->dirty_top = 1;
528 s->dirty_playlist = 1;
529 } else if (!strcmp(command, "chatMsg") &&
530 YAJL_GET_ARRAY(tree)->len >= 2) {
531 yajl_val e = YAJL_GET_ARRAY(tree)->values[1];
532 char *username = get_string_from(e, chatmsg_username_path);
533 char *msg = get_string_from(e, chatmsg_msg_path);
535 append_chat_entry(s, username, msg, 0);
537 if (!s->uri_or_chat) {
538 s->dirty_top = 1;
540 } else if (!strcmp(command, "queue") &&
541 YAJL_GET_ARRAY(tree)->len >= 2) {
542 yajl_val e = YAJL_GET_ARRAY(tree)->values[1];
543 char *id = get_string_from(e, queue_id_path);
544 char *title = get_string_from(e, queue_title_path);
545 char *runtime = get_string_from(e, queue_runtime_path);
546 char *type = get_string_from(e, queue_type_path);
547 int uid = get_integer_from(e, queue_uid_path);
548 int after = get_integer_from(e, queue_after_path);
550 ensure_playlist_entry(s, id, title, runtime, type, after, uid,
551 &uid);
552 s->dirty_playlist = 1;
555 done:
556 yajl_tree_free(tree);
557 free(null_terminated);
559 return ret;