/* * This file is part of GNOME Twitch - 'Enjoy Twitch on your GNU/Linux desktop' * Copyright © 2017 Vincent Szolnoky * * GNOME Twitch is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * GNOME Twitch is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with GNOME Twitch. If not, see . */ #include "gt-twitch.h" #include "gt-resource-downloader.h" #include "config.h" #include #include #include #include #include #include #include #include "utils.h" #define TAG "GtTwitch" #include "gnome-twitch/gt-log.h" //TODO: Use https://streams.twitch.tv/kraken/streams/{channel}?stream_type=all instead to get is_playlist info //TODO: Use https://tmi.twitch.tv/servers?channel=%s to get chat server info #define ACCESS_TOKEN_URI "https://api.twitch.tv/api/channels/%s/access_token" #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" #define TOP_CHANNELS_URI "https://api.twitch.tv/kraken/streams?limit=%d&offset=%d&game=%s&broadcaster_language=%s" #define TOP_GAMES_URI "https://api.twitch.tv/kraken/games/top?limit=%d&offset=%d" #define SEARCH_STREAMS_URI "https://api.twitch.tv/kraken/search/streams?query=%s&limit=%d&offset=%d" #define SEARCH_CHANNELS_URI "https://api.twitch.tv/kraken/search/channels?query=%s&limit=%d&offset=%d" #define SEARCH_GAMES_URI "https://api.twitch.tv/kraken/search/games?query=%s&type=suggest" #define FETCH_STREAM_URI "https://api.twitch.tv/kraken/streams/%s" #define FETCH_CHANNEL_URI "https://api.twitch.tv/kraken/channels/%s" #define CHAT_BADGES_URI "https://api.twitch.tv/kraken/chat/%s/badges/" #define TWITCH_EMOTE_URI "https://static-cdn.jtvnw.net/emoticons/v1/%d/%d.0" #define CHANNEL_INFO_URI "https://api.twitch.tv/api/channels/%s/panels" #define CHAT_SERVERS_URI "https://api.twitch.tv/api/channels/%s/chat_properties" #define FOLLOWED_STREAMS_URI "https://api.twitch.tv/kraken/streams/followed?limit=%d&offset=%d&oauth_token=%s&stream_type=live" #define FOLLOWED_CHANNELS_URI "https://api.twitch.tv/kraken/users/%s/follows/channels?limit=%d&offset=%d" #define FOLLOW_CHANNEL_URI "https://api.twitch.tv/kraken/users/%s/follows/channels/%s?oauth_token=%s" #define UNFOLLOW_CHANNEL_URI "https://api.twitch.tv/kraken/users/%s/follows/channels/%s?oauth_token=%s" #define USER_EMOTICONS_URI "https://api.twitch.tv/kraken/users/%s/emotes" #define EMOTICON_IMAGES_URI "https://api.twitch.tv/kraken/chat/emoticon_images?emotesets=%s" #define USER_INFO_URI "https://api.twitch.tv/kraken/user?oauth_token=%s" #define GLOBAL_CHAT_BADGES_URI "https://badges.twitch.tv/v1/badges/global/display" #define NEW_CHAT_BADGES_URI "https://badges.twitch.tv/v1/badges/channels/%s/display" #define OAUTH_INFO_URI "https://api.twitch.tv/kraken/?oauth_token=%s" #define STREAM_INFO "#EXT-X-STREAM-INF" #define TWITCH_API_VERSION_3 "3" #define TWITCH_API_VERSION_4 "4" #define TWITCH_API_VERSION_5 "5" #define END_JSON_MEMBER() json_reader_end_member(reader) // Just for consistency's sake #define END_JSON_ELEMENT() json_reader_end_element(reader) // Just for consistency's sake #define READ_JSON_MEMBER(name) \ if (!json_reader_read_member(reader, name)) \ { \ const GError* e = json_reader_get_error(reader); \ \ g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \ "Unable to read JSON member with name '%s' because: %s", name, e->message); \ \ WARNINGF("Unable to read JSON member with name '%s' because: %s", name, e->message); \ \ goto error; \ } \ #define READ_JSON_ELEMENT(i) \ if (!json_reader_read_element(reader, i)) \ { \ const GError* e = json_reader_get_error(reader); \ \ g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \ "Unable to read JSON element with position '%d' because: %s", i, e->message); \ \ WARNINGF("Unable to read JSON element with position '%d' because: %s", i, e->message); \ \ goto error; \ } \ #define READ_JSON_ELEMENT_VALUE(i, p) \ if (json_reader_read_element(reader, i)) \ { \ p = _Generic(p, \ gchar*: g_strdup(json_reader_get_string_value(reader)), \ gint64: json_reader_get_int_value(reader), \ gdouble: json_reader_get_double_value(reader), \ gboolean: json_reader_get_boolean_value(reader), \ GDateTime*: parse_time(json_reader_get_string_value(reader))); \ } \ else \ { \ const GError* e = json_reader_get_error(reader); \ \ g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \ "Unable to read JSON element with position '%d' because: %s", i, e->message); \ \ WARNINGF("Unable to read JSON element with position '%d' because: %s", i, e->message); \ \ goto error; \ } \ json_reader_end_element(reader); \ #define READ_JSON_VALUE(name, p) \ if (json_reader_read_member(reader, name)) \ { \ p = _Generic(p, \ gchar*: g_strdup(json_reader_get_string_value(reader)), \ gint64: json_reader_get_int_value(reader), \ gdouble: json_reader_get_double_value(reader), \ gboolean: json_reader_get_boolean_value(reader), \ GDateTime*: parse_time(json_reader_get_string_value(reader))); \ } \ else \ { \ const GError* e = json_reader_get_error(reader); \ \ g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \ "Unable to read JSON member with name '%s' because: %s", name, e->message); \ \ WARNINGF("Unable to read JSON member with name '%s' because: %s", name, e->message); \ \ goto error; \ } \ json_reader_end_member(reader); \ #define READ_JSON_VALUE_NULL(name, p, def) \ if (json_reader_read_member(reader, name)) \ { \ if (json_reader_get_null_value(reader)) \ { \ p = _Generic(p, \ gchar*: g_strdup(def), \ default: def); \ } \ else \ { \ p = _Generic(p, \ gchar*: g_strdup(json_reader_get_string_value(reader)), \ gint64: json_reader_get_int_value(reader), \ gdouble: json_reader_get_double_value(reader), \ gboolean: json_reader_get_boolean_value(reader), \ GDateTime*: parse_time(json_reader_get_string_value(reader))); \ } \ } \ else \ { \ g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, \ "Unable to read JSON member with name '%s'", name); \ \ WARNINGF("Unable to read JSON member with name '%s'", name); \ \ goto error; \ } \ json_reader_end_member(reader); \ #define CHECK_AND_PROPAGATE_ERROR(msg, ...) \ if (err) \ { \ WARNINGF(msg " because: %s", ##__VA_ARGS__, err->message); \ \ g_propagate_prefixed_error(error, err, msg " because: ", ##__VA_ARGS__); \ \ goto error; \ } \ typedef struct { SoupSession* soup; GThreadPool* image_download_pool; GHashTable* emote_table; GHashTable* badge_table; } GtTwitchPrivate; G_DEFINE_TYPE_WITH_PRIVATE(GtTwitch, gt_twitch, G_TYPE_OBJECT) static GtResourceDownloader* emote_downloader; static GtResourceDownloader* badge_downloader; static GtTwitchStreamAccessToken* gt_twitch_stream_access_token_new() { return g_new0(GtTwitchStreamAccessToken, 1); } void gt_twitch_stream_access_token_free(GtTwitchStreamAccessToken* token) { g_free(token->token); g_free(token->sig); g_free(token); } GtChatEmote* gt_chat_emote_new() { GtChatEmote* emote = g_new0(GtChatEmote, 1); emote->start = -1; emote->end = -1; return emote; } void gt_chat_emote_free(GtChatEmote* emote) { g_assert_nonnull(emote); g_assert(GDK_IS_PIXBUF(emote->pixbuf)); g_object_unref(emote->pixbuf); g_free(emote->code); g_free(emote); } void gt_chat_emote_list_free(GList* list) { g_list_free_full(list, (GDestroyNotify) gt_chat_emote_free); } GQuark gt_spawn_twitch_error_quark() { return g_quark_from_static_string("gt-twitch-error-quark"); } GtTwitch* gt_twitch_new(void) { return g_object_new(GT_TYPE_TWITCH, NULL); } static void gt_twitch_class_init(GtTwitchClass* klass) { } static void gt_twitch_init(GtTwitch* self) { GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); priv->soup = soup_session_new(); priv->emote_table = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) g_object_unref); priv->badge_table = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, (GDestroyNotify) gt_chat_badge_free); g_autofree gchar* emotes_filepath = g_build_filename(g_get_user_cache_dir(), "gnome-twitch", "emotes", NULL); g_autofree gchar* badges_filepath = g_build_filename(g_get_user_cache_dir(), "gnome-twitch", "badges", NULL); emote_downloader = gt_resource_downloader_new_with_cache(emotes_filepath); gt_resource_downloader_set_image_filetype(emote_downloader, GT_IMAGE_FILETYPE_PNG); badge_downloader = gt_resource_downloader_new_with_cache(badges_filepath); gt_resource_downloader_set_image_filetype(badge_downloader, GT_IMAGE_FILETYPE_PNG); g_signal_connect_swapped(main_app, "shutdown", G_CALLBACK(g_object_unref), emote_downloader); g_signal_connect_swapped(main_app, "shutdown", G_CALLBACK(g_object_unref), badge_downloader); } static gboolean send_message(GtTwitch* self, SoupMessage* msg) { GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); gboolean ret; char* uri = soup_uri_to_string(soup_message_get_uri(msg), FALSE); DEBUGF("Sending message to uri '%s'", uri); soup_message_headers_append(msg->request_headers, "Client-ID", CLIENT_ID); soup_session_send_message(priv->soup, msg); ret = SOUP_STATUS_IS_SUCCESSFUL(msg->status_code); if (ret) TRACEF("Received response from url '%s' with code '%d' and body '%s'", uri, msg->status_code, msg->response_body->data); else WARNINGF("Received unsuccessful response from url '%s' with code '%d' and body '%s'", uri, msg->status_code, msg->response_body->data); g_free(uri); return ret; } //TODO: Refactor GtTwitch to use these new functions static void new_send_message(GtTwitch* self, SoupMessage* msg, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(SOUP_IS_MESSAGE(msg)); GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); char* uri = soup_uri_to_string(soup_message_get_uri(msg), FALSE); DEBUGF("Sending message to uri '%s'", uri); soup_message_headers_append(msg->request_headers, "Client-ID", CLIENT_ID); soup_session_send_message(priv->soup, msg); if (SOUP_STATUS_IS_SUCCESSFUL(msg->status_code)) { TRACEF("Received response from url '%s' with code '%d' and body '%s'", uri, msg->status_code, msg->response_body->data); } else { gint code; WARNINGF("Received unsuccessful response from url '%s' with code '%d' and body '%s'", uri, msg->status_code, msg->response_body->data); code = msg->status_code == GT_TWITCH_ERROR_SOUP_NOT_FOUND ? GT_TWITCH_ERROR_SOUP_NOT_FOUND : GT_TWITCH_ERROR_SOUP_GENERIC; g_set_error(error, GT_TWITCH_ERROR, code, "Received unsuccessful response from url '%s' with code '%d' and body '%s'", uri, msg->status_code, msg->response_body->data); } g_free(uri); } static JsonReader* new_send_message_json_with_version(GtTwitch* self, SoupMessage* msg, const gchar* version, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(SOUP_IS_MESSAGE(msg)); JsonReader* ret = NULL; g_autofree gchar* accept_header = g_strdup_printf("application/vnd.twitchtv.v%s+json", version); soup_message_headers_append(msg->request_headers, "Accept", accept_header); new_send_message(self, msg, error); if (!*error) { JsonParser* parser = json_parser_new(); JsonNode* node = NULL; GError* e = NULL; json_parser_load_from_data(parser, msg->response_body->data, -1, &e); if (e) { g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_JSON, "Error parsing JSON response because: %s", e->message); WARNINGF("Error parsing JSON response because: %s", e->message); g_error_free(e); goto finish; } node = json_parser_get_root(parser); ret = json_reader_new(json_node_ref(node)); //NOTE: Parser doesn't seem to have its own reference to node g_object_unref(G_OBJECT(parser)); } finish: return ret; } static JsonReader* new_send_message_json(GtTwitch* self, SoupMessage* msg, GError** error) { return new_send_message_json_with_version(self, msg, TWITCH_API_VERSION_5, error); } static GDateTime* parse_time(const gchar* time) { GDateTime* ret = NULL; gint year, month, day, hour, min, sec; sscanf(time, "%d-%d-%dT%d:%d:%dZ", &year, &month, &day, &hour, &min, &sec); ret = g_date_time_new_utc(year, month, day, hour, min, sec); return ret; } static GList* parse_playlist(const gchar* playlist) { GList* ret = NULL; g_auto(GStrv) lines = g_strsplit(playlist, "\n", 0); for (gchar** l = lines; *l != NULL; l++) { if (strncmp(*l, STREAM_INFO, strlen(STREAM_INFO)) == 0) { GtTwitchStreamData* stream = g_malloc0(sizeof(GtTwitchStreamData)); g_auto(GStrv) values = g_strsplit(*l, ",", 0); for (gchar** m = values; *m != NULL; m++) { g_auto(GStrv) split = g_strsplit(*m, "=", 0); if (strcmp(*split, "BANDWIDTH") == 0) { stream->bandwidth = atol(*(split+1)); } else if (strcmp(*split, "RESOLUTION") == 0) { gchar** res = g_strsplit(*(split+1), "x", 0); stream->width = atoi(*res); stream->height = atoi(*(res+1)); g_strfreev(res); } else if (strcmp(*split, "VIDEO") == 0) { //TODO: Replace these with an error g_assert(g_str_has_prefix(*(split+1), "\"")); g_assert(g_str_has_suffix(*(split+1), "\"")); //NOTE: quality isn't const but still shouldn't be freed, //it will be freed when split is freed. gchar* quality = *(split+1)+1; quality[strlen(quality) - 1] = '\0'; if (STRING_EQUALS(quality, "chunked")) stream->quality = g_strdup("source"); else if (STRING_EQUALS(quality, "audio_only")) //NOTE: Remove audio only streams { gt_twitch_stream_data_free(stream); stream = NULL; goto next; } else stream->quality = g_strdup(quality); } } l++; stream->url = g_strdup(*l); next: if (stream) ret = g_list_append(ret, stream); } } return ret; } static GtChannelData* parse_channel(JsonReader* reader, GError** error) { GtChannelData* data = gt_channel_data_new(); //NOTE: Need this hack until Twitch gets their shit together //and standardizes on either integers or strings as IDs READ_JSON_MEMBER("_id"); JsonNode* node = json_reader_get_value(reader); if (STRING_EQUALS(json_node_type_name(node), "Integer")) data->id = g_strdup_printf("%" G_GINT64_FORMAT, json_reader_get_int_value(reader)); else if (STRING_EQUALS(json_node_type_name(node), "String")) data->id = g_strdup(json_reader_get_string_value(reader)); else g_assert_not_reached(); END_JSON_MEMBER(); READ_JSON_VALUE("name", data->name); READ_JSON_VALUE_NULL("display_name", data->display_name, NULL); READ_JSON_VALUE_NULL("status", data->status, _("Untitled broadcast")); READ_JSON_VALUE_NULL("video_banner", data->video_banner_url, NULL); READ_JSON_VALUE_NULL("logo", data->logo_url, NULL); READ_JSON_VALUE("url", data->profile_url); data->online = FALSE; return data; error: gt_channel_data_free(data); return NULL; } static GtChannelData* parse_stream(JsonReader* reader, GError** error) { GtChannelData* data = NULL; READ_JSON_MEMBER("channel"); data = parse_channel(reader, error); END_JSON_MEMBER(); if (*error) goto error; READ_JSON_VALUE_NULL("game", data->game, NULL); READ_JSON_VALUE("viewers", data->viewers); READ_JSON_VALUE("created_at", data->stream_started_time); READ_JSON_MEMBER("preview"); READ_JSON_VALUE("large", data->preview_url); END_JSON_MEMBER(); data->online = TRUE; return data; error: gt_channel_data_free(data); return NULL; } static GtGameData* parse_game(JsonReader* reader, GError** error) { GtGameData* data = gt_game_data_new(); //NOTE: Same hack as above for channel READ_JSON_MEMBER("_id"); JsonNode* node = json_reader_get_value(reader); if (STRING_EQUALS(json_node_type_name(node), "Integer")) data->id = g_strdup_printf("%" G_GINT64_FORMAT, json_reader_get_int_value(reader)); else if (STRING_EQUALS(json_node_type_name(node), "String")) data->id = g_strdup(json_reader_get_string_value(reader)); else g_assert_not_reached(); END_JSON_MEMBER(); READ_JSON_VALUE("name", data->name); READ_JSON_MEMBER("box"); READ_JSON_VALUE("large", data->preview_url); END_JSON_MEMBER(); READ_JSON_MEMBER("logo"); READ_JSON_VALUE("large", data->logo_url); END_JSON_MEMBER(); return data; error: gt_game_data_free(data); return NULL; } GtTwitchStreamAccessToken* gt_twitch_stream_access_token(GtTwitch* self, const gchar* channel, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(channel)); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader = NULL; g_autofree gchar* uri = NULL; GtTwitchStreamAccessToken* ret = NULL; GError* err = NULL; uri = g_strdup_printf(ACCESS_TOKEN_URI, channel); msg = soup_message_new("GET", uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Error getting stream access token for channel '%s'", channel); ret = gt_twitch_stream_access_token_new(); READ_JSON_VALUE("sig", ret->sig); READ_JSON_VALUE("token", ret->token); return ret; error: gt_twitch_stream_access_token_free(ret); return NULL; } GList* gt_twitch_all_streams(GtTwitch* self, const gchar* channel, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(channel)); g_autoptr(SoupMessage) msg = NULL; g_autofree gchar* uri = NULL; GtTwitchStreamAccessToken* token = NULL; GList* ret = NULL; GError* err = NULL; token = gt_twitch_stream_access_token(self, channel, &err); CHECK_AND_PROPAGATE_ERROR("Unable to get streams for channel '%s'", channel); uri = g_strdup_printf(STREAM_PLAYLIST_URI, channel, token->token, token->sig, g_random_int_range(0, 999999)); gt_twitch_stream_access_token_free(token); msg = soup_message_new("GET", uri); new_send_message(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to get all streams for channel '%s'", channel); ret = parse_playlist(msg->response_body->data); error: return ret; } static void all_streams_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(G_IS_TASK(task)); g_assert(GT_IS_TWITCH(source)); g_assert_nonnull(task_data); GenericTaskData* data = NULL; GList* ret = NULL; GError* err = NULL; if (g_task_return_error_if_cancelled(task)) return; data = task_data; ret = gt_twitch_all_streams(GT_TWITCH(source), data->str_1, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_twitch_stream_data_list_free); } void gt_twitch_all_streams_async(GtTwitch* self, const gchar* channel, GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata) { GTask* task; GenericTaskData* data; task = g_task_new(self, cancel, cb, udata); g_task_set_return_on_cancel(task, FALSE); data = generic_task_data_new(); data->str_1 = g_strdup(channel); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, all_streams_cb); } GList* gt_twitch_all_streams_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GList* ret = g_task_propagate_pointer(G_TASK(result), error); return ret; } const GtTwitchStreamData* gt_twitch_stream_list_filter_quality(GList* list, const gchar* quality) { GtTwitchStreamData* ret = NULL; for (GList* l = list; l != NULL; l = l->next) { ret = (GtTwitchStreamData*) l->data; if (STRING_EQUALS(ret->quality, quality)) break; else ret = NULL; } if (!ret) { ret = list->data; g_assert_nonnull(ret); } return ret; } GList* gt_twitch_top_channels(GtTwitch* self, gint n, gint offset, const gchar* game, const gchar* language, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(n, >=, 0); g_assert_cmpint(n, <=, 100); g_assert_cmpint(offset, >=, 0); g_assert_nonnull(game); g_assert_nonnull(language); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader = NULL; g_autofree gchar* uri = NULL; GList* ret = NULL; GError* err = NULL; uri = g_strdup_printf(TOP_CHANNELS_URI, n, offset, game, language); msg = soup_message_new("GET", uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch top channels with amount '%d', offset '%d' and game '%s'", n, offset, game); READ_JSON_MEMBER("streams"); for (gint i = 0; i < json_reader_count_elements(reader); i++) { GtChannel* channel = NULL; GtChannelData* data = NULL; READ_JSON_ELEMENT(i); data = parse_stream(reader, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch top channels with amount '%d', offset '%d' and game '%s'", n, offset, game); channel = gt_channel_new(data); END_JSON_ELEMENT(); ret = g_list_append(ret, channel); } END_JSON_MEMBER(); return ret; error: gt_channel_list_free(ret); return NULL; } static void top_channels_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data = task_data; GList* ret = NULL; GError* err = NULL; if (g_task_return_error_if_cancelled(task)) return; ret = gt_twitch_top_channels(GT_TWITCH(source), data->int_1, data->int_2, data->str_1, data->str_2, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_channel_list_free); } void gt_twitch_top_channels_async(GtTwitch* self, gint n, gint offset, const gchar* game, const gchar* language, GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(n, >=, 0); g_assert_cmpint(n, <=, 100); g_assert_cmpint(offset, >=, 0); g_assert_nonnull(game); g_assert_nonnull(language); GTask* task = NULL; GenericTaskData* data = NULL; task = g_task_new(self, cancel, cb, udata); g_task_set_return_on_cancel(task, FALSE); data = generic_task_data_new(); data->int_1 = n; data->int_2 = offset; data->str_1 = g_strdup(game); data->str_2 = g_strdup(language); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, top_channels_async_cb); g_object_unref(task); } GList* gt_twitch_top_channels_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GList* ret = g_task_propagate_pointer(G_TASK(result), error); return ret; } GList* gt_twitch_top_games(GtTwitch* self, gint n, gint offset, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(n, >=, 0); g_assert_cmpint(n, <=, 100); g_assert_cmpint(offset, >=, 0); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader = NULL; g_autofree gchar* uri = NULL; GList* ret = NULL; GError* err = NULL; uri = g_strdup_printf(TOP_GAMES_URI, n, offset); msg = soup_message_new("GET", uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to get top games with amount '%d' and offset '%d'", n, offset); READ_JSON_MEMBER("top"); for (gint i = 0; i < json_reader_count_elements(reader); i++) { GtGame* game = NULL; GtGameData* data = NULL; READ_JSON_ELEMENT(i); READ_JSON_MEMBER("game"); data = parse_game(reader, &err); CHECK_AND_PROPAGATE_ERROR("Unable to get top games with amount '%d' and offset '%d'", n, offset); END_JSON_MEMBER(); READ_JSON_VALUE("viewers", data->viewers); READ_JSON_VALUE("channels", data->channels); game = gt_game_new(data); END_JSON_ELEMENT(); ret = g_list_append(ret, game); } END_JSON_MEMBER(); return ret; error: gt_game_list_free(ret); return NULL; } static void top_games_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data = task_data; GList* ret = NULL; GError* err = NULL; if (g_task_return_error_if_cancelled(task)) return; ret = gt_twitch_top_games(GT_TWITCH(source), data->int_1, data->int_2, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_game_list_free); } void gt_twitch_top_games_async(GtTwitch* self, gint n, gint offset, GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(n, >=, 0); g_assert_cmpint(n, <=, 100); g_assert_cmpint(offset, >=, 0); GTask* task = NULL; GenericTaskData* data = NULL; task = g_task_new(self, cancel, cb, udata); g_task_set_return_on_cancel(task, FALSE); data = generic_task_data_new(); data->int_1 = n; data->int_2 = offset; g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, top_games_async_cb); g_object_unref(task); } GList* gt_twitch_top_games_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GList* ret = g_task_propagate_pointer(G_TASK(result), error); return ret; } //NOTE: Twitch's stream search API is retarted (see https://github.com/justintv/Twitch-API/issues/513) //so we need to do this hack to get anything remotely usable. It will return duplicates unless //amount=offset*k where k is some multiple, i.e. it works in 'pages' GList* gt_twitch_search_channels(GtTwitch* self, const gchar* query, gint n, gint offset, gboolean offline, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(n, >=, 0); g_assert_cmpint(n, <=, 100); g_assert_cmpint(offset, >=, 0); g_assert_false(utils_str_empty(query)); #define SEARCH_AMOUNT 100 const gint PAGE_AMOUNT = offline ? 100 : 90; MESSAGEF("Searching for channels with query '%s', amount '%d' ('%d') and offset '%d' ('%d')", query, 100, SEARCH_AMOUNT, (offset / PAGE_AMOUNT) * 100, offset); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader = NULL; g_autofree gchar* uri = NULL; gint total; GList* ret = NULL; GError* err = NULL; uri = g_strdup_printf(offline ? SEARCH_CHANNELS_URI : SEARCH_STREAMS_URI, query, SEARCH_AMOUNT, (offset / PAGE_AMOUNT) * SEARCH_AMOUNT); msg = soup_message_new("GET", uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to search channels with query '%s', amount '%d' ('%d') and offset '%d' ('%d')", query, PAGE_AMOUNT, SEARCH_AMOUNT, (offset / PAGE_AMOUNT) * PAGE_AMOUNT, offset); READ_JSON_MEMBER(offline ? "channels" : "streams"); total = MIN(n + offset % PAGE_AMOUNT, json_reader_count_elements(reader)); for (gint i = offset % PAGE_AMOUNT; i < total; i++) { GtChannel* channel = NULL; GtChannelData* data = NULL; READ_JSON_ELEMENT(i); data = offline ? parse_channel(reader, &err) : parse_stream(reader, &err); CHECK_AND_PROPAGATE_ERROR("Unable to search channels with query '%s', amount '%d' ('%d') and offset '%d' ('%d')", query, PAGE_AMOUNT, SEARCH_AMOUNT, (offset / PAGE_AMOUNT) * PAGE_AMOUNT, offset); channel = gt_channel_new(data); END_JSON_ELEMENT(); ret = g_list_append(ret, channel); } json_reader_end_member(reader); return ret; error: gt_channel_list_free(ret); return NULL; } static void search_channels_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data = task_data; if (g_task_return_error_if_cancelled(task)) return; GError* err = NULL; GList* ret = gt_twitch_search_channels(GT_TWITCH(source), data->str_1, data->int_1, data->int_2, data->bool_1, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_channel_list_free); } void gt_twitch_search_channels_async(GtTwitch* self, const gchar* query, gint n, gint offset, gboolean offline, GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(n, >=, 0); g_assert_cmpint(n, <=, 100); g_assert_cmpint(offset, >=, 0); g_assert_false(utils_str_empty(query)); GTask* task = NULL; GenericTaskData* data = NULL; task = g_task_new(self, cancel, cb, udata); g_task_set_return_on_cancel(task, FALSE); data = generic_task_data_new(); data->int_1 = n; data->int_2 = offset; data->str_1 = g_strdup(query); data->bool_1 = offline; g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, search_channels_async_cb); g_object_unref(task); } GList* gt_twitch_search_channels_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GList* ret = g_task_propagate_pointer(G_TASK(result), error); return ret; } GList* gt_twitch_search_games(GtTwitch* self, const gchar* query, gint n, gint offset, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(n, >=, 0); g_assert_cmpint(n, <=, 100); g_assert_cmpint(offset, >=, 0); g_assert_false(utils_str_empty(query)); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader = NULL; g_autofree gchar* uri = NULL; GList* ret = NULL; GError* err = NULL; uri = g_strdup_printf(SEARCH_GAMES_URI, query); msg = soup_message_new("GET", uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to search games with query '%s', amount '%d' and offset '%d'", query, n, offset); READ_JSON_MEMBER("games"); for (gint i = 0; i < json_reader_count_elements(reader); i++) { GtGame* game; GtGameData* data = NULL; READ_JSON_ELEMENT(i); data = parse_game(reader, &err); CHECK_AND_PROPAGATE_ERROR("Unable to search games with query '%s', amount '%d' and offset '%d'", query, n, offset); game = gt_game_new(data); END_JSON_ELEMENT(); ret = g_list_append(ret, game); } END_JSON_MEMBER(); return ret; error: gt_game_list_free(ret); return NULL; } static void search_games_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data = task_data; if (g_task_return_error_if_cancelled(task)) return; GError* err = NULL; GList* ret = gt_twitch_search_games(GT_TWITCH(source), data->str_1, data->int_1, data->int_2, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_game_list_free); } void gt_twitch_search_games_async(GtTwitch* self, const gchar* query, gint n, gint offset, GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(n, >=, 0); g_assert_cmpint(n, <=, 100); g_assert_cmpint(offset, >=, 0); g_assert_false(utils_str_empty(query)); GTask* task = NULL; GenericTaskData* data = NULL; task = g_task_new(self, cancel, cb, udata); g_task_set_return_on_cancel(task, FALSE); data = generic_task_data_new(); data->int_1 = n; data->int_2 = offset; data->str_1 = g_strdup(query); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, search_games_async_cb); g_object_unref(task); } GList* gt_twitch_search_games_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GList* ret = g_task_propagate_pointer(G_TASK(result), error); return ret; } void gt_twitch_stream_data_free(GtTwitchStreamData* data) { g_free(data->quality); g_free(data->url); g_free(data); } void gt_twitch_stream_data_list_free(GList* list) { g_list_free_full(list, (GDestroyNotify) gt_twitch_stream_data_free); } GtChannelData* gt_twitch_fetch_channel_data(GtTwitch* self, const gchar* id, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(id)); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader = NULL; g_autofree gchar* uri = NULL; GError* err = NULL; GtChannelData* ret = NULL; uri = g_strdup_printf(FETCH_STREAM_URI, id); msg = soup_message_new("GET", uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch channel data with id '%s'", id); ret = gt_channel_data_new(); READ_JSON_MEMBER("stream"); if (json_reader_get_null_value(reader)) { //NOTE: Free these here as they will be used again g_object_unref(msg); g_object_unref(reader); g_free(uri); uri = g_strdup_printf(FETCH_CHANNEL_URI, id); msg = soup_message_new("GET", uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch channel data with id '%s'", id); ret = parse_channel(reader, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch channel data with id '%s'", id); } else { ret = parse_stream(reader, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch channel data with id '%s'", id); END_JSON_MEMBER(); } return ret; error: gt_channel_data_free(ret); return NULL; } GtChannel* gt_twitch_fetch_channel(GtTwitch* self, const gchar* id, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(id)); GtChannelData* data = NULL; GtChannel* channel = NULL; DEBUG("Fetching channel with id '%s'", id); data = gt_twitch_fetch_channel_data(self, id, error); if (*error) return NULL; channel = gt_channel_new(data); g_assert(g_object_is_floating(channel)); g_object_ref_sink(channel); return channel; } static void fetch_channel_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GError* err = NULL; GenericTaskData* data = task_data; GtChannel* ret = gt_twitch_fetch_channel(GT_TWITCH(source), data->str_1, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) g_object_unref); } void gt_twitch_fetch_channel_async(GtTwitch* self, const gchar* id, GAsyncReadyCallback cb, GCancellable* cancel, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(id)); GTask* task = NULL; GenericTaskData* data = generic_task_data_new(); task = g_task_new(self, cancel, cb, udata); data->str_1 = g_strdup(id); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, fetch_channel_async_cb); g_object_unref(task); } GtChannel* gt_twitch_fetch_channel_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GtChannel* ret = g_task_propagate_pointer(G_TASK(result), error); return ret; } /* GdkPixbuf* */ /* gt_twitch_download_picture_new(GtTwitch* self, const gchar* uri, */ /* gboolean check_cache, GError** error) */ /* { */ /* g_return_val_if_fail(GT_IS_TWITCH(self), NULL); */ /* g_return_val_if_fail(!utils_str_empty(uri), NULL); */ /* GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); */ /* GdkPixbuf* ret = NULL; */ /* g_autoptr(SoupMessage) msg = NULL; */ /* if (check_cache) */ /* { */ /* g_autofree gchar* filename= NULL; */ /* gchar hash_str[15]; */ /* guint hash = 0; */ /* gint64 timestamp = 0; */ /* hash = g_str_hash(uri); */ /* g_sprintf(hash_str, "%ud", hash); */ /* filename = g_build_filename(g_get_user_cache_dir(), */ /* "gnome-twitch", "images", NULL); */ /* if (g_file_test(filename, G_FILE_TEST_EXISTS)) */ /* { */ /* ret = */ /* } */ /* } */ /* } */ GdkPixbuf* gt_twitch_download_picture(GtTwitch* self, const gchar* url, gint64 timestamp, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(url)); GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); g_autoptr(SoupMessage) msg = NULL; GError* err = NULL; GdkPixbuf* ret = NULL; g_autoptr(GInputStream) input_stream = NULL; DEBUG("Downloading picture from url '%s'", url); if (timestamp) { msg = soup_message_new(SOUP_METHOD_HEAD, url); soup_message_headers_append(msg->request_headers, "Client-ID", CLIENT_ID); soup_session_send_message(priv->soup, msg); if (SOUP_STATUS_IS_SUCCESSFUL(msg->status_code)) { const gchar* last_modified; if ((last_modified = soup_message_headers_get_one(msg->response_headers, "Last-Modified")) != NULL && utils_http_full_date_to_timestamp(last_modified) < timestamp) { DEBUG("No new content at url '%s'", url); return NULL; } } else { WARNING("Unable to download picture from url '%s' because received unsuccessful response with code '%d' and body '%s'", url, msg->status_code, msg->response_body->data); g_set_error(error, GT_TWITCH_ERROR, GT_TWITCH_ERROR_SOUP_GENERIC, "Unable to download picture from url '%s' because received unsuccessful response with code '%d' and body '%s'", url, msg->status_code, msg->response_body->data); return NULL; } } #define CHECK_ERROR \ if (err) \ { \ WARNINGF("Error downloading picture for url '%s' because: %s", \ url, err->message); \ \ g_propagate_prefixed_error(error, err, "Unable to download picture for url '%s' because: ", \ url); \ \ return NULL; \ } \ msg = soup_message_new(SOUP_METHOD_GET, url); soup_message_headers_append(msg->request_headers, "Client-ID", CLIENT_ID); input_stream = soup_session_send(priv->soup, msg, NULL, &err); CHECK_ERROR; ret = gdk_pixbuf_new_from_stream(input_stream, NULL, &err); CHECK_ERROR; //TODO: Need to free ret if an error is encountered here g_input_stream_close(input_stream, NULL, &err); CHECK_ERROR; return ret; } static void download_picture_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { GenericTaskData* data = (GenericTaskData*) task_data; GdkPixbuf* ret = NULL; GError* err = NULL; if (g_task_return_error_if_cancelled(task)) return; ret = gt_twitch_download_picture(GT_TWITCH(source), data->str_1, data->int_1, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) g_object_unref); } void gt_twitch_download_picture_async(GtTwitch* self, const gchar* url, gint64 timestamp, GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(url)); GTask* task = NULL; GenericTaskData* data = NULL; task = g_task_new(self, cancel, cb, udata); g_task_set_return_on_cancel(task, FALSE); data = generic_task_data_new(); data->str_1 = g_strdup(url); data->int_1 = timestamp; g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, download_picture_async_cb); g_object_unref(task); } GdkPixbuf* gt_twitch_download_emote(GtTwitch* self, gint id) { GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); GdkPixbuf* ret = NULL; if (!g_hash_table_contains(priv->emote_table, GINT_TO_POINTER(id))) { g_autofree gchar* uri = NULL; g_autoptr(GError) err = NULL; g_autoptr(GdkPixbuf) emote = NULL; gchar id_str[15]; uri = g_strdup_printf(TWITCH_EMOTE_URI, id, 1); g_sprintf(id_str, "%d", id); DEBUGF("Downloading emote form url='%s'", uri); emote = gt_resource_downloader_download_image(emote_downloader, uri, id_str, &err); /* NOTE: If we encountered an error here we'll just insert a generic error emote */ if (err) { g_autoptr(GtkIconInfo) icon_info; WARNING("Unable to download emote with id '%d' because: %s", id, err->message); g_clear_error(&err); icon_info = gtk_icon_theme_lookup_icon(gtk_icon_theme_get_default(), "software-update-urgent-symbolic", 1, 0); emote = gtk_icon_info_load_icon(icon_info, &err); RETURN_VAL_IF_FAIL(err == NULL, NULL); } g_hash_table_insert(priv->emote_table, GINT_TO_POINTER(id), g_steal_pointer(&emote)); //TODO: Propagate this error further RETURN_VAL_IF_FAIL(err == NULL, NULL); } ret = GDK_PIXBUF(g_hash_table_lookup(priv->emote_table, GINT_TO_POINTER(id))); g_object_ref(ret); return ret; } static void fetch_chat_badge_set(GtTwitch* self, const gchar* set_name, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(set_name)); GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader = NULL; g_autofree gchar* uri = NULL; GError* err = NULL; g_assert_false(g_hash_table_contains(priv->badge_table, set_name)); g_hash_table_add(priv->badge_table, g_strdup(set_name)); //NOTE: This will be freed by the hash table when it's destroyed INFOF("Fetching chat badge set with name '%s'", set_name); uri = g_strcmp0(set_name, "global") == 0 ? g_strdup_printf(GLOBAL_CHAT_BADGES_URI) : g_strdup_printf(NEW_CHAT_BADGES_URI, set_name); msg = soup_message_new("GET", uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Error fetching chat badges for set %s", set_name); READ_JSON_MEMBER("badge_sets"); for (gint i = 0; i < json_reader_count_members(reader); i++) { READ_JSON_ELEMENT(i); const gchar* badge_name = json_reader_get_member_name(reader); READ_JSON_MEMBER("versions"); for (gint j = 0; j < json_reader_count_members(reader); j++) { GtChatBadge* badge = gt_chat_badge_new(); /* NOTE: Don't need to free this as it's freed by the hash table when it's destroyed. */ gchar* key = NULL; g_autofree gchar* uri = NULL; READ_JSON_ELEMENT(j); badge->name = g_strdup(badge_name); badge->version = g_strdup(json_reader_get_member_name(reader)); key = g_strdup_printf("%s-%s-%s", set_name, badge->name, badge->version); READ_JSON_VALUE("image_url_1x", uri); badge->pixbuf = gt_resource_downloader_download_image(badge_downloader, uri, key, &err); if (err) { g_autoptr(GtkIconInfo) icon_info; WARNING("Unable to fetch chat badge set with name '%s' because: %s", set_name, err->message); g_clear_error(&err); /* NOTE: If we encountered an error here we'll just insert a generic error emote */ g_clear_error(&err); icon_info = gtk_icon_theme_lookup_icon(gtk_icon_theme_get_default(), "software-update-urgent-symbolic", 1, 0); badge->pixbuf = gtk_icon_info_load_icon(icon_info, &err); RETURN_IF_FAIL(err == NULL); } END_JSON_ELEMENT(); g_assert_false(g_hash_table_contains(priv->badge_table, key)); g_hash_table_insert(priv->badge_table, key, badge); DEBUGF("Downloaded emote for set '%s' with name '%s' and version '%s'", set_name, badge->name, badge->version); } END_JSON_MEMBER(); END_JSON_ELEMENT(); } END_JSON_MEMBER(); error: return; } void gt_twitch_load_chat_badge_sets_for_channel(GtTwitch* self, const gchar* chan_id, GError** error) { g_assert(GT_IS_TWITCH(self)); GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); #define FETCH_BADGE_SET(s) \ if (!g_hash_table_contains(priv->badge_table, s)) \ { \ GError* err = NULL; \ \ fetch_chat_badge_set(self, s, &err); \ \ if (err) \ { \ WARNINGF("Unable to load chat badge sets for channel '%s'", \ chan_id); \ \ g_propagate_prefixed_error(error, err, \ "Unable to load chat badge sets for channel '%s' because: ", chan_id); \ \ return; \ } \ } \ FETCH_BADGE_SET("global"); FETCH_BADGE_SET(chan_id); #undef FETCH_BADGE_SET } // NOTE: This will automatically download any badge sets if they // aren't already GtChatBadge* gt_twitch_fetch_chat_badge(GtTwitch* self, const gchar* chan_id, const gchar* badge_name, const gchar* version, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(badge_name)); g_assert_false(utils_str_empty(version)); GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); g_autofree gchar* global_key = NULL; g_autofree gchar* chan_key = NULL; GtChatBadge* ret = NULL; GError* err = NULL; gt_twitch_load_chat_badge_sets_for_channel(self, chan_id, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch chat badge for channel '%s with badge name '%s' and version '%s'", chan_id, badge_name, version); global_key = g_strdup_printf("global-%s-%s", badge_name, version); chan_key = g_strdup_printf("%s-%s-%s", chan_id, badge_name, version); if (g_hash_table_contains(priv->badge_table, chan_key)) ret = g_hash_table_lookup(priv->badge_table, chan_key); else if (g_hash_table_contains(priv->badge_table, global_key)) ret = g_hash_table_lookup(priv->badge_table, global_key); else g_assert_not_reached(); //NOTE: We might as well crash here as the badge being null would lead to many problems error: return ret; } void fetch_chat_badge_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data; GtChatBadge* ret = NULL; GError* err = NULL; if (g_task_return_error_if_cancelled(task)) return; data = task_data; ret = gt_twitch_fetch_chat_badge(GT_TWITCH(source), data->str_1, data->str_2, data->str_3, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_chat_badge_list_free); } 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) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(badge_name)); g_assert_false(utils_str_empty(version)); GTask* task; GenericTaskData* data; task = g_task_new(self, cancel, cb, udata); g_task_set_return_on_cancel(task, FALSE); data = generic_task_data_new(); data->str_1 = g_strdup(chan_id); data->str_2 = g_strdup(badge_name); data->str_3 = g_strdup(version); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, fetch_chat_badge_async_cb); g_object_unref(task); } GtChatBadge* gt_twitch_fetch_chat_badge_finish(GtTwitch* self, GAsyncResult* result, GError** err) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GtChatBadge* ret = NULL; ret = g_task_propagate_pointer(G_TASK(result), err); return ret; } static GtTwitchChannelInfoPanel* gt_twitch_channel_info_panel_new() { return g_new0(GtTwitchChannelInfoPanel, 1); } void gt_twitch_channel_info_panel_free(GtTwitchChannelInfoPanel* panel) { g_free(panel->html_description); g_free(panel->markdown_description); g_clear_object(&panel->image); g_free(panel->link); g_free(panel); } void gt_twitch_channel_info_panel_list_free(GList* list) { g_list_free_full(list, (GDestroyNotify) gt_twitch_channel_info_panel_free); } //TODO: Rewrite this once we start displaying channel info again GList* gt_twitch_channel_info(GtTwitch* self, const gchar* chan) { GtTwitchPrivate* priv = gt_twitch_get_instance_private(self); SoupMessage* msg; gchar* uri = NULL; JsonParser* parser; JsonNode* node; JsonReader* reader; GList* ret = NULL; INFOF("Getting channel info for='%s'", chan); uri = g_strdup_printf(CHANNEL_INFO_URI, chan); msg = soup_message_new("GET", uri); if (!send_message(self, msg)) { WARNINGF("Error getting chat badges for channel='%s'", chan); goto finish; } parser = json_parser_new(); json_parser_load_from_data(parser, msg->response_body->data, msg->response_body->length, NULL); //TODO: Error handling node = json_parser_get_root(parser); reader = json_reader_new(node); for (gint i = 0; i < json_reader_count_elements(reader); i++) { GtTwitchChannelInfoPanel* panel = gt_twitch_channel_info_panel_new(); const gchar* type = NULL; json_reader_read_element(reader, i); json_reader_read_member(reader, "display_order"); panel->order = json_reader_get_int_value(reader) - 1; json_reader_end_member(reader); json_reader_read_member(reader, "kind"); type = json_reader_get_string_value(reader); if (g_strcmp0(type, "default") == 0) { panel->type = GT_TWITCH_CHANNEL_INFO_PANEL_TYPE_DEFAULT; } else { //TODO: Eventually handle other types of panels gt_twitch_channel_info_panel_free(panel); json_reader_end_member(reader); json_reader_end_element(reader); continue; } json_reader_end_member(reader); json_reader_read_member(reader, "html_description"); if (!json_reader_get_null_value(reader)) panel->html_description = g_strdup(json_reader_get_string_value(reader)); json_reader_end_member(reader); json_reader_read_member(reader, "data"); json_reader_read_member(reader, "link"); panel->link = g_strdup(json_reader_get_string_value(reader)); json_reader_end_member(reader); json_reader_read_member(reader, "image"); /* panel->image = gt_twitch_download_picture(self, json_reader_get_string_value(reader), 0); */ json_reader_end_member(reader); if (json_reader_read_member(reader, "description")) panel->markdown_description = g_strdup(json_reader_get_string_value(reader)); json_reader_end_member(reader); if (json_reader_read_member(reader, "title")) panel->title = g_strdup(json_reader_get_string_value(reader)); json_reader_end_member(reader); json_reader_end_member(reader); json_reader_end_element(reader); ret = g_list_append(ret, panel); } g_object_unref(parser); g_object_unref(reader); finish: g_free(uri); g_object_unref(msg); return ret; } static void channel_info_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data = NULL; GList* ret = NULL; if (g_task_return_error_if_cancelled(task)) return; data = task_data; ret = gt_twitch_channel_info(GT_TWITCH(source), data->str_1); g_task_return_pointer(task, ret, (GDestroyNotify) gt_twitch_channel_info_panel_list_free); } void gt_twitch_channel_info_async(GtTwitch* self, const gchar* chan, GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(chan)); GTask* task; GenericTaskData* data; task = g_task_new(self, cancel, cb, udata); g_task_set_return_on_cancel(task, FALSE); data = generic_task_data_new(); data->str_1 = g_strdup(chan); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, channel_info_async_cb); g_object_unref(task); } GList* gt_twitch_chat_servers(GtTwitch* self, const gchar* chan, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(chan)); g_autoptr(SoupMessage) msg; g_autoptr(JsonReader) reader; g_autofree gchar* uri; GList* ret = NULL; GError* err = NULL; uri = g_strdup_printf(CHAT_SERVERS_URI, chan); msg = soup_message_new("GET", uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch chat servers for channel %s", chan); READ_JSON_MEMBER("chat_servers"); for (int i = 0; i < json_reader_count_elements(reader); i++) { READ_JSON_ELEMENT(i); ret = g_list_append(ret, g_strdup(json_reader_get_string_value(reader))); END_JSON_ELEMENT(); } END_JSON_MEMBER(); return ret; error: g_list_free_full(ret, g_free); return NULL; } static GList* fetch_followed_streams(GtTwitch* self, const gchar* oauth_token, gint limit, gint offset, gint64* total, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(limit, >, 0); g_assert_cmpint(offset, >=, 0); g_assert_false(utils_str_empty(oauth_token)); g_autoptr(SoupMessage) msg; g_autoptr(JsonReader) reader; g_autofree gchar* uri; GList* ret = NULL; GError* err = NULL; uri = g_strdup_printf(FOLLOWED_STREAMS_URI, limit, offset, oauth_token); msg = soup_message_new(SOUP_METHOD_GET, uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch followed streams with oauth token '%s', limit '%d' and offset '%d'", oauth_token, limit, offset); if (total) { READ_JSON_VALUE("_total", *total); } READ_JSON_MEMBER("streams"); for (gint i = 0; i < json_reader_count_elements(reader); i++) { GtChannelData* data = NULL; READ_JSON_ELEMENT(i); data = parse_stream(reader, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch followed streams with oauth token '%s', limit '%d' and offset '%d'", oauth_token, limit, offset); ret = g_list_append(ret, data); END_JSON_ELEMENT(); } return ret; error: gt_channel_data_list_free(ret); return NULL; } static GList* fetch_followed_channels(GtTwitch* self, const gchar* id, gint limit, gint offset, gint64* total, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_cmpint(limit, >, 0); g_assert_cmpint(offset, >=, 0); g_autoptr(SoupMessage) msg; g_autoptr(JsonReader) reader; g_autofree gchar* uri; GList* ret = NULL; GError* err = NULL; uri = g_strdup_printf(FOLLOWED_CHANNELS_URI, id, limit, offset); msg = soup_message_new(SOUP_METHOD_GET, uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch followed channels with name '%s', limit '%d' and offset '%d'", id, limit, offset); if (total) { READ_JSON_VALUE("_total", *total); } READ_JSON_MEMBER("follows"); for (gint i = 0; i < json_reader_count_elements(reader); i++) { GtChannelData* data = NULL; READ_JSON_ELEMENT(i); READ_JSON_MEMBER("channel"); data = parse_channel(reader, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch followed channels with name '%s', limit '%d' and offset '%d'", id, limit, offset); ret = g_list_append(ret, data); END_JSON_MEMBER(); END_JSON_ELEMENT(); } return ret; error: gt_channel_data_list_free(ret); return NULL; } GList* gt_twitch_fetch_all_followed_channels(GtTwitch* self, const gchar* id, const gchar* oauth_token, GError** error) { g_assert(GT_IS_TWITCH(self)); #define LIMIT 100 gint64 total = 0; GList* chans = NULL; GList* streams = NULL; GList* ret = NULL; GError* err = NULL; streams = g_list_concat(streams, fetch_followed_streams(self, oauth_token, LIMIT, 0, &total, &err)); CHECK_AND_PROPAGATE_ERROR("Unable to fetch all followed channels"); if (total > LIMIT) { for (gint i = LIMIT; i < total; i += LIMIT) { streams = g_list_concat(streams, fetch_followed_streams(self, oauth_token, LIMIT, i, NULL, &err)); CHECK_AND_PROPAGATE_ERROR("Unable to fetch all followed channels"); } } chans = g_list_concat(chans, fetch_followed_channels(self, id, LIMIT, 0, &total, &err)); CHECK_AND_PROPAGATE_ERROR("Unable to fetch all followed channels"); if (total > LIMIT) { for (gint i = LIMIT; i < total; i += LIMIT) { chans = g_list_concat(chans, fetch_followed_channels(self, id, LIMIT, i, NULL, &err)); CHECK_AND_PROPAGATE_ERROR("Unable to fetch all followed channels"); } } //NOTE: Remove duplicates for (GList* l = streams; l != NULL; l = l->next) { GList* found = g_list_find_custom(chans, l->data, (GCompareFunc) gt_channel_data_compare); if (!found) { WARNINGF("Unable to fetch all followed channels with id '%s' and oauth token '%s' because: " "The followed stream '%s' did not exist as a followed channel", id, oauth_token, ((GtChannelData*) l->data)->name); /* NOTE: This is commented out because we won't treat it * as a hard error and try to load follows anyways */ /* 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: " */ /* "A followed stream did not exist as a followed channel", id, oauth_token); */ /* goto error; */ } else { g_assert_nonnull(found->data); g_assert_false(((GtChannelData*) found->data)->online); gt_channel_data_free(found->data); chans = g_list_delete_link(chans, found); } } chans = g_list_concat(chans, streams); for (GList* l = chans; l != NULL; l = l->next) { GtChannelData* data = l->data; GtChannel* chan = gt_channel_new(data); ret = g_list_append(ret, chan); } g_list_free(chans); //NOTE: Don't need to free entire list because gt_channel_new takes ownership of the data return ret; error: gt_channel_data_list_free(chans); gt_channel_data_list_free(streams); return NULL; } static void fetch_all_followed_channels_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data = task_data; GList* ret = NULL; GError* err = NULL; if (g_task_return_error_if_cancelled(task)) return; ret = gt_twitch_fetch_all_followed_channels(GT_TWITCH(source), data->str_1, data->str_2, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_channel_list_free); } void gt_twitch_fetch_all_followed_channels_async(GtTwitch* self, const gchar* id, const gchar* oauth_token, GCancellable* cancel, GAsyncReadyCallback cb, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(oauth_token)); GTask* task = NULL; GenericTaskData* data = NULL; task = g_task_new(self, cancel, cb, udata); g_task_set_return_on_cancel(task, FALSE); data = generic_task_data_new(); data->str_1 = g_strdup(id); data->str_2 = g_strdup(oauth_token); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, fetch_all_followed_channels_async_cb); g_object_unref(task); } GList* gt_twitch_fetch_all_followed_channels_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GList* ret = g_task_propagate_pointer(G_TASK(result), error); return ret; } void gt_twitch_follow_channel(GtTwitch* self, const gchar* chan_name, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(chan_name)); g_autoptr(SoupMessage) msg = NULL; g_autofree gchar* uri = NULL; const GtOAuthInfo* oauth_info = NULL; GError* err = NULL; oauth_info = gt_app_get_oauth_info(main_app); uri = g_strdup_printf(FOLLOW_CHANNEL_URI, oauth_info->user_name, chan_name, oauth_info->oauth_token); msg = soup_message_new(SOUP_METHOD_PUT, uri); new_send_message(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to follow channel '%s'", chan_name); error: return; } static void follow_channel_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data = task_data; GError* err = NULL; gt_twitch_follow_channel(GT_TWITCH(source), data->str_1, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, NULL, NULL); } // Not cancellable; hard to guarantee that channel is not followed void gt_twitch_follow_channel_async(GtTwitch* self, const gchar* chan_name, GAsyncReadyCallback cb, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(chan_name)); GTask* task = NULL; GenericTaskData* data = NULL; task = g_task_new(self, NULL, cb, udata); data = generic_task_data_new(); data->str_1 = g_strdup(chan_name); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, follow_channel_async_cb); g_object_unref(task); } void gt_twitch_follow_channel_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); g_task_propagate_pointer(G_TASK(result), error); } void gt_twitch_unfollow_channel(GtTwitch* self, const gchar* chan_name, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(chan_name)); g_autoptr(SoupMessage) msg = NULL; g_autofree gchar* uri = NULL; const GtOAuthInfo* oauth_info = NULL; GError* err = NULL; oauth_info = gt_app_get_oauth_info(main_app); uri = g_strdup_printf(UNFOLLOW_CHANNEL_URI, oauth_info->user_name, chan_name, oauth_info->oauth_token); msg = soup_message_new(SOUP_METHOD_DELETE, uri); new_send_message(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to unfollow channel '%s'", chan_name); error: return; } static void unfollow_channel_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data = task_data; GError* err = NULL; gt_twitch_unfollow_channel(GT_TWITCH(source), data->str_1, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, NULL, NULL); //NOTE: Just return null as this is a void function } //NOTE: Not cancellable; hard to guarantee that channel is not unfollowed void gt_twitch_unfollow_channel_async(GtTwitch* self, const gchar* chan_name, GAsyncReadyCallback cb, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(chan_name)); GTask* task = NULL; GenericTaskData* data = NULL; task = g_task_new(self, NULL, cb, udata); data = generic_task_data_new(); data->str_1 = g_strdup(chan_name); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, unfollow_channel_async_cb); g_object_unref(task); } void gt_twitch_unfollow_channel_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); g_task_propagate_pointer(G_TASK(result), error); } GList* gt_twitch_emoticons(GtTwitch* self, const gchar* emotesets, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(emotesets)); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader = NULL; g_autofree gchar* uri = NULL; g_auto(GStrv) sets = NULL; GList* ret = NULL; GError* err = NULL; uri = g_strdup_printf(EMOTICON_IMAGES_URI, emotesets); msg = soup_message_new(SOUP_METHOD_GET, uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to get emoticons for emote sets '%s'", emotesets); sets = g_strsplit(emotesets, ",", 0); READ_JSON_MEMBER("emoticon_sets"); for (gchar** c = sets; *c != NULL; c++) { READ_JSON_MEMBER(*c); for (gint i = 0; i < json_reader_count_elements(reader); i++) { GtChatEmote* emote = gt_chat_emote_new(); ret = g_list_append(ret, emote); READ_JSON_ELEMENT(i); READ_JSON_VALUE("id", emote->id); READ_JSON_VALUE("code", emote->code); END_JSON_ELEMENT(); emote->set = atoi(*c); emote->pixbuf = gt_twitch_download_emote(self, emote->id); } END_JSON_MEMBER(); } END_JSON_MEMBER(); return ret; error: gt_chat_emote_list_free(ret); return NULL; } static void emoticon_images_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GenericTaskData* data = task_data; GList* ret = NULL; GError* err = NULL; ret = gt_twitch_emoticons(GT_TWITCH(source), data->str_1, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_chat_emote_list_free); } void gt_twitch_emoticons_async(GtTwitch* self, const char* emotesets, GAsyncReadyCallback cb, GCancellable* cancel, gpointer udata) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(emotesets)); GTask* task = NULL; GenericTaskData* data = NULL; task = g_task_new(self, cancel, cb, udata); data = generic_task_data_new(); data->str_1 = g_strdup(emotesets); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, emoticon_images_async_cb); g_object_unref(task); } GtUserInfo* gt_twitch_fetch_user_info(GtTwitch* self, const gchar* oauth_token, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(oauth_token)); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader; g_autofree gchar* uri = NULL; GtUserInfo* ret = NULL; GError* err = NULL; uri = g_strdup_printf(USER_INFO_URI, oauth_token); msg = soup_message_new(SOUP_METHOD_GET, uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch user info"); ret = gt_user_info_new(); ret->oauth_token = g_strdup(oauth_token); READ_JSON_VALUE("_id", ret->id); READ_JSON_VALUE("name", ret->name); READ_JSON_VALUE_NULL("bio", ret->bio, NULL); READ_JSON_VALUE_NULL("display_name", ret->display_name, NULL); READ_JSON_VALUE("email", ret->email); READ_JSON_VALUE("email_verified", ret->email_verified); READ_JSON_VALUE_NULL("logo", ret->logo_url, NULL); READ_JSON_MEMBER("notifications"); READ_JSON_VALUE("email", ret->notifications.email); READ_JSON_VALUE("push", ret->notifications.push); END_JSON_MEMBER(); READ_JSON_VALUE("partnered", ret->partnered); READ_JSON_VALUE("twitter_connected", ret->twitter_connected); READ_JSON_VALUE("type", ret->type); READ_JSON_VALUE("created_at", ret->created_at); READ_JSON_VALUE("updated_at", ret->updated_at); return ret; error: gt_user_info_free(ret); return NULL; } static void fetch_user_info_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GError* err = NULL; GenericTaskData* data = task_data; GtUserInfo* ret = gt_twitch_fetch_user_info(GT_TWITCH(source), data->str_1, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_user_info_free); } void gt_twitch_fetch_user_info_async(GtTwitch* self, const gchar* oauth_token, GAsyncReadyCallback cb, GCancellable* cancel, gpointer udata) { g_assert(GT_IS_TWITCH(self)); GTask* task = NULL; GenericTaskData* data = generic_task_data_new(); task = g_task_new(self, cancel, cb, udata); data->str_1 = g_strdup(oauth_token); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, fetch_user_info_async_cb); g_object_unref(task); } GtUserInfo* gt_twitch_fetch_user_info_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GtUserInfo* ret = g_task_propagate_pointer(G_TASK(result), error); return ret; } GtOAuthInfo* gt_twitch_fetch_oauth_info(GtTwitch* self, const gchar* oauth_token, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert_false(utils_str_empty(oauth_token)); g_autoptr(SoupMessage) msg = NULL; g_autoptr(JsonReader) reader; g_autofree gchar* uri = NULL; GtOAuthInfo* ret = NULL; GError* err = NULL; gint num_scopes; uri = g_strdup_printf(OAUTH_INFO_URI, oauth_token); msg = soup_message_new(SOUP_METHOD_GET, uri); reader = new_send_message_json(self, msg, &err); CHECK_AND_PROPAGATE_ERROR("Unable to fetch oauth info"); ret = gt_oauth_info_new(); ret->oauth_token = g_strdup(oauth_token); READ_JSON_MEMBER("token"); READ_JSON_VALUE("user_id", ret->user_id); READ_JSON_VALUE("user_name", ret->user_name); READ_JSON_VALUE("client_id", ret->client_id); READ_JSON_VALUE("valid", ret->valid); READ_JSON_MEMBER("authorization"); READ_JSON_VALUE("created_at", ret->created_at); READ_JSON_VALUE("created_at", ret->updated_at); READ_JSON_MEMBER("scopes"); num_scopes = json_reader_count_elements(reader); ret->scopes = g_malloc_n(num_scopes + 1, sizeof(gchar*)); for (gint i = 0; i < num_scopes; i++) { READ_JSON_ELEMENT_VALUE(i, *(ret->scopes + i)); } *(ret->scopes + num_scopes) = NULL; END_JSON_MEMBER(); END_JSON_MEMBER(); END_JSON_MEMBER(); return ret; error: gt_oauth_info_free(ret); return NULL; } static void fetch_oauth_info_async_cb(GTask* task, gpointer source, gpointer task_data, GCancellable* cancel) { g_assert(GT_IS_TWITCH(source)); g_assert(G_IS_TASK(task)); g_assert_nonnull(task_data); GError* err = NULL; GenericTaskData* data = task_data; GtOAuthInfo* ret = gt_twitch_fetch_oauth_info(GT_TWITCH(source), data->str_1, &err); if (err) g_task_return_error(task, err); else g_task_return_pointer(task, ret, (GDestroyNotify) gt_oauth_info_free); } void gt_twitch_fetch_oauth_info_async(GtTwitch* self, const gchar* oauth_token, GAsyncReadyCallback cb, GCancellable* cancel, gpointer udata) { g_assert(GT_IS_TWITCH(self)); GTask* task = NULL; GenericTaskData* data = generic_task_data_new(); task = g_task_new(self, cancel, cb, udata); data->str_1 = g_strdup(oauth_token); g_task_set_task_data(task, data, (GDestroyNotify) generic_task_data_free); g_task_run_in_thread(task, fetch_oauth_info_async_cb); g_object_unref(task); } GtOAuthInfo* gt_twitch_fetch_oauth_info_finish(GtTwitch* self, GAsyncResult* result, GError** error) { g_assert(GT_IS_TWITCH(self)); g_assert(G_IS_ASYNC_RESULT(result)); GtOAuthInfo* ret = g_task_propagate_pointer(G_TASK(result), error); return ret; } GtChatBadge* gt_chat_badge_new() { return g_slice_new0(GtChatBadge); } void gt_chat_badge_free(GtChatBadge* badge) { g_assert_nonnull(badge); g_free(badge->name); g_free(badge->version); g_object_unref(badge->pixbuf); g_slice_free(GtChatBadge, badge); } void gt_chat_badge_list_free(GList* list) { g_list_free_full(list, (GDestroyNotify) gt_chat_badge_free); }