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