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-app.h"
20 #include "gt-win.h"
21 #include "gt-http-soup.h"
22 #include "config.h"
23 #include <glib/gi18n.h>
24 #include <glib/gstdio.h>
25 #include <errno.h>
26 #include <string.h>
27 #include <json-glib/json-glib.h>
28 #include <stdlib.h>
29 #include "utils.h"
30 #include "version.h"
31 
32 #define TAG "GtApp"
33 #include "gnome-twitch/gt-log.h"
34 
35 struct _GtAppPrivate
36 {
37     GtWin* win;
38 
39     GtUserInfo* user_info;
40     GtOAuthInfo* oauth_info;
41     GMenuItem* login_item;
42     GMenuModel* app_menu;
43 
44     gchar* language_filter;
45 
46     GCancellable* open_channel_cancel;
47     GHashTable* soup_inflight_table;
48     GQueue* soup_message_queue;
49 };
50 
51 gint LOG_LEVEL = GT_LOG_LEVEL_MESSAGE;
52 gboolean NO_FANCY_LOGGING = FALSE;
53 gboolean VERSION = FALSE;
54 
55 const gchar* TWITCH_AUTH_SCOPES[] =
56 {
57     "chat_login",
58     "user_follows_edit",
59     "user_read",
60     NULL
61 };
62 
63 
64 G_DEFINE_TYPE_WITH_PRIVATE(GtApp, gt_app, GTK_TYPE_APPLICATION)
65 
66 enum
67 {
68     PROP_0,
69     PROP_LOGGED_IN,
70     PROP_LANGUAGE_FILTER,
71     NUM_PROPS
72 };
73 
74 static GParamSpec* props[NUM_PROPS];
75 
76 static gboolean
set_log_level(const gchar * name,const gchar * arg,gpointer data,GError ** err)77 set_log_level(const gchar* name,
78               const gchar* arg,
79               gpointer data,
80               GError** err)
81 {
82     if (g_strcmp0(arg, "error") == 0)
83         LOG_LEVEL = GT_LOG_LEVEL_ERROR;
84     else if (g_strcmp0(arg, "critical") == 0)
85         LOG_LEVEL = GT_LOG_LEVEL_CRITICAL;
86     else if (g_strcmp0(arg, "warning") == 0)
87         LOG_LEVEL = GT_LOG_LEVEL_WARNING;
88     else if (g_strcmp0(arg, "message") == 0)
89         LOG_LEVEL = GT_LOG_LEVEL_MESSAGE;
90     else if (g_strcmp0(arg, "info") == 0)
91         LOG_LEVEL = GT_LOG_LEVEL_INFO;
92     else if (g_strcmp0(arg, "debug") == 0)
93         LOG_LEVEL = GT_LOG_LEVEL_DEBUG;
94     else if (g_strcmp0(arg, "trace") == 0)
95         LOG_LEVEL = GT_LOG_LEVEL_TRACE;
96     else
97         WARNING("Invalid logging level"); //TODO: Use g_set_error and return false
98 
99     return TRUE;
100 }
101 
102 static GOptionEntry
103 cli_options[] =
104 {
105     {"log-level", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_CALLBACK, set_log_level, "Set logging level", "level"},
106     {"no-fancy-logging", 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &NO_FANCY_LOGGING, "Don't print pretty log messages", NULL},
107     {"version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &VERSION, "Display version", NULL},
108     {NULL}
109 };
110 
111 GtApp*
gt_app_new(void)112 gt_app_new(void)
113 {
114     return g_object_new(GT_TYPE_APP,
115                         "application-id", "com.vinszent.GnomeTwitch",
116                         NULL);
117 }
118 
119 static void
oauth_info_cb(GObject * source,GAsyncResult * res,gpointer udata)120 oauth_info_cb(GObject* source,
121     GAsyncResult* res, gpointer udata)
122 {
123     g_assert(GT_IS_APP(udata));
124     g_assert(GT_IS_TWITCH(source));
125 
126     GtApp* self = GT_APP(udata);
127     GError* err = NULL;
128 
129     GtOAuthInfo* oauth_info = gt_twitch_fetch_oauth_info_finish(GT_TWITCH(source), res, &err);
130 
131     if (err)
132     {
133         WARNING("Unable to update oauth info because: %s", err->message);
134 
135         GtWin* win = GT_WIN_ACTIVE;
136 
137         g_assert(GT_IS_WIN(win));
138 
139         gt_win_show_error_message(win, "Unable to update oauth info",
140             "Unable to update oauth info because: %s", err->message);
141 
142         g_error_free(err);
143 
144         return;
145     }
146 
147     gt_app_set_oauth_info(self, oauth_info);
148 
149     MESSAGE("Successfully fetched oauth info with name '%s', id '%s' and oauth token '%s'",
150         oauth_info->user_name, oauth_info->user_id, oauth_info->oauth_token);
151 }
152 
153 static void
user_info_cb(GObject * source,GAsyncResult * res,gpointer udata)154 user_info_cb(GObject* source,
155     GAsyncResult* res, gpointer udata)
156 {
157     g_assert(GT_IS_APP(udata));
158     g_assert(GT_IS_TWITCH(source));
159 
160     GtApp* self = GT_APP(udata);
161     GtAppPrivate* priv = gt_app_get_instance_private(self);
162     GError* err = NULL;
163 
164     GtUserInfo* user_info = gt_twitch_fetch_user_info_finish(GT_TWITCH(source), res, &err);
165 
166     if (err)
167     {
168         WARNING("Unable to update user info because: %s", err->message);
169 
170         GtWin* win = GT_WIN_ACTIVE;
171 
172         g_assert(GT_IS_WIN(win));
173 
174         gt_win_show_error_message(win, "Unable to update user info",
175             "Unable to update user info because: %s", err->message);
176 
177         g_error_free(err);
178 
179         return;
180     }
181 
182     priv->user_info = user_info;
183 
184     MESSAGE("Successfully fetched user info with name '%s', id '%s' and oauth token '%s'",
185         user_info->name, user_info->id, user_info->oauth_token);
186 }
187 
188 static void
logged_in_cb(GObject * src,GParamSpec * pspec,gpointer udata)189 logged_in_cb(GObject* src,
190     GParamSpec* pspec, gpointer udata)
191 {
192     g_assert(GT_IS_APP(src));
193 
194     GtApp* self = GT_APP(src);
195     GtAppPrivate* priv = gt_app_get_instance_private(self);
196 
197     if (gt_app_is_logged_in(self))
198     {
199         g_menu_remove(G_MENU(priv->app_menu), 0);
200         g_menu_item_set_label(priv->login_item, _("Refresh login"));
201         g_menu_prepend_item(G_MENU(priv->app_menu), priv->login_item);
202     }
203 }
204 
205 /* FIXME: Move this into GtResourceDownloader */
206 static inline void
init_dirs()207 init_dirs()
208 {
209     gchar* fp;
210     int err;
211 
212     fp = g_build_filename(g_get_user_data_dir(), "gnome-twitch", NULL);
213     err = g_mkdir_with_parents(fp, 0777);
214     if (err != 0 && g_file_error_from_errno(errno) != G_FILE_ERROR_EXIST)
215         WARNING("Error creating data directory");
216     g_free(fp);
217 
218     fp = g_build_filename(g_get_user_cache_dir(), "gnome-twitch", "channels", NULL);
219     err = g_mkdir_with_parents(fp, 0777);
220     if (err != 0 && g_file_error_from_errno(errno) != G_FILE_ERROR_EXIST)
221         WARNING("Error creating channel cache directory");
222     g_free(fp);
223 
224     fp = g_build_filename(g_get_user_cache_dir(), "gnome-twitch", "games", NULL);
225     err = g_mkdir_with_parents(fp, 0777);
226     if (err != 0 && g_file_error_from_errno(errno) != G_FILE_ERROR_EXIST)
227         WARNING("Error creating game cache directory");
228     g_free(fp);
229 
230     fp = g_build_filename(g_get_user_cache_dir(), "gnome-twitch", "emotes", NULL);
231     err = g_mkdir_with_parents(fp, 0777);
232     if (err != 0 && g_file_error_from_errno(errno) != G_FILE_ERROR_EXIST)
233         WARNING("Error creating game cache directory");
234     g_free(fp);
235 
236     fp = g_build_filename(g_get_user_cache_dir(), "gnome-twitch", "badges", NULL);
237     err = g_mkdir_with_parents(fp, 0777);
238     if (err != 0 && g_file_error_from_errno(errno) != G_FILE_ERROR_EXIST)
239         WARNING("Error creating game cache directory");
240     g_free(fp);
241 }
242 
243 static void
open_channel_after_fetch_cb(GObject * source,GAsyncResult * res,gpointer udata)244 open_channel_after_fetch_cb(GObject* source,
245     GAsyncResult* res, gpointer udata)
246 {
247     g_assert(GT_IS_APP(udata));
248     g_assert(GT_IS_TWITCH(source));
249     g_assert(G_IS_ASYNC_RESULT(res));
250 
251     GtApp* self = udata;
252     g_autoptr(GError) err = NULL;
253     //NOTE: autoptr because we want this unrefed as we won't be using it anymore
254     g_autoptr(GtChannel) channel = NULL;
255     GtWin* win = NULL;
256 
257     channel = gt_twitch_fetch_channel_finish(GT_TWITCH(source),
258         res, &err);
259 
260     win = GT_WIN_ACTIVE;
261 
262     g_assert(GT_IS_WIN(win));
263 
264     if (err)
265     {
266         WARNING("Couldn't open channel because: %s", err->message);
267 
268         gt_win_show_error_message(win, "Couldn't open channel",
269             "Couldn't open channel because: %s", err->message);
270 
271     }
272     else if (!gt_channel_is_online(channel))
273     {
274         gt_win_show_info_message(win, _("Unable to open channel %s because it’s not online"),
275             gt_channel_get_name(channel));
276     }
277     else
278         gt_win_open_channel(win, channel); //NOTE: Win will take a reference to channel
279 
280     g_object_unref(self);
281 }
282 
283 static void
open_channel_from_id_cb(GSimpleAction * action,GVariant * var,gpointer udata)284 open_channel_from_id_cb(GSimpleAction* action,
285     GVariant* var, gpointer udata)
286 {
287     g_assert(G_IS_SIMPLE_ACTION(action));
288     g_assert(GT_IS_APP(udata));
289     g_assert(g_variant_is_of_type(var, G_VARIANT_TYPE_STRING));
290 
291     GtApp* self = GT_APP(udata);
292     GtAppPrivate* priv = gt_app_get_instance_private(self);
293     const gchar* id = g_variant_get_string(var, NULL);
294 
295     utils_refresh_cancellable(&priv->open_channel_cancel);
296 
297     MESSAGE("Opening channel from id '%s'", id);
298 
299     gt_twitch_fetch_channel_async(self->twitch, id,
300         open_channel_after_fetch_cb, priv->open_channel_cancel,
301         g_object_ref(self));
302 }
303 
304 static void
quit_cb(GSimpleAction * action,GVariant * par,gpointer udata)305 quit_cb(GSimpleAction* action,
306     GVariant* par, gpointer udata)
307 {
308     MESSAGE("Quitting");
309 
310     GtApp* self = GT_APP(udata);
311 
312     g_application_quit(G_APPLICATION(self));
313 }
314 
315 static gint
handle_command_line_cb(GApplication * self,GVariantDict * options,gpointer udata)316 handle_command_line_cb(GApplication* self,
317     GVariantDict* options, gpointer udata)
318 {
319     if (VERSION)
320     {
321         g_print("GNOME Twitch - Version %s\n", GT_VERSION);
322         return 0;
323     }
324 
325     return -1;
326 }
327 
328 static GActionEntry app_actions[] =
329 {
330     {"open-channel-from-id", open_channel_from_id_cb, "s", NULL, NULL},
331     {"quit", quit_cb, NULL, NULL, NULL}
332 };
333 
334 static void
activate(GApplication * app)335 activate(GApplication* app)
336 {
337     GtApp* self = GT_APP(app);
338     GtAppPrivate* priv = gt_app_get_instance_private(self);
339 
340     G_APPLICATION_CLASS(gt_app_parent_class)->activate(app);
341 
342     MESSAGE("Activate");
343 
344     priv->win = gt_win_new(self);
345 
346     gtk_window_present(GTK_WINDOW(priv->win));
347 }
348 
349 static void
gt_app_prefer_dark_theme_changed_cb(GSettings * settings,const char * key,GtkSettings * gtk_settings)350 gt_app_prefer_dark_theme_changed_cb(GSettings* settings,
351                                     const char* key,
352                                     GtkSettings* gtk_settings)
353 {
354     gboolean prefer_dark_theme = g_settings_get_boolean(settings, key);
355 
356     g_object_set(gtk_settings,
357                  "gtk-application-prefer-dark-theme",
358                  prefer_dark_theme,
359                  NULL);
360 }
361 
362 //Only called once
363 static void
startup(GApplication * app)364 startup(GApplication* app)
365 {
366     GtApp* self = GT_APP(app);
367     GtAppPrivate* priv = gt_app_get_instance_private(self);
368     GtkSettings* gtk_settings;
369     g_autoptr(GtkBuilder) menu_bld;
370 
371     G_APPLICATION_CLASS(gt_app_parent_class)->startup(app);
372 
373     MESSAGE("Startup, running version '%s'", GT_VERSION);
374 
375     self->fav_mgr = gt_follows_manager_new();
376     self->twitch = gt_twitch_new();
377 
378     init_dirs();
379 
380     g_action_map_add_action_entries(G_ACTION_MAP(self),
381         app_actions, G_N_ELEMENTS(app_actions), self);
382 
383     menu_bld = gtk_builder_new_from_resource("/com/vinszent/GnomeTwitch/ui/app-menu.ui");
384     priv->app_menu = G_MENU_MODEL(gtk_builder_get_object(menu_bld, "app_menu"));
385     g_object_ref(priv->app_menu);
386     priv->login_item = g_menu_item_new(_("Log in to Twitch"), "win.show_twitch_login");
387     g_menu_prepend_item(G_MENU(priv->app_menu), priv->login_item);
388 
389     gtk_application_set_app_menu(GTK_APPLICATION(app), G_MENU_MODEL(priv->app_menu));
390 
391     gtk_settings = gtk_settings_get_default();
392 
393     gt_app_prefer_dark_theme_changed_cb(self->settings,
394         "prefer-dark-theme", gtk_settings);
395 
396     g_signal_connect(self->settings, "changed::prefer-dark-theme",
397         G_CALLBACK(gt_app_prefer_dark_theme_changed_cb), gtk_settings);
398 
399     //NOTE: This is to refresh oauth info
400     if (gt_app_is_logged_in(self))
401     {
402         gt_twitch_fetch_oauth_info_async(self->twitch, priv->oauth_info->oauth_token,
403             oauth_info_cb, NULL, self);
404     }
405     else
406         g_object_notify_by_pspec(G_OBJECT(self), props[PROP_LOGGED_IN]);
407 }
408 
409 static void
shutdown(GApplication * app)410 shutdown(GApplication* app)
411 {
412     GtApp* self = GT_APP(app);
413 
414     MESSAGE("Shutting down");
415 
416     //TODO: Add a setting to allow user to use local follows even when logged in
417     if (!gt_app_is_logged_in(self))
418         gt_follows_manager_save(self->fav_mgr);
419 
420     G_APPLICATION_CLASS(gt_app_parent_class)->shutdown(app);
421 }
422 
423 static void
dispose(GObject * object)424 dispose(GObject* object)
425 {
426     g_assert(GT_IS_APP(object));
427 
428     GtApp* self = GT_APP(object);
429     GtAppPrivate* priv = gt_app_get_instance_private(self);
430 
431     g_hash_table_unref(priv->soup_inflight_table);
432     g_queue_free_full(priv->soup_message_queue, g_object_unref);
433     g_object_unref(self->http);
434 
435     G_OBJECT_CLASS(gt_app_parent_class)->dispose(object);
436 }
437 
438 static void
get_property(GObject * obj,guint prop,GValue * val,GParamSpec * pspec)439 get_property (GObject* obj, guint prop,
440     GValue* val, GParamSpec* pspec)
441 {
442     GtApp* self = GT_APP(obj);
443     GtAppPrivate* priv = gt_app_get_instance_private(self);
444 
445     switch (prop)
446     {
447         case PROP_LOGGED_IN:
448             g_value_set_boolean(val, priv->oauth_info != NULL);
449             break;
450         case PROP_LANGUAGE_FILTER:
451             g_value_set_string(val, priv->language_filter);
452             break;
453         default:
454             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
455     }
456 }
457 
458 static void
set_property(GObject * obj,guint prop,const GValue * val,GParamSpec * pspec)459 set_property(GObject* obj, guint prop,
460     const GValue* val, GParamSpec* pspec)
461 {
462     GtApp* self = GT_APP(obj);
463     GtAppPrivate* priv = gt_app_get_instance_private(self);
464 
465     switch (prop)
466     {
467         case PROP_LANGUAGE_FILTER:
468             g_free(priv->language_filter);
469             priv->language_filter = g_value_dup_string(val);
470             break;
471         default:
472             G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec);
473     }
474 }
475 
476 static void
gt_app_class_init(GtAppClass * klass)477 gt_app_class_init(GtAppClass* klass)
478 {
479     GObjectClass* object_class = G_OBJECT_CLASS(klass);
480 
481     G_OBJECT_CLASS(klass)->dispose = dispose;
482 
483     G_APPLICATION_CLASS(klass)->activate = activate;
484     G_APPLICATION_CLASS(klass)->startup = startup;
485     G_APPLICATION_CLASS(klass)->shutdown = shutdown;
486 
487     object_class->get_property = get_property;
488     object_class->set_property = set_property;
489 
490     props[PROP_LOGGED_IN] = g_param_spec_boolean("logged-in",
491         "Logged in", "Whether logged in", FALSE, G_PARAM_READABLE);
492 
493     props[PROP_LANGUAGE_FILTER] = g_param_spec_string("language-filter",
494         "Language filter", "Current language filter", "", G_PARAM_READWRITE);
495 
496     g_object_class_install_properties(object_class, NUM_PROPS, props);
497 }
498 
499 static void
gt_app_init(GtApp * self)500 gt_app_init(GtApp* self)
501 {
502     GtAppPrivate* priv = gt_app_get_instance_private(self);
503 
504     priv->oauth_info = gt_oauth_info_new();
505     priv->soup_inflight_table = g_hash_table_new(g_str_hash, g_str_equal);
506     priv->soup_message_queue = g_queue_new();
507 
508     g_application_add_main_option_entries(G_APPLICATION(self), cli_options);
509 
510     self->settings = g_settings_new("com.vinszent.GnomeTwitch");
511     self->players_engine = peas_engine_get_default();
512     peas_engine_enable_loader(self->players_engine, "python3");
513     self->soup = soup_session_new();
514     self->http = GT_HTTP(gt_http_soup_new());
515 
516     gchar* plugin_dir;
517 
518 #ifdef G_OS_WIN32
519     plugin_dir = g_build_filename(
520         g_win32_get_package_installation_directory_of_module(NULL),
521         "lib", "gnome-twitch", "player-backends", NULL);
522 
523     peas_engine_add_search_path(self->players_engine, plugin_dir, NULL);
524 
525     g_free(plugin_dir);
526 #else
527     plugin_dir = g_build_filename(GT_LIB_DIR, "gnome-twitch",
528         "player-backends", NULL);
529 
530     peas_engine_add_search_path(self->players_engine, plugin_dir, NULL);
531 
532     g_free(plugin_dir);
533 
534     plugin_dir = g_build_filename(g_get_user_data_dir(), "gnome-twitch",
535         "player-backends", NULL);
536 
537     peas_engine_add_search_path(self->players_engine, plugin_dir, NULL);
538 
539     g_free(plugin_dir);
540 #endif
541 
542     g_settings_bind(self->settings, "player-backend",
543         self->players_engine, "loaded-plugins",
544         G_SETTINGS_BIND_DEFAULT);
545     g_settings_bind(self->settings, "language-filter",
546         self, "language-filter",
547         G_SETTINGS_BIND_GET);
548 
549     priv->oauth_info->oauth_token = g_settings_get_string(self->settings, "oauth-token");
550     priv->oauth_info->user_id = g_settings_get_string(self->settings, "user-id");
551     priv->oauth_info->user_name = g_settings_get_string(self->settings, "user-name");
552 
553     g_signal_connect(self, "notify::logged-in", G_CALLBACK(logged_in_cb), self);
554     g_signal_connect(self, "handle-local-options", G_CALLBACK(handle_command_line_cb), self);
555 
556 }
557 
558 //TODO: Turn this into a property
559 void
gt_app_set_oauth_info(GtApp * self,GtOAuthInfo * info)560 gt_app_set_oauth_info(GtApp* self, GtOAuthInfo* info)
561 {
562     GtAppPrivate* priv = gt_app_get_instance_private(self);
563 
564     if (priv->oauth_info)
565         gt_oauth_info_free(priv->oauth_info);
566 
567     priv->oauth_info = info;
568 
569     for (gchar** s = (gchar**) TWITCH_AUTH_SCOPES; *s != NULL; s++)
570     {
571         if (!g_strv_contains((const gchar**) info->scopes, *s))
572         {
573             GtWin* win = GT_WIN_ACTIVE;
574 
575             g_assert(GT_IS_WIN(win));
576 
577             WARNING("Unable to update oauth info because the auth scope '%s' was not included", *s);
578 
579             gt_win_show_error_message(win, "Unable to update oauth info, try refreshing your login",
580                 "Unable to update oauth info because the auth scope '%s' was not included", *s);
581 
582             g_object_notify_by_pspec(G_OBJECT(self), props[PROP_LOGGED_IN]);
583 
584             return;
585         }
586     }
587 
588     g_settings_set_string(self->settings, "oauth-token", info->oauth_token);
589     g_settings_set_string(self->settings, "user-id", info->user_id);
590     g_settings_set_string(self->settings, "user-name", info->user_name);
591 
592     gt_twitch_fetch_user_info_async(self->twitch, priv->oauth_info->oauth_token,
593         user_info_cb, NULL, self);
594 
595     g_object_notify_by_pspec(G_OBJECT(self), props[PROP_LOGGED_IN]);
596 }
597 
598 const GtUserInfo*
gt_app_get_user_info(GtApp * self)599 gt_app_get_user_info(GtApp* self)
600 {
601     g_assert(GT_IS_APP(self));
602 
603     GtAppPrivate* priv = gt_app_get_instance_private(self);
604 
605     return priv->user_info;
606 }
607 
608 const GtOAuthInfo*
gt_app_get_oauth_info(GtApp * self)609 gt_app_get_oauth_info(GtApp* self)
610 {
611     g_assert(GT_IS_APP(self));
612 
613     GtAppPrivate* priv = gt_app_get_instance_private(self);
614 
615     return priv->oauth_info;
616 }
617 
618 gboolean
gt_app_is_logged_in(GtApp * self)619 gt_app_is_logged_in(GtApp* self)
620 {
621     GtAppPrivate* priv = gt_app_get_instance_private(self);
622 
623     return
624         priv->oauth_info &&
625         !utils_str_empty(priv->oauth_info->oauth_token) &&
626         !utils_str_empty(priv->oauth_info->user_name) &&
627         !utils_str_empty(priv->oauth_info->user_id);
628 }
629 
630 const gchar*
gt_app_get_language_filter(GtApp * self)631 gt_app_get_language_filter(GtApp* self)
632 {
633     g_assert(GT_IS_APP(self));
634 
635     GtAppPrivate* priv = gt_app_get_instance_private(self);
636 
637     return priv->language_filter;
638 }
639 
640 gboolean
gt_app_should_show_notifications(GtApp * self)641 gt_app_should_show_notifications(GtApp* self)
642 {
643     g_assert(GT_IS_APP(self));
644 
645     return g_settings_get_boolean(main_app->settings, "show-notifications");
646 }
647 
648 GtUserInfo*
gt_user_info_new()649 gt_user_info_new()
650 {
651     return g_slice_new0(GtUserInfo);
652 }
653 
654 void
gt_user_info_free(GtUserInfo * info)655 gt_user_info_free(GtUserInfo* info)
656 {
657     if (!info) return;
658 
659     g_free(info->name);
660     g_free(info->oauth_token);
661     g_free(info->display_name);
662     g_free(info->bio);
663     g_free(info->logo_url);
664     g_free(info->type);
665     g_free(info->email);
666 
667     if (info->created_at) g_date_time_unref(info->created_at);
668     if (info->updated_at) g_date_time_unref(info->updated_at);
669 
670     g_slice_free(GtUserInfo, info);
671 }
672 
673 GtOAuthInfo*
gt_oauth_info_new()674 gt_oauth_info_new()
675 {
676     return g_slice_new0(GtOAuthInfo);
677 }
678 
679 void
gt_oauth_info_free(GtOAuthInfo * info)680 gt_oauth_info_free(GtOAuthInfo* info)
681 {
682     if (!info) return;
683 
684     g_free(info->user_name);
685     g_free(info->user_id);
686     g_free(info->client_id);
687     g_strfreev(info->scopes);
688 
689     if (info->created_at) g_date_time_unref(info->created_at);
690     if (info->updated_at) g_date_time_unref(info->updated_at);
691 
692     g_slice_free(GtOAuthInfo, info);
693 }
694