1 /*
2 * This file is part of GNOME Twitch - 'Enjoy Twitch on your GNU/Linux desktop'
3 * Copyright © 2017 Vincent Szolnoky <vinszent@vinszent.com>
4 *
5 * GNOME Twitch is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * GNOME Twitch is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with GNOME Twitch. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "gt-twitch.h"
20 #include "gt-resource-downloader.h"
21 #include "config.h"
22 #include <libsoup/soup.h>
23 #include <glib/gprintf.h>
24 #include <glib/gi18n.h>
25 #include <glib.h>
26 #include <json-glib/json-glib.h>
27 #include <string.h>
28 #include <stdlib.h>
29 #include "utils.h"
30
31 #define TAG "GtTwitch"
32 #include "gnome-twitch/gt-log.h"
33
34 //TODO: Use https://streams.twitch.tv/kraken/streams/{channel}?stream_type=all instead to get is_playlist info
35 //TODO: Use https://tmi.twitch.tv/servers?channel=%s to get chat server info
36
37 #define ACCESS_TOKEN_URI "https://api.twitch.tv/api/channels/%s/access_token"
38 #define STREAM_PLAYLIST_URI "https://usher.ttvnw.net/api/channel/hls/%s.m3u8?player=twitchweb&token=%s&sig=%s&allow_audio_only=true&allow_source=true&type=any&allow_spectre=true&p=%d"
39 #define TOP_CHANNELS_URI "https://api.twitch.tv/kraken/streams?limit=%d&offset=%d&game=%s&broadcaster_language=%s"
40 #define TOP_GAMES_URI "https://api.twitch.tv/kraken/games/top?limit=%d&offset=%d"
41 #define SEARCH_STREAMS_URI "https://api.twitch.tv/kraken/search/streams?query=%s&limit=%d&offset=%d"
42 #define SEARCH_CHANNELS_URI "https://api.twitch.tv/kraken/search/channels?query=%s&limit=%d&offset=%d"
43 #define SEARCH_GAMES_URI "https://api.twitch.tv/kraken/search/games?query=%s&type=suggest"
44 #define FETCH_STREAM_URI "https://api.twitch.tv/kraken/streams/%s"
45 #define FETCH_CHANNEL_URI "https://api.twitch.tv/kraken/channels/%s"
46 #define CHAT_BADGES_URI "https://api.twitch.tv/kraken/chat/%s/badges/"
47 #define TWITCH_EMOTE_URI "https://static-cdn.jtvnw.net/emoticons/v1/%d/%d.0"
48 #define CHANNEL_INFO_URI "https://api.twitch.tv/api/channels/%s/panels"
49 #define CHAT_SERVERS_URI "https://api.twitch.tv/api/channels/%s/chat_properties"
50 #define FOLLOWED_STREAMS_URI "https://api.twitch.tv/kraken/streams/followed?limit=%d&offset=%d&oauth_token=%s&stream_type=live"
51 #define FOLLOWED_CHANNELS_URI "https://api.twitch.tv/kraken/users/%s/follows/channels?limit=%d&offset=%d"
52 #define FOLLOW_CHANNEL_URI "https://api.twitch.tv/kraken/users/%s/follows/channels/%s?oauth_token=%s"
53 #define UNFOLLOW_CHANNEL_URI "https://api.twitch.tv/kraken/users/%s/follows/channels/%s?oauth_token=%s"
54 #define USER_EMOTICONS_URI "https://api.twitch.tv/kraken/users/%s/emotes"
55 #define EMOTICON_IMAGES_URI "https://api.twitch.tv/kraken/chat/emoticon_images?emotesets=%s"
56 #define USER_INFO_URI "https://api.twitch.tv/kraken/user?oauth_token=%s"
57 #define GLOBAL_CHAT_BADGES_URI "https://badges.twitch.tv/v1/badges/global/display"
58 #define NEW_CHAT_BADGES_URI "https://badges.twitch.tv/v1/badges/channels/%s/display"
59 #define OAUTH_INFO_URI "https://api.twitch.tv/kraken/?oauth_token=%s"
60
61 #define STREAM_INFO "#EXT-X-STREAM-INF"
62
63 #define TWITCH_API_VERSION_3 "3"
64 #define TWITCH_API_VERSION_4 "4"
65 #define TWITCH_API_VERSION_5 "5"
66
67 #define END_JSON_MEMBER() json_reader_end_member(reader) // Just for consistency's sake
68 #define END_JSON_ELEMENT() json_reader_end_element(reader) // Just for consistency's sake
69
70 #define READ_JSON_MEMBER(name) \
71 if (!json_reader_read_member(reader, name)) \
72 { \
73 const GError* e = json_reader_get_error(reader); \
74 \
75 g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \
76 "Unable to read JSON member with name '%s' because: %s", name, e->message); \
77 \
78 WARNINGF("Unable to read JSON member with name '%s' because: %s", name, e->message); \
79 \
80 goto error; \
81 } \
82
83 #define READ_JSON_ELEMENT(i) \
84 if (!json_reader_read_element(reader, i)) \
85 { \
86 const GError* e = json_reader_get_error(reader); \
87 \
88 g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \
89 "Unable to read JSON element with position '%d' because: %s", i, e->message); \
90 \
91 WARNINGF("Unable to read JSON element with position '%d' because: %s", i, e->message); \
92 \
93 goto error; \
94 } \
95
96 #define READ_JSON_ELEMENT_VALUE(i, p) \
97 if (json_reader_read_element(reader, i)) \
98 { \
99 p = _Generic(p, \
100 gchar*: g_strdup(json_reader_get_string_value(reader)), \
101 gint64: json_reader_get_int_value(reader), \
102 gdouble: json_reader_get_double_value(reader), \
103 gboolean: json_reader_get_boolean_value(reader), \
104 GDateTime*: parse_time(json_reader_get_string_value(reader))); \
105 } \
106 else \
107 { \
108 const GError* e = json_reader_get_error(reader); \
109 \
110 g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \
111 "Unable to read JSON element with position '%d' because: %s", i, e->message); \
112 \
113 WARNINGF("Unable to read JSON element with position '%d' because: %s", i, e->message); \
114 \
115 goto error; \
116 } \
117 json_reader_end_element(reader); \
118
119
120 #define READ_JSON_VALUE(name, p) \
121 if (json_reader_read_member(reader, name)) \
122 { \
123 p = _Generic(p, \
124 gchar*: g_strdup(json_reader_get_string_value(reader)), \
125 gint64: json_reader_get_int_value(reader), \
126 gdouble: json_reader_get_double_value(reader), \
127 gboolean: json_reader_get_boolean_value(reader), \
128 GDateTime*: parse_time(json_reader_get_string_value(reader))); \
129 } \
130 else \
131 { \
132 const GError* e = json_reader_get_error(reader); \
133 \
134 g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \
135 "Unable to read JSON member with name '%s' because: %s", name, e->message); \
136 \
137 WARNINGF("Unable to read JSON member with name '%s' because: %s", name, e->message); \
138 \
139 goto error; \
140 } \
141 json_reader_end_member(reader); \
142
143 #define READ_JSON_VALUE_NULL(name, p, def) \
144 if (json_reader_read_member(reader, name)) \
145 { \
146 if (json_reader_get_null_value(reader)) \
147 { \
148 p = _Generic(p, \
149 gchar*: g_strdup(def), \
150 default: def); \
151 } \
152 else \
153 { \
154 p = _Generic(p, \
155 gchar*: g_strdup(json_reader_get_string_value(reader)), \
156 gint64: json_reader_get_int_value(reader), \
157 gdouble: json_reader_get_double_value(reader), \
158 gboolean: json_reader_get_boolean_value(reader), \
159 GDateTime*: parse_time(json_reader_get_string_value(reader))); \
160 } \
161 } \
162 else \
163 { \
164 g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \
165 "Unable to read JSON member with name '%s'", name); \
166 \
167 WARNINGF("Unable to read JSON member with name '%s'", name); \
168 \
169 goto error; \
170 } \
171 json_reader_end_member(reader); \
172
173 #define CHECK_AND_PROPAGATE_ERROR(msg, ...) \
174 if (err) \
175 { \
176 WARNINGF(msg " because: %s", ##__VA_ARGS__, err->message); \
177 \
178 g_propagate_prefixed_error(error, err, msg " because: ", ##__VA_ARGS__); \
179 \
180 goto error; \
181 } \
182
183 typedef struct
184 {
185 SoupSession* soup;
186
187 GThreadPool* image_download_pool;
188
189 GHashTable* emote_table;
190 GHashTable* badge_table;
191 } GtTwitchPrivate;
192
G_DEFINE_TYPE_WITH_PRIVATE(GtTwitch,gt_twitch,G_TYPE_OBJECT)193 G_DEFINE_TYPE_WITH_PRIVATE(GtTwitch, gt_twitch, G_TYPE_OBJECT)
194
195 static GtResourceDownloader* emote_downloader;
196 static GtResourceDownloader* badge_downloader;
197
198 static GtTwitchStreamAccessToken*
199 gt_twitch_stream_access_token_new()
200 {
201 return g_new0(GtTwitchStreamAccessToken, 1);
202 }
203
204 void
gt_twitch_stream_access_token_free(GtTwitchStreamAccessToken * token)205 gt_twitch_stream_access_token_free(GtTwitchStreamAccessToken* token)
206 {
207 g_free(token->token);
208 g_free(token->sig);
209 g_free(token);
210 }
211
212 GtChatEmote*
gt_chat_emote_new()213 gt_chat_emote_new()
214 {
215 GtChatEmote* emote = g_new0(GtChatEmote, 1);
216
217 emote->start = -1;
218 emote->end = -1;
219
220 return emote;
221 }
222
223 void
gt_chat_emote_free(GtChatEmote * emote)224 gt_chat_emote_free(GtChatEmote* emote)
225 {
226 g_assert_nonnull(emote);
227 g_assert(GDK_IS_PIXBUF(emote->pixbuf));
228
229 g_object_unref(emote->pixbuf);
230 g_free(emote->code);
231 g_free(emote);
232 }
233
234 void
gt_chat_emote_list_free(GList * list)235 gt_chat_emote_list_free(GList* list)
236 {
237 g_list_free_full(list, (GDestroyNotify) gt_chat_emote_free);
238 }
239
240 GQuark
gt_spawn_twitch_error_quark()241 gt_spawn_twitch_error_quark()
242 {
243 return g_quark_from_static_string("gt-twitch-error-quark");
244 }
245
246 GtTwitch*
gt_twitch_new(void)247 gt_twitch_new(void)
248 {
249 return g_object_new(GT_TYPE_TWITCH,
250 NULL);
251 }
252
253 static void
gt_twitch_class_init(GtTwitchClass * klass)254 gt_twitch_class_init(GtTwitchClass* klass)
255 {
256 }
257
258 static void
gt_twitch_init(GtTwitch * self)259 gt_twitch_init(GtTwitch* self)
260 {
261 GtTwitchPrivate* priv = gt_twitch_get_instance_private(self);
262
263 priv->soup = soup_session_new();
264 priv->emote_table = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) g_object_unref);
265 priv->badge_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify) gt_chat_badge_free);
266
267 g_autofree gchar* emotes_filepath = g_build_filename(g_get_user_cache_dir(),
268 "gnome-twitch", "emotes", NULL);
269
270 g_autofree gchar* badges_filepath = g_build_filename(g_get_user_cache_dir(),
271 "gnome-twitch", "badges", NULL);
272
273 emote_downloader = gt_resource_downloader_new_with_cache(emotes_filepath);
274 gt_resource_downloader_set_image_filetype(emote_downloader, GT_IMAGE_FILETYPE_PNG);
275
276 badge_downloader = gt_resource_downloader_new_with_cache(badges_filepath);
277 gt_resource_downloader_set_image_filetype(badge_downloader, GT_IMAGE_FILETYPE_PNG);
278
279 g_signal_connect_swapped(main_app, "shutdown", G_CALLBACK(g_object_unref), emote_downloader);
280 g_signal_connect_swapped(main_app, "shutdown", G_CALLBACK(g_object_unref), badge_downloader);
281 }
282
283 static gboolean
send_message(GtTwitch * self,SoupMessage * msg)284 send_message(GtTwitch* self, SoupMessage* msg)
285 {
286 GtTwitchPrivate* priv = gt_twitch_get_instance_private(self);
287 gboolean ret;
288 char* uri = soup_uri_to_string(soup_message_get_uri(msg), FALSE);
289
290 DEBUGF("Sending message to uri '%s'", uri);
291
292 soup_message_headers_append(msg->request_headers, "Client-ID", CLIENT_ID);
293
294 soup_session_send_message(priv->soup, msg);
295
296 ret = SOUP_STATUS_IS_SUCCESSFUL(msg->status_code);
297
298 if (ret)
299 TRACEF("Received response from url '%s' with code '%d' and body '%s'",
300 uri, msg->status_code, msg->response_body->data);
301 else
302 WARNINGF("Received unsuccessful response from url '%s' with code '%d' and body '%s'",
303 uri, msg->status_code, msg->response_body->data);
304
305 g_free(uri);
306
307 return ret;
308 }
309
310 //TODO: Refactor GtTwitch to use these new functions
311 static void
new_send_message(GtTwitch * self,SoupMessage * msg,GError ** error)312 new_send_message(GtTwitch* self, SoupMessage* msg, GError** error)
313 {
314 g_assert(GT_IS_TWITCH(self));
315 g_assert(SOUP_IS_MESSAGE(msg));
316
317 GtTwitchPrivate* priv = gt_twitch_get_instance_private(self);
318 char* uri = soup_uri_to_string(soup_message_get_uri(msg), FALSE);
319
320 DEBUGF("Sending message to uri '%s'", uri);
321
322 soup_message_headers_append(msg->request_headers, "Client-ID", CLIENT_ID);
323
324 soup_session_send_message(priv->soup, msg);
325
326 if (SOUP_STATUS_IS_SUCCESSFUL(msg->status_code))
327 {
328 TRACEF("Received response from url '%s' with code '%d' and body '%s'",
329 uri, msg->status_code, msg->response_body->data);
330 }
331 else
332 {
333 gint code;
334
335 WARNINGF("Received unsuccessful response from url '%s' with code '%d' and body '%s'",
336 uri, msg->status_code, msg->response_body->data);
337
338 code = msg->status_code == GT_TWITCH_ERROR_SOUP_NOT_FOUND ?
339 GT_TWITCH_ERROR_SOUP_NOT_FOUND : GT_TWITCH_ERROR_SOUP_GENERIC;
340
341 g_set_error(error, GT_TWITCH_ERROR, code,
342 "Received unsuccessful response from url '%s' with code '%d' and body '%s'",
343 uri, msg->status_code, msg->response_body->data);
344 }
345
346 g_free(uri);
347 }
348
349 static JsonReader*
new_send_message_json_with_version(GtTwitch * self,SoupMessage * msg,const gchar * version,GError ** error)350 new_send_message_json_with_version(GtTwitch* self, SoupMessage* msg, const gchar* version, GError** error)
351 {
352 g_assert(GT_IS_TWITCH(self));
353 g_assert(SOUP_IS_MESSAGE(msg));
354
355 JsonReader* ret = NULL;
356 g_autofree gchar* accept_header = g_strdup_printf("application/vnd.twitchtv.v%s+json", version);
357
358 soup_message_headers_append(msg->request_headers, "Accept", accept_header);
359
360 new_send_message(self, msg, error);
361
362 if (!*error)
363 {
364 JsonParser* parser = json_parser_new();
365 JsonNode* node = NULL;
366 GError* e = NULL;
367
368 json_parser_load_from_data(parser, msg->response_body->data, -1, &e);
369
370 if (e)
371 {
372 g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON,
373 "Error parsing JSON response because: %s", e->message);
374
375 WARNINGF("Error parsing JSON response because: %s", e->message);
376
377 g_error_free(e);
378
379 goto finish;
380 }
381
382 node = json_parser_get_root(parser);
383 ret = json_reader_new(json_node_ref(node)); //NOTE: Parser doesn't seem to have its own reference to node
384
385 g_object_unref(G_OBJECT(parser));
386 }
387
388 finish:
389 return ret;
390 }
391
392 static JsonReader*
new_send_message_json(GtTwitch * self,SoupMessage * msg,GError ** error)393 new_send_message_json(GtTwitch* self, SoupMessage* msg, GError** error)
394 {
395 return new_send_message_json_with_version(self, msg, TWITCH_API_VERSION_5, error);
396 }
397
398 static GDateTime*
parse_time(const gchar * time)399 parse_time(const gchar* time)
400 {
401 GDateTime* ret = NULL;
402
403 gint year, month, day, hour, min, sec;
404
405 sscanf(time, "%d-%d-%dT%d:%d:%dZ", &year, &month, &day, &hour, &min, &sec);
406
407 ret = g_date_time_new_utc(year, month, day, hour, min, sec);
408
409 return ret;
410 }
411
412 static GList*
parse_playlist(const gchar * playlist)413 parse_playlist(const gchar* playlist)
414 {
415 GList* ret = NULL;
416 g_auto(GStrv) lines = g_strsplit(playlist, "\n", 0);
417
418 for (gchar** l = lines; *l != NULL; l++)
419 {
420 if (strncmp(*l, STREAM_INFO, strlen(STREAM_INFO)) == 0)
421 {
422 GtTwitchStreamData* stream = g_malloc0(sizeof(GtTwitchStreamData));
423
424 g_auto(GStrv) values = g_strsplit(*l, ",", 0);
425
426 for (gchar** m = values; *m != NULL; m++)
427 {
428 g_auto(GStrv) split = g_strsplit(*m, "=", 0);
429
430 if (strcmp(*split, "BANDWIDTH") == 0)
431 {
432 stream->bandwidth = atol(*(split+1));
433 }
434 else if (strcmp(*split, "RESOLUTION") == 0)
435 {
436 gchar** res = g_strsplit(*(split+1), "x", 0);
437
438 stream->width = atoi(*res);
439 stream->height = atoi(*(res+1));
440
441 g_strfreev(res);
442 }
443 else if (strcmp(*split, "VIDEO") == 0)
444 {
445 //TODO: Replace these with an error
446 g_assert(g_str_has_prefix(*(split+1), "\""));
447 g_assert(g_str_has_suffix(*(split+1), "\""));
448
449 //NOTE: quality isn't const but still shouldn't be freed,
450 //it will be freed when split is freed.
451 gchar* quality = *(split+1)+1;
452 quality[strlen(quality) - 1] = '\0';
453
454 if (STRING_EQUALS(quality, "chunked"))
455 stream->quality = g_strdup("source");
456 else if (STRING_EQUALS(quality, "audio_only")) //NOTE: Remove audio only streams
457 {
458 gt_twitch_stream_data_free(stream);
459
460 stream = NULL;
461
462 goto next;
463 }
464 else
465 stream->quality = g_strdup(quality);
466 }
467 }
468
469 l++;
470
471 stream->url = g_strdup(*l);
472
473 next:
474 if (stream) ret = g_list_append(ret, stream);
475 }
476 }
477
478 return ret;
479 }
480
481 static GtChannelData*
parse_channel(JsonReader * reader,GError ** error)482 parse_channel(JsonReader* reader, GError** error)
483 {
484 GtChannelData* data = gt_channel_data_new();
485
486 //NOTE: Need this hack until Twitch gets their shit together
487 //and standardizes on either integers or strings as IDs
488 READ_JSON_MEMBER("_id");
489 JsonNode* node = json_reader_get_value(reader);
490 if (STRING_EQUALS(json_node_type_name(node), "Integer"))
491 data->id = g_strdup_printf("%" G_GINT64_FORMAT, json_reader_get_int_value(reader));
492 else if (STRING_EQUALS(json_node_type_name(node), "String"))
493 data->id = g_strdup(json_reader_get_string_value(reader));
494 else
495 g_assert_not_reached();
496 END_JSON_MEMBER();
497
498 READ_JSON_VALUE("name", data->name);
499 READ_JSON_VALUE_NULL("display_name", data->display_name, NULL);
500 READ_JSON_VALUE_NULL("status", data->status, _("Untitled broadcast"));
501 READ_JSON_VALUE_NULL("video_banner", data->video_banner_url, NULL);
502 READ_JSON_VALUE_NULL("logo", data->logo_url, NULL);
503 READ_JSON_VALUE("url", data->profile_url);
504
505 data->online = FALSE;
506
507 return data;
508
509 error:
510 gt_channel_data_free(data);
511
512 return NULL;
513 }
514
515 static GtChannelData*
parse_stream(JsonReader * reader,GError ** error)516 parse_stream(JsonReader* reader, GError** error)
517 {
518 GtChannelData* data = NULL;
519
520 READ_JSON_MEMBER("channel");
521 data = parse_channel(reader, error);
522 END_JSON_MEMBER();
523
524 if (*error) goto error;
525
526 READ_JSON_VALUE_NULL("game", data->game, NULL);
527 READ_JSON_VALUE("viewers", data->viewers);
528 READ_JSON_VALUE("created_at", data->stream_started_time);
529 READ_JSON_MEMBER("preview");
530 READ_JSON_VALUE("large", data->preview_url);
531 END_JSON_MEMBER();
532
533 data->online = TRUE;
534
535 return data;
536
537 error:
538 gt_channel_data_free(data);
539
540 return NULL;
541 }
542
543 static GtGameData*
parse_game(JsonReader * reader,GError ** error)544 parse_game(JsonReader* reader, GError** error)
545 {
546 GtGameData* data = gt_game_data_new();
547
548 //NOTE: Same hack as above for channel
549 READ_JSON_MEMBER("_id");
550 JsonNode* node = json_reader_get_value(reader);
551 if (STRING_EQUALS(json_node_type_name(node), "Integer"))
552 data->id = g_strdup_printf("%" G_GINT64_FORMAT, json_reader_get_int_value(reader));
553 else if (STRING_EQUALS(json_node_type_name(node), "String"))
554 data->id = g_strdup(json_reader_get_string_value(reader));
555 else
556 g_assert_not_reached();
557 END_JSON_MEMBER();
558
559 READ_JSON_VALUE("name", data->name);
560 READ_JSON_MEMBER("box");
561 READ_JSON_VALUE("large", data->preview_url);
562 END_JSON_MEMBER();
563 READ_JSON_MEMBER("logo");
564 READ_JSON_VALUE("large", data->logo_url);
565 END_JSON_MEMBER();
566
567 return data;
568
569 error:
570 gt_game_data_free(data);
571
572 return NULL;
573 }
574
575 GtTwitchStreamAccessToken*
gt_twitch_stream_access_token(GtTwitch * self,const gchar * channel,GError ** error)576 gt_twitch_stream_access_token(GtTwitch* self, const gchar* channel, GError** error)
577 {
578 g_assert(GT_IS_TWITCH(self));
579 g_assert_false(utils_str_empty(channel));
580
581 g_autoptr(SoupMessage) msg = NULL;
582 g_autoptr(JsonReader) reader = NULL;
583 g_autofree gchar* uri = NULL;
584 GtTwitchStreamAccessToken* ret = NULL;
585 GError* err = NULL;
586
587 uri = g_strdup_printf(ACCESS_TOKEN_URI, channel);
588
589 msg = soup_message_new("GET", uri);
590
591 reader = new_send_message_json(self, msg, &err);
592
593 CHECK_AND_PROPAGATE_ERROR("Error getting stream access token for channel '%s'",
594 channel);
595
596 ret = gt_twitch_stream_access_token_new();
597
598 READ_JSON_VALUE("sig", ret->sig);
599 READ_JSON_VALUE("token", ret->token);
600
601 return ret;
602
603 error:
604 gt_twitch_stream_access_token_free(ret);
605
606 return NULL;
607 }
608
609 GList*
gt_twitch_all_streams(GtTwitch * self,const gchar * channel,GError ** error)610 gt_twitch_all_streams(GtTwitch* self, const gchar* channel, GError** error)
611 {
612 g_assert(GT_IS_TWITCH(self));
613 g_assert_false(utils_str_empty(channel));
614
615 g_autoptr(SoupMessage) msg = NULL;
616 g_autofree gchar* uri = NULL;
617 GtTwitchStreamAccessToken* token = NULL;
618 GList* ret = NULL;
619 GError* err = NULL;
620
621 token = gt_twitch_stream_access_token(self, channel, &err);
622
623 CHECK_AND_PROPAGATE_ERROR("Unable to get streams for channel '%s'",
624 channel);
625
626 uri = g_strdup_printf(STREAM_PLAYLIST_URI, channel,
627 token->token, token->sig, g_random_int_range(0, 999999));
628
629 gt_twitch_stream_access_token_free(token);
630
631 msg = soup_message_new("GET", uri);
632
633 new_send_message(self, msg, &err);
634
635 CHECK_AND_PROPAGATE_ERROR("Unable to get all streams for channel '%s'",
636 channel);
637
638 ret = parse_playlist(msg->response_body->data);
639
640 error:
641 return ret;
642 }
643
644 static void
all_streams_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)645 all_streams_cb(GTask* task, gpointer source,
646 gpointer task_data, GCancellable* cancel)
647 {
648 g_assert(G_IS_TASK(task));
649 g_assert(GT_IS_TWITCH(source));
650 g_assert_nonnull(task_data);
651
652 GenericTaskData* data = NULL;
653 GList* ret = NULL;
654 GError* err = NULL;
655
656 if (g_task_return_error_if_cancelled(task))
657 return;
658
659 data = task_data;
660
661 ret = gt_twitch_all_streams(GT_TWITCH(source), data->str_1, &err);
662
663 if (err)
664 g_task_return_error(task, err);
665 else
666 g_task_return_pointer(task, ret, (GDestroyNotify) gt_twitch_stream_data_list_free);
667 }
668
669 void
gt_twitch_all_streams_async(GtTwitch * self,const gchar * channel,GCancellable * cancel,GAsyncReadyCallback cb,gpointer udata)670 gt_twitch_all_streams_async(GtTwitch* self, const gchar* channel,
671 GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata)
672 {
673 GTask* task;
674 GenericTaskData* data;
675
676 task = g_task_new(self, cancel, cb, udata);
677
678 g_task_set_return_on_cancel(task, FALSE);
679
680 data = generic_task_data_new();
681 data->str_1 = g_strdup(channel);
682
683 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
684
685 g_task_run_in_thread(task, all_streams_cb);
686 }
687
688 GList*
gt_twitch_all_streams_finish(GtTwitch * self,GAsyncResult * result,GError ** error)689 gt_twitch_all_streams_finish(GtTwitch* self, GAsyncResult* result, GError** error)
690 {
691 g_assert(GT_IS_TWITCH(self));
692 g_assert(G_IS_ASYNC_RESULT(result));
693
694 GList* ret = g_task_propagate_pointer(G_TASK(result), error);
695
696 return ret;
697 }
698
699 const GtTwitchStreamData*
gt_twitch_stream_list_filter_quality(GList * list,const gchar * quality)700 gt_twitch_stream_list_filter_quality(GList* list,
701 const gchar* quality)
702 {
703 GtTwitchStreamData* ret = NULL;
704
705 for (GList* l = list; l != NULL; l = l->next)
706 {
707 ret = (GtTwitchStreamData*) l->data;
708
709 if (STRING_EQUALS(ret->quality, quality))
710 break;
711 else
712 ret = NULL;
713 }
714
715 if (!ret)
716 {
717 ret = list->data;
718 g_assert_nonnull(ret);
719 }
720
721 return ret;
722 }
723
724 GList*
gt_twitch_top_channels(GtTwitch * self,gint n,gint offset,const gchar * game,const gchar * language,GError ** error)725 gt_twitch_top_channels(GtTwitch* self, gint n, gint offset,
726 const gchar* game, const gchar* language, GError** error)
727 {
728 g_assert(GT_IS_TWITCH(self));
729 g_assert_cmpint(n, >=, 0);
730 g_assert_cmpint(n, <=, 100);
731 g_assert_cmpint(offset, >=, 0);
732 g_assert_nonnull(game);
733 g_assert_nonnull(language);
734
735 g_autoptr(SoupMessage) msg = NULL;
736 g_autoptr(JsonReader) reader = NULL;
737 g_autofree gchar* uri = NULL;
738 GList* ret = NULL;
739 GError* err = NULL;
740
741 uri = g_strdup_printf(TOP_CHANNELS_URI, n, offset, game, language);
742
743 msg = soup_message_new("GET", uri);
744
745 reader = new_send_message_json(self, msg, &err);
746
747 CHECK_AND_PROPAGATE_ERROR("Unable to fetch top channels with amount '%d', offset '%d' and game '%s'",
748 n, offset, game);
749
750 READ_JSON_MEMBER("streams");
751
752 for (gint i = 0; i < json_reader_count_elements(reader); i++)
753 {
754 GtChannel* channel = NULL;
755 GtChannelData* data = NULL;
756
757 READ_JSON_ELEMENT(i);
758
759 data = parse_stream(reader, &err);
760
761 CHECK_AND_PROPAGATE_ERROR("Unable to fetch top channels with amount '%d', offset '%d' and game '%s'",
762 n, offset, game);
763
764 channel = gt_channel_new(data);
765
766 END_JSON_ELEMENT();
767
768 ret = g_list_append(ret, channel);
769 }
770
771 END_JSON_MEMBER();
772
773 return ret;
774
775 error:
776 gt_channel_list_free(ret);
777
778 return NULL;
779 }
780 static void
top_channels_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)781 top_channels_async_cb(GTask* task,
782 gpointer source,
783 gpointer task_data,
784 GCancellable* cancel)
785 {
786 g_assert(GT_IS_TWITCH(source));
787 g_assert(G_IS_TASK(task));
788 g_assert_nonnull(task_data);
789
790 GenericTaskData* data = task_data;
791 GList* ret = NULL;
792 GError* err = NULL;
793
794 if (g_task_return_error_if_cancelled(task))
795 return;
796
797 ret = gt_twitch_top_channels(GT_TWITCH(source), data->int_1, data->int_2,
798 data->str_1, data->str_2, &err);
799
800 if (err)
801 g_task_return_error(task, err);
802 else
803 g_task_return_pointer(task, ret, (GDestroyNotify) gt_channel_list_free);
804 }
805
806 void
gt_twitch_top_channels_async(GtTwitch * self,gint n,gint offset,const gchar * game,const gchar * language,GCancellable * cancel,GAsyncReadyCallback cb,gpointer udata)807 gt_twitch_top_channels_async(GtTwitch* self, gint n, gint offset, const gchar* game,
808 const gchar* language, GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata)
809 {
810 g_assert(GT_IS_TWITCH(self));
811 g_assert_cmpint(n, >=, 0);
812 g_assert_cmpint(n, <=, 100);
813 g_assert_cmpint(offset, >=, 0);
814 g_assert_nonnull(game);
815 g_assert_nonnull(language);
816
817 GTask* task = NULL;
818 GenericTaskData* data = NULL;
819
820 task = g_task_new(self, cancel, cb, udata);
821 g_task_set_return_on_cancel(task, FALSE);
822
823 data = generic_task_data_new();
824 data->int_1 = n;
825 data->int_2 = offset;
826 data->str_1 = g_strdup(game);
827 data->str_2 = g_strdup(language);
828
829 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
830
831 g_task_run_in_thread(task, top_channels_async_cb);
832
833 g_object_unref(task);
834 }
835
836 GList*
gt_twitch_top_channels_finish(GtTwitch * self,GAsyncResult * result,GError ** error)837 gt_twitch_top_channels_finish(GtTwitch* self,
838 GAsyncResult* result, GError** error)
839 {
840 g_assert(GT_IS_TWITCH(self));
841 g_assert(G_IS_ASYNC_RESULT(result));
842
843 GList* ret = g_task_propagate_pointer(G_TASK(result), error);
844
845 return ret;
846 }
847
848 GList*
gt_twitch_top_games(GtTwitch * self,gint n,gint offset,GError ** error)849 gt_twitch_top_games(GtTwitch* self,
850 gint n, gint offset, GError** error)
851 {
852 g_assert(GT_IS_TWITCH(self));
853 g_assert_cmpint(n, >=, 0);
854 g_assert_cmpint(n, <=, 100);
855 g_assert_cmpint(offset, >=, 0);
856
857 g_autoptr(SoupMessage) msg = NULL;
858 g_autoptr(JsonReader) reader = NULL;
859 g_autofree gchar* uri = NULL;
860 GList* ret = NULL;
861 GError* err = NULL;
862
863 uri = g_strdup_printf(TOP_GAMES_URI, n, offset);
864
865 msg = soup_message_new("GET", uri);
866
867 reader = new_send_message_json(self, msg, &err);
868
869 CHECK_AND_PROPAGATE_ERROR("Unable to get top games with amount '%d' and offset '%d'",
870 n, offset);
871
872 READ_JSON_MEMBER("top");
873
874 for (gint i = 0; i < json_reader_count_elements(reader); i++)
875 {
876 GtGame* game = NULL;
877 GtGameData* data = NULL;
878
879 READ_JSON_ELEMENT(i);
880
881 READ_JSON_MEMBER("game");
882
883 data = parse_game(reader, &err);
884
885 CHECK_AND_PROPAGATE_ERROR("Unable to get top games with amount '%d' and offset '%d'",
886 n, offset);
887
888 END_JSON_MEMBER();
889
890 READ_JSON_VALUE("viewers", data->viewers);
891 READ_JSON_VALUE("channels", data->channels);
892
893 game = gt_game_new(data);
894
895 END_JSON_ELEMENT();
896
897 ret = g_list_append(ret, game);
898 }
899
900 END_JSON_MEMBER();
901
902 return ret;
903
904 error:
905 gt_game_list_free(ret);
906
907 return NULL;
908 }
909
910
911 static void
top_games_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)912 top_games_async_cb(GTask* task,
913 gpointer source,
914 gpointer task_data,
915 GCancellable* cancel)
916 {
917 g_assert(GT_IS_TWITCH(source));
918 g_assert(G_IS_TASK(task));
919 g_assert_nonnull(task_data);
920
921 GenericTaskData* data = task_data;
922 GList* ret = NULL;
923 GError* err = NULL;
924
925 if (g_task_return_error_if_cancelled(task))
926 return;
927
928 ret = gt_twitch_top_games(GT_TWITCH(source), data->int_1, data->int_2, &err);
929
930 if (err)
931 g_task_return_error(task, err);
932 else
933 g_task_return_pointer(task, ret, (GDestroyNotify) gt_game_list_free);
934 }
935
936 void
gt_twitch_top_games_async(GtTwitch * self,gint n,gint offset,GCancellable * cancel,GAsyncReadyCallback cb,gpointer udata)937 gt_twitch_top_games_async(GtTwitch* self, gint n, gint offset,
938 GCancellable* cancel,
939 GAsyncReadyCallback cb,
940 gpointer udata)
941 {
942 g_assert(GT_IS_TWITCH(self));
943 g_assert_cmpint(n, >=, 0);
944 g_assert_cmpint(n, <=, 100);
945 g_assert_cmpint(offset, >=, 0);
946
947 GTask* task = NULL;
948 GenericTaskData* data = NULL;
949
950 task = g_task_new(self, cancel, cb, udata);
951 g_task_set_return_on_cancel(task, FALSE);
952
953 data = generic_task_data_new();
954 data->int_1 = n;
955 data->int_2 = offset;
956
957 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
958
959 g_task_run_in_thread(task, top_games_async_cb);
960
961 g_object_unref(task);
962 }
963
964 GList*
gt_twitch_top_games_finish(GtTwitch * self,GAsyncResult * result,GError ** error)965 gt_twitch_top_games_finish(GtTwitch* self,
966 GAsyncResult* result, GError** error)
967 {
968 g_assert(GT_IS_TWITCH(self));
969 g_assert(G_IS_ASYNC_RESULT(result));
970
971 GList* ret = g_task_propagate_pointer(G_TASK(result), error);
972
973 return ret;
974 }
975
976 //NOTE: Twitch's stream search API is retarted (see https://github.com/justintv/Twitch-API/issues/513)
977 //so we need to do this hack to get anything remotely usable. It will return duplicates unless
978 //amount=offset*k where k is some multiple, i.e. it works in 'pages'
979 GList*
gt_twitch_search_channels(GtTwitch * self,const gchar * query,gint n,gint offset,gboolean offline,GError ** error)980 gt_twitch_search_channels(GtTwitch* self, const gchar* query, gint n, gint offset, gboolean offline, GError** error)
981 {
982 g_assert(GT_IS_TWITCH(self));
983 g_assert_cmpint(n, >=, 0);
984 g_assert_cmpint(n, <=, 100);
985 g_assert_cmpint(offset, >=, 0);
986 g_assert_false(utils_str_empty(query));
987
988 #define SEARCH_AMOUNT 100
989
990 const gint PAGE_AMOUNT = offline ? 100 : 90;
991
992 MESSAGEF("Searching for channels with query '%s', amount '%d' ('%d') and offset '%d' ('%d')",
993 query, 100, SEARCH_AMOUNT, (offset / PAGE_AMOUNT) * 100, offset);
994
995 g_autoptr(SoupMessage) msg = NULL;
996 g_autoptr(JsonReader) reader = NULL;
997 g_autofree gchar* uri = NULL;
998 gint total;
999 GList* ret = NULL;
1000 GError* err = NULL;
1001
1002 uri = g_strdup_printf(offline ? SEARCH_CHANNELS_URI : SEARCH_STREAMS_URI,
1003 query, SEARCH_AMOUNT, (offset / PAGE_AMOUNT) * SEARCH_AMOUNT);
1004
1005 msg = soup_message_new("GET", uri);
1006
1007 reader = new_send_message_json(self, msg, &err);
1008
1009 CHECK_AND_PROPAGATE_ERROR("Unable to search channels with query '%s', amount '%d' ('%d') and offset '%d' ('%d')",
1010 query, PAGE_AMOUNT, SEARCH_AMOUNT, (offset / PAGE_AMOUNT) * PAGE_AMOUNT, offset);
1011
1012 READ_JSON_MEMBER(offline ? "channels" : "streams");
1013
1014 total = MIN(n + offset % PAGE_AMOUNT, json_reader_count_elements(reader));
1015
1016 for (gint i = offset % PAGE_AMOUNT; i < total; i++)
1017 {
1018 GtChannel* channel = NULL;
1019 GtChannelData* data = NULL;
1020
1021 READ_JSON_ELEMENT(i);
1022
1023 data = offline ? parse_channel(reader, &err) : parse_stream(reader, &err);
1024
1025 CHECK_AND_PROPAGATE_ERROR("Unable to search channels with query '%s', amount '%d' ('%d') and offset '%d' ('%d')",
1026 query, PAGE_AMOUNT, SEARCH_AMOUNT, (offset / PAGE_AMOUNT) * PAGE_AMOUNT, offset);
1027
1028 channel = gt_channel_new(data);
1029
1030 END_JSON_ELEMENT();
1031
1032 ret = g_list_append(ret, channel);
1033 }
1034
1035 json_reader_end_member(reader);
1036
1037 return ret;
1038
1039 error:
1040 gt_channel_list_free(ret);
1041
1042 return NULL;
1043 }
1044
1045 static void
search_channels_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)1046 search_channels_async_cb(GTask* task, gpointer source,
1047 gpointer task_data, GCancellable* cancel)
1048 {
1049 g_assert(GT_IS_TWITCH(source));
1050 g_assert(G_IS_TASK(task));
1051 g_assert_nonnull(task_data);
1052
1053 GenericTaskData* data = task_data;
1054
1055 if (g_task_return_error_if_cancelled(task))
1056 return;
1057
1058 GError* err = NULL;
1059 GList* ret = gt_twitch_search_channels(GT_TWITCH(source), data->str_1,
1060 data->int_1, data->int_2, data->bool_1, &err);
1061
1062 if (err)
1063 g_task_return_error(task, err);
1064 else
1065 g_task_return_pointer(task, ret, (GDestroyNotify) gt_channel_list_free);
1066 }
1067
1068 void
gt_twitch_search_channels_async(GtTwitch * self,const gchar * query,gint n,gint offset,gboolean offline,GCancellable * cancel,GAsyncReadyCallback cb,gpointer udata)1069 gt_twitch_search_channels_async(GtTwitch* self,
1070 const gchar* query, gint n, gint offset, gboolean offline,
1071 GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata)
1072 {
1073 g_assert(GT_IS_TWITCH(self));
1074 g_assert_cmpint(n, >=, 0);
1075 g_assert_cmpint(n, <=, 100);
1076 g_assert_cmpint(offset, >=, 0);
1077 g_assert_false(utils_str_empty(query));
1078
1079 GTask* task = NULL;
1080 GenericTaskData* data = NULL;
1081
1082 task = g_task_new(self, cancel, cb, udata);
1083 g_task_set_return_on_cancel(task, FALSE);
1084
1085 data = generic_task_data_new();
1086 data->int_1 = n;
1087 data->int_2 = offset;
1088 data->str_1 = g_strdup(query);
1089 data->bool_1 = offline;
1090
1091 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
1092
1093 g_task_run_in_thread(task, search_channels_async_cb);
1094
1095 g_object_unref(task);
1096 }
1097
1098 GList*
gt_twitch_search_channels_finish(GtTwitch * self,GAsyncResult * result,GError ** error)1099 gt_twitch_search_channels_finish(GtTwitch* self,
1100 GAsyncResult* result, GError** error)
1101
1102 {
1103 g_assert(GT_IS_TWITCH(self));
1104 g_assert(G_IS_ASYNC_RESULT(result));
1105
1106 GList* ret = g_task_propagate_pointer(G_TASK(result), error);
1107
1108 return ret;
1109 }
1110
1111 GList*
gt_twitch_search_games(GtTwitch * self,const gchar * query,gint n,gint offset,GError ** error)1112 gt_twitch_search_games(GtTwitch* self,
1113 const gchar* query, gint n, gint offset,
1114 GError** error)
1115 {
1116 g_assert(GT_IS_TWITCH(self));
1117 g_assert_cmpint(n, >=, 0);
1118 g_assert_cmpint(n, <=, 100);
1119 g_assert_cmpint(offset, >=, 0);
1120 g_assert_false(utils_str_empty(query));
1121
1122 g_autoptr(SoupMessage) msg = NULL;
1123 g_autoptr(JsonReader) reader = NULL;
1124 g_autofree gchar* uri = NULL;
1125 GList* ret = NULL;
1126 GError* err = NULL;
1127
1128 uri = g_strdup_printf(SEARCH_GAMES_URI, query);
1129
1130 msg = soup_message_new("GET", uri);
1131
1132 reader = new_send_message_json(self, msg, &err);
1133
1134 CHECK_AND_PROPAGATE_ERROR("Unable to search games with query '%s', amount '%d' and offset '%d'",
1135 query, n, offset);
1136
1137 READ_JSON_MEMBER("games");
1138
1139 for (gint i = 0; i < json_reader_count_elements(reader); i++)
1140 {
1141 GtGame* game;
1142 GtGameData* data = NULL;
1143
1144 READ_JSON_ELEMENT(i);
1145
1146 data = parse_game(reader, &err);
1147
1148 CHECK_AND_PROPAGATE_ERROR("Unable to search games with query '%s', amount '%d' and offset '%d'",
1149 query, n, offset);
1150
1151 game = gt_game_new(data);
1152
1153 END_JSON_ELEMENT();
1154
1155 ret = g_list_append(ret, game);
1156 }
1157
1158 END_JSON_MEMBER();
1159
1160 return ret;
1161
1162 error:
1163 gt_game_list_free(ret);
1164
1165 return NULL;
1166 }
1167
1168
1169 static void
search_games_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)1170 search_games_async_cb(GTask* task, gpointer source,
1171 gpointer task_data, GCancellable* cancel)
1172 {
1173 g_assert(GT_IS_TWITCH(source));
1174 g_assert(G_IS_TASK(task));
1175 g_assert_nonnull(task_data);
1176
1177 GenericTaskData* data = task_data;
1178
1179 if (g_task_return_error_if_cancelled(task))
1180 return;
1181
1182 GError* err = NULL;
1183 GList* ret = gt_twitch_search_games(GT_TWITCH(source), data->str_1, data->int_1, data->int_2, &err);
1184
1185 if (err)
1186 g_task_return_error(task, err);
1187 else
1188 g_task_return_pointer(task, ret, (GDestroyNotify) gt_game_list_free);
1189 }
1190
1191 void
gt_twitch_search_games_async(GtTwitch * self,const gchar * query,gint n,gint offset,GCancellable * cancel,GAsyncReadyCallback cb,gpointer udata)1192 gt_twitch_search_games_async(GtTwitch* self,
1193 const gchar* query, gint n, gint offset,
1194 GCancellable* cancel, GAsyncReadyCallback cb,
1195 gpointer udata)
1196 {
1197 g_assert(GT_IS_TWITCH(self));
1198 g_assert_cmpint(n, >=, 0);
1199 g_assert_cmpint(n, <=, 100);
1200 g_assert_cmpint(offset, >=, 0);
1201 g_assert_false(utils_str_empty(query));
1202
1203 GTask* task = NULL;
1204 GenericTaskData* data = NULL;
1205
1206 task = g_task_new(self, cancel, cb, udata);
1207 g_task_set_return_on_cancel(task, FALSE);
1208
1209 data = generic_task_data_new();
1210 data->int_1 = n;
1211 data->int_2 = offset;
1212 data->str_1 = g_strdup(query);
1213
1214 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
1215
1216 g_task_run_in_thread(task, search_games_async_cb);
1217
1218 g_object_unref(task);
1219 }
1220
1221 GList*
gt_twitch_search_games_finish(GtTwitch * self,GAsyncResult * result,GError ** error)1222 gt_twitch_search_games_finish(GtTwitch* self,
1223 GAsyncResult* result, GError** error)
1224 {
1225 g_assert(GT_IS_TWITCH(self));
1226 g_assert(G_IS_ASYNC_RESULT(result));
1227
1228 GList* ret = g_task_propagate_pointer(G_TASK(result), error);
1229
1230 return ret;
1231 }
1232
1233 void
gt_twitch_stream_data_free(GtTwitchStreamData * data)1234 gt_twitch_stream_data_free(GtTwitchStreamData* data)
1235 {
1236 g_free(data->quality);
1237 g_free(data->url);
1238 g_free(data);
1239 }
1240
1241 void
gt_twitch_stream_data_list_free(GList * list)1242 gt_twitch_stream_data_list_free(GList* list)
1243 {
1244 g_list_free_full(list, (GDestroyNotify) gt_twitch_stream_data_free);
1245 }
1246
1247 GtChannelData*
gt_twitch_fetch_channel_data(GtTwitch * self,const gchar * id,GError ** error)1248 gt_twitch_fetch_channel_data(GtTwitch* self, const gchar* id, GError** error)
1249 {
1250 g_assert(GT_IS_TWITCH(self));
1251 g_assert_false(utils_str_empty(id));
1252
1253 g_autoptr(SoupMessage) msg = NULL;
1254 g_autoptr(JsonReader) reader = NULL;
1255 g_autofree gchar* uri = NULL;
1256 GError* err = NULL;
1257 GtChannelData* ret = NULL;
1258
1259 uri = g_strdup_printf(FETCH_STREAM_URI, id);
1260
1261 msg = soup_message_new("GET", uri);
1262
1263 reader = new_send_message_json(self, msg, &err);
1264
1265 CHECK_AND_PROPAGATE_ERROR("Unable to fetch channel data with id '%s'",
1266 id);
1267
1268 ret = gt_channel_data_new();
1269
1270 READ_JSON_MEMBER("stream");
1271
1272 if (json_reader_get_null_value(reader))
1273 {
1274 //NOTE: Free these here as they will be used again
1275 g_object_unref(msg);
1276 g_object_unref(reader);
1277 g_free(uri);
1278
1279 uri = g_strdup_printf(FETCH_CHANNEL_URI, id);
1280
1281 msg = soup_message_new("GET", uri);
1282
1283 reader = new_send_message_json(self, msg, &err);
1284
1285 CHECK_AND_PROPAGATE_ERROR("Unable to fetch channel data with id '%s'",
1286 id);
1287
1288 ret = parse_channel(reader, &err);
1289
1290 CHECK_AND_PROPAGATE_ERROR("Unable to fetch channel data with id '%s'",
1291 id);
1292 }
1293 else
1294 {
1295 ret = parse_stream(reader, &err);
1296
1297 CHECK_AND_PROPAGATE_ERROR("Unable to fetch channel data with id '%s'",
1298 id);
1299
1300 END_JSON_MEMBER();
1301 }
1302
1303 return ret;
1304
1305 error:
1306
1307 gt_channel_data_free(ret);
1308
1309 return NULL;
1310 }
1311
1312 GtChannel*
gt_twitch_fetch_channel(GtTwitch * self,const gchar * id,GError ** error)1313 gt_twitch_fetch_channel(GtTwitch* self, const gchar* id, GError** error)
1314 {
1315 g_assert(GT_IS_TWITCH(self));
1316 g_assert_false(utils_str_empty(id));
1317
1318 GtChannelData* data = NULL;
1319 GtChannel* channel = NULL;
1320
1321 DEBUG("Fetching channel with id '%s'", id);
1322
1323 data = gt_twitch_fetch_channel_data(self, id, error);
1324
1325 if (*error)
1326 return NULL;
1327
1328 channel = gt_channel_new(data);
1329
1330 g_assert(g_object_is_floating(channel));
1331
1332 g_object_ref_sink(channel);
1333
1334 return channel;
1335 }
1336
1337 static void
fetch_channel_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)1338 fetch_channel_async_cb(GTask* task, gpointer source,
1339 gpointer task_data, GCancellable* cancel)
1340 {
1341 g_assert(GT_IS_TWITCH(source));
1342 g_assert(G_IS_TASK(task));
1343 g_assert_nonnull(task_data);
1344
1345 GError* err = NULL;
1346 GenericTaskData* data = task_data;
1347
1348 GtChannel* ret = gt_twitch_fetch_channel(GT_TWITCH(source), data->str_1, &err);
1349
1350 if (err)
1351 g_task_return_error(task, err);
1352 else
1353 g_task_return_pointer(task, ret, (GDestroyNotify) g_object_unref);
1354 }
1355
1356 void
gt_twitch_fetch_channel_async(GtTwitch * self,const gchar * id,GAsyncReadyCallback cb,GCancellable * cancel,gpointer udata)1357 gt_twitch_fetch_channel_async(GtTwitch* self,
1358 const gchar* id, GAsyncReadyCallback cb,
1359 GCancellable* cancel, gpointer udata)
1360 {
1361 g_assert(GT_IS_TWITCH(self));
1362 g_assert_false(utils_str_empty(id));
1363
1364 GTask* task = NULL;
1365 GenericTaskData* data = generic_task_data_new();
1366
1367 task = g_task_new(self, cancel, cb, udata);
1368
1369 data->str_1 = g_strdup(id);
1370
1371 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
1372
1373 g_task_run_in_thread(task, fetch_channel_async_cb);
1374
1375 g_object_unref(task);
1376 }
1377
1378 GtChannel*
gt_twitch_fetch_channel_finish(GtTwitch * self,GAsyncResult * result,GError ** error)1379 gt_twitch_fetch_channel_finish(GtTwitch* self,
1380 GAsyncResult* result, GError** error)
1381 {
1382 g_assert(GT_IS_TWITCH(self));
1383 g_assert(G_IS_ASYNC_RESULT(result));
1384
1385 GtChannel* ret = g_task_propagate_pointer(G_TASK(result), error);
1386
1387 return ret;
1388 }
1389
1390 /* GdkPixbuf* */
1391 /* gt_twitch_download_picture_new(GtTwitch* self, const gchar* uri, */
1392 /* gboolean check_cache, GError** error) */
1393 /* { */
1394 /* g_return_val_if_fail(GT_IS_TWITCH(self), NULL); */
1395 /* g_return_val_if_fail(!utils_str_empty(uri), NULL); */
1396
1397 /* GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); */
1398 /* GdkPixbuf* ret = NULL; */
1399
1400 /* g_autoptr(SoupMessage) msg = NULL; */
1401
1402 /* if (check_cache) */
1403 /* { */
1404 /* g_autofree gchar* filename= NULL; */
1405 /* gchar hash_str[15]; */
1406 /* guint hash = 0; */
1407 /* gint64 timestamp = 0; */
1408
1409 /* hash = g_str_hash(uri); */
1410
1411 /* g_sprintf(hash_str, "%ud", hash); */
1412
1413 /* filename = g_build_filename(g_get_user_cache_dir(), */
1414 /* "gnome-twitch", "images", NULL); */
1415
1416 /* if (g_file_test(filename, G_FILE_TEST_EXISTS)) */
1417 /* { */
1418 /* ret = */
1419 /* } */
1420 /* } */
1421 /* } */
1422
1423 GdkPixbuf*
gt_twitch_download_picture(GtTwitch * self,const gchar * url,gint64 timestamp,GError ** error)1424 gt_twitch_download_picture(GtTwitch* self, const gchar* url, gint64 timestamp, GError** error)
1425 {
1426 g_assert(GT_IS_TWITCH(self));
1427 g_assert_false(utils_str_empty(url));
1428
1429 GtTwitchPrivate* priv = gt_twitch_get_instance_private(self);
1430 g_autoptr(SoupMessage) msg = NULL;
1431 GError* err = NULL;
1432 GdkPixbuf* ret = NULL;
1433 g_autoptr(GInputStream) input_stream = NULL;
1434
1435 DEBUG("Downloading picture from url '%s'", url);
1436
1437 if (timestamp)
1438 {
1439 msg = soup_message_new(SOUP_METHOD_HEAD, url);
1440 soup_message_headers_append(msg->request_headers, "Client-ID", CLIENT_ID);
1441 soup_session_send_message(priv->soup, msg);
1442
1443 if (SOUP_STATUS_IS_SUCCESSFUL(msg->status_code))
1444 {
1445 const gchar* last_modified;
1446
1447 if ((last_modified = soup_message_headers_get_one(msg->response_headers, "Last-Modified")) != NULL &&
1448 utils_http_full_date_to_timestamp(last_modified) < timestamp)
1449 {
1450 DEBUG("No new content at url '%s'", url);
1451
1452 return NULL;
1453 }
1454 }
1455 else
1456 {
1457 WARNING("Unable to download picture from url '%s' because received unsuccessful response with code '%d' and body '%s'",
1458 url, msg->status_code, msg->response_body->data);
1459
1460 g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_SOUP_GENERIC,
1461 "Unable to download picture from url '%s' because received unsuccessful response with code '%d' and body '%s'",
1462 url, msg->status_code, msg->response_body->data);
1463
1464 return NULL;
1465 }
1466 }
1467
1468 #define CHECK_ERROR \
1469 if (err) \
1470 { \
1471 WARNINGF("Error downloading picture for url '%s' because: %s", \
1472 url, err->message); \
1473 \
1474 g_propagate_prefixed_error(error, err, "Unable to download picture for url '%s' because: ", \
1475 url); \
1476 \
1477 return NULL; \
1478 } \
1479
1480
1481 msg = soup_message_new(SOUP_METHOD_GET, url);
1482 soup_message_headers_append(msg->request_headers, "Client-ID", CLIENT_ID);
1483 input_stream = soup_session_send(priv->soup, msg, NULL, &err);
1484
1485 CHECK_ERROR;
1486
1487 ret = gdk_pixbuf_new_from_stream(input_stream, NULL, &err);
1488
1489 CHECK_ERROR;
1490
1491 //TODO: Need to free ret if an error is encountered here
1492 g_input_stream_close(input_stream, NULL, &err);
1493
1494 CHECK_ERROR;
1495
1496 return ret;
1497 }
1498
1499 static void
download_picture_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)1500 download_picture_async_cb(GTask* task,
1501 gpointer source,
1502 gpointer task_data,
1503 GCancellable* cancel)
1504 {
1505 GenericTaskData* data = (GenericTaskData*) task_data;
1506 GdkPixbuf* ret = NULL;
1507 GError* err = NULL;
1508
1509 if (g_task_return_error_if_cancelled(task))
1510 return;
1511
1512 ret = gt_twitch_download_picture(GT_TWITCH(source), data->str_1, data->int_1, &err);
1513
1514 if (err)
1515 g_task_return_error(task, err);
1516 else
1517 g_task_return_pointer(task, ret, (GDestroyNotify) g_object_unref);
1518 }
1519
1520 void
gt_twitch_download_picture_async(GtTwitch * self,const gchar * url,gint64 timestamp,GCancellable * cancel,GAsyncReadyCallback cb,gpointer udata)1521 gt_twitch_download_picture_async(GtTwitch* self,
1522 const gchar* url, gint64 timestamp,
1523 GCancellable* cancel, GAsyncReadyCallback cb,
1524 gpointer udata)
1525 {
1526 g_assert(GT_IS_TWITCH(self));
1527 g_assert_false(utils_str_empty(url));
1528
1529 GTask* task = NULL;
1530 GenericTaskData* data = NULL;
1531
1532 task = g_task_new(self, cancel, cb, udata);
1533 g_task_set_return_on_cancel(task, FALSE);
1534
1535 data = generic_task_data_new();
1536 data->str_1 = g_strdup(url);
1537 data->int_1 = timestamp;
1538
1539 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
1540
1541 g_task_run_in_thread(task, download_picture_async_cb);
1542
1543 g_object_unref(task);
1544 }
1545
1546 GdkPixbuf*
gt_twitch_download_emote(GtTwitch * self,gint id)1547 gt_twitch_download_emote(GtTwitch* self, gint id)
1548 {
1549 GtTwitchPrivate* priv = gt_twitch_get_instance_private(self);
1550 GdkPixbuf* ret = NULL;
1551
1552 if (!g_hash_table_contains(priv->emote_table, GINT_TO_POINTER(id)))
1553 {
1554 g_autofree gchar* uri = NULL;
1555 g_autoptr(GError) err = NULL;
1556 g_autoptr(GdkPixbuf) emote = NULL;
1557 gchar id_str[15];
1558
1559 uri = g_strdup_printf(TWITCH_EMOTE_URI, id, 1);
1560 g_sprintf(id_str, "%d", id);
1561
1562 DEBUGF("Downloading emote form url='%s'", uri);
1563
1564 emote = gt_resource_downloader_download_image(emote_downloader, uri, id_str, &err);
1565
1566 /* NOTE: If we encountered an error here we'll just insert a generic error emote */
1567 if (err)
1568 {
1569 g_autoptr(GtkIconInfo) icon_info;
1570
1571 WARNING("Unable to download emote with id '%d' because: %s", id, err->message);
1572
1573 g_clear_error(&err);
1574
1575 icon_info = gtk_icon_theme_lookup_icon(gtk_icon_theme_get_default(),
1576 "software-update-urgent-symbolic", 1, 0);
1577
1578 emote = gtk_icon_info_load_icon(icon_info, &err);
1579
1580 RETURN_VAL_IF_FAIL(err == NULL, NULL);
1581 }
1582
1583 g_hash_table_insert(priv->emote_table, GINT_TO_POINTER(id),
1584 g_steal_pointer(&emote));
1585
1586 //TODO: Propagate this error further
1587 RETURN_VAL_IF_FAIL(err == NULL, NULL);
1588 }
1589
1590 ret = GDK_PIXBUF(g_hash_table_lookup(priv->emote_table, GINT_TO_POINTER(id)));
1591
1592 g_object_ref(ret);
1593
1594 return ret;
1595 }
1596
1597 static void
fetch_chat_badge_set(GtTwitch * self,const gchar * set_name,GError ** error)1598 fetch_chat_badge_set(GtTwitch* self, const gchar* set_name, GError** error)
1599 {
1600 g_assert(GT_IS_TWITCH(self));
1601 g_assert_false(utils_str_empty(set_name));
1602
1603 GtTwitchPrivate* priv = gt_twitch_get_instance_private(self);
1604 g_autoptr(SoupMessage) msg = NULL;
1605 g_autoptr(JsonReader) reader = NULL;
1606 g_autofree gchar* uri = NULL;
1607 GError* err = NULL;
1608
1609 g_assert_false(g_hash_table_contains(priv->badge_table, set_name));
1610
1611 g_hash_table_add(priv->badge_table, g_strdup(set_name)); //NOTE: This will be freed by the hash table when it's destroyed
1612
1613 INFOF("Fetching chat badge set with name '%s'", set_name);
1614
1615 uri = g_strcmp0(set_name, "global") == 0 ? g_strdup_printf(GLOBAL_CHAT_BADGES_URI) :
1616 g_strdup_printf(NEW_CHAT_BADGES_URI, set_name);
1617
1618 msg = soup_message_new("GET", uri);
1619
1620 reader = new_send_message_json(self, msg, &err);
1621
1622 CHECK_AND_PROPAGATE_ERROR("Error fetching chat badges for set %s", set_name);
1623
1624 READ_JSON_MEMBER("badge_sets");
1625
1626 for (gint i = 0; i < json_reader_count_members(reader); i++)
1627 {
1628 READ_JSON_ELEMENT(i);
1629
1630 const gchar* badge_name = json_reader_get_member_name(reader);
1631
1632 READ_JSON_MEMBER("versions");
1633
1634 for (gint j = 0; j < json_reader_count_members(reader); j++)
1635 {
1636 GtChatBadge* badge = gt_chat_badge_new();
1637 /* NOTE: Don't need to free this as it's freed by the hash table when it's destroyed. */
1638 gchar* key = NULL;
1639 g_autofree gchar* uri = NULL;
1640
1641 READ_JSON_ELEMENT(j);
1642
1643 badge->name = g_strdup(badge_name);
1644 badge->version = g_strdup(json_reader_get_member_name(reader));
1645
1646 key = g_strdup_printf("%s-%s-%s", set_name, badge->name, badge->version);
1647
1648 READ_JSON_VALUE("image_url_1x", uri);
1649 badge->pixbuf = gt_resource_downloader_download_image(badge_downloader,
1650 uri, key, &err);
1651
1652 if (err)
1653 {
1654 g_autoptr(GtkIconInfo) icon_info;
1655
1656 WARNING("Unable to fetch chat badge set with name '%s' because: %s",
1657 set_name, err->message);
1658
1659 g_clear_error(&err);
1660
1661 /* NOTE: If we encountered an error here we'll just insert a generic error emote */
1662
1663 g_clear_error(&err);
1664
1665 icon_info = gtk_icon_theme_lookup_icon(gtk_icon_theme_get_default(),
1666 "software-update-urgent-symbolic", 1, 0);
1667
1668 badge->pixbuf = gtk_icon_info_load_icon(icon_info, &err);
1669
1670 RETURN_IF_FAIL(err == NULL);
1671
1672 }
1673
1674 END_JSON_ELEMENT();
1675
1676
1677 g_assert_false(g_hash_table_contains(priv->badge_table, key));
1678
1679 g_hash_table_insert(priv->badge_table, key, badge);
1680
1681 DEBUGF("Downloaded emote for set '%s' with name '%s' and version '%s'", set_name,
1682 badge->name, badge->version);
1683 }
1684
1685 END_JSON_MEMBER();
1686 END_JSON_ELEMENT();
1687 }
1688
1689 END_JSON_MEMBER();
1690
1691 error:
1692 return;
1693 }
1694
1695 void
gt_twitch_load_chat_badge_sets_for_channel(GtTwitch * self,const gchar * chan_id,GError ** error)1696 gt_twitch_load_chat_badge_sets_for_channel(GtTwitch* self, const gchar* chan_id, GError** error)
1697 {
1698 g_assert(GT_IS_TWITCH(self));
1699
1700 GtTwitchPrivate* priv = gt_twitch_get_instance_private(self);
1701
1702 #define FETCH_BADGE_SET(s) \
1703 if (!g_hash_table_contains(priv->badge_table, s)) \
1704 { \
1705 GError* err = NULL; \
1706 \
1707 fetch_chat_badge_set(self, s, &err); \
1708 \
1709 if (err) \
1710 { \
1711 WARNINGF("Unable to load chat badge sets for channel '%s'", \
1712 chan_id); \
1713 \
1714 g_propagate_prefixed_error(error, err, \
1715 "Unable to load chat badge sets for channel '%s' because: ", chan_id); \
1716 \
1717 return; \
1718 } \
1719 } \
1720
1721 FETCH_BADGE_SET("global");
1722 FETCH_BADGE_SET(chan_id);
1723
1724 #undef FETCH_BADGE_SET
1725 }
1726
1727 // NOTE: This will automatically download any badge sets if they
1728 // aren't already
1729 GtChatBadge*
gt_twitch_fetch_chat_badge(GtTwitch * self,const gchar * chan_id,const gchar * badge_name,const gchar * version,GError ** error)1730 gt_twitch_fetch_chat_badge(GtTwitch* self,
1731 const gchar* chan_id, const gchar* badge_name,
1732 const gchar* version, GError** error)
1733 {
1734 g_assert(GT_IS_TWITCH(self));
1735 g_assert_false(utils_str_empty(badge_name));
1736 g_assert_false(utils_str_empty(version));
1737
1738 GtTwitchPrivate* priv = gt_twitch_get_instance_private(self);
1739 g_autofree gchar* global_key = NULL;
1740 g_autofree gchar* chan_key = NULL;
1741 GtChatBadge* ret = NULL;
1742 GError* err = NULL;
1743
1744 gt_twitch_load_chat_badge_sets_for_channel(self, chan_id, &err);
1745
1746 CHECK_AND_PROPAGATE_ERROR("Unable to fetch chat badge for channel '%s with badge name '%s' and version '%s'",
1747 chan_id, badge_name, version);
1748
1749 global_key = g_strdup_printf("global-%s-%s", badge_name, version);
1750 chan_key = g_strdup_printf("%s-%s-%s", chan_id, badge_name, version);
1751
1752 if (g_hash_table_contains(priv->badge_table, chan_key))
1753 ret = g_hash_table_lookup(priv->badge_table, chan_key);
1754 else if (g_hash_table_contains(priv->badge_table, global_key))
1755 ret = g_hash_table_lookup(priv->badge_table, global_key);
1756 else
1757 g_assert_not_reached(); //NOTE: We might as well crash here as the badge being null would lead to many problems
1758
1759 error:
1760 return ret;
1761 }
1762
1763 void
fetch_chat_badge_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)1764 fetch_chat_badge_async_cb(GTask* task, gpointer source,
1765 gpointer task_data, GCancellable* cancel)
1766 {
1767 g_assert(GT_IS_TWITCH(source));
1768 g_assert(G_IS_TASK(task));
1769 g_assert_nonnull(task_data);
1770
1771 GenericTaskData* data;
1772 GtChatBadge* ret = NULL;
1773 GError* err = NULL;
1774
1775 if (g_task_return_error_if_cancelled(task))
1776 return;
1777
1778 data = task_data;
1779
1780 ret = gt_twitch_fetch_chat_badge(GT_TWITCH(source),
1781 data->str_1, data->str_2, data->str_3, &err);
1782
1783 if (err)
1784 g_task_return_error(task, err);
1785 else
1786 g_task_return_pointer(task, ret, (GDestroyNotify) gt_chat_badge_list_free);
1787 }
1788
1789 void
gt_twitch_fetch_chat_badge_async(GtTwitch * self,const gchar * chan_id,const gchar * badge_name,const gchar * version,GCancellable * cancel,GAsyncReadyCallback cb,gpointer udata)1790 gt_twitch_fetch_chat_badge_async(GtTwitch* self,
1791 const gchar* chan_id, const gchar* badge_name, const gchar* version,
1792 GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata)
1793 {
1794 g_assert(GT_IS_TWITCH(self));
1795 g_assert_false(utils_str_empty(badge_name));
1796 g_assert_false(utils_str_empty(version));
1797
1798 GTask* task;
1799 GenericTaskData* data;
1800
1801 task = g_task_new(self, cancel, cb, udata);
1802 g_task_set_return_on_cancel(task, FALSE);
1803
1804 data = generic_task_data_new();
1805 data->str_1 = g_strdup(chan_id);
1806 data->str_2 = g_strdup(badge_name);
1807 data->str_3 = g_strdup(version);
1808
1809 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
1810
1811 g_task_run_in_thread(task, fetch_chat_badge_async_cb);
1812
1813 g_object_unref(task);
1814 }
1815
1816 GtChatBadge*
gt_twitch_fetch_chat_badge_finish(GtTwitch * self,GAsyncResult * result,GError ** err)1817 gt_twitch_fetch_chat_badge_finish(GtTwitch* self,
1818 GAsyncResult* result, GError** err)
1819 {
1820 g_assert(GT_IS_TWITCH(self));
1821 g_assert(G_IS_ASYNC_RESULT(result));
1822
1823 GtChatBadge* ret = NULL;
1824
1825 ret = g_task_propagate_pointer(G_TASK(result), err);
1826
1827 return ret;
1828 }
1829
1830 static GtTwitchChannelInfoPanel*
gt_twitch_channel_info_panel_new()1831 gt_twitch_channel_info_panel_new()
1832 {
1833 return g_new0(GtTwitchChannelInfoPanel, 1);
1834 }
1835
1836 void
gt_twitch_channel_info_panel_free(GtTwitchChannelInfoPanel * panel)1837 gt_twitch_channel_info_panel_free(GtTwitchChannelInfoPanel* panel)
1838 {
1839 g_free(panel->html_description);
1840 g_free(panel->markdown_description);
1841 g_clear_object(&panel->image);
1842 g_free(panel->link);
1843 g_free(panel);
1844 }
1845
1846 void
gt_twitch_channel_info_panel_list_free(GList * list)1847 gt_twitch_channel_info_panel_list_free(GList* list)
1848 {
1849 g_list_free_full(list, (GDestroyNotify) gt_twitch_channel_info_panel_free);
1850 }
1851
1852 //TODO: Rewrite this once we start displaying channel info again
1853 GList*
gt_twitch_channel_info(GtTwitch * self,const gchar * chan)1854 gt_twitch_channel_info(GtTwitch* self, const gchar* chan)
1855 {
1856 GtTwitchPrivate* priv = gt_twitch_get_instance_private(self);
1857 SoupMessage* msg;
1858 gchar* uri = NULL;
1859 JsonParser* parser;
1860 JsonNode* node;
1861 JsonReader* reader;
1862 GList* ret = NULL;
1863
1864 INFOF("Getting channel info for='%s'", chan);
1865
1866 uri = g_strdup_printf(CHANNEL_INFO_URI, chan);
1867
1868 msg = soup_message_new("GET", uri);
1869
1870 if (!send_message(self, msg))
1871 {
1872 WARNINGF("Error getting chat badges for channel='%s'", chan);
1873 goto finish;
1874 }
1875
1876 parser = json_parser_new();
1877 json_parser_load_from_data(parser, msg->response_body->data, msg->response_body->length, NULL); //TODO: Error handling
1878 node = json_parser_get_root(parser);
1879 reader = json_reader_new(node);
1880
1881 for (gint i = 0; i < json_reader_count_elements(reader); i++)
1882 {
1883 GtTwitchChannelInfoPanel* panel = gt_twitch_channel_info_panel_new();
1884 const gchar* type = NULL;
1885
1886 json_reader_read_element(reader, i);
1887
1888 json_reader_read_member(reader, "display_order");
1889 panel->order = json_reader_get_int_value(reader) - 1;
1890 json_reader_end_member(reader);
1891
1892 json_reader_read_member(reader, "kind");
1893 type = json_reader_get_string_value(reader);
1894 if (g_strcmp0(type, "default") == 0)
1895 {
1896 panel->type = GT_TWITCH_CHANNEL_INFO_PANEL_TYPE_DEFAULT;
1897 }
1898 else
1899 {
1900 //TODO: Eventually handle other types of panels
1901 gt_twitch_channel_info_panel_free(panel);
1902 json_reader_end_member(reader);
1903 json_reader_end_element(reader);
1904 continue;
1905 }
1906 json_reader_end_member(reader);
1907
1908 json_reader_read_member(reader, "html_description");
1909 if (!json_reader_get_null_value(reader))
1910 panel->html_description = g_strdup(json_reader_get_string_value(reader));
1911 json_reader_end_member(reader);
1912
1913 json_reader_read_member(reader, "data");
1914
1915 json_reader_read_member(reader, "link");
1916 panel->link = g_strdup(json_reader_get_string_value(reader));
1917 json_reader_end_member(reader);
1918
1919 json_reader_read_member(reader, "image");
1920 /* panel->image = gt_twitch_download_picture(self, json_reader_get_string_value(reader), 0); */
1921 json_reader_end_member(reader);
1922
1923 if (json_reader_read_member(reader, "description"))
1924 panel->markdown_description = g_strdup(json_reader_get_string_value(reader));
1925 json_reader_end_member(reader);
1926
1927 if (json_reader_read_member(reader, "title"))
1928 panel->title = g_strdup(json_reader_get_string_value(reader));
1929 json_reader_end_member(reader);
1930
1931 json_reader_end_member(reader);
1932
1933 json_reader_end_element(reader);
1934
1935 ret = g_list_append(ret, panel);
1936 }
1937
1938 g_object_unref(parser);
1939 g_object_unref(reader);
1940
1941 finish:
1942 g_free(uri);
1943 g_object_unref(msg);
1944
1945 return ret;
1946 }
1947
1948
1949 static void
channel_info_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)1950 channel_info_async_cb(GTask* task, gpointer source,
1951 gpointer task_data, GCancellable* cancel)
1952 {
1953 g_assert(GT_IS_TWITCH(source));
1954 g_assert(G_IS_TASK(task));
1955 g_assert_nonnull(task_data);
1956
1957 GenericTaskData* data = NULL;
1958 GList* ret = NULL;
1959
1960 if (g_task_return_error_if_cancelled(task))
1961 return;
1962
1963 data = task_data;
1964
1965 ret = gt_twitch_channel_info(GT_TWITCH(source), data->str_1);
1966
1967 g_task_return_pointer(task, ret, (GDestroyNotify) gt_twitch_channel_info_panel_list_free);
1968 }
1969
1970 void
gt_twitch_channel_info_async(GtTwitch * self,const gchar * chan,GCancellable * cancel,GAsyncReadyCallback cb,gpointer udata)1971 gt_twitch_channel_info_async(GtTwitch* self, const gchar* chan,
1972 GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata)
1973 {
1974 g_assert(GT_IS_TWITCH(self));
1975 g_assert_false(utils_str_empty(chan));
1976
1977 GTask* task;
1978 GenericTaskData* data;
1979
1980 task = g_task_new(self, cancel, cb, udata);
1981 g_task_set_return_on_cancel(task, FALSE);
1982
1983 data = generic_task_data_new();
1984 data->str_1 = g_strdup(chan);
1985
1986 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
1987
1988 g_task_run_in_thread(task, channel_info_async_cb);
1989
1990 g_object_unref(task);
1991 }
1992
1993 GList*
gt_twitch_chat_servers(GtTwitch * self,const gchar * chan,GError ** error)1994 gt_twitch_chat_servers(GtTwitch* self,
1995 const gchar* chan, GError** error)
1996 {
1997 g_assert(GT_IS_TWITCH(self));
1998 g_assert_false(utils_str_empty(chan));
1999
2000 g_autoptr(SoupMessage) msg;
2001 g_autoptr(JsonReader) reader;
2002 g_autofree gchar* uri;
2003 GList* ret = NULL;
2004 GError* err = NULL;
2005
2006 uri = g_strdup_printf(CHAT_SERVERS_URI, chan);
2007
2008 msg = soup_message_new("GET", uri);
2009
2010 reader = new_send_message_json(self, msg, &err);
2011
2012 CHECK_AND_PROPAGATE_ERROR("Unable to fetch chat servers for channel %s", chan);
2013
2014 READ_JSON_MEMBER("chat_servers");
2015
2016 for (int i = 0; i < json_reader_count_elements(reader); i++)
2017 {
2018 READ_JSON_ELEMENT(i);
2019 ret = g_list_append(ret, g_strdup(json_reader_get_string_value(reader)));
2020 END_JSON_ELEMENT();
2021 }
2022
2023 END_JSON_MEMBER();
2024
2025 return ret;
2026
2027 error:
2028 g_list_free_full(ret, g_free);
2029
2030 return NULL;
2031 }
2032
2033 static GList*
fetch_followed_streams(GtTwitch * self,const gchar * oauth_token,gint limit,gint offset,gint64 * total,GError ** error)2034 fetch_followed_streams(GtTwitch* self, const gchar* oauth_token,
2035 gint limit, gint offset, gint64* total, GError** error)
2036 {
2037 g_assert(GT_IS_TWITCH(self));
2038 g_assert_cmpint(limit, >, 0);
2039 g_assert_cmpint(offset, >=, 0);
2040 g_assert_false(utils_str_empty(oauth_token));
2041
2042 g_autoptr(SoupMessage) msg;
2043 g_autoptr(JsonReader) reader;
2044 g_autofree gchar* uri;
2045 GList* ret = NULL;
2046 GError* err = NULL;
2047
2048 uri = g_strdup_printf(FOLLOWED_STREAMS_URI, limit, offset, oauth_token);
2049
2050 msg = soup_message_new(SOUP_METHOD_GET, uri);
2051
2052 reader = new_send_message_json(self, msg, &err);
2053
2054 CHECK_AND_PROPAGATE_ERROR("Unable to fetch followed streams with oauth token '%s', limit '%d' and offset '%d'",
2055 oauth_token, limit, offset);
2056
2057 if (total)
2058 {
2059 READ_JSON_VALUE("_total", *total);
2060 }
2061
2062 READ_JSON_MEMBER("streams");
2063
2064 for (gint i = 0; i < json_reader_count_elements(reader); i++)
2065 {
2066 GtChannelData* data = NULL;
2067
2068 READ_JSON_ELEMENT(i);
2069
2070 data = parse_stream(reader, &err);
2071
2072 CHECK_AND_PROPAGATE_ERROR("Unable to fetch followed streams with oauth token '%s', limit '%d' and offset '%d'",
2073 oauth_token, limit, offset);
2074
2075 ret = g_list_append(ret, data);
2076
2077 END_JSON_ELEMENT();
2078 }
2079
2080 return ret;
2081
2082 error:
2083 gt_channel_data_list_free(ret);
2084
2085 return NULL;
2086 }
2087
2088 static GList*
fetch_followed_channels(GtTwitch * self,const gchar * id,gint limit,gint offset,gint64 * total,GError ** error)2089 fetch_followed_channels(GtTwitch* self, const gchar* id,
2090 gint limit, gint offset, gint64* total, GError** error)
2091 {
2092 g_assert(GT_IS_TWITCH(self));
2093 g_assert_cmpint(limit, >, 0);
2094 g_assert_cmpint(offset, >=, 0);
2095
2096 g_autoptr(SoupMessage) msg;
2097 g_autoptr(JsonReader) reader;
2098 g_autofree gchar* uri;
2099 GList* ret = NULL;
2100 GError* err = NULL;
2101
2102 uri = g_strdup_printf(FOLLOWED_CHANNELS_URI, id, limit, offset);
2103
2104 msg = soup_message_new(SOUP_METHOD_GET, uri);
2105
2106 reader = new_send_message_json(self, msg, &err);
2107
2108 CHECK_AND_PROPAGATE_ERROR("Unable to fetch followed channels with name '%s', limit '%d' and offset '%d'",
2109 id, limit, offset);
2110
2111 if (total)
2112 {
2113 READ_JSON_VALUE("_total", *total);
2114 }
2115
2116 READ_JSON_MEMBER("follows");
2117
2118 for (gint i = 0; i < json_reader_count_elements(reader); i++)
2119 {
2120 GtChannelData* data = NULL;
2121
2122 READ_JSON_ELEMENT(i);
2123 READ_JSON_MEMBER("channel");
2124
2125 data = parse_channel(reader, &err);
2126
2127 CHECK_AND_PROPAGATE_ERROR("Unable to fetch followed channels with name '%s', limit '%d' and offset '%d'",
2128 id, limit, offset);
2129
2130 ret = g_list_append(ret, data);
2131
2132 END_JSON_MEMBER();
2133 END_JSON_ELEMENT();
2134 }
2135
2136 return ret;
2137
2138 error:
2139 gt_channel_data_list_free(ret);
2140
2141 return NULL;
2142 }
2143
2144 GList*
gt_twitch_fetch_all_followed_channels(GtTwitch * self,const gchar * id,const gchar * oauth_token,GError ** error)2145 gt_twitch_fetch_all_followed_channels(GtTwitch* self,
2146 const gchar* id, const gchar* oauth_token, GError** error)
2147 {
2148 g_assert(GT_IS_TWITCH(self));
2149
2150 #define LIMIT 100
2151
2152 gint64 total = 0;
2153 GList* chans = NULL;
2154 GList* streams = NULL;
2155 GList* ret = NULL;
2156 GError* err = NULL;
2157
2158 streams = g_list_concat(streams,
2159 fetch_followed_streams(self, oauth_token, LIMIT, 0, &total, &err));
2160
2161 CHECK_AND_PROPAGATE_ERROR("Unable to fetch all followed channels");
2162
2163 if (total > LIMIT)
2164 {
2165 for (gint i = LIMIT; i < total; i += LIMIT)
2166 {
2167 streams = g_list_concat(streams,
2168 fetch_followed_streams(self, oauth_token, LIMIT, i, NULL, &err));
2169
2170 CHECK_AND_PROPAGATE_ERROR("Unable to fetch all followed channels");
2171 }
2172 }
2173
2174 chans = g_list_concat(chans,
2175 fetch_followed_channels(self, id, LIMIT, 0, &total, &err));
2176
2177 CHECK_AND_PROPAGATE_ERROR("Unable to fetch all followed channels");
2178
2179 if (total > LIMIT)
2180 {
2181 for (gint i = LIMIT; i < total; i += LIMIT)
2182 {
2183 chans = g_list_concat(chans,
2184 fetch_followed_channels(self, id, LIMIT, i, NULL, &err));
2185
2186 CHECK_AND_PROPAGATE_ERROR("Unable to fetch all followed channels");
2187 }
2188 }
2189
2190 //NOTE: Remove duplicates
2191 for (GList* l = streams; l != NULL; l = l->next)
2192 {
2193 GList* found = g_list_find_custom(chans, l->data, (GCompareFunc) gt_channel_data_compare);
2194
2195 if (!found)
2196 {
2197 WARNINGF("Unable to fetch all followed channels with id '%s' and oauth token '%s' because: "
2198 "The followed stream '%s' did not exist as a followed channel", id, oauth_token, ((GtChannelData*) l->data)->name);
2199
2200 /* NOTE: This is commented out because we won't treat it
2201 * as a hard error and try to load follows anyways */
2202 /* g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_MISC, "Unable to fetch all followed channels with id '%s' and oauth token '%s' because: " */
2203 /* "A followed stream did not exist as a followed channel", id, oauth_token); */
2204
2205 /* goto error; */
2206 }
2207 else
2208 {
2209 g_assert_nonnull(found->data);
2210 g_assert_false(((GtChannelData*) found->data)->online);
2211
2212 gt_channel_data_free(found->data);
2213
2214 chans = g_list_delete_link(chans, found);
2215 }
2216 }
2217
2218 chans = g_list_concat(chans, streams);
2219
2220 for (GList* l = chans; l != NULL; l = l->next)
2221 {
2222 GtChannelData* data = l->data;
2223 GtChannel* chan = gt_channel_new(data);
2224
2225 ret = g_list_append(ret, chan);
2226 }
2227
2228 g_list_free(chans); //NOTE: Don't need to free entire list because gt_channel_new takes ownership of the data
2229
2230 return ret;
2231
2232 error:
2233 gt_channel_data_list_free(chans);
2234 gt_channel_data_list_free(streams);
2235
2236 return NULL;
2237 }
2238
2239 static void
fetch_all_followed_channels_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)2240 fetch_all_followed_channels_async_cb(GTask* task, gpointer source,
2241 gpointer task_data, GCancellable* cancel)
2242 {
2243 g_assert(GT_IS_TWITCH(source));
2244 g_assert(G_IS_TASK(task));
2245 g_assert_nonnull(task_data);
2246
2247 GenericTaskData* data = task_data;
2248 GList* ret = NULL;
2249 GError* err = NULL;
2250
2251 if (g_task_return_error_if_cancelled(task))
2252 return;
2253
2254 ret = gt_twitch_fetch_all_followed_channels(GT_TWITCH(source), data->str_1, data->str_2, &err);
2255
2256 if (err)
2257 g_task_return_error(task, err);
2258 else
2259 g_task_return_pointer(task, ret, (GDestroyNotify) gt_channel_list_free);
2260 }
2261
2262 void
gt_twitch_fetch_all_followed_channels_async(GtTwitch * self,const gchar * id,const gchar * oauth_token,GCancellable * cancel,GAsyncReadyCallback cb,gpointer udata)2263 gt_twitch_fetch_all_followed_channels_async(GtTwitch* self,
2264 const gchar* id, const gchar* oauth_token, GCancellable* cancel,
2265 GAsyncReadyCallback cb, gpointer udata)
2266 {
2267 g_assert(GT_IS_TWITCH(self));
2268 g_assert_false(utils_str_empty(oauth_token));
2269
2270 GTask* task = NULL;
2271 GenericTaskData* data = NULL;
2272
2273 task = g_task_new(self, cancel, cb, udata);
2274 g_task_set_return_on_cancel(task, FALSE);
2275
2276 data = generic_task_data_new();
2277 data->str_1 = g_strdup(id);
2278 data->str_2 = g_strdup(oauth_token);
2279
2280 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
2281
2282 g_task_run_in_thread(task, fetch_all_followed_channels_async_cb);
2283
2284 g_object_unref(task);
2285 }
2286
2287 GList*
gt_twitch_fetch_all_followed_channels_finish(GtTwitch * self,GAsyncResult * result,GError ** error)2288 gt_twitch_fetch_all_followed_channels_finish(GtTwitch* self,
2289 GAsyncResult* result, GError** error)
2290 {
2291 g_assert(GT_IS_TWITCH(self));
2292 g_assert(G_IS_ASYNC_RESULT(result));
2293
2294 GList* ret = g_task_propagate_pointer(G_TASK(result), error);
2295
2296 return ret;
2297 }
2298
2299 void
gt_twitch_follow_channel(GtTwitch * self,const gchar * chan_name,GError ** error)2300 gt_twitch_follow_channel(GtTwitch* self,
2301 const gchar* chan_name, GError** error)
2302 {
2303 g_assert(GT_IS_TWITCH(self));
2304 g_assert_false(utils_str_empty(chan_name));
2305
2306 g_autoptr(SoupMessage) msg = NULL;
2307 g_autofree gchar* uri = NULL;
2308 const GtOAuthInfo* oauth_info = NULL;
2309 GError* err = NULL;
2310
2311 oauth_info = gt_app_get_oauth_info(main_app);
2312
2313 uri = g_strdup_printf(FOLLOW_CHANNEL_URI,
2314 oauth_info->user_name, chan_name, oauth_info->oauth_token);
2315
2316 msg = soup_message_new(SOUP_METHOD_PUT, uri);
2317
2318 new_send_message(self, msg, &err);
2319
2320 CHECK_AND_PROPAGATE_ERROR("Unable to follow channel '%s'",
2321 chan_name);
2322
2323 error:
2324 return;
2325 }
2326
2327 static void
follow_channel_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)2328 follow_channel_async_cb(GTask* task, gpointer source,
2329 gpointer task_data, GCancellable* cancel)
2330 {
2331 g_assert(GT_IS_TWITCH(source));
2332 g_assert(G_IS_TASK(task));
2333 g_assert_nonnull(task_data);
2334
2335 GenericTaskData* data = task_data;
2336 GError* err = NULL;
2337
2338 gt_twitch_follow_channel(GT_TWITCH(source), data->str_1, &err);
2339
2340 if (err)
2341 g_task_return_error(task, err);
2342 else
2343 g_task_return_pointer(task, NULL, NULL);
2344 }
2345
2346 // Not cancellable; hard to guarantee that channel is not followed
2347 void
gt_twitch_follow_channel_async(GtTwitch * self,const gchar * chan_name,GAsyncReadyCallback cb,gpointer udata)2348 gt_twitch_follow_channel_async(GtTwitch* self, const gchar* chan_name,
2349 GAsyncReadyCallback cb, gpointer udata)
2350 {
2351 g_assert(GT_IS_TWITCH(self));
2352 g_assert_false(utils_str_empty(chan_name));
2353
2354 GTask* task = NULL;
2355 GenericTaskData* data = NULL;
2356
2357 task = g_task_new(self, NULL, cb, udata);
2358
2359 data = generic_task_data_new();
2360 data->str_1 = g_strdup(chan_name);
2361
2362 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
2363
2364 g_task_run_in_thread(task, follow_channel_async_cb);
2365
2366 g_object_unref(task);
2367 }
2368
2369 void
gt_twitch_follow_channel_finish(GtTwitch * self,GAsyncResult * result,GError ** error)2370 gt_twitch_follow_channel_finish(GtTwitch* self,
2371 GAsyncResult* result, GError** error)
2372 {
2373 g_assert(GT_IS_TWITCH(self));
2374 g_assert(G_IS_ASYNC_RESULT(result));
2375
2376 g_task_propagate_pointer(G_TASK(result), error);
2377 }
2378
2379 void
gt_twitch_unfollow_channel(GtTwitch * self,const gchar * chan_name,GError ** error)2380 gt_twitch_unfollow_channel(GtTwitch* self,
2381 const gchar* chan_name, GError** error)
2382 {
2383 g_assert(GT_IS_TWITCH(self));
2384 g_assert_false(utils_str_empty(chan_name));
2385
2386 g_autoptr(SoupMessage) msg = NULL;
2387 g_autofree gchar* uri = NULL;
2388 const GtOAuthInfo* oauth_info = NULL;
2389 GError* err = NULL;
2390
2391 oauth_info = gt_app_get_oauth_info(main_app);
2392
2393 uri = g_strdup_printf(UNFOLLOW_CHANNEL_URI,
2394 oauth_info->user_name, chan_name, oauth_info->oauth_token);
2395
2396 msg = soup_message_new(SOUP_METHOD_DELETE, uri);
2397
2398 new_send_message(self, msg, &err);
2399
2400 CHECK_AND_PROPAGATE_ERROR("Unable to unfollow channel '%s'",
2401 chan_name);
2402
2403 error:
2404 return;
2405 }
2406
2407 static void
unfollow_channel_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)2408 unfollow_channel_async_cb(GTask* task, gpointer source,
2409 gpointer task_data, GCancellable* cancel)
2410 {
2411 g_assert(GT_IS_TWITCH(source));
2412 g_assert(G_IS_TASK(task));
2413 g_assert_nonnull(task_data);
2414
2415 GenericTaskData* data = task_data;
2416 GError* err = NULL;
2417
2418 gt_twitch_unfollow_channel(GT_TWITCH(source), data->str_1, &err);
2419
2420 if (err)
2421 g_task_return_error(task, err);
2422 else
2423 g_task_return_pointer(task, NULL, NULL); //NOTE: Just return null as this is a void function
2424 }
2425
2426 //NOTE: Not cancellable; hard to guarantee that channel is not unfollowed
2427 void
gt_twitch_unfollow_channel_async(GtTwitch * self,const gchar * chan_name,GAsyncReadyCallback cb,gpointer udata)2428 gt_twitch_unfollow_channel_async(GtTwitch* self, const gchar* chan_name,
2429 GAsyncReadyCallback cb, gpointer udata)
2430 {
2431 g_assert(GT_IS_TWITCH(self));
2432 g_assert_false(utils_str_empty(chan_name));
2433
2434 GTask* task = NULL;
2435 GenericTaskData* data = NULL;
2436
2437 task = g_task_new(self, NULL, cb, udata);
2438
2439 data = generic_task_data_new();
2440 data->str_1 = g_strdup(chan_name);
2441
2442 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
2443
2444 g_task_run_in_thread(task, unfollow_channel_async_cb);
2445
2446 g_object_unref(task);
2447 }
2448
2449 void
gt_twitch_unfollow_channel_finish(GtTwitch * self,GAsyncResult * result,GError ** error)2450 gt_twitch_unfollow_channel_finish(GtTwitch* self,
2451 GAsyncResult* result, GError** error)
2452 {
2453 g_assert(GT_IS_TWITCH(self));
2454 g_assert(G_IS_ASYNC_RESULT(result));
2455
2456 g_task_propagate_pointer(G_TASK(result), error);
2457 }
2458
2459 GList*
gt_twitch_emoticons(GtTwitch * self,const gchar * emotesets,GError ** error)2460 gt_twitch_emoticons(GtTwitch* self,
2461 const gchar* emotesets, GError** error)
2462 {
2463 g_assert(GT_IS_TWITCH(self));
2464 g_assert_false(utils_str_empty(emotesets));
2465
2466 g_autoptr(SoupMessage) msg = NULL;
2467 g_autoptr(JsonReader) reader = NULL;
2468 g_autofree gchar* uri = NULL;
2469 g_auto(GStrv) sets = NULL;
2470 GList* ret = NULL;
2471 GError* err = NULL;
2472
2473 uri = g_strdup_printf(EMOTICON_IMAGES_URI,
2474 emotesets);
2475
2476 msg = soup_message_new(SOUP_METHOD_GET, uri);
2477
2478 reader = new_send_message_json(self, msg, &err);
2479
2480 CHECK_AND_PROPAGATE_ERROR("Unable to get emoticons for emote sets '%s'",
2481 emotesets);
2482
2483 sets = g_strsplit(emotesets, ",", 0);
2484
2485 READ_JSON_MEMBER("emoticon_sets");
2486
2487 for (gchar** c = sets; *c != NULL; c++)
2488 {
2489 READ_JSON_MEMBER(*c);
2490
2491 for (gint i = 0; i < json_reader_count_elements(reader); i++)
2492 {
2493 GtChatEmote* emote = gt_chat_emote_new();
2494
2495 ret = g_list_append(ret, emote);
2496
2497 READ_JSON_ELEMENT(i);
2498 READ_JSON_VALUE("id", emote->id);
2499 READ_JSON_VALUE("code", emote->code);
2500 END_JSON_ELEMENT();
2501
2502 emote->set = atoi(*c);
2503
2504 emote->pixbuf = gt_twitch_download_emote(self, emote->id);
2505 }
2506
2507 END_JSON_MEMBER();
2508 }
2509
2510 END_JSON_MEMBER();
2511
2512 return ret;
2513
2514 error:
2515 gt_chat_emote_list_free(ret);
2516
2517 return NULL;
2518 }
2519
2520 static void
emoticon_images_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)2521 emoticon_images_async_cb(GTask* task, gpointer source,
2522 gpointer task_data, GCancellable* cancel)
2523 {
2524 g_assert(GT_IS_TWITCH(source));
2525 g_assert(G_IS_TASK(task));
2526 g_assert_nonnull(task_data);
2527
2528 GenericTaskData* data = task_data;
2529 GList* ret = NULL;
2530 GError* err = NULL;
2531
2532 ret = gt_twitch_emoticons(GT_TWITCH(source), data->str_1, &err);
2533
2534 if (err)
2535 g_task_return_error(task, err);
2536 else
2537 g_task_return_pointer(task, ret, (GDestroyNotify) gt_chat_emote_list_free);
2538 }
2539
2540 void
gt_twitch_emoticons_async(GtTwitch * self,const char * emotesets,GAsyncReadyCallback cb,GCancellable * cancel,gpointer udata)2541 gt_twitch_emoticons_async(GtTwitch* self, const char* emotesets,
2542 GAsyncReadyCallback cb, GCancellable* cancel, gpointer udata)
2543 {
2544 g_assert(GT_IS_TWITCH(self));
2545 g_assert_false(utils_str_empty(emotesets));
2546
2547 GTask* task = NULL;
2548 GenericTaskData* data = NULL;
2549
2550 task = g_task_new(self, cancel, cb, udata);
2551
2552 data = generic_task_data_new();
2553 data->str_1 = g_strdup(emotesets);
2554
2555 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
2556
2557 g_task_run_in_thread(task, emoticon_images_async_cb);
2558
2559 g_object_unref(task);
2560 }
2561
2562 GtUserInfo*
gt_twitch_fetch_user_info(GtTwitch * self,const gchar * oauth_token,GError ** error)2563 gt_twitch_fetch_user_info(GtTwitch* self,
2564 const gchar* oauth_token, GError** error)
2565 {
2566 g_assert(GT_IS_TWITCH(self));
2567 g_assert_false(utils_str_empty(oauth_token));
2568
2569 g_autoptr(SoupMessage) msg = NULL;
2570 g_autoptr(JsonReader) reader;
2571 g_autofree gchar* uri = NULL;
2572 GtUserInfo* ret = NULL;
2573 GError* err = NULL;
2574
2575 uri = g_strdup_printf(USER_INFO_URI, oauth_token);
2576
2577 msg = soup_message_new(SOUP_METHOD_GET, uri);
2578
2579 reader = new_send_message_json(self, msg, &err);
2580
2581 CHECK_AND_PROPAGATE_ERROR("Unable to fetch user info");
2582
2583 ret = gt_user_info_new();
2584
2585 ret->oauth_token = g_strdup(oauth_token);
2586
2587 READ_JSON_VALUE("_id", ret->id);
2588 READ_JSON_VALUE("name", ret->name);
2589 READ_JSON_VALUE_NULL("bio", ret->bio, NULL);
2590 READ_JSON_VALUE_NULL("display_name", ret->display_name, NULL);
2591 READ_JSON_VALUE("email", ret->email);
2592 READ_JSON_VALUE("email_verified", ret->email_verified);
2593 READ_JSON_VALUE_NULL("logo", ret->logo_url, NULL);
2594 READ_JSON_MEMBER("notifications");
2595 READ_JSON_VALUE("email", ret->notifications.email);
2596 READ_JSON_VALUE("push", ret->notifications.push);
2597 END_JSON_MEMBER();
2598 READ_JSON_VALUE("partnered", ret->partnered);
2599 READ_JSON_VALUE("twitter_connected", ret->twitter_connected);
2600 READ_JSON_VALUE("type", ret->type);
2601 READ_JSON_VALUE("created_at", ret->created_at);
2602 READ_JSON_VALUE("updated_at", ret->updated_at);
2603
2604 return ret;
2605
2606 error:
2607 gt_user_info_free(ret);
2608
2609 return NULL;
2610 }
2611
2612 static void
fetch_user_info_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)2613 fetch_user_info_async_cb(GTask* task, gpointer source,
2614 gpointer task_data, GCancellable* cancel)
2615 {
2616 g_assert(GT_IS_TWITCH(source));
2617 g_assert(G_IS_TASK(task));
2618 g_assert_nonnull(task_data);
2619
2620 GError* err = NULL;
2621 GenericTaskData* data = task_data;
2622
2623 GtUserInfo* ret = gt_twitch_fetch_user_info(GT_TWITCH(source), data->str_1, &err);
2624
2625 if (err)
2626 g_task_return_error(task, err);
2627 else
2628 g_task_return_pointer(task, ret, (GDestroyNotify) gt_user_info_free);
2629 }
2630
2631 void
gt_twitch_fetch_user_info_async(GtTwitch * self,const gchar * oauth_token,GAsyncReadyCallback cb,GCancellable * cancel,gpointer udata)2632 gt_twitch_fetch_user_info_async(GtTwitch* self,
2633 const gchar* oauth_token, GAsyncReadyCallback cb,
2634 GCancellable* cancel, gpointer udata)
2635 {
2636 g_assert(GT_IS_TWITCH(self));
2637
2638 GTask* task = NULL;
2639 GenericTaskData* data = generic_task_data_new();
2640
2641 task = g_task_new(self, cancel, cb, udata);
2642
2643 data->str_1 = g_strdup(oauth_token);
2644
2645 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
2646
2647 g_task_run_in_thread(task, fetch_user_info_async_cb);
2648
2649 g_object_unref(task);
2650 }
2651
2652 GtUserInfo*
gt_twitch_fetch_user_info_finish(GtTwitch * self,GAsyncResult * result,GError ** error)2653 gt_twitch_fetch_user_info_finish(GtTwitch* self,
2654 GAsyncResult* result, GError** error)
2655 {
2656 g_assert(GT_IS_TWITCH(self));
2657 g_assert(G_IS_ASYNC_RESULT(result));
2658
2659 GtUserInfo* ret = g_task_propagate_pointer(G_TASK(result), error);
2660
2661 return ret;
2662 }
2663
2664 GtOAuthInfo*
gt_twitch_fetch_oauth_info(GtTwitch * self,const gchar * oauth_token,GError ** error)2665 gt_twitch_fetch_oauth_info(GtTwitch* self,
2666 const gchar* oauth_token, GError** error)
2667
2668 {
2669 g_assert(GT_IS_TWITCH(self));
2670 g_assert_false(utils_str_empty(oauth_token));
2671
2672 g_autoptr(SoupMessage) msg = NULL;
2673 g_autoptr(JsonReader) reader;
2674 g_autofree gchar* uri = NULL;
2675 GtOAuthInfo* ret = NULL;
2676 GError* err = NULL;
2677 gint num_scopes;
2678
2679 uri = g_strdup_printf(OAUTH_INFO_URI, oauth_token);
2680
2681 msg = soup_message_new(SOUP_METHOD_GET, uri);
2682
2683 reader = new_send_message_json(self, msg, &err);
2684
2685 CHECK_AND_PROPAGATE_ERROR("Unable to fetch oauth info");
2686
2687 ret = gt_oauth_info_new();
2688
2689 ret->oauth_token = g_strdup(oauth_token);
2690
2691 READ_JSON_MEMBER("token");
2692 READ_JSON_VALUE("user_id", ret->user_id);
2693 READ_JSON_VALUE("user_name", ret->user_name);
2694 READ_JSON_VALUE("client_id", ret->client_id);
2695 READ_JSON_VALUE("valid", ret->valid);
2696 READ_JSON_MEMBER("authorization");
2697 READ_JSON_VALUE("created_at", ret->created_at);
2698 READ_JSON_VALUE("created_at", ret->updated_at);
2699 READ_JSON_MEMBER("scopes");
2700 num_scopes = json_reader_count_elements(reader);
2701 ret->scopes = g_malloc_n(num_scopes + 1, sizeof(gchar*));
2702 for (gint i = 0; i < num_scopes; i++)
2703 {
2704 READ_JSON_ELEMENT_VALUE(i, *(ret->scopes + i));
2705 }
2706 *(ret->scopes + num_scopes) = NULL;
2707 END_JSON_MEMBER();
2708 END_JSON_MEMBER();
2709 END_JSON_MEMBER();
2710
2711 return ret;
2712
2713 error:
2714 gt_oauth_info_free(ret);
2715
2716 return NULL;
2717 }
2718
2719 static void
fetch_oauth_info_async_cb(GTask * task,gpointer source,gpointer task_data,GCancellable * cancel)2720 fetch_oauth_info_async_cb(GTask* task, gpointer source,
2721 gpointer task_data, GCancellable* cancel)
2722 {
2723 g_assert(GT_IS_TWITCH(source));
2724 g_assert(G_IS_TASK(task));
2725 g_assert_nonnull(task_data);
2726
2727 GError* err = NULL;
2728 GenericTaskData* data = task_data;
2729
2730 GtOAuthInfo* ret = gt_twitch_fetch_oauth_info(GT_TWITCH(source), data->str_1, &err);
2731
2732 if (err)
2733 g_task_return_error(task, err);
2734 else
2735 g_task_return_pointer(task, ret, (GDestroyNotify) gt_oauth_info_free);
2736 }
2737
2738 void
gt_twitch_fetch_oauth_info_async(GtTwitch * self,const gchar * oauth_token,GAsyncReadyCallback cb,GCancellable * cancel,gpointer udata)2739 gt_twitch_fetch_oauth_info_async(GtTwitch* self,
2740 const gchar* oauth_token, GAsyncReadyCallback cb,
2741 GCancellable* cancel, gpointer udata)
2742 {
2743 g_assert(GT_IS_TWITCH(self));
2744
2745 GTask* task = NULL;
2746 GenericTaskData* data = generic_task_data_new();
2747
2748 task = g_task_new(self, cancel, cb, udata);
2749
2750 data->str_1 = g_strdup(oauth_token);
2751
2752 g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free);
2753
2754 g_task_run_in_thread(task, fetch_oauth_info_async_cb);
2755
2756 g_object_unref(task);
2757 }
2758
2759 GtOAuthInfo*
gt_twitch_fetch_oauth_info_finish(GtTwitch * self,GAsyncResult * result,GError ** error)2760 gt_twitch_fetch_oauth_info_finish(GtTwitch* self,
2761 GAsyncResult* result, GError** error)
2762 {
2763 g_assert(GT_IS_TWITCH(self));
2764 g_assert(G_IS_ASYNC_RESULT(result));
2765
2766 GtOAuthInfo* ret = g_task_propagate_pointer(G_TASK(result), error);
2767
2768 return ret;
2769 }
2770
2771 GtChatBadge*
gt_chat_badge_new()2772 gt_chat_badge_new()
2773 {
2774 return g_slice_new0(GtChatBadge);
2775 }
2776
2777 void
gt_chat_badge_free(GtChatBadge * badge)2778 gt_chat_badge_free(GtChatBadge* badge)
2779 {
2780 g_assert_nonnull(badge);
2781
2782 g_free(badge->name);
2783 g_free(badge->version);
2784 g_object_unref(badge->pixbuf);
2785 g_slice_free(GtChatBadge, badge);
2786 }
2787
2788 void
gt_chat_badge_list_free(GList * list)2789 gt_chat_badge_list_free(GList* list)
2790 {
2791 g_list_free_full(list, (GDestroyNotify) gt_chat_badge_free);
2792 }
2793