/*
This file is part of GNUnet.
Copyright (C) 2021--2024 GNUnet e.V.
GNUnet is free software: you can redistribute it and/or modify it
under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License,
or (at your option) any later version.
GNUnet 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
Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see .
SPDX-License-Identifier: AGPL3.0-or-later
*/
/*
* @author Tobias Frisch
* @file application.c
*/
#include "application.h"
#include "request.h"
#include "resources.h"
#include
#include
#include
#include
#include
#ifndef MESSENGER_APPLICATION_NO_PORTAL
#include
#endif
static void
_load_ui_stylesheets(MESSENGER_Application *app)
{
g_assert(app);
GdkScreen* screen = gdk_screen_get_default();
GtkCssProvider* provider = gtk_css_provider_new();
gtk_css_provider_load_from_resource(
provider,
application_get_resource_path(app, "css/style.css")
);
gtk_style_context_add_provider_for_screen(
screen,
GTK_STYLE_PROVIDER(provider),
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION
);
}
static gboolean
_application_accounts(gpointer user_data)
{
g_assert(user_data);
MESSENGER_Application *app = (MESSENGER_Application*) user_data;
app->init = 0;
ui_accounts_dialog_init(app, &(app->ui.accounts));
ui_accounts_dialog_refresh(app, &(app->ui.accounts));
gtk_widget_show(GTK_WIDGET(app->ui.accounts.dialog));
return FALSE;
}
static void
_application_init(MESSENGER_Application *app)
{
g_assert(app);
ui_messenger_init(app, &(app->ui.messenger));
#ifndef MESSENGER_APPLICATION_NO_PORTAL
if (app->portal)
app->parent = xdp_parent_new_gtk(GTK_WINDOW(app->ui.messenger.main_window));
#endif
if (app->chat.identity)
application_show_window(app);
else
app->init = g_idle_add(G_SOURCE_FUNC(_application_accounts), app);
}
static void
_application_activate(GApplication* application,
gpointer user_data)
{
g_assert((application) && (user_data));
MESSENGER_Application *app = (MESSENGER_Application*) user_data;
g_application_hold(application);
_application_init(app);
g_application_release(application);
}
static void
_application_open(GApplication* application,
GFile **files,
gint n_files,
UNUSED gchar* hint,
gpointer user_data)
{
g_assert((application) && (files) && (user_data));
MESSENGER_Application *app = (MESSENGER_Application*) user_data;
g_application_hold(application);
_application_init(app);
for (gint i = 0; i < n_files; i++) {
if (!g_file_has_uri_scheme(files[i], "gnunet"))
continue;
gchar *uri_string = g_file_get_uri(files[i]);
if (!uri_string)
continue;
char *emsg = NULL;
struct GNUNET_CHAT_Uri *uri = GNUNET_CHAT_uri_parse(uri_string, &emsg);
if (emsg)
{
g_printerr("ERROR: %s\n", emsg);
GNUNET_free(emsg);
}
if (!uri)
goto free_string;
GNUNET_CHAT_lobby_join(app->chat.messenger.handle, uri);
GNUNET_CHAT_uri_destroy(uri);
free_string:
g_free(uri_string);
}
g_application_release(application);
}
static void
on_core_done(void *data,
UNUSED uint32_t id,
int seq)
{
g_assert(data);
MESSENGER_Application *app = (MESSENGER_Application*) data;
if (seq == app->pw.pending)
pw_main_loop_quit(app->pw.main_loop);
}
static void
on_core_error(void *data,
UNUSED uint32_t id,
UNUSED int seq,
int res,
const char *message)
{
g_assert((data) && (message));
MESSENGER_Application *app = (MESSENGER_Application*) data;
g_printerr("ERROR: %s\n", message);
if ((id == PW_ID_CORE) && (res == -EPIPE))
pw_main_loop_quit(app->pw.main_loop);
}
static const struct pw_core_events remote_core_events = {
PW_VERSION_CORE_EVENTS,
.done = on_core_done,
.error = on_core_error,
};
static void
registry_event_global(void *data,
uint32_t id,
uint32_t permissions,
const char *type,
uint32_t version,
const struct spa_dict *props)
{
g_assert(data);
MESSENGER_Application *app = (MESSENGER_Application*) data;
if (!props)
return;
struct pw_properties *properties = pw_properties_new_dict(props);
if (!properties)
return;
size_t size = pw_map_get_size(&(app->pw.globals));
while (id > size)
pw_map_insert_at(&(app->pw.globals), size++, NULL);
pw_map_insert_at(&(app->pw.globals), id, properties);
app->pw.pending = pw_core_sync(app->pw.core, 0, 0);
}
static void
registry_event_global_remove(void *data,
uint32_t id)
{
g_assert(data);
MESSENGER_Application *app = (MESSENGER_Application*) data;
struct pw_properties *properties = pw_map_lookup(&(app->pw.globals), id);
if (!properties)
return;
pw_map_insert_at(&(app->pw.globals), id, NULL);
pw_properties_free(properties);
}
static const struct pw_registry_events registry_events = {
PW_VERSION_REGISTRY_EVENTS,
.global = registry_event_global,
.global_remove = registry_event_global_remove,
};
void
application_init(MESSENGER_Application *app,
int argc,
char **argv)
{
g_assert((app) && (argv));
memset(app, 0, sizeof(*app));
app->argc = argc;
app->argv = argv;
pw_init(&argc, &argv);
gst_init(&argc, &argv);
gtk_init(&argc, &argv);
hdy_init();
app->application = gtk_application_new(
MESSENGER_APPLICATION_ID,
G_APPLICATION_HANDLES_OPEN |
G_APPLICATION_NON_UNIQUE
);
resources_register();
#ifndef MESSENGER_APPLICATION_NO_PORTAL
GError *error = NULL;
app->portal = xdp_portal_initable_new(&error);
if (error)
{
g_printerr("ERROR: %s\n", error->message);
g_error_free(error);
}
#endif
notify_init(MESSENGER_APPLICATION_NAME);
app->notifications = NULL;
app->requests = NULL;
_load_ui_stylesheets(app);
app->chat.status = EXIT_FAILURE;
app->chat.tid = 0;
pipe(app->chat.pipe);
pthread_mutex_init(&(app->chat.mutex), NULL);
app->quarks.widget = g_quark_from_string("messenger_widget");
app->quarks.data = g_quark_from_string("messenger_data");
app->quarks.ui = g_quark_from_string("messenger_ui");
app->pw.main_loop = pw_main_loop_new(NULL);
app->pw.loop = pw_main_loop_get_loop(app->pw.main_loop);
app->pw.context = pw_context_new(
app->pw.loop,
pw_properties_new(
PW_KEY_CORE_DAEMON,
NULL,
NULL
),
0
);
pw_context_load_module(app->pw.context, "libpipewire-module-link-factory", NULL, NULL);
app->pw.core = pw_context_connect(app->pw.context, NULL, 0);
app->pw.registry = pw_core_get_registry(app->pw.core, PW_VERSION_REGISTRY, 0);
pw_map_init(&(app->pw.globals), 64, 16);
pw_core_add_listener(
app->pw.core,
&(app->pw.core_listener),
&remote_core_events,
app
);
pw_registry_add_listener(
app->pw.registry,
&(app->pw.registry_listener),
®istry_events,
app
);
app->pw.pending = pw_core_sync(app->pw.core, 0, 0);
pw_main_loop_run(app->pw.main_loop);
g_application_add_main_option(
G_APPLICATION(app->application),
"mobile",
'm',
G_OPTION_FLAG_NONE,
G_OPTION_ARG_NONE,
"Optimize UI spacing for mobile devices",
NULL
);
g_application_add_main_option(
G_APPLICATION(app->application),
"ego",
'e',
G_OPTION_FLAG_NONE,
G_OPTION_ARG_STRING,
"Identity to select for messaging",
"IDENTITY"
);
g_signal_connect(
app->application,
"activate",
G_CALLBACK(_application_activate),
app
);
g_signal_connect(
app->application,
"open",
G_CALLBACK(_application_open),
app
);
}
const gchar*
application_get_resource_path(MESSENGER_Application *app,
const char *path)
{
g_assert((app) && (path));
static gchar resource_path [PATH_MAX];
const gchar *base_path = g_application_get_resource_base_path(
G_APPLICATION(app->application)
);
snprintf(resource_path, PATH_MAX, "%s/%s", base_path, path);
return resource_path;
}
static void*
_application_chat_thread(void *args)
{
g_assert(args);
MESSENGER_Application *app = (MESSENGER_Application*) args;
struct GNUNET_GETOPT_CommandLineOption options[] = {
GNUNET_GETOPT_option_flag (
'm',
"mobile",
"Optimize UI spacing for mobile devices",
&(app->settings.mobile_design)
),
GNUNET_GETOPT_option_string (
'e',
"ego",
"IDENTITY",
"Identity to select for messaging",
&(app->chat.identity)
),
GNUNET_GETOPT_OPTION_END
};
app->chat.status = (GNUNET_PROGRAM_run(
app->argc,
app->argv,
MESSENGER_APPLICATION_BINARY,
gettext_noop(MESSENGER_APPLICATION_DESCRIPTION),
options,
&chat_messenger_run,
app
) == GNUNET_OK? EXIT_SUCCESS : EXIT_FAILURE);
return NULL;
}
void
application_run(MESSENGER_Application *app)
{
g_assert(app);
// Start thread to run GNUnet scheduler
pthread_create(
&(app->chat.tid),
NULL,
_application_chat_thread,
app
);
app->ui.status = g_application_run(
G_APPLICATION(app->application),
app->argc,
app->argv
);
if (app->ui.status != 0)
application_exit(app, MESSENGER_FAIL);
// Wait for other thread to stop properly
pthread_join(app->chat.tid, NULL);
close(app->chat.pipe[0]);
close(app->chat.pipe[1]);
pthread_mutex_destroy(&(app->chat.mutex));
GList *list;
// Get rid of open requests
list = app->requests;
while (list)
{
if (list->data)
{
request_cancel((MESSENGER_Request*) list->data);
request_delete((MESSENGER_Request*) list->data);
}
list = list->next;
}
// Get rid of open notifications
list = app->notifications;
while (list)
{
if (list->data)
notify_notification_close(NOTIFY_NOTIFICATION(list->data), NULL);
list = list->next;
}
if (app->requests)
g_list_free(app->requests);
if (app->notifications)
g_list_free(app->notifications);
notify_uninit();
resources_unregister();
g_object_unref(app->application);
}
static void
_request_background_callback(MESSENGER_Application *app,
gboolean success,
gboolean error,
gpointer user_data)
{
g_assert((app) && (user_data));
gboolean *setting = (gboolean*) user_data;
*setting = success;
}
void
application_show_window(MESSENGER_Application *app)
{
g_assert(app);
gtk_widget_show(GTK_WIDGET(app->ui.messenger.main_window));
request_new_background(
app,
XDP_BACKGROUND_FLAG_AUTOSTART,
_request_background_callback,
&(app->settings.autostart)
);
request_new_background(
app,
XDP_BACKGROUND_FLAG_ACTIVATABLE,
_request_background_callback,
&(app->settings.background_task)
);
}
typedef struct MESSENGER_ApplicationEventCall
{
MESSENGER_Application *app;
MESSENGER_ApplicationEvent event;
} MESSENGER_ApplicationEventCall;
static gboolean
_application_event_call(gpointer user_data)
{
g_assert(user_data);
MESSENGER_ApplicationEventCall *call;
call = (MESSENGER_ApplicationEventCall*) user_data;
// Locking the mutex for synchronization
pthread_mutex_lock(&(call->app->chat.mutex));
call->event(call->app);
pthread_mutex_unlock(&(call->app->chat.mutex));
GNUNET_free(call);
return FALSE;
}
void
application_call_event(MESSENGER_Application *app,
MESSENGER_ApplicationEvent event)
{
g_assert((app) && (event));
MESSENGER_ApplicationEventCall *call;
call = (MESSENGER_ApplicationEventCall*) GNUNET_malloc(
sizeof(MESSENGER_ApplicationEventCall)
);
call->app = app;
call->event = event;
g_timeout_add(0, G_SOURCE_FUNC(_application_event_call), call);
}
typedef struct MESSENGER_ApplicationMessageEventCall
{
MESSENGER_Application *app;
MESSENGER_ApplicationMessageEvent event;
struct GNUNET_CHAT_Context *context;
const struct GNUNET_CHAT_Message *message;
} MESSENGER_ApplicationMessageEventCall;
static gboolean
_application_message_event_call(gpointer user_data)
{
g_assert(user_data);
MESSENGER_ApplicationMessageEventCall *call;
call = (MESSENGER_ApplicationMessageEventCall*) user_data;
// Locking the mutex for synchronization
pthread_mutex_lock(&(call->app->chat.mutex));
call->event(call->app, call->context, call->message);
pthread_mutex_unlock(&(call->app->chat.mutex));
GNUNET_free(call);
return FALSE;
}
void
application_call_message_event(MESSENGER_Application *app,
MESSENGER_ApplicationMessageEvent event,
struct GNUNET_CHAT_Context *context,
const struct GNUNET_CHAT_Message *message)
{
g_assert((app) && (event) && (message));
MESSENGER_ApplicationMessageEventCall *call;
call = (MESSENGER_ApplicationMessageEventCall*) GNUNET_malloc(
sizeof(MESSENGER_ApplicationMessageEventCall)
);
call->app = app;
call->event = event;
call->context = context;
call->message = message;
g_idle_add(G_SOURCE_FUNC(_application_message_event_call), call);
}
static int
destroy_global(void *obj,
UNUSED void *data)
{
struct pw_properties *properties = (struct pw_properties*) obj;
if (!properties)
return 0;
pw_properties_free(properties);
return 0;
}
void
application_exit(MESSENGER_Application *app,
MESSENGER_ApplicationSignal signal)
{
g_assert(app);
// Forward a signal to the other thread causing it to shutdown the
// GNUnet handles of the application.
write(app->chat.pipe[1], &signal, sizeof(signal));
#ifndef MESSENGER_APPLICATION_NO_PORTAL
if (app->portal)
g_object_unref(app->portal);
app->portal = NULL;
#endif
if (app->pw.registry)
pw_proxy_destroy((struct pw_proxy*) app->pw.registry);
if (app->pw.core)
{
pw_map_for_each(&(app->pw.globals), destroy_global, NULL);
pw_map_clear(&(app->pw.globals));
pw_core_disconnect(app->pw.core);
}
if (app->pw.context)
pw_context_destroy(app->pw.context);
if (app->pw.main_loop)
{
pw_main_loop_quit(app->pw.main_loop);
pw_main_loop_destroy(app->pw.main_loop);
}
gst_deinit();
pw_deinit();
}
int
application_status(MESSENGER_Application *app)
{
g_assert(app);
if (EXIT_SUCCESS != app->chat.status)
return app->chat.status;
return app->ui.status;
}