Files
zoitechat/src/fe-gtk/servlistgui.c
T
2026-09-30 13:34:46 -06:00

5769 lines
180 KiB
C

/* X-Chat
* Copyright (C) 2004-2008 Peter Zelezny.
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <sys/stat.h>
#include <gdk/gdkkeysyms.h>
#include <gio/gio.h>
#include "../common/zoitechat.h"
#include "../common/zoitechatc.h"
#include "../common/servlist.h"
#include "../common/cfgfiles.h"
#include "../common/fe.h"
#include "../common/secretstore.h"
#include "../common/server.h"
#include "../common/util.h"
#include "fe-gtk.h"
#include "gtkutil.h"
#include "menu.h"
#include "pixmaps.h"
#include "fkeys.h"
#include "theme/theme-manager.h"
#define SERVLIST_X_PADDING 4 /* horizontal paddig in the network editor */
#define SERVLIST_Y_PADDING 0 /* vertical padding in the network editor */
#define ICON_SERVLIST_CONNECT "zc-menu-connect"
#define ICON_SERVLIST_ADD "list-add"
#define ICON_SERVLIST_REMOVE "list-remove"
#define ICON_SERVLIST_CLOSE "gtk-close"
#define ICON_SERVLIST_ERROR "dialog-error"
#ifdef USE_OPENSSL
# define DEFAULT_SERVER "newserver/6697"
#else
# define DEFAULT_SERVER "newserver/6667"
#endif
/* servlistgui.c globals */
static GtkWidget *serverlist_win = NULL;
static GtkWidget *networks_tree; /* network TreeView */
static int netlist_win_width = 0; /* don't hardcode pixels, just use as much as needed by default, save if resized */
static int netlist_win_height = 0;
static int netedit_win_width = 0;
static int netedit_win_height = 0;
static int netedit_active_tab = 0;
/* global user info */
static GtkWidget *entry_nick1;
static GtkWidget *entry_nick2;
static GtkWidget *entry_nick3;
static GtkWidget *entry_guser;
/* static GtkWidget *entry_greal; */
enum {
SERVER_TREE,
CHANNEL_TREE,
CMD_TREE,
N_TREES,
};
/* edit area */
static GtkWidget *edit_win;
static GtkWidget *edit_entry_nick;
static GtkWidget *edit_entry_nick2;
static GtkWidget *edit_entry_user;
static GtkWidget *edit_entry_real;
static GtkWidget *edit_entry_pass;
static GtkWidget *edit_check_show_pass;
static GtkWidget *edit_check_use_keyring;
static GtkWidget *edit_check_ask_pass;
static GtkWidget *edit_button_encrypt_pass;
static GtkWidget *edit_button_import_pass;
static int edit_pass_changed;
static char *edit_loaded_password;
static GtkWidget *edit_label_nick;
static GtkWidget *edit_label_nick2;
static GtkWidget *edit_label_real;
static GtkWidget *edit_label_user;
static GtkWidget *edit_trees[N_TREES];
static GtkWidget *edit_button_cert_generate;
static GtkWidget *edit_button_cert_import;
static GtkWidget *edit_button_cert_info;
static GtkWidget *edit_button_cert_delete;
static ircnet *selected_net = NULL;
static ircserver *selected_serv = NULL;
static commandentry *selected_cmd = NULL;
static favchannel *selected_chan = NULL;
static session *servlist_sess;
static void servlist_network_row_cb (GtkTreeSelection *sel, gpointer user_data);
static GtkWidget *servlist_open_edit (GtkWidget *parent, ircnet *net);
static void servlist_password_changed_cb (GtkEditable *editable, gpointer userdata);
static void
servlist_update_password_tools (ircnet *net)
{
gboolean has_local;
gboolean use_keyring;
if (!edit_button_encrypt_pass || !edit_button_import_pass)
return;
use_keyring = net && (net->flags & FLAG_USE_KEYRING);
if ((net && (net->flags & FLAG_PROMPT_PASSWORD)) ||
(edit_check_ask_pass && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass))))
use_keyring = FALSE;
has_local = net && net->pass && *net->pass && !use_keyring && !edit_pass_changed &&
(!edit_check_ask_pass || !gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass)));
gtk_widget_set_sensitive (edit_button_encrypt_pass, has_local && !servlist_password_is_encrypted (net->pass));
gtk_widget_set_sensitive (edit_button_import_pass, has_local);
}
static void
servlist_entry_set_text_silent (GtkWidget *entry, const char *text)
{
g_signal_handlers_block_by_func (G_OBJECT (entry), G_CALLBACK (servlist_password_changed_cb), NULL);
gtk_entry_set_text (GTK_ENTRY (entry), text);
g_signal_handlers_unblock_by_func (G_OBJECT (entry), G_CALLBACK (servlist_password_changed_cb), NULL);
}
static void
servlist_toggle_ask_pass_cb (GtkToggleButton *toggle, gpointer userdata)
{
gboolean active = gtk_toggle_button_get_active (toggle);
gtk_widget_set_sensitive (edit_check_use_keyring, !active);
gtk_widget_set_sensitive (edit_entry_pass, !active && (!selected_net || selected_net->logintype != LOGIN_SASLEXTERNAL));
gtk_widget_set_sensitive (edit_check_show_pass, !active);
servlist_update_password_tools (selected_net);
}
static char *
servlist_display_password (ircnet *net)
{
if (!net)
return NULL;
if (edit_pass_changed)
return g_strdup (gtk_entry_get_text (GTK_ENTRY (edit_entry_pass)));
if (edit_loaded_password)
return g_strdup (edit_loaded_password);
if (net->flags & FLAG_USE_KEYRING)
return secretstore_get_network_password (net->name);
return servlist_password_decrypt_for_storage (net->pass);
}
static void
servlist_toggle_show_password_cb (GtkToggleButton *toggle, gpointer userdata)
{
if (gtk_toggle_button_get_active (toggle))
{
char *password = servlist_display_password (selected_net);
if (password)
{
if (edit_loaded_password)
{
memset (edit_loaded_password, 0, strlen (edit_loaded_password));
g_free (edit_loaded_password);
}
edit_loaded_password = g_strdup (password);
servlist_entry_set_text_silent (userdata, password);
memset (password, 0, strlen (password));
g_free (password);
}
gtk_entry_set_visibility (GTK_ENTRY (userdata), TRUE);
}
else
{
gtk_entry_set_visibility (GTK_ENTRY (userdata), FALSE);
if (edit_loaded_password && !edit_pass_changed)
servlist_entry_set_text_silent (userdata, "***");
}
}
static void
servlist_toggle_keyring_cb (GtkToggleButton *toggle, gpointer userdata)
{
servlist_update_password_tools (selected_net);
}
static void
servlist_password_changed_cb (GtkEditable *editable, gpointer userdata)
{
edit_pass_changed = 1;
if (edit_loaded_password && strcmp (gtk_entry_get_text (GTK_ENTRY (editable)), "***"))
{
memset (edit_loaded_password, 0, strlen (edit_loaded_password));
g_free (edit_loaded_password);
edit_loaded_password = NULL;
}
servlist_update_password_tools (selected_net);
}
static void
servlist_encrypt_password_cb (GtkWidget *button, gpointer userdata)
{
ircnet *net = userdata;
char *plain;
char *enc;
if (!net || (net->flags & FLAG_USE_KEYRING) || !net->pass || servlist_password_is_encrypted (net->pass))
return;
plain = servlist_password_decrypt_for_storage (net->pass);
if (!plain || !*plain)
{
if (plain)
{
memset (plain, 0, strlen (plain));
g_free (plain);
}
return;
}
enc = servlist_password_encrypt_for_storage (plain);
memset (plain, 0, strlen (plain));
g_free (plain);
if (!enc)
{
fe_message (_("Could not encrypt this password."), FE_MSG_WARN);
return;
}
g_free (net->pass);
net->pass = enc;
servlist_save ();
servlist_update_password_tools (net);
}
static void
servlist_import_password_cb (GtkWidget *button, gpointer userdata)
{
ircnet *net = userdata;
char *plain;
if (!net || !net->name || (net->flags & FLAG_USE_KEYRING) || !net->pass || !*net->pass)
return;
plain = servlist_password_decrypt_for_storage (net->pass);
if (!plain || !*plain)
{
if (plain)
{
memset (plain, 0, strlen (plain));
g_free (plain);
}
return;
}
if (!secretstore_set_network_password (net->name, plain))
{
memset (plain, 0, strlen (plain));
g_free (plain);
fe_message (_("Could not move this password into the system keyring."), FE_MSG_WARN);
return;
}
memset (plain, 0, strlen (plain));
g_free (plain);
g_free (net->pass);
net->pass = NULL;
net->flags |= FLAG_USE_KEYRING;
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring), TRUE);
servlist_entry_set_text_silent (edit_entry_pass, "***");
edit_pass_changed = 0;
servlist_save ();
servlist_update_password_tools (net);
}
static char *
servlist_get_cert_file (ircnet *net)
{
if (!net || !net->name || !net->name[0])
return NULL;
return g_strdup_printf ("%s" G_DIR_SEPARATOR_S "certs" G_DIR_SEPARATOR_S "%s.pem",
get_xdir (), net->name);
}
static gboolean
servlist_network_cert_exists (ircnet *net)
{
char *cert_file;
gboolean exists;
cert_file = servlist_get_cert_file (net);
if (!cert_file)
return FALSE;
exists = g_file_test (cert_file, G_FILE_TEST_IS_REGULAR);
g_free (cert_file);
return exists;
}
static void
servlist_update_cert_buttons (ircnet *net)
{
gboolean has_cert = servlist_network_cert_exists (net);
if (edit_button_cert_generate)
gtk_widget_set_visible (edit_button_cert_generate, !has_cert);
if (edit_button_cert_import)
gtk_widget_set_visible (edit_button_cert_import, !has_cert);
if (edit_button_cert_info)
gtk_widget_set_visible (edit_button_cert_info, has_cert);
if (edit_button_cert_delete)
gtk_widget_set_visible (edit_button_cert_delete, has_cert);
}
static void
servlist_import_client_cert_cb (GtkWidget *button, gpointer userdata)
{
ircnet *net = (ircnet *)userdata;
GtkWidget *dialog;
GtkWidget *message;
GtkFileFilter *filter;
char *cert_dir;
char *cert_file;
char *source_file;
char *contents;
gsize length;
if (!net || !net->name || !net->name[0])
return;
dialog = gtk_file_chooser_dialog_new (_("Import Client Certificate"),
GTK_WINDOW (edit_win),
GTK_FILE_CHOOSER_ACTION_OPEN,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_Open"), GTK_RESPONSE_ACCEPT,
NULL);
filter = gtk_file_filter_new ();
gtk_file_filter_set_name (filter, _("Certificate files"));
gtk_file_filter_add_pattern (filter, "*.pem");
gtk_file_filter_add_pattern (filter, "*.crt");
gtk_file_filter_add_pattern (filter, "*.cer");
gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
filter = gtk_file_filter_new ();
gtk_file_filter_set_name (filter, _("All files"));
gtk_file_filter_add_pattern (filter, "*");
gtk_file_chooser_add_filter (GTK_FILE_CHOOSER (dialog), filter);
theme_manager_attach_window (dialog);
if (gtk_dialog_run (GTK_DIALOG (dialog)) != GTK_RESPONSE_ACCEPT)
{
gtk_widget_destroy (dialog);
return;
}
source_file = gtk_file_chooser_get_filename (GTK_FILE_CHOOSER (dialog));
gtk_widget_destroy (dialog);
if (!source_file)
return;
cert_dir = g_build_filename (get_xdir (), "certs", NULL);
cert_file = servlist_get_cert_file (net);
contents = NULL;
length = 0;
if (cert_file &&
g_mkdir_with_parents (cert_dir, 0700) == 0 &&
g_file_get_contents (source_file, &contents, &length, NULL) &&
g_file_set_contents (cert_file, contents, length, NULL))
{
chmod (cert_file, 0600);
servlist_update_cert_buttons (net);
message = gtk_message_dialog_new (GTK_WINDOW (edit_win),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO,
GTK_BUTTONS_CLOSE,
_("Client certificate imported for \"%s\"."),
net->name);
}
else
{
message = gtk_message_dialog_new (GTK_WINDOW (edit_win),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
_("Failed to import client certificate for \"%s\"."),
net->name);
}
theme_manager_attach_window (message);
g_signal_connect_swapped (message, "response", G_CALLBACK (gtk_widget_destroy), message);
gtk_widget_show (message);
g_free (contents);
g_free (cert_file);
g_free (cert_dir);
g_free (source_file);
}
static void
servlist_generate_client_cert_cb (GtkWidget *button, gpointer userdata)
{
#ifdef USE_OPENSSL
ircnet *net = (ircnet *)userdata;
GtkWidget *dialog;
char *cert_dir;
char *cert_file;
char *key_file;
char *crt_file;
char *subject;
char *openssl_conf;
const char *conf_data;
char *key_data;
char *crt_data;
char *pem_data;
char *stderr_data;
char *stdout_data;
gsize key_len;
gsize crt_len;
gboolean spawned;
gboolean success;
gint status;
char *argv[20];
char **envp;
if (!net || !net->name || !net->name[0])
return;
cert_dir = g_build_filename (get_xdir (), "certs", NULL);
cert_file = servlist_get_cert_file (net);
key_file = g_strdup_printf ("%s" G_DIR_SEPARATOR_S "%s.key", cert_dir, net->name);
crt_file = g_strdup_printf ("%s" G_DIR_SEPARATOR_S "%s.crt", cert_dir, net->name);
subject = g_strdup_printf ("/CN=%s", net->name);
openssl_conf = g_build_filename (cert_dir, "openssl.cnf", NULL);
conf_data = "[req]\n"
"distinguished_name=req_distinguished_name\n"
"[req_distinguished_name]\n";
key_data = NULL;
crt_data = NULL;
pem_data = NULL;
stderr_data = NULL;
stdout_data = NULL;
key_len = 0;
crt_len = 0;
success = FALSE;
status = 0;
envp = g_environ_unsetenv (g_get_environ (), "LD_LIBRARY_PATH");
if (g_mkdir_with_parents (cert_dir, 0700) == 0 &&
g_file_set_contents (openssl_conf, conf_data, -1, NULL))
{
argv[0] = "openssl";
argv[1] = "req";
argv[2] = "-x509";
argv[3] = "-newkey";
argv[4] = "ec";
argv[5] = "-pkeyopt";
argv[6] = "ec_paramgen_curve:P-256";
argv[7] = "-sha256";
argv[8] = "-days";
argv[9] = "3650";
argv[10] = "-nodes";
argv[11] = "-keyout";
argv[12] = key_file;
argv[13] = "-out";
argv[14] = crt_file;
argv[15] = "-config";
argv[16] = openssl_conf;
argv[17] = "-subj";
argv[18] = subject;
argv[19] = NULL;
spawned = g_spawn_sync (NULL, argv, envp, G_SPAWN_SEARCH_PATH, NULL, NULL,
&stdout_data, &stderr_data, &status, NULL);
if (spawned && g_spawn_check_exit_status (status, NULL) &&
g_file_get_contents (key_file, &key_data, &key_len, NULL) &&
g_file_get_contents (crt_file, &crt_data, &crt_len, NULL))
{
pem_data = g_strconcat (key_data, crt_data, NULL);
if (pem_data && g_file_set_contents (cert_file, pem_data, -1, NULL))
{
chmod (cert_file, 0600);
success = TRUE;
}
}
}
g_remove (key_file);
g_remove (crt_file);
g_remove (openssl_conf);
if (success)
{
servlist_update_cert_buttons (net);
dialog = gtk_message_dialog_new (GTK_WINDOW (edit_win),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO,
GTK_BUTTONS_CLOSE,
_("Client certificate generated for \"%s\"."),
net->name);
}
else
{
dialog = gtk_message_dialog_new (GTK_WINDOW (edit_win),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
_("Failed to generate the client certificate for \"%s\"."),
net->name);
if (stderr_data && stderr_data[0])
gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", stderr_data);
}
theme_manager_attach_window (dialog);
g_signal_connect_swapped (dialog, "response", G_CALLBACK (gtk_widget_destroy), dialog);
gtk_widget_show (dialog);
g_free (stdout_data);
g_free (stderr_data);
g_free (pem_data);
g_free (key_data);
g_free (crt_data);
g_free (subject);
g_free (crt_file);
g_free (key_file);
g_free (openssl_conf);
g_free (cert_file);
g_free (cert_dir);
g_strfreev (envp);
#else
return;
#endif
}
static void
servlist_cert_info_cb (GtkWidget *button, gpointer userdata)
{
#ifdef USE_OPENSSL
ircnet *net = (ircnet *)userdata;
GtkWidget *dialog;
char *cert_file;
char *stdout_data;
char *stderr_data;
gboolean spawned;
gint status;
char *argv[12];
char **envp;
cert_file = servlist_get_cert_file (net);
if (!cert_file)
return;
stdout_data = NULL;
stderr_data = NULL;
status = 0;
envp = g_environ_unsetenv (g_get_environ (), "LD_LIBRARY_PATH");
argv[0] = "openssl";
argv[1] = "x509";
argv[2] = "-in";
argv[3] = cert_file;
argv[4] = "-noout";
argv[5] = "-subject";
argv[6] = "-issuer";
argv[7] = "-startdate";
argv[8] = "-enddate";
argv[9] = "-fingerprint";
argv[10] = "-sha256";
argv[11] = NULL;
spawned = g_spawn_sync (NULL, argv, envp, G_SPAWN_SEARCH_PATH, NULL, NULL,
&stdout_data, &stderr_data, &status, NULL);
if (spawned && g_spawn_check_exit_status (status, NULL) && stdout_data && stdout_data[0])
{
dialog = gtk_message_dialog_new (GTK_WINDOW (edit_win),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO,
GTK_BUTTONS_CLOSE,
_("Client certificate information for \"%s\"."),
net->name);
gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", stdout_data);
}
else
{
dialog = gtk_message_dialog_new (GTK_WINDOW (edit_win),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
_("Failed to read client certificate information for \"%s\"."),
net->name);
if (stderr_data && stderr_data[0])
gtk_message_dialog_format_secondary_text (GTK_MESSAGE_DIALOG (dialog), "%s", stderr_data);
}
theme_manager_attach_window (dialog);
g_signal_connect_swapped (dialog, "response", G_CALLBACK (gtk_widget_destroy), dialog);
gtk_widget_show (dialog);
g_free (stdout_data);
g_free (stderr_data);
g_free (cert_file);
g_strfreev (envp);
#else
return;
#endif
}
static void
servlist_delete_client_cert_cb (GtkWidget *button, gpointer userdata)
{
ircnet *net = (ircnet *)userdata;
GtkWidget *dialog;
char *cert_file;
cert_file = servlist_get_cert_file (net);
if (!cert_file)
return;
if (g_remove (cert_file) == 0)
{
servlist_update_cert_buttons (net);
dialog = gtk_message_dialog_new (GTK_WINDOW (edit_win),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
GTK_MESSAGE_INFO,
GTK_BUTTONS_CLOSE,
_("Client certificate removed for \"%s\"."),
net->name);
}
else
{
dialog = gtk_message_dialog_new (GTK_WINDOW (edit_win),
GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL,
GTK_MESSAGE_ERROR,
GTK_BUTTONS_CLOSE,
_("Failed to remove client certificate for \"%s\"."),
net->name);
}
theme_manager_attach_window (dialog);
g_signal_connect_swapped (dialog, "response", G_CALLBACK (gtk_widget_destroy), dialog);
gtk_widget_show (dialog);
g_free (cert_file);
}
static GtkWidget *
servlist_icon_button_new (const char *label, const char *icon_name)
{
GtkWidget *button;
(void)icon_name;
button = gtk_button_new_with_mnemonic (label);
return button;
}
static const char *pages[]=
{
IRC_DEFAULT_CHARSET,
"CP1252 (Windows-1252)",
"ISO-8859-15 (Western Europe)",
"ISO-8859-2 (Central Europe)",
"ISO-8859-7 (Greek)",
"ISO-8859-8 (Hebrew)",
"ISO-8859-9 (Turkish)",
"ISO-2022-JP (Japanese)",
"SJIS (Japanese)",
"CP949 (Korean)",
"KOI8-R (Cyrillic)",
"CP1251 (Cyrillic)",
"CP1256 (Arabic)",
"CP1257 (Baltic)",
"GB18030 (Chinese)",
"TIS-620 (Thai)",
NULL
};
/* This is our dictionary for authentication types. Keep these in sync with
* login_types[]! This allows us to re-order the login type dropdown in the
* network list without breaking config compatibility.
*
* Also make sure inbound_nickserv_login() won't break, i.e. if you add a new
* type that is NickServ-based, add it there as well so that ZoiteChat knows to
* treat it as such.
*/
static int login_types_conf[] =
{
LOGIN_DEFAULT, /* default entry - we don't use this but it makes indexing consistent with login_types[] so it's nice */
LOGIN_SASL,
#ifdef USE_OPENSSL
LOGIN_SASLEXTERNAL,
LOGIN_SASL_SCRAM_SHA_1,
LOGIN_SASL_SCRAM_SHA_256,
LOGIN_SASL_SCRAM_SHA_512,
#endif
LOGIN_PASS,
LOGIN_MSG_NICKSERV,
LOGIN_NICKSERV,
#ifdef USE_OPENSSL
LOGIN_CHALLENGEAUTH,
#endif
LOGIN_CUSTOM
#if 0
LOGIN_NS,
LOGIN_MSG_NS,
LOGIN_AUTH,
#endif
};
static const char *login_types[]=
{
"Default",
"SASL PLAIN (username + password)",
#ifdef USE_OPENSSL
"SASL EXTERNAL (cert)",
"SASL SCRAM-SHA-1",
"SASL SCRAM-SHA-256",
"SASL SCRAM-SHA-512",
#endif
"Server password (/PASS password)",
"NickServ (/MSG NickServ + password)",
"NickServ (/NICKSERV + password)",
#ifdef USE_OPENSSL
"Challenge Auth (username + password)",
#endif
"Custom... (connect commands)",
#if 0
"NickServ (/NS + password)",
"NickServ (/MSG NS + password)",
"AUTH (/AUTH nickname password)",
#endif
NULL
};
/* poor man's IndexOf() - find the dropdown string index that belongs to the given config value */
static int
servlist_get_login_desc_index (int conf_value)
{
int i;
int length = sizeof (login_types_conf) / sizeof (login_types_conf[0]); /* the number of elements in the conf array */
for (i = 0; i < length; i++)
{
if (login_types_conf[i] == conf_value)
{
return i;
}
}
return 0; /* make the compiler happy */
}
static void
servlist_select_and_show (GtkTreeView *treeview, GtkTreeIter *iter,
GtkListStore *store)
{
GtkTreePath *path;
GtkTreeSelection *sel;
sel = gtk_tree_view_get_selection (treeview);
/* select this network */
gtk_tree_selection_select_iter (sel, iter);
/* and make sure it's visible */
path = gtk_tree_model_get_path (GTK_TREE_MODEL (store), iter);
if (path)
{
gtk_tree_view_scroll_to_cell (treeview, path, NULL, TRUE, 0.5, 0.5);
gtk_tree_view_set_cursor (treeview, path, NULL, FALSE);
gtk_tree_path_free (path);
}
}
static void
servlist_channels_populate (ircnet *net, GtkWidget *treeview)
{
GtkListStore *store;
GtkTreeIter iter;
int i;
favchannel *favchan;
GSList *list = net->favchanlist;
store = (GtkListStore *)gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
gtk_list_store_clear (store);
i = 0;
while (list)
{
favchan = list->data;
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter, 0, favchan->name, 1, favchan->key, 2, TRUE, -1);
if (net->selected == i)
{
/* select this server */
servlist_select_and_show (GTK_TREE_VIEW (treeview), &iter, store);
}
i++;
list = list->next;
}
}
static void
servlist_servers_populate (ircnet *net, GtkWidget *treeview)
{
GtkListStore *store;
GtkTreeIter iter;
int i;
ircserver *serv;
GSList *list = net->servlist;
store = (GtkListStore *)gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
gtk_list_store_clear (store);
i = 0;
while (list)
{
serv = list->data;
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter, 0, serv->hostname, 1, 1, -1);
if (net->selected == i)
{
/* select this server */
servlist_select_and_show (GTK_TREE_VIEW (treeview), &iter, store);
}
i++;
list = list->next;
}
}
static void
servlist_commands_populate (ircnet *net, GtkWidget *treeview)
{
GtkListStore *store;
GtkTreeIter iter;
int i;
commandentry *entry;
GSList *list = net->commandlist;
store = (GtkListStore *)gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
gtk_list_store_clear (store);
i = 0;
while (list)
{
entry = list->data;
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter, 0, entry->command, 1, 1, -1);
if (net->selected == i)
{
/* select this server */
servlist_select_and_show (GTK_TREE_VIEW (treeview), &iter, store);
}
i++;
list = list->next;
}
}
static void
servlist_networks_populate_ (GtkWidget *treeview, GSList *netlist, gboolean favorites)
{
GtkListStore *store;
GtkTreeIter iter;
int i;
ircnet *net;
if (!netlist)
{
net = servlist_net_add (_("New Network"), "", FALSE);
servlist_server_add (net, DEFAULT_SERVER);
netlist = network_list;
}
store = (GtkListStore *)gtk_tree_view_get_model (GTK_TREE_VIEW (treeview));
gtk_list_store_clear (store);
i = 0;
while (netlist)
{
net = netlist->data;
if (!favorites || (net->flags & FLAG_FAVORITE))
{
if (favorites)
gtk_list_store_insert_with_values (store, &iter, 0x7fffffff, 0, net->name, 1, 1, 2, 400, -1);
else
gtk_list_store_insert_with_values (store, &iter, 0x7fffffff, 0, net->name, 1, 1, 2, (net->flags & FLAG_FAVORITE) ? 800 : 400, -1);
if (i == prefs.hex_gui_slist_select)
{
/* select this network */
servlist_select_and_show (GTK_TREE_VIEW (treeview), &iter, store);
selected_net = net;
}
}
i++;
netlist = netlist->next;
}
}
static void
servlist_networks_populate (GtkWidget *treeview, GSList *netlist)
{
servlist_networks_populate_ (treeview, netlist, prefs.hex_gui_slist_fav);
}
static void
servlist_server_row_cb (GtkTreeSelection *sel, gpointer user_data)
{
GtkTreeModel *model;
GtkTreeIter iter;
ircserver *serv;
char *servname;
int pos;
if (!selected_net)
return;
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_tree_model_get (model, &iter, 0, &servname, -1);
serv = servlist_server_find (selected_net, servname, &pos);
g_free (servname);
if (serv)
selected_net->selected = pos;
selected_serv = serv;
}
}
static void
servlist_command_row_cb (GtkTreeSelection *sel, gpointer user_data)
{
GtkTreeModel *model;
GtkTreeIter iter;
commandentry *cmd;
char *cmdname;
int pos;
if (!selected_net)
return;
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_tree_model_get (model, &iter, 0, &cmdname, -1);
cmd = servlist_command_find (selected_net, cmdname, &pos);
g_free (cmdname);
if (cmd)
selected_net->selected = pos;
selected_cmd = cmd;
}
}
static void
servlist_channel_row_cb (GtkTreeSelection *sel, gpointer user_data)
{
GtkTreeModel *model;
GtkTreeIter iter;
favchannel *channel;
char *channame;
int pos;
if (!selected_net)
return;
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_tree_model_get (model, &iter, 0, &channame, -1);
channel = servlist_favchan_find (selected_net, channame, &pos);
g_free (channame);
if (channel)
selected_net->selected = pos;
selected_chan = channel;
}
}
static void
servlist_start_editing (GtkTreeView *tree)
{
GtkTreeSelection *sel;
GtkTreeModel *model;
GtkTreeIter iter;
GtkTreePath *path;
sel = gtk_tree_view_get_selection (tree);
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
path = gtk_tree_model_get_path (GTK_TREE_MODEL (model), &iter);
if (path)
{
gtk_tree_view_set_cursor (tree, path,
gtk_tree_view_get_column (tree, 0), TRUE);
gtk_tree_path_free (path);
}
}
}
static void
servlist_addserver (void)
{
GtkTreeIter iter;
GtkListStore *store;
if (!selected_net)
return;
store = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (edit_trees[SERVER_TREE])));
servlist_server_add (selected_net, DEFAULT_SERVER);
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter, 0, DEFAULT_SERVER, 1, TRUE, -1);
/* select this server */
servlist_select_and_show (GTK_TREE_VIEW (edit_trees[SERVER_TREE]), &iter, store);
servlist_start_editing (GTK_TREE_VIEW (edit_trees[SERVER_TREE]));
servlist_server_row_cb (gtk_tree_view_get_selection (GTK_TREE_VIEW (networks_tree)), NULL);
}
static void
servlist_addcommand (void)
{
GtkTreeIter iter;
GtkListStore *store;
if (!selected_net)
return;
store = GTK_LIST_STORE (gtk_tree_view_get_model (GTK_TREE_VIEW (edit_trees[CMD_TREE])));
servlist_command_add (selected_net, "ECHO hello");
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter, 0, "ECHO hello", 1, TRUE, -1);
servlist_select_and_show (GTK_TREE_VIEW (edit_trees[CMD_TREE]), &iter, store);
servlist_start_editing (GTK_TREE_VIEW (edit_trees[CMD_TREE]));
servlist_command_row_cb (gtk_tree_view_get_selection (GTK_TREE_VIEW (networks_tree)), NULL);
}
static void
servlist_addchannel (void)
{
GtkTreeIter iter;
GtkListStore *store;
if (!selected_net)
return;
store = GTK_LIST_STORE(gtk_tree_view_get_model (GTK_TREE_VIEW (edit_trees[CHANNEL_TREE])));
servlist_favchan_add (selected_net, "#channel");
gtk_list_store_append (store, &iter);
gtk_list_store_set (store, &iter, 0, "#channel", 1, "", 2, TRUE, -1);
/* select this server */
servlist_select_and_show (GTK_TREE_VIEW (edit_trees[CHANNEL_TREE]), &iter, store);
servlist_start_editing (GTK_TREE_VIEW (edit_trees[CHANNEL_TREE]));
servlist_channel_row_cb (gtk_tree_view_get_selection (GTK_TREE_VIEW (networks_tree)), NULL);
}
static void
servlist_addnet_cb (GtkWidget *item, GtkTreeView *treeview)
{
GtkTreeIter iter;
GtkListStore *store;
ircnet *net;
net = servlist_net_add (_("New Network"), "", TRUE);
net->encoding = g_strdup (IRC_DEFAULT_CHARSET);
servlist_server_add (net, DEFAULT_SERVER);
store = (GtkListStore *)gtk_tree_view_get_model (treeview);
gtk_list_store_prepend (store, &iter);
gtk_list_store_set (store, &iter, 0, net->name, 1, 1, -1);
/* select this network */
servlist_select_and_show (GTK_TREE_VIEW (networks_tree), &iter, store);
servlist_start_editing (GTK_TREE_VIEW (networks_tree));
servlist_network_row_cb (gtk_tree_view_get_selection (GTK_TREE_VIEW (networks_tree)), NULL);
}
static void
servlist_deletenetwork (ircnet *net)
{
GtkTreeSelection *sel;
GtkTreeModel *model;
GtkTreeIter iter;
/* remove from GUI */
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (networks_tree));
if (gtk_tree_selection_get_selected (sel, &model, &iter))
gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
/* remove from list */
servlist_net_remove (net);
/* force something to be selected */
gtk_tree_model_get_iter_first (model, &iter);
servlist_select_and_show (GTK_TREE_VIEW (networks_tree), &iter,
GTK_LIST_STORE (model));
servlist_network_row_cb (sel, NULL);
}
static void
servlist_deletenetdialog_cb (GtkDialog *dialog, gint arg1, ircnet *net)
{
gtk_widget_destroy (GTK_WIDGET (dialog));
if (arg1 == GTK_RESPONSE_OK)
servlist_deletenetwork (net);
}
static GSList *
servlist_move_item (GtkTreeView *view, GSList *list, gpointer item, int delta)
{
GtkTreeModel *store;
GtkTreeIter iter1, iter2;
GtkTreeSelection *sel;
GtkTreePath *path;
int pos;
/* Keep tree in sync w/ list, there has to be an easier way to get iters */
sel = gtk_tree_view_get_selection (view);
gtk_tree_selection_get_selected (sel, &store, &iter1);
path = gtk_tree_model_get_path (store, &iter1);
if (delta == 1)
gtk_tree_path_next (path);
else
gtk_tree_path_prev (path);
gtk_tree_model_get_iter (store, &iter2, path);
gtk_tree_path_free (path);
pos = g_slist_index (list, item);
if (pos >= 0)
{
pos += delta;
if (pos >= 0)
{
list = g_slist_remove (list, item);
list = g_slist_insert (list, item, pos);
gtk_list_store_swap (GTK_LIST_STORE (store), &iter1, &iter2);
}
}
return list;
}
static gboolean
servlist_net_keypress_cb (GtkWidget *wid, GdkEventKey *evt, gpointer tree)
{
gboolean handled = FALSE;
if (!selected_net || prefs.hex_gui_slist_fav)
return FALSE;
if (evt->state & STATE_SHIFT)
{
if (evt->keyval == GDK_KEY_Up)
{
handled = TRUE;
network_list = servlist_move_item (GTK_TREE_VIEW (tree), network_list, selected_net, -1);
}
else if (evt->keyval == GDK_KEY_Down)
{
handled = TRUE;
network_list = servlist_move_item (GTK_TREE_VIEW (tree), network_list, selected_net, +1);
}
}
return handled;
}
static gint
servlist_compare (ircnet *net1, ircnet *net2)
{
gchar *net1_casefolded, *net2_casefolded;
int result=0;
net1_casefolded=g_utf8_casefold(net1->name,-1),
net2_casefolded=g_utf8_casefold(net2->name,-1),
result=g_utf8_collate(net1_casefolded,net2_casefolded);
g_free(net1_casefolded);
g_free(net2_casefolded);
return result;
}
static void
servlist_sort (GtkWidget *button, gpointer none)
{
network_list=g_slist_sort(network_list,(GCompareFunc)servlist_compare);
servlist_networks_populate (networks_tree, network_list);
}
static gboolean
servlist_has_selection (GtkTreeView *tree)
{
GtkTreeSelection *sel;
GtkTreeModel *model;
GtkTreeIter iter;
/* make sure something is selected */
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (tree));
return gtk_tree_selection_get_selected (sel, &model, &iter);
}
static void
servlist_favor (GtkWidget *button, gpointer none)
{
GtkTreeSelection *sel;
GtkTreeModel *model;
GtkTreeIter iter;
if (!selected_net)
return;
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (networks_tree));
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
if (selected_net->flags & FLAG_FAVORITE)
{
gtk_list_store_set (GTK_LIST_STORE (model), &iter, 2, 400, -1);
selected_net->flags &= ~FLAG_FAVORITE;
}
else
{
gtk_list_store_set (GTK_LIST_STORE (model), &iter, 2, 800, -1);
selected_net->flags |= FLAG_FAVORITE;
}
}
}
static void
servlist_update_from_entry (char **str, GtkWidget *entry)
{
g_free (*str);
if (gtk_entry_get_text (GTK_ENTRY (entry))[0] == 0)
*str = NULL;
else
*str = g_strdup (gtk_entry_get_text (GTK_ENTRY (entry)));
}
static char *
servlist_edit_current_password (ircnet *net)
{
if (!net)
return NULL;
if (net->flags & FLAG_USE_KEYRING)
return secretstore_get_network_password (net->name);
return servlist_password_decrypt_for_storage (net->pass);
}
static void
servlist_edit_update (ircnet *net)
{
gboolean use_keyring;
gboolean ask_pass;
gboolean keyring_changed;
char *password = NULL;
servlist_update_from_entry (&net->nick, edit_entry_nick);
servlist_update_from_entry (&net->nick2, edit_entry_nick2);
servlist_update_from_entry (&net->user, edit_entry_user);
servlist_update_from_entry (&net->real, edit_entry_real);
if (net && net->name)
{
ask_pass = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass));
use_keyring = !ask_pass && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring));
keyring_changed = !!(net->flags & FLAG_USE_KEYRING) != !!use_keyring;
if (ask_pass)
{
secretstore_delete_network_password (net->name);
if (net->pass)
{
memset (net->pass, 0, strlen (net->pass));
g_free (net->pass);
net->pass = NULL;
}
net->flags &= ~FLAG_USE_KEYRING;
net->flags |= FLAG_PROMPT_PASSWORD;
return;
}
net->flags &= ~FLAG_PROMPT_PASSWORD;
if (!edit_pass_changed && !keyring_changed)
return;
if (edit_pass_changed)
password = g_strdup (gtk_entry_get_text (GTK_ENTRY (edit_entry_pass)));
else
password = servlist_edit_current_password (net);
if (use_keyring)
{
if (password && *password)
{
if (!secretstore_set_network_password (net->name, password))
{
fe_message (_("No system keyring is available. ZoiteChat can save this password using local encrypted fallback storage, but it is less protected than your desktop keyring."), FE_MSG_WARN);
memset (password, 0, strlen (password));
g_free (password);
return;
}
}
else
secretstore_delete_network_password (net->name);
net->flags |= FLAG_USE_KEYRING;
g_free (net->pass);
net->pass = NULL;
}
else
{
char *enc = NULL;
if (password && *password)
{
enc = servlist_password_encrypt_for_storage (password);
if (!enc)
{
fe_message (_("Could not encrypt this password."), FE_MSG_WARN);
memset (password, 0, strlen (password));
g_free (password);
return;
}
}
secretstore_delete_network_password (net->name);
net->flags &= ~FLAG_USE_KEYRING;
g_free (net->pass);
net->pass = enc;
}
if (password)
{
memset (password, 0, strlen (password));
g_free (password);
}
}
}
static void
servlist_edit_close_cb (GtkWidget *button, gpointer userdata)
{
if (selected_net)
servlist_edit_update (selected_net);
if (edit_loaded_password)
{
memset (edit_loaded_password, 0, strlen (edit_loaded_password));
g_free (edit_loaded_password);
edit_loaded_password = NULL;
}
gtk_widget_destroy (edit_win);
edit_win = NULL;
edit_entry_pass = NULL;
edit_check_show_pass = NULL;
edit_check_ask_pass = NULL;
edit_button_encrypt_pass = NULL;
edit_button_import_pass = NULL;
}
static gint
servlist_editwin_delete_cb (GtkWidget *win, GdkEventAny *event, gpointer none)
{
servlist_edit_close_cb (NULL, NULL);
return FALSE;
}
static gboolean
servlist_configure_cb (GtkWindow *win, GdkEventConfigure *event, gpointer none)
{
/* remember the window size */
gtk_window_get_size (win, &netlist_win_width, &netlist_win_height);
return FALSE;
}
static gboolean
servlist_edit_configure_cb (GtkWindow *win, GdkEventConfigure *event, gpointer none)
{
/* remember the window size */
gtk_window_get_size (win, &netedit_win_width, &netedit_win_height);
return FALSE;
}
static void
servlist_edit_cb (GtkWidget *but, gpointer none)
{
if (!servlist_has_selection (GTK_TREE_VIEW (networks_tree)))
return;
if (!selected_net || !selected_net->name)
return;
if ((selected_net->flags & FLAG_USE_KEYRING) && !secretstore_require_unlock (selected_net->name))
return;
edit_win = servlist_open_edit (serverlist_win, selected_net);
gtkutil_set_icon (edit_win);
servlist_servers_populate (selected_net, edit_trees[SERVER_TREE]);
servlist_channels_populate (selected_net, edit_trees[CHANNEL_TREE]);
servlist_commands_populate (selected_net, edit_trees[CMD_TREE]);
g_signal_connect (G_OBJECT (edit_win), "delete-event",
G_CALLBACK (servlist_editwin_delete_cb), 0);
g_signal_connect (G_OBJECT (edit_win), "configure-event",
G_CALLBACK (servlist_edit_configure_cb), 0);
gtk_widget_show (edit_win);
}
static void
servlist_deletenet_cb (GtkWidget *item, ircnet *net)
{
GtkWidget *dialog;
if (!servlist_has_selection (GTK_TREE_VIEW (networks_tree)))
return;
net = selected_net;
if (!net)
return;
dialog = gtk_message_dialog_new (GTK_WINDOW (serverlist_win),
GTK_DIALOG_DESTROY_WITH_PARENT |
GTK_DIALOG_MODAL,
GTK_MESSAGE_QUESTION,
GTK_BUTTONS_OK_CANCEL,
_("Really remove network \"%s\" and all its servers?"),
net->name);
theme_manager_attach_window (dialog);
g_signal_connect (dialog, "response",
G_CALLBACK (servlist_deletenetdialog_cb), net);
gtk_window_set_position (GTK_WINDOW (dialog), GTK_WIN_POS_MOUSE);
gtk_widget_show (dialog);
}
static void
servlist_deleteserver (ircserver *serv, GtkTreeModel *model)
{
GtkTreeSelection *sel;
GtkTreeIter iter;
/* don't remove the last server */
if (selected_net && g_slist_length (selected_net->servlist) < 2)
return;
/* remove from GUI */
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (edit_trees[SERVER_TREE]));
if (gtk_tree_selection_get_selected (sel, &model, &iter))
gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
/* remove from list */
if (selected_net)
servlist_server_remove (selected_net, serv);
}
static void
servlist_editbutton_cb (GtkWidget *item, GtkNotebook *notebook)
{
servlist_start_editing (GTK_TREE_VIEW (edit_trees[gtk_notebook_get_current_page(notebook)]));
}
static void
servlist_deleteserver_cb (void)
{
GtkTreeSelection *sel;
GtkTreeModel *model;
GtkTreeIter iter;
char *servname;
ircserver *serv;
int pos;
/* find the selected item in the GUI */
model = gtk_tree_view_get_model (GTK_TREE_VIEW (edit_trees[SERVER_TREE]));
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (edit_trees[SERVER_TREE]));
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_tree_model_get (model, &iter, 0, &servname, -1);
serv = servlist_server_find (selected_net, servname, &pos);
g_free (servname);
if (serv)
{
servlist_deleteserver (serv, model);
}
}
}
static void
servlist_deletecommand (commandentry *entry, GtkTreeModel *model)
{
GtkTreeSelection *sel;
GtkTreeIter iter;
/* remove from GUI */
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (edit_trees[CMD_TREE]));
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
}
/* remove from list */
if (selected_net)
{
servlist_command_remove (selected_net, entry);
}
}
static void
servlist_deletecommand_cb (void)
{
GtkTreeSelection *sel;
GtkTreeModel *model;
GtkTreeIter iter;
char *command;
commandentry *entry;
int pos;
/* find the selected item in the GUI */
model = gtk_tree_view_get_model (GTK_TREE_VIEW (edit_trees[CMD_TREE]));
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (edit_trees[CMD_TREE]));
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_tree_model_get (model, &iter, 0, &command, -1); /* query the content of the selection */
entry = servlist_command_find (selected_net, command, &pos);
g_free (command);
if (entry)
{
servlist_deletecommand (entry, model);
}
}
}
static void
servlist_deletechannel (favchannel *favchan, GtkTreeModel *model)
{
GtkTreeSelection *sel;
GtkTreeIter iter;
/* remove from GUI */
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (edit_trees[CHANNEL_TREE]));
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_list_store_remove (GTK_LIST_STORE (model), &iter);
}
/* remove from list */
if (selected_net)
{
servlist_favchan_remove (selected_net, favchan);
}
}
static void
servlist_deletechannel_cb (void)
{
GtkTreeSelection *sel;
GtkTreeModel *model;
GtkTreeIter iter;
char *name;
char *key;
favchannel *favchan;
int pos;
/* find the selected item in the GUI */
model = gtk_tree_view_get_model (GTK_TREE_VIEW (edit_trees[CHANNEL_TREE]));
sel = gtk_tree_view_get_selection (GTK_TREE_VIEW (edit_trees[CHANNEL_TREE]));
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_tree_model_get (model, &iter, 0, &name, 1, &key, -1); /* query the content of the selection */
favchan = servlist_favchan_find (selected_net, name, &pos);
g_free (name);
if (favchan)
{
servlist_deletechannel (favchan, model);
}
}
}
static ircnet *
servlist_find_selected_net (GtkTreeSelection *sel)
{
GtkTreeModel *model;
GtkTreeIter iter;
char *netname;
int pos;
ircnet *net = NULL;
if (gtk_tree_selection_get_selected (sel, &model, &iter))
{
gtk_tree_model_get (model, &iter, 0, &netname, -1);
net = servlist_net_find (netname, &pos, strcmp);
g_free (netname);
if (net)
prefs.hex_gui_slist_select = pos;
}
return net;
}
static void
servlist_network_row_cb (GtkTreeSelection *sel, gpointer user_data)
{
ircnet *net;
selected_net = NULL;
net = servlist_find_selected_net (sel);
if (net)
selected_net = net;
}
static int
servlist_savegui (void)
{
char *sp;
const char *nick1, *nick2;
/* check for blank username, ircd will not allow this */
if (gtk_entry_get_text (GTK_ENTRY (entry_guser))[0] == 0)
return 1;
/* if (gtk_entry_get_text (GTK_ENTRY (entry_greal))[0] == 0)
return 1; */
nick1 = gtk_entry_get_text (GTK_ENTRY (entry_nick1));
nick2 = gtk_entry_get_text (GTK_ENTRY (entry_nick2));
/* ensure unique nicknames */
if (!rfc_casecmp (nick1, nick2))
return 2;
safe_strcpy (prefs.hex_irc_nick1, nick1, sizeof(prefs.hex_irc_nick1));
safe_strcpy (prefs.hex_irc_nick2, nick2, sizeof(prefs.hex_irc_nick2));
safe_strcpy (prefs.hex_irc_nick3, gtk_entry_get_text (GTK_ENTRY (entry_nick3)), sizeof(prefs.hex_irc_nick3));
safe_strcpy (prefs.hex_irc_user_name, gtk_entry_get_text (GTK_ENTRY (entry_guser)), sizeof(prefs.hex_irc_user_name));
sp = strchr (prefs.hex_irc_user_name, ' ');
if (sp)
sp[0] = 0; /* spaces will break the login */
/* strcpy (prefs.hex_irc_real_name, gtk_entry_get_text (GTK_ENTRY (entry_greal))); */
servlist_save ();
if (!save_config ())
fe_message (_("Could not save zoitechat.conf."), FE_MSG_WARN);
return 0;
}
static gboolean
servlist_get_iter_from_name (GtkTreeModel *model, gchar *name, GtkTreeIter *iter)
{
GtkTreePath *path = gtk_tree_path_new_from_string (name);
if (!gtk_tree_model_get_iter (model, iter, path))
{
gtk_tree_path_free (path);
return FALSE;
}
gtk_tree_path_free (path);
return TRUE;
}
static void
servlist_addbutton_cb (GtkWidget *item, GtkNotebook *notebook)
{
switch (gtk_notebook_get_current_page (notebook))
{
case SERVER_TREE:
servlist_addserver ();
break;
case CHANNEL_TREE:
servlist_addchannel ();
break;
case CMD_TREE:
servlist_addcommand ();
break;
default:
break;
}
}
static void
servlist_deletebutton_cb (GtkWidget *item, GtkNotebook *notebook)
{
switch (gtk_notebook_get_current_page (notebook))
{
case SERVER_TREE:
servlist_deleteserver_cb ();
break;
case CHANNEL_TREE:
servlist_deletechannel_cb ();
break;
case CMD_TREE:
servlist_deletecommand_cb ();
break;
default:
break;
}
}
static gboolean
servlist_keypress_cb (GtkWidget *wid, GdkEventKey *evt, GtkNotebook *notebook)
{
gboolean handled = FALSE;
int delta = 0;
if (!selected_net)
return FALSE;
if (evt->state & STATE_SHIFT)
{
if (evt->keyval == GDK_KEY_Up)
{
handled = TRUE;
delta = -1;
}
else if (evt->keyval == GDK_KEY_Down)
{
handled = TRUE;
delta = +1;
}
}
if (handled)
{
switch (gtk_notebook_get_current_page (notebook))
{
case SERVER_TREE:
if (selected_serv)
selected_net->servlist = servlist_move_item (GTK_TREE_VIEW (edit_trees[SERVER_TREE]),
selected_net->servlist, selected_serv, delta);
break;
case CHANNEL_TREE:
if (selected_chan)
selected_net->favchanlist = servlist_move_item (GTK_TREE_VIEW (edit_trees[CHANNEL_TREE]),
selected_net->favchanlist, selected_chan, delta);
break;
case CMD_TREE:
if (selected_cmd)
selected_net->commandlist = servlist_move_item (GTK_TREE_VIEW (edit_trees[CMD_TREE]),
selected_net->commandlist, selected_cmd, delta);
break;
}
}
return handled;
}
void
servlist_autojoinedit (ircnet *net, char *channel, gboolean add)
{
favchannel *fav;
if (add)
{
servlist_favchan_add (net, channel);
servlist_save ();
}
else
{
fav = servlist_favchan_find (net, channel, NULL);
if (fav)
{
servlist_favchan_remove (net, fav);
servlist_save ();
}
}
}
static void
servlist_toggle_global_user (gboolean sensitive)
{
gtk_widget_set_sensitive (edit_entry_nick, sensitive);
gtk_widget_set_sensitive (edit_label_nick, sensitive);
gtk_widget_set_sensitive (edit_entry_nick2, sensitive);
gtk_widget_set_sensitive (edit_label_nick2, sensitive);
gtk_widget_set_sensitive (edit_entry_user, sensitive);
gtk_widget_set_sensitive (edit_label_user, sensitive);
gtk_widget_set_sensitive (edit_entry_real, sensitive);
gtk_widget_set_sensitive (edit_label_real, sensitive);
}
static void
servlist_connect_cb (GtkWidget *button, gpointer userdata)
{
int servlist_err;
if (!selected_net)
return;
servlist_err = servlist_savegui ();
if (servlist_err == 1)
{
fe_message (_("User name cannot be left blank."), FE_MSG_ERROR);
return;
}
if (!is_session (servlist_sess))
servlist_sess = NULL; /* open a new one */
{
GSList *list;
session *sess;
session *chosen = servlist_sess;
servlist_sess = NULL; /* open a new one */
for (list = sess_list; list; list = list->next)
{
sess = list->data;
if (sess->server->network == selected_net)
{
servlist_sess = sess;
if (sess->server->connected)
servlist_sess = NULL; /* open a new one */
break;
}
}
/* use the chosen one, if it's empty */
if (!servlist_sess &&
chosen &&
!chosen->server->connected &&
chosen->server->server_session->channel[0] == 0)
{
servlist_sess = chosen;
}
}
servlist_connect (servlist_sess, selected_net, TRUE);
gtk_widget_destroy (serverlist_win);
serverlist_win = NULL;
selected_net = NULL;
}
static void
servlist_celledit_cb (GtkCellRendererText *cell, gchar *arg1, gchar *arg2,
gpointer user_data)
{
GtkTreeModel *model = (GtkTreeModel *)user_data;
GtkTreeIter iter;
GtkTreePath *path;
char *netname;
ircnet *net;
if (!arg1 || !arg2)
return;
path = gtk_tree_path_new_from_string (arg1);
if (!path)
return;
if (!gtk_tree_model_get_iter (model, &iter, path))
{
gtk_tree_path_free (path);
return;
}
gtk_tree_model_get (model, &iter, 0, &netname, -1);
net = servlist_net_find (netname, NULL, strcmp);
g_free (netname);
if (net)
{
/* delete empty item */
if (arg2[0] == 0)
{
servlist_deletenetwork (net);
gtk_tree_path_free (path);
return;
}
netname = net->name;
net->name = g_strdup (arg2);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, net->name, -1);
g_free (netname);
}
gtk_tree_path_free (path);
}
static void
servlist_check_cb (GtkWidget *but, gpointer num_p)
{
int num = GPOINTER_TO_INT (num_p);
if (!selected_net)
return;
if ((1 << num) == FLAG_CYCLE || (1 << num) == FLAG_USE_PROXY)
{
/* these ones are reversed, so it's compat with 2.0.x */
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (but)))
selected_net->flags &= ~(1 << num);
else
selected_net->flags |= (1 << num);
} else
{
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (but)))
selected_net->flags |= (1 << num);
else
selected_net->flags &= ~(1 << num);
}
if ((1 << num) == FLAG_USE_GLOBAL)
{
servlist_toggle_global_user (!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (but)));
}
}
typedef enum
{
SERVLIST_ALIGN_START,
SERVLIST_ALIGN_CENTER,
SERVLIST_ALIGN_FILL
} servlist_align;
static GtkAlign
servlist_align_to_gtk (servlist_align align)
{
switch (align)
{
case SERVLIST_ALIGN_FILL:
return GTK_ALIGN_FILL;
case SERVLIST_ALIGN_CENTER:
return GTK_ALIGN_CENTER;
case SERVLIST_ALIGN_START:
default:
return GTK_ALIGN_START;
}
}
static void
servlist_table_attach (GtkWidget *table, GtkWidget *child,
guint left_attach, guint right_attach,
guint top_attach, guint bottom_attach,
gboolean hexpand, gboolean vexpand,
servlist_align halign, servlist_align valign,
guint xpad, guint ypad)
{
gtk_widget_set_hexpand (child, hexpand);
gtk_widget_set_vexpand (child, vexpand);
gtk_widget_set_halign (child, servlist_align_to_gtk (halign));
gtk_widget_set_valign (child, servlist_align_to_gtk (valign));
gtk_widget_set_margin_start (child, xpad);
gtk_widget_set_margin_end (child, xpad);
gtk_widget_set_margin_top (child, ypad);
gtk_widget_set_margin_bottom (child, ypad);
gtk_grid_attach (GTK_GRID (table), child, left_attach, top_attach,
right_attach - left_attach, bottom_attach - top_attach);
}
static GtkWidget *
servlist_create_check (int num, int state, GtkWidget *table, int row, int col, char *labeltext)
{
GtkWidget *but;
but = gtk_check_button_new_with_label (labeltext);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (but), state);
g_signal_connect (G_OBJECT (but), "toggled",
G_CALLBACK (servlist_check_cb), GINT_TO_POINTER (num));
servlist_table_attach (table, but, col, col + 2, row, row + 1,
TRUE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
gtk_widget_show (but);
return but;
}
static GtkWidget *
servlist_create_entry (GtkWidget *table, char *labeltext, int row,
char *def, GtkWidget **label_ret, char *tip)
{
GtkWidget *label, *entry;
label = gtk_label_new_with_mnemonic (labeltext);
if (label_ret)
*label_ret = label;
gtk_widget_show (label);
servlist_table_attach (table, label, 0, 1, row, row + 1,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
gtk_widget_set_halign (label, GTK_ALIGN_START);
gtk_widget_set_valign (label, GTK_ALIGN_CENTER);
entry = gtk_entry_new ();
gtk_widget_set_tooltip_text (entry, tip);
gtk_widget_show (entry);
gtk_entry_set_text (GTK_ENTRY (entry), def ? def : "");
gtk_label_set_mnemonic_widget (GTK_LABEL (label), entry);
servlist_table_attach (table, entry, 1, 2, row, row + 1,
TRUE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
return entry;
}
static gint
servlist_delete_cb (GtkWidget *win, GdkEventAny *event, gpointer userdata)
{
servlist_savegui ();
serverlist_win = NULL;
selected_net = NULL;
if (sess_list == NULL)
zoitechat_exit ();
return FALSE;
}
static void
servlist_close_cb (GtkWidget *button, gpointer userdata)
{
servlist_savegui ();
gtk_widget_destroy (serverlist_win);
serverlist_win = NULL;
selected_net = NULL;
if (sess_list == NULL)
zoitechat_exit ();
}
/* convert "host:port" format to "host/port" */
static char *
servlist_sanitize_hostname (char *host)
{
char *ret, *c, *e;
ret = g_strdup (host);
c = strchr (ret, ':');
e = strrchr (ret, ':');
/* if only one colon exists it's probably not IPv6 */
if (c && c == e)
*c = '/';
return g_strstrip(ret);
}
/* remove leading slash */
static char *
servlist_sanitize_command (char *cmd)
{
if (cmd[0] == '/')
{
return (g_strdup (cmd + 1));
}
else
{
return (g_strdup (cmd));
}
}
static void
servlist_editserver_cb (GtkCellRendererText *cell, gchar *name, gchar *newval, gpointer user_data)
{
GtkTreeModel *model = (GtkTreeModel *)user_data;
GtkTreeIter iter;
char *servname;
ircserver *serv;
if (!selected_net)
{
return;
}
if (!servlist_get_iter_from_name (model, name, &iter))
{
return;
}
gtk_tree_model_get (model, &iter, 0, &servname, -1);
serv = servlist_server_find (selected_net, servname, NULL);
g_free (servname);
if (serv)
{
/* delete empty item */
if (newval[0] == 0)
{
servlist_deleteserver (serv, model);
return;
}
servname = serv->hostname;
serv->hostname = servlist_sanitize_hostname (newval);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, serv->hostname, -1);
g_free (servname);
}
}
static void
servlist_editcommand_cb (GtkCellRendererText *cell, gchar *name, gchar *newval, gpointer user_data)
{
GtkTreeModel *model = (GtkTreeModel *)user_data;
GtkTreeIter iter;
char *cmd;
commandentry *entry;
if (!selected_net)
{
return;
}
if (!servlist_get_iter_from_name (model, name, &iter))
{
return;
}
gtk_tree_model_get (model, &iter, 0, &cmd, -1);
entry = servlist_command_find (selected_net, cmd, NULL);
g_free (cmd);
if (entry)
{
/* delete empty item */
if (newval[0] == 0)
{
servlist_deletecommand (entry, model);
return;
}
cmd = entry->command;
entry->command = servlist_sanitize_command (newval);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, entry->command, -1);
g_free (cmd);
}
}
static void
servlist_editchannel_cb (GtkCellRendererText *cell, gchar *name, gchar *newval, gpointer user_data)
{
GtkTreeModel *model = (GtkTreeModel *)user_data;
GtkTreeIter iter;
char *chan;
char *key;
favchannel *favchan;
if (!selected_net)
{
return;
}
if (!servlist_get_iter_from_name (model, name, &iter))
{
return;
}
gtk_tree_model_get (model, &iter, 0, &chan, 1, &key, -1);
favchan = servlist_favchan_find (selected_net, chan, NULL);
g_free (chan);
if (favchan)
{
/* delete empty item */
if (newval[0] == 0)
{
servlist_deletechannel (favchan, model);
return;
}
chan = favchan->name;
favchan->name = g_strdup (newval);
gtk_list_store_set (GTK_LIST_STORE (model), &iter, 0, favchan->name, -1);
g_free (chan);
}
}
static void
servlist_editkey_cb (GtkCellRendererText *cell, gchar *name, gchar *newval, gpointer user_data)
{
GtkTreeModel *model = (GtkTreeModel *)user_data;
GtkTreeIter iter;
char *chan;
char *key;
favchannel *favchan;
if (!selected_net)
{
return;
}
if (!servlist_get_iter_from_name (model, name, &iter))
{
return;
}
gtk_tree_model_get (model, &iter, 0, &chan, 1, &key, -1);
favchan = servlist_favchan_find (selected_net, chan, NULL);
g_free (chan);
if (favchan)
{
key = favchan->key;
if (strlen (newval)) /* check key length, the field can be empty in order to delete the key! */
{
favchan->key = g_strdup (newval);
}
else /* if key's empty, make sure we actually remove the key */
{
favchan->key = NULL;
}
gtk_list_store_set (GTK_LIST_STORE (model), &iter, 1, favchan->key, -1);
g_free (key);
}
}
static gboolean
servlist_edit_tabswitch_cb (GtkNotebook *nb, gpointer *newtab, guint newindex, gpointer user_data)
{
/* remember the active tab */
netedit_active_tab = newindex;
return FALSE;
}
static void
servlist_combo_cb (GtkEntry *entry, gpointer userdata)
{
if (!selected_net)
return;
g_free (selected_net->encoding);
selected_net->encoding = g_strdup (gtk_entry_get_text (entry));
}
/* Fills up the network's authentication type so that it's guaranteed to be either NULL or a valid value. */
static void
servlist_logintypecombo_cb (GtkComboBox *cb, gpointer *userdata)
{
int index;
if (!selected_net)
{
return;
}
index = gtk_combo_box_get_active (cb); /* starts at 0, returns -1 for invalid selections */
if (index == -1)
return; /* Invalid */
/* The selection is valid. It can be 0, which is the default type, but we need to allow
* that so that you can revert from other types. servlist_save() will dump 0 anyway.
*/
selected_net->logintype = login_types_conf[index];
if (login_types_conf[index] == LOGIN_CUSTOM)
{
gtk_notebook_set_current_page (GTK_NOTEBOOK (userdata), 2); /* FIXME avoid hardcoding? */
}
/* EXTERNAL uses a cert, not a pass */
if (login_types_conf[index] == LOGIN_SASLEXTERNAL)
gtk_widget_set_sensitive (edit_entry_pass, FALSE);
else
gtk_widget_set_sensitive (edit_entry_pass, TRUE);
}
static void
servlist_username_changed_cb (GtkEntry *entry, gpointer userdata)
{
GtkWidget *connect_btn = GTK_WIDGET (userdata);
if (gtk_entry_get_text (entry)[0] == 0)
{
gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, ICON_SERVLIST_ERROR);
gtk_entry_set_icon_tooltip_text (entry, GTK_ENTRY_ICON_SECONDARY,
_("User name cannot be left blank."));
gtk_widget_set_sensitive (connect_btn, FALSE);
}
else
{
gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, NULL);
gtk_widget_set_sensitive (connect_btn, TRUE);
}
}
static void
servlist_nick_changed_cb (GtkEntry *entry, gpointer userdata)
{
GtkWidget *connect_btn = GTK_WIDGET (userdata);
const gchar *nick1 = gtk_entry_get_text (GTK_ENTRY (entry_nick1));
const gchar *nick2 = gtk_entry_get_text (GTK_ENTRY (entry_nick2));
if (!nick1[0] || !nick2[0])
{
entry = GTK_ENTRY(!nick1[0] ? entry_nick1 : entry_nick2);
gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, ICON_SERVLIST_ERROR);
gtk_entry_set_icon_tooltip_text (entry, GTK_ENTRY_ICON_SECONDARY,
_("You cannot have an empty nick name."));
gtk_widget_set_sensitive (connect_btn, FALSE);
}
else if (!rfc_casecmp (nick1, nick2))
{
gtk_entry_set_icon_from_icon_name (entry, GTK_ENTRY_ICON_SECONDARY, ICON_SERVLIST_ERROR);
gtk_entry_set_icon_tooltip_text (entry, GTK_ENTRY_ICON_SECONDARY,
_("You must have two unique nick names."));
gtk_widget_set_sensitive (connect_btn, FALSE);
}
else
{
gtk_entry_set_icon_from_icon_name (GTK_ENTRY(entry_nick1), GTK_ENTRY_ICON_SECONDARY, NULL);
gtk_entry_set_icon_from_icon_name (GTK_ENTRY(entry_nick2), GTK_ENTRY_ICON_SECONDARY, NULL);
gtk_widget_set_sensitive (connect_btn, TRUE);
}
}
static GtkWidget *
servlist_create_charsetcombo (void)
{
GtkWidget *cb;
int i;
cb = gtk_combo_box_text_new_with_entry ();
i = 0;
while (pages[i])
{
gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (cb), (char *)pages[i]);
i++;
}
gtk_entry_set_text (GTK_ENTRY (gtk_bin_get_child (GTK_BIN(cb))), selected_net->encoding ? selected_net->encoding : pages[0]);
g_signal_connect (G_OBJECT (gtk_bin_get_child (GTK_BIN (cb))), "changed",
G_CALLBACK (servlist_combo_cb), NULL);
return cb;
}
static GtkWidget *
servlist_create_logintypecombo (GtkWidget *data)
{
GtkWidget *cb;
int i;
cb = gtk_combo_box_text_new ();
i = 0;
while (login_types[i])
{
gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (cb), (char *)login_types[i]);
i++;
}
gtk_combo_box_set_active (GTK_COMBO_BOX (cb), servlist_get_login_desc_index (selected_net->logintype));
gtk_widget_set_tooltip_text (cb, _("The way you identify yourself to the server. For custom login methods use connect commands."));
g_signal_connect (G_OBJECT (GTK_BIN (cb)), "changed", G_CALLBACK (servlist_logintypecombo_cb), data);
return cb;
}
static void
no_servlist (GtkWidget * igad, gpointer serv)
{
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (igad)))
prefs.hex_gui_slist_skip = TRUE;
else
prefs.hex_gui_slist_skip = FALSE;
}
static void
fav_servlist (GtkWidget * igad, gpointer serv)
{
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (igad)))
prefs.hex_gui_slist_fav = TRUE;
else
prefs.hex_gui_slist_fav = FALSE;
servlist_networks_populate (networks_tree, network_list);
}
static GtkWidget *
bold_label (char *text)
{
char buf[128];
GtkWidget *label;
g_snprintf (buf, sizeof (buf), "<b>%s</b>", text);
label = gtk_label_new (buf);
gtk_label_set_use_markup (GTK_LABEL (label), TRUE);
gtk_widget_set_halign (label, GTK_ALIGN_START);
gtk_widget_set_valign (label, GTK_ALIGN_CENTER);
gtk_widget_show (label);
return label;
}
static GtkWidget *
servlist_open_edit (GtkWidget *parent, ircnet *net)
{
GtkWidget *editwindow;
GtkWidget *vbox5;
GtkWidget *table3;
GtkWidget *label34;
GtkWidget *label_logintype;
GtkWidget *comboboxentry_charset;
GtkWidget *combobox_logintypes;
GtkWidget *hbox1;
GtkWidget *scrolledwindow2;
GtkWidget *scrolledwindow4;
GtkWidget *scrolledwindow5;
GtkWidget *treeview_servers;
GtkWidget *treeview_channels;
GtkWidget *treeview_commands;
GtkWidget *vbuttonbox1;
GtkWidget *buttonadd;
GtkWidget *buttonremove;
GtkWidget *buttonedit;
GtkWidget *hbox_cert_buttons;
GtkWidget *hseparator2;
GtkWidget *hbuttonbox4;
GtkWidget *button10;
GtkWidget *check;
GtkWidget *notebook;
GtkTreeModel *model;
GtkListStore *store;
GtkCellRenderer *renderer;
char buf[128];
editwindow = gtk_window_new (GTK_WINDOW_TOPLEVEL);
theme_manager_attach_window (editwindow);
gtk_container_set_border_width (GTK_CONTAINER (editwindow), 4);
g_snprintf (buf, sizeof (buf), _("Edit %s - %s"), net->name, _(DISPLAY_NAME));
gtk_window_set_title (GTK_WINDOW (editwindow), buf);
gtk_window_set_default_size (GTK_WINDOW (editwindow), netedit_win_width, netedit_win_height);
gtk_window_set_transient_for (GTK_WINDOW (editwindow), GTK_WINDOW (parent));
gtk_window_set_modal (GTK_WINDOW (editwindow), TRUE);
gtk_window_set_type_hint (GTK_WINDOW (editwindow), GDK_WINDOW_TYPE_HINT_DIALOG);
gtk_window_set_role (GTK_WINDOW (editwindow), "editserv");
vbox5 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_container_add (GTK_CONTAINER (editwindow), vbox5);
/* Tabs and buttons */
hbox1 = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
gtk_box_pack_start (GTK_BOX (vbox5), hbox1, TRUE, TRUE, 4);
scrolledwindow2 = gtk_scrolled_window_new (NULL, NULL);
scrolledwindow4 = gtk_scrolled_window_new (NULL, NULL);
scrolledwindow5 = gtk_scrolled_window_new (NULL, NULL);
notebook = gtk_notebook_new ();
gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolledwindow2, gtk_label_new (_("Servers")));
gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolledwindow4, gtk_label_new (_("Autojoin channels")));
gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolledwindow5, gtk_label_new (_("Connect commands")));
gtk_notebook_set_tab_pos (GTK_NOTEBOOK (notebook), GTK_POS_BOTTOM);
gtk_box_pack_start (GTK_BOX (hbox1), notebook, TRUE, TRUE, SERVLIST_X_PADDING);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwindow2), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolledwindow2), GTK_SHADOW_IN);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwindow4), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolledwindow4), GTK_SHADOW_IN);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwindow5), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolledwindow5), GTK_SHADOW_IN);
gtk_widget_set_tooltip_text (scrolledwindow5, _("%n=Nick name\n%p=Password\n%r=Real name\n%u=User name"));
/* Server Tree */
store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_BOOLEAN);
model = GTK_TREE_MODEL (store);
edit_trees[SERVER_TREE] = treeview_servers = gtk_tree_view_new_with_model (model);
g_signal_connect (G_OBJECT (treeview_servers), "key-press-event",
G_CALLBACK (servlist_keypress_cb), notebook);
g_signal_connect (G_OBJECT (gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview_servers))),
"changed", G_CALLBACK (servlist_server_row_cb), NULL);
g_object_unref (model);
gtk_container_add (GTK_CONTAINER (scrolledwindow2), treeview_servers);
gtk_widget_set_size_request (treeview_servers, -1, 80);
gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview_servers),
FALSE);
renderer = gtk_cell_renderer_text_new ();
g_signal_connect (G_OBJECT (renderer), "edited",
G_CALLBACK (servlist_editserver_cb), model);
gtk_tree_view_insert_column_with_attributes (
GTK_TREE_VIEW (treeview_servers), -1,
0, renderer,
"text", 0,
"editable", 1,
NULL);
/* Channel Tree */
store = gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_BOOLEAN);
model = GTK_TREE_MODEL (store);
edit_trees[CHANNEL_TREE] = treeview_channels = gtk_tree_view_new_with_model (model);
g_signal_connect (G_OBJECT (treeview_channels), "key-press-event",
G_CALLBACK (servlist_keypress_cb), notebook);
g_signal_connect (G_OBJECT (gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview_channels))),
"changed", G_CALLBACK (servlist_channel_row_cb), NULL);
g_object_unref (model);
gtk_container_add (GTK_CONTAINER (scrolledwindow4), treeview_channels);
gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview_channels), TRUE);
renderer = gtk_cell_renderer_text_new ();
g_signal_connect (G_OBJECT (renderer), "edited",
G_CALLBACK (servlist_editchannel_cb), model);
gtk_tree_view_insert_column_with_attributes (
GTK_TREE_VIEW (treeview_channels), -1,
_("Channel"), renderer,
"text", 0,
"editable", 2,
NULL);
renderer = gtk_cell_renderer_text_new ();
g_signal_connect (G_OBJECT (renderer), "edited",
G_CALLBACK (servlist_editkey_cb), model);
gtk_tree_view_insert_column_with_attributes (
GTK_TREE_VIEW (treeview_channels), -1,
_("Key (Password)"), renderer,
"text", 1,
"editable", 2,
NULL);
gtk_tree_view_column_set_expand (gtk_tree_view_get_column (GTK_TREE_VIEW (treeview_channels), 0), TRUE);
gtk_tree_view_column_set_expand (gtk_tree_view_get_column (GTK_TREE_VIEW (treeview_channels), 1), TRUE);
/* Command Tree */
store = gtk_list_store_new (2, G_TYPE_STRING, G_TYPE_BOOLEAN);
model = GTK_TREE_MODEL (store);
edit_trees[CMD_TREE] = treeview_commands = gtk_tree_view_new_with_model (model);
g_signal_connect (G_OBJECT (treeview_commands), "key-press-event",
G_CALLBACK (servlist_keypress_cb), notebook);
g_signal_connect (G_OBJECT (gtk_tree_view_get_selection (GTK_TREE_VIEW (treeview_commands))),
"changed", G_CALLBACK (servlist_command_row_cb), NULL);
g_object_unref (model);
gtk_container_add (GTK_CONTAINER (scrolledwindow5), treeview_commands);
gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview_commands),
FALSE);
renderer = gtk_cell_renderer_text_new ();
g_signal_connect (G_OBJECT (renderer), "edited",
G_CALLBACK (servlist_editcommand_cb), model);
gtk_tree_view_insert_column_with_attributes (
GTK_TREE_VIEW (treeview_commands), -1,
0, renderer,
"text", 0,
"editable", 1,
NULL);
/* Button Box */
vbuttonbox1 = gtk_button_box_new (GTK_ORIENTATION_VERTICAL);
gtk_box_set_spacing (GTK_BOX (vbuttonbox1), 3);
gtk_button_box_set_layout (GTK_BUTTON_BOX (vbuttonbox1), GTK_BUTTONBOX_START);
gtk_box_pack_start (GTK_BOX (hbox1), vbuttonbox1, FALSE, FALSE, 3);
buttonadd = servlist_icon_button_new (_("_Add"), ICON_SERVLIST_ADD);
g_signal_connect (G_OBJECT (buttonadd), "clicked",
G_CALLBACK (servlist_addbutton_cb), notebook);
gtk_container_add (GTK_CONTAINER (vbuttonbox1), buttonadd);
gtk_widget_set_can_default (buttonadd, TRUE);
buttonremove = servlist_icon_button_new (_("_Remove"), ICON_SERVLIST_REMOVE);
g_signal_connect (G_OBJECT (buttonremove), "clicked",
G_CALLBACK (servlist_deletebutton_cb), notebook);
gtk_container_add (GTK_CONTAINER (vbuttonbox1), buttonremove);
gtk_widget_set_can_default (buttonremove, TRUE);
buttonedit = gtk_button_new_with_mnemonic (_("_Edit"));
g_signal_connect (G_OBJECT (buttonedit), "clicked",
G_CALLBACK (servlist_editbutton_cb), notebook);
gtk_container_add (GTK_CONTAINER (vbuttonbox1), buttonedit);
gtk_widget_set_can_default (buttonedit, TRUE);
/* Checkboxes and entries */
table3 = gtkutil_grid_new (19, 2, FALSE);
gtk_box_pack_start (GTK_BOX (vbox5), table3, FALSE, FALSE, 0);
gtk_grid_set_row_spacing (GTK_GRID (table3), 2);
gtk_grid_set_column_spacing (GTK_GRID (table3), 8);
check = servlist_create_check (0, !(net->flags & FLAG_CYCLE), table3, 0, 0, _("Connect to selected server only"));
gtk_widget_set_tooltip_text (check, _("Don't cycle through all the servers when the connection fails."));
servlist_create_check (3, net->flags & FLAG_AUTO_CONNECT, table3, 1, 0, _("Connect to this network automatically"));
servlist_create_check (4, !(net->flags & FLAG_USE_PROXY), table3, 2, 0, _("Bypass proxy server"));
check = servlist_create_check (2, net->flags & FLAG_USE_SSL, table3, 3, 0, _("Use SSL for all the servers on this network"));
#ifndef USE_OPENSSL
gtk_widget_set_sensitive (check, FALSE);
#endif
check = servlist_create_check (5, net->flags & FLAG_ALLOW_INVALID, table3, 4, 0, _("Accept invalid SSL certificates"));
#ifndef USE_OPENSSL
gtk_widget_set_sensitive (check, FALSE);
#endif
servlist_create_check (1, net->flags & FLAG_USE_GLOBAL, table3, 5, 0, _("Use global user information"));
edit_check_ask_pass = gtk_check_button_new_with_mnemonic (_("Ask for password on connect"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (edit_check_ask_pass), net->flags & FLAG_PROMPT_PASSWORD);
servlist_table_attach (table3, edit_check_ask_pass, 0, 2, 6, 7,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
g_signal_connect (G_OBJECT (edit_check_ask_pass), "toggled",
G_CALLBACK (servlist_toggle_ask_pass_cb), NULL);
edit_check_use_keyring = gtk_check_button_new_with_mnemonic (_("Use system keyring"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (edit_check_use_keyring), net->flags & FLAG_USE_KEYRING);
servlist_table_attach (table3, edit_check_use_keyring, 0, 2, 7, 8,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
g_signal_connect (G_OBJECT (edit_check_use_keyring), "toggled",
G_CALLBACK (servlist_toggle_keyring_cb), NULL);
edit_entry_nick = servlist_create_entry (table3, _("_Nick name:"), 8, net->nick, &edit_label_nick, 0);
edit_entry_nick2 = servlist_create_entry (table3, _("Second choice:"), 9, net->nick2, &edit_label_nick2, 0);
edit_entry_real = servlist_create_entry (table3, _("Rea_l name:"), 10, net->real, &edit_label_real, 0);
edit_entry_user = servlist_create_entry (table3, _("_User name:"), 11, net->user, &edit_label_user, 0);
label_logintype = gtk_label_new (_("Login method:"));
servlist_table_attach (table3, label_logintype, 0, 1, 12, 13,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
gtk_widget_set_halign (label_logintype, GTK_ALIGN_START);
gtk_widget_set_valign (label_logintype, GTK_ALIGN_CENTER);
combobox_logintypes = servlist_create_logintypecombo (notebook);
servlist_table_attach (table3, combobox_logintypes, 1, 2, 12, 13,
FALSE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
4, 2);
edit_entry_pass = servlist_create_entry (table3, _("Password:"), 13, NULL, 0, _("Password used for login. If in doubt, leave blank."));
if (edit_loaded_password)
{
memset (edit_loaded_password, 0, strlen (edit_loaded_password));
g_free (edit_loaded_password);
edit_loaded_password = NULL;
}
edit_pass_changed = 0;
g_signal_connect (G_OBJECT (edit_entry_pass), "changed",
G_CALLBACK (servlist_password_changed_cb), NULL);
if (net->flags & FLAG_USE_KEYRING)
{
char *stored = secretstore_get_network_password (net->name);
if (stored && *stored)
{
edit_loaded_password = g_strdup (stored);
servlist_entry_set_text_silent (edit_entry_pass, "***");
}
if (stored)
{
memset (stored, 0, strlen (stored));
g_free (stored);
}
}
else if (net->pass && *net->pass)
{
servlist_entry_set_text_silent (edit_entry_pass, "***");
}
edit_pass_changed = 0;
gtk_entry_set_visibility (GTK_ENTRY (edit_entry_pass), FALSE);
if (selected_net && selected_net->logintype == LOGIN_SASLEXTERNAL)
gtk_widget_set_sensitive (edit_entry_pass, FALSE);
edit_check_show_pass = gtk_check_button_new_with_mnemonic (_("Show password"));
servlist_table_attach (table3, edit_check_show_pass, 0, 2, 14, 15,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
4, 2);
g_signal_connect (G_OBJECT (edit_check_show_pass), "toggled",
G_CALLBACK (servlist_toggle_show_password_cb), edit_entry_pass);
edit_button_encrypt_pass = gtk_button_new_with_mnemonic (_("Encrypt saved password"));
servlist_table_attach (table3, edit_button_encrypt_pass, 0, 1, 15, 16,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
g_signal_connect (G_OBJECT (edit_button_encrypt_pass), "clicked",
G_CALLBACK (servlist_encrypt_password_cb), net);
edit_button_import_pass = gtk_button_new_with_mnemonic (_("Move password to keyring"));
servlist_table_attach (table3, edit_button_import_pass, 1, 2, 15, 16,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
4, 2);
g_signal_connect (G_OBJECT (edit_button_import_pass), "clicked",
G_CALLBACK (servlist_import_password_cb), net);
label34 = gtk_label_new (_("Character set:"));
servlist_table_attach (table3, label34, 0, 1, 16, 17,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
gtk_widget_set_halign (label34, GTK_ALIGN_START);
gtk_widget_set_valign (label34, GTK_ALIGN_CENTER);
comboboxentry_charset = servlist_create_charsetcombo ();
servlist_table_attach (table3, comboboxentry_charset, 1, 2, 16, 17,
FALSE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
4, 2);
hbox_cert_buttons = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 6);
edit_button_cert_generate = gtk_button_new_with_mnemonic (_("Generate client SSL cert"));
g_signal_connect (G_OBJECT (edit_button_cert_generate), "clicked",
G_CALLBACK (servlist_generate_client_cert_cb), net);
gtk_box_pack_start (GTK_BOX (hbox_cert_buttons), edit_button_cert_generate, FALSE, FALSE, 0);
edit_button_cert_import = gtk_button_new_with_mnemonic (_("Import client SSL cert"));
g_signal_connect (G_OBJECT (edit_button_cert_import), "clicked",
G_CALLBACK (servlist_import_client_cert_cb), net);
gtk_box_pack_start (GTK_BOX (hbox_cert_buttons), edit_button_cert_import, FALSE, FALSE, 0);
edit_button_cert_info = gtk_button_new_with_mnemonic (_("Client SSL cert info"));
g_signal_connect (G_OBJECT (edit_button_cert_info), "clicked",
G_CALLBACK (servlist_cert_info_cb), net);
gtk_box_pack_start (GTK_BOX (hbox_cert_buttons), edit_button_cert_info, FALSE, FALSE, 0);
edit_button_cert_delete = gtk_button_new_with_mnemonic (_("Delete cert"));
g_signal_connect (G_OBJECT (edit_button_cert_delete), "clicked",
G_CALLBACK (servlist_delete_client_cert_cb), net);
gtk_box_pack_start (GTK_BOX (hbox_cert_buttons), edit_button_cert_delete, FALSE, FALSE, 0);
servlist_table_attach (table3, hbox_cert_buttons, 0, 2, 17, 18,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
SERVLIST_X_PADDING, SERVLIST_Y_PADDING);
/* Rule and Close button */
hseparator2 = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
gtk_box_pack_start (GTK_BOX (vbox5), hseparator2, FALSE, FALSE, 8);
hbuttonbox4 = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
gtk_button_box_set_layout (GTK_BUTTON_BOX (hbuttonbox4), GTK_BUTTONBOX_END);
gtk_box_pack_start (GTK_BOX (vbox5), hbuttonbox4, FALSE, FALSE, 0);
button10 = servlist_icon_button_new (_("_Close"), ICON_SERVLIST_CLOSE);
g_signal_connect (G_OBJECT (button10), "clicked",
G_CALLBACK (servlist_edit_close_cb), 0);
gtk_container_add (GTK_CONTAINER (hbuttonbox4), button10);
gtk_widget_set_can_default (button10, TRUE);
if (net->flags & FLAG_USE_GLOBAL)
{
servlist_toggle_global_user (FALSE);
}
servlist_toggle_keyring_cb (GTK_TOGGLE_BUTTON (edit_check_use_keyring), NULL);
servlist_update_password_tools (net);
gtk_widget_grab_focus (button10);
gtk_widget_grab_default (button10);
gtk_widget_show_all (editwindow);
servlist_update_cert_buttons (net);
/* We can't set the active tab without child elements being shown, so this must be *after* gtk_widget_show()s! */
gtk_notebook_set_current_page (GTK_NOTEBOOK (notebook), netedit_active_tab);
/* We need to connect this *after* setting the active tab so that the value doesn't get overriden. */
g_signal_connect (G_OBJECT (notebook), "switch-page", G_CALLBACK (servlist_edit_tabswitch_cb), notebook);
return editwindow;
}
static GtkWidget *
servlist_open_networks (void)
{
GtkWidget *servlist;
GtkWidget *vbox1;
GtkWidget *label2;
GtkWidget *table1;
GtkWidget *label3;
GtkWidget *label4;
GtkWidget *label5;
GtkWidget *label6;
/* GtkWidget *label7; */
GtkWidget *entry1;
GtkWidget *entry2;
GtkWidget *entry3;
GtkWidget *entry4;
/* GtkWidget *entry5; */
GtkWidget *vbox2;
GtkWidget *label1;
GtkWidget *table4;
GtkWidget *scrolledwindow3;
GtkWidget *treeview_networks;
GtkWidget *checkbutton_skip;
GtkWidget *checkbutton_fav;
GtkWidget *hbox;
GtkWidget *vbuttonbox2;
GtkWidget *button_add;
GtkWidget *button_remove;
GtkWidget *button_edit;
GtkWidget *button_sort;
GtkWidget *hseparator1;
GtkWidget *hbuttonbox1;
GtkWidget *button_connect;
GtkWidget *button_close;
GtkTreeModel *model;
GtkListStore *store;
GtkCellRenderer *renderer;
char buf[128];
servlist = gtk_window_new (GTK_WINDOW_TOPLEVEL);
theme_manager_attach_window (servlist);
gtk_container_set_border_width (GTK_CONTAINER (servlist), 4);
g_snprintf(buf, sizeof(buf), _("Network List - %s"), _(DISPLAY_NAME));
gtk_window_set_title (GTK_WINDOW (servlist), buf);
gtk_window_set_default_size (GTK_WINDOW (servlist), netlist_win_width, netlist_win_height);
gtk_window_set_role (GTK_WINDOW (servlist), "servlist");
gtk_window_set_type_hint (GTK_WINDOW (servlist), GDK_WINDOW_TYPE_HINT_DIALOG);
if (current_sess)
gtk_window_set_transient_for (GTK_WINDOW (servlist), GTK_WINDOW (current_sess->gui->window));
vbox1 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_widget_show (vbox1);
gtk_container_add (GTK_CONTAINER (servlist), vbox1);
label2 = bold_label (_("User Information"));
gtk_box_pack_start (GTK_BOX (vbox1), label2, FALSE, FALSE, 0);
table1 = gtkutil_grid_new (5, 2, FALSE);
gtk_widget_show (table1);
gtk_box_pack_start (GTK_BOX (vbox1), table1, FALSE, FALSE, 0);
gtk_container_set_border_width (GTK_CONTAINER (table1), 8);
gtk_grid_set_row_spacing (GTK_GRID (table1), 2);
gtk_grid_set_column_spacing (GTK_GRID (table1), 4);
label3 = gtk_label_new_with_mnemonic (_("_Nick name:"));
gtk_widget_show (label3);
servlist_table_attach (table1, label3, 0, 1, 0, 1,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
0, 0);
gtk_widget_set_halign (label3, GTK_ALIGN_START);
gtk_widget_set_valign (label3, GTK_ALIGN_CENTER);
label4 = gtk_label_new (_("Second choice:"));
gtk_widget_show (label4);
servlist_table_attach (table1, label4, 0, 1, 1, 2,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
0, 0);
gtk_widget_set_halign (label4, GTK_ALIGN_START);
gtk_widget_set_valign (label4, GTK_ALIGN_CENTER);
label5 = gtk_label_new (_("Third choice:"));
gtk_widget_show (label5);
servlist_table_attach (table1, label5, 0, 1, 2, 3,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
0, 0);
gtk_widget_set_halign (label5, GTK_ALIGN_START);
gtk_widget_set_valign (label5, GTK_ALIGN_CENTER);
label6 = gtk_label_new_with_mnemonic (_("_User name:"));
gtk_widget_show (label6);
servlist_table_attach (table1, label6, 0, 1, 3, 4,
FALSE, FALSE,
SERVLIST_ALIGN_START, SERVLIST_ALIGN_CENTER,
0, 0);
gtk_widget_set_halign (label6, GTK_ALIGN_START);
gtk_widget_set_valign (label6, GTK_ALIGN_CENTER);
/* label7 = gtk_label_new_with_mnemonic (_("Rea_l name:"));
gtk_widget_show (label7);
gtk_table_attach (GTK_TABLE (table1), label7, 0, 1, 4, 5,
(GtkAttachOptions) (GTK_FILL),
(GtkAttachOptions) (0), 0, 0);
*/
entry_nick1 = entry1 = gtk_entry_new ();
gtk_entry_set_text (GTK_ENTRY (entry1), prefs.hex_irc_nick1);
gtk_widget_show (entry1);
servlist_table_attach (table1, entry1, 1, 2, 0, 1,
TRUE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
0, 0);
entry_nick2 = entry2 = gtk_entry_new ();
gtk_entry_set_text (GTK_ENTRY (entry2), prefs.hex_irc_nick2);
gtk_widget_show (entry2);
servlist_table_attach (table1, entry2, 1, 2, 1, 2,
TRUE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
0, 0);
entry_nick3 = entry3 = gtk_entry_new ();
gtk_entry_set_text (GTK_ENTRY (entry3), prefs.hex_irc_nick3);
gtk_widget_show (entry3);
servlist_table_attach (table1, entry3, 1, 2, 2, 3,
TRUE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
0, 0);
entry_guser = entry4 = gtk_entry_new ();
gtk_entry_set_text (GTK_ENTRY (entry4), prefs.hex_irc_user_name);
gtk_widget_show (entry4);
servlist_table_attach (table1, entry4, 1, 2, 3, 4,
TRUE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
0, 0);
/* entry_greal = entry5 = gtk_entry_new ();
gtk_entry_set_text (GTK_ENTRY (entry5), prefs.hex_irc_real_name);
gtk_widget_show (entry5);
gtk_table_attach (GTK_TABLE (table1), entry5, 1, 2, 4, 5,
(GtkAttachOptions) (GTK_EXPAND | GTK_FILL),
(GtkAttachOptions) (0), 0, 0); */
vbox2 = gtkutil_box_new (GTK_ORIENTATION_VERTICAL, FALSE, 0);
gtk_widget_show (vbox2);
gtk_box_pack_start (GTK_BOX (vbox1), vbox2, TRUE, TRUE, 0);
label1 = bold_label (_("Networks"));
gtk_box_pack_start (GTK_BOX (vbox2), label1, FALSE, FALSE, 0);
table4 = gtkutil_grid_new (2, 2, FALSE);
gtk_widget_show (table4);
gtk_box_pack_start (GTK_BOX (vbox2), table4, TRUE, TRUE, 0);
gtk_container_set_border_width (GTK_CONTAINER (table4), 8);
gtk_grid_set_row_spacing (GTK_GRID (table4), 2);
gtk_grid_set_column_spacing (GTK_GRID (table4), 3);
scrolledwindow3 = gtk_scrolled_window_new (NULL, NULL);
gtk_widget_show (scrolledwindow3);
servlist_table_attach (table4, scrolledwindow3, 0, 1, 0, 1,
TRUE, TRUE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
0, 0);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwindow3),
GTK_POLICY_NEVER, GTK_POLICY_ALWAYS);
gtk_scrolled_window_set_shadow_type (GTK_SCROLLED_WINDOW (scrolledwindow3),
GTK_SHADOW_IN);
store = gtk_list_store_new (3, G_TYPE_STRING, G_TYPE_BOOLEAN, G_TYPE_INT);
model = GTK_TREE_MODEL (store);
networks_tree = treeview_networks = gtk_tree_view_new_with_model (model);
g_object_unref (model);
gtk_widget_show (treeview_networks);
gtk_container_add (GTK_CONTAINER (scrolledwindow3), treeview_networks);
gtk_tree_view_set_headers_visible (GTK_TREE_VIEW (treeview_networks),
FALSE);
renderer = gtk_cell_renderer_text_new ();
g_signal_connect (G_OBJECT (renderer), "edited",
G_CALLBACK (servlist_celledit_cb), model);
gtk_tree_view_insert_column_with_attributes (
GTK_TREE_VIEW (treeview_networks), -1,
0, renderer,
"text", 0,
"editable", 1,
"weight", 2,
NULL);
hbox = gtkutil_box_new (GTK_ORIENTATION_HORIZONTAL, FALSE, 0);
servlist_table_attach (table4, hbox, 0, 2, 1, 2,
FALSE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_CENTER,
0, 0);
gtk_widget_show (hbox);
checkbutton_skip =
gtk_check_button_new_with_mnemonic (_("Skip network list on startup"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (checkbutton_skip),
prefs.hex_gui_slist_skip);
gtk_container_add (GTK_CONTAINER (hbox), checkbutton_skip);
g_signal_connect (G_OBJECT (checkbutton_skip), "toggled",
G_CALLBACK (no_servlist), 0);
gtk_widget_show (checkbutton_skip);
checkbutton_fav =
gtk_check_button_new_with_mnemonic (_("Show favorites only"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (checkbutton_fav),
prefs.hex_gui_slist_fav);
gtk_container_add (GTK_CONTAINER (hbox), checkbutton_fav);
g_signal_connect (G_OBJECT (checkbutton_fav), "toggled",
G_CALLBACK (fav_servlist), 0);
gtk_widget_show (checkbutton_fav);
vbuttonbox2 = gtk_button_box_new (GTK_ORIENTATION_VERTICAL);
gtk_box_set_spacing (GTK_BOX (vbuttonbox2), 3);
gtk_button_box_set_layout (GTK_BUTTON_BOX (vbuttonbox2), GTK_BUTTONBOX_START);
gtk_widget_show (vbuttonbox2);
servlist_table_attach (table4, vbuttonbox2, 1, 2, 0, 1,
FALSE, FALSE,
SERVLIST_ALIGN_FILL, SERVLIST_ALIGN_FILL,
0, 0);
button_add = servlist_icon_button_new (_("_Add"), ICON_SERVLIST_ADD);
g_signal_connect (G_OBJECT (button_add), "clicked",
G_CALLBACK (servlist_addnet_cb), networks_tree);
gtk_widget_show (button_add);
gtk_container_add (GTK_CONTAINER (vbuttonbox2), button_add);
gtk_widget_set_can_default (button_add, TRUE);
button_remove = servlist_icon_button_new (_("_Remove"), ICON_SERVLIST_REMOVE);
g_signal_connect (G_OBJECT (button_remove), "clicked",
G_CALLBACK (servlist_deletenet_cb), 0);
gtk_widget_show (button_remove);
gtk_container_add (GTK_CONTAINER (vbuttonbox2), button_remove);
gtk_widget_set_can_default (button_remove, TRUE);
button_edit = gtk_button_new_with_mnemonic (_("_Edit..."));
g_signal_connect (G_OBJECT (button_edit), "clicked",
G_CALLBACK (servlist_edit_cb), 0);
gtk_widget_show (button_edit);
gtk_container_add (GTK_CONTAINER (vbuttonbox2), button_edit);
gtk_widget_set_can_default (button_edit, TRUE);
button_sort = gtk_button_new_with_mnemonic (_("_Sort"));
gtk_widget_set_tooltip_text (button_sort, _("Sorts the network list in alphabetical order. "
"Use Shift+Up and Shift+Down keys to move a row."));
g_signal_connect (G_OBJECT (button_sort), "clicked",
G_CALLBACK (servlist_sort), 0);
gtk_widget_show (button_sort);
gtk_container_add (GTK_CONTAINER (vbuttonbox2), button_sort);
gtk_widget_set_can_default (button_sort, TRUE);
button_sort = gtk_button_new_with_mnemonic (_("_Favor"));
gtk_widget_set_tooltip_text (button_sort, _("Mark or unmark this network as a favorite."));
g_signal_connect (G_OBJECT (button_sort), "clicked",
G_CALLBACK (servlist_favor), 0);
gtk_widget_show (button_sort);
gtk_container_add (GTK_CONTAINER (vbuttonbox2), button_sort);
gtk_widget_set_can_default (button_sort, TRUE);
hseparator1 = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL);
gtk_widget_show (hseparator1);
gtk_box_pack_start (GTK_BOX (vbox1), hseparator1, FALSE, TRUE, 4);
hbuttonbox1 = gtk_button_box_new (GTK_ORIENTATION_HORIZONTAL);
gtk_button_box_set_layout (GTK_BUTTON_BOX (hbuttonbox1), GTK_BUTTONBOX_SPREAD);
gtk_widget_show (hbuttonbox1);
gtk_box_pack_start (GTK_BOX (vbox1), hbuttonbox1, FALSE, TRUE, 0);
gtk_container_set_border_width (GTK_CONTAINER (hbuttonbox1), 8);
button_close = servlist_icon_button_new (_("_Close"), ICON_SERVLIST_CLOSE);
gtk_widget_show (button_close);
g_signal_connect (G_OBJECT (button_close), "clicked",
G_CALLBACK (servlist_close_cb), 0);
gtk_container_add (GTK_CONTAINER (hbuttonbox1), button_close);
gtk_widget_set_can_default (button_close, TRUE);
button_connect = gtkutil_button (hbuttonbox1, ICON_SERVLIST_CONNECT, NULL,
servlist_connect_cb, NULL, _("C_onnect"));
gtk_widget_set_can_default (button_connect, TRUE);
g_signal_connect (G_OBJECT (entry_guser), "changed",
G_CALLBACK(servlist_username_changed_cb), button_connect);
g_signal_connect (G_OBJECT (entry_nick1), "changed",
G_CALLBACK(servlist_nick_changed_cb), button_connect);
g_signal_connect (G_OBJECT (entry_nick2), "changed",
G_CALLBACK(servlist_nick_changed_cb), button_connect);
/* Run validity checks now */
servlist_nick_changed_cb (GTK_ENTRY(entry_nick2), button_connect);
servlist_username_changed_cb (GTK_ENTRY(entry_guser), button_connect);
gtk_label_set_mnemonic_widget (GTK_LABEL (label3), entry1);
gtk_label_set_mnemonic_widget (GTK_LABEL (label6), entry4);
/* gtk_label_set_mnemonic_widget (GTK_LABEL (label7), entry5); */
gtk_widget_grab_focus (networks_tree);
gtk_widget_grab_default (button_close);
return servlist;
}
/* First-run onboarding ---------------------------------------------------- */
#define ONBOARDING_ATLAS_HOST "atlas.zoitechat.org"
#define ONBOARDING_ATLAS_CLIENT_PATH "/api/v1/client/networks"
#define ONBOARDING_ATLAS_PUBLIC_PATH "/api/v1/networks"
#define ONBOARDING_ATLAS_MAX_RESPONSE (2 * 1024 * 1024)
#define ONBOARDING_REGISTRATION_TIMEOUT_SECONDS 60
typedef enum
{
ONBOARDING_ACCOUNT_SKIP = 0,
ONBOARDING_ACCOUNT_EXISTING,
ONBOARDING_ACCOUNT_NEW
} ServlistOnboardingAccountMode;
typedef struct
{
ircnet *net;
char *slug;
char *name;
char *description;
char *homepage;
char *endpoint_host;
int endpoint_port;
gboolean endpoint_tls;
gboolean atlas_approved;
int users;
char *services_family;
int services_confidence;
gboolean services_present;
gboolean supports_sasl;
gboolean has_nickserv;
char *help_channel;
GPtrArray *starter_channels;
GPtrArray *popular_channels;
gboolean detail_loaded;
gboolean onboarding_verified;
gboolean registration_verified;
char *registration_service;
char *registration_template;
gboolean registration_email_required;
char *identify_method;
} ServlistOnboardingChoice;
typedef struct
{
char *client_json;
char *public_json;
char *error;
} ServlistAtlasFetchResult;
typedef struct
{
char *slug;
char *json;
char *error;
guint generation;
} ServlistAtlasDetailResult;
typedef struct
{
char *slug;
guint generation;
} ServlistAtlasDetailRequest;
typedef struct
{
GtkWidget *dialog;
GtkWidget *notebook;
GtkWidget *back_button;
GtkWidget *next_button;
GtkWidget *connect_button;
GtkWidget *use_atlas;
GtkWidget *network_combo;
GtkWidget *atlas_status;
GtkWidget *network_info;
GtkWidget *nick_entry;
GtkWidget *realname_entry;
GtkWidget *account_skip;
GtkWidget *account_existing;
GtkWidget *account_new;
GtkWidget *account_info;
GtkWidget *account_label;
GtkWidget *account_entry;
GtkWidget *password_label;
GtkWidget *password_entry;
GtkWidget *password_confirm_label;
GtkWidget *password_confirm_entry;
GtkWidget *email_label;
GtkWidget *email_entry;
GtkWidget *remember_password;
GtkWidget *channel_suggestions;
GtkWidget *channel_entry;
GtkWidget *summary_label;
GPtrArray *choices;
GCancellable *atlas_cancel;
GCancellable *detail_cancel;
gboolean atlas_pending;
guint detail_pending;
guint detail_generation;
gboolean closing;
session *sess;
} ServlistOnboardingState;
typedef struct
{
server *serv;
ServlistOnboardingAccountMode account_mode;
ircnet *net;
char *network_name;
char *nick;
char *password;
char *email;
char *service;
char *command_template;
char *homepage;
char *services_family;
char *identify_method;
int services_confidence;
int preferred_login;
gboolean email_required;
gboolean remember_password;
gboolean verified_recipe;
gboolean common_recipe;
gboolean has_nickserv;
gint64 deadline;
} ServlistRegistrationGuide;
static void servlist_open_networks_window (session *sess);
static void servlist_onboarding_update_network_info (ServlistOnboardingState *state);
static void servlist_onboarding_account_changed (GtkToggleButton *button, gpointer userdata);
static void servlist_onboarding_refresh_channel_suggestions (ServlistOnboardingState *state);
static void servlist_onboarding_start_detail (ServlistOnboardingState *state, ServlistOnboardingChoice *choice);
static void
servlist_onboarding_choice_free (ServlistOnboardingChoice *choice)
{
if (!choice)
return;
g_free (choice->slug);
g_free (choice->name);
g_free (choice->description);
g_free (choice->homepage);
g_free (choice->endpoint_host);
g_free (choice->services_family);
g_free (choice->help_channel);
if (choice->starter_channels)
g_ptr_array_free (choice->starter_channels, TRUE);
if (choice->popular_channels)
g_ptr_array_free (choice->popular_channels, TRUE);
g_free (choice->registration_service);
g_free (choice->registration_template);
g_free (choice->identify_method);
g_free (choice);
}
static void
servlist_atlas_fetch_result_free (ServlistAtlasFetchResult *result)
{
if (!result)
return;
g_free (result->client_json);
g_free (result->public_json);
g_free (result->error);
g_free (result);
}
static void
servlist_atlas_detail_result_free (ServlistAtlasDetailResult *result)
{
if (!result)
return;
g_free (result->slug);
g_free (result->json);
g_free (result->error);
g_free (result);
}
static void
servlist_atlas_detail_request_free (ServlistAtlasDetailRequest *request)
{
if (!request)
return;
g_free (request->slug);
g_free (request);
}
static void
servlist_onboarding_state_free (ServlistOnboardingState *state)
{
if (!state)
return;
if (state->choices)
g_ptr_array_free (state->choices, TRUE);
if (state->atlas_cancel)
g_object_unref (state->atlas_cancel);
if (state->detail_cancel)
g_object_unref (state->detail_cancel);
g_free (state);
}
static void
servlist_onboarding_close_state (ServlistOnboardingState *state)
{
state->closing = TRUE;
state->dialog = NULL;
if (state->atlas_cancel)
g_cancellable_cancel (state->atlas_cancel);
if (state->detail_cancel)
g_cancellable_cancel (state->detail_cancel);
if (!state->atlas_pending && state->detail_pending == 0)
servlist_onboarding_state_free (state);
}
static ServlistOnboardingChoice *
servlist_onboarding_choice_new (ircnet *net, const char *name)
{
ServlistOnboardingChoice *choice = g_new0 (ServlistOnboardingChoice, 1);
choice->net = net;
choice->name = g_strdup (name ? name : "");
choice->users = -1;
choice->services_confidence = -1;
choice->endpoint_port = 6697;
choice->starter_channels = g_ptr_array_new_with_free_func (g_free);
choice->popular_channels = g_ptr_array_new_with_free_func (g_free);
return choice;
}
static gboolean
servlist_onboarding_token_is_safe (const char *text)
{
const unsigned char *p;
if (!text || !*text)
return FALSE;
for (p = (const unsigned char *) text; *p; p++)
{
if (g_ascii_iscntrl (*p) || g_ascii_isspace (*p))
return FALSE;
}
return TRUE;
}
static gboolean
servlist_onboarding_slug_is_safe (const char *text)
{
const unsigned char *p;
gsize len;
if (!text || !*text)
return FALSE;
len = strlen (text);
if (len > 80 || !g_ascii_isalnum (text[0]))
return FALSE;
for (p = (const unsigned char *) text; *p; p++)
{
if (!g_ascii_isalnum (*p) && *p != '-')
return FALSE;
}
return TRUE;
}
static gboolean
servlist_onboarding_endpoint_is_safe (const char *text)
{
const unsigned char *p;
if (!servlist_onboarding_token_is_safe (text) || strlen (text) > 255)
return FALSE;
for (p = (const unsigned char *) text; *p; p++)
{
if (*p == '/' || *p == '\\' || *p == ',' || *p == '+')
return FALSE;
}
return TRUE;
}
static gboolean
servlist_onboarding_service_target_is_safe (const char *text)
{
const unsigned char *p;
if (!servlist_onboarding_token_is_safe (text) || strlen (text) > 64)
return FALSE;
for (p = (const unsigned char *) text; *p; p++)
{
if (*p == ':' || *p == ',' || *p == '/' || *p == '\\')
return FALSE;
}
return TRUE;
}
static gboolean
servlist_onboarding_registration_template_is_safe (const char *command)
{
const char *p;
if (!command || !*command || strlen (command) > 300 ||
strchr (command, '\r') || strchr (command, '\n'))
return FALSE;
while (g_ascii_isspace (*command))
command++;
if (g_ascii_strncasecmp (command, "REGISTER", 8) ||
(command[8] && !g_ascii_isspace (command[8])))
return FALSE;
if (!strstr (command, "{password}"))
return FALSE;
for (p = command; (p = strchr (p, '{')) != NULL; )
{
const char *end = strchr (p, '}');
char *token;
gboolean allowed;
if (!end)
return FALSE;
token = g_strndup (p, end - p + 1);
allowed = !strcmp (token, "{password}") || !strcmp (token, "{email}") ||
!strcmp (token, "{nick}") || !strcmp (token, "{nickname}") ||
!strcmp (token, "{account}");
g_free (token);
if (!allowed)
return FALSE;
p = end + 1;
}
return TRUE;
}
static void
servlist_onboarding_set_string (char **dest, const char *value)
{
if (!value || !*value)
return;
g_free (*dest);
*dest = g_strdup (value);
}
static gboolean
servlist_onboarding_ptr_array_contains (GPtrArray *array, const char *value)
{
guint i;
if (!array || !value)
return FALSE;
for (i = 0; i < array->len; i++)
{
if (!g_ascii_strcasecmp (g_ptr_array_index (array, i), value))
return TRUE;
}
return FALSE;
}
static void
servlist_onboarding_add_starter_channel (ServlistOnboardingChoice *choice, const char *channel)
{
if (!choice || !channel || !strchr ("#&+!", channel[0]))
return;
if (!servlist_onboarding_ptr_array_contains (choice->starter_channels, channel))
g_ptr_array_add (choice->starter_channels, g_strdup (channel));
}
/* Small bounded JSON reader. Atlas responses are deliberately parsed here
* without adding json-glib or libsoup to every ZoiteChat platform build. It
* understands the JSON types needed by the read-only Atlas feed and ignores
* additive fields it does not know about. */
static const char *
servlist_json_skip_ws (const char *p, const char *end)
{
while (p < end && g_ascii_isspace (*p))
p++;
return p;
}
static const char *
servlist_json_string_end (const char *p, const char *end)
{
if (p >= end || *p != '"')
return NULL;
p++;
while (p < end)
{
if (*p == '\\')
{
p += 2;
continue;
}
if (*p == '"')
return p + 1;
p++;
}
return NULL;
}
static const char *
servlist_json_value_end (const char *p, const char *end)
{
char open;
char close;
int depth;
p = servlist_json_skip_ws (p, end);
if (p >= end)
return NULL;
if (*p == '"')
return servlist_json_string_end (p, end);
if (*p != '{' && *p != '[')
{
while (p < end && *p != ',' && *p != '}' && *p != ']')
p++;
return p;
}
open = *p;
close = open == '{' ? '}' : ']';
depth = 1;
p++;
while (p < end && depth > 0)
{
if (*p == '"')
{
p = servlist_json_string_end (p, end);
if (!p)
return NULL;
continue;
}
if (*p == open)
depth++;
else if (*p == close)
depth--;
else if ((open == '{' && *p == '[') || (open == '[' && *p == '{'))
{
const char *nested = servlist_json_value_end (p, end);
if (!nested)
return NULL;
p = nested;
continue;
}
p++;
}
return depth == 0 ? p : NULL;
}
static char *
servlist_json_dup_string (const char *p, const char *end, const char **after)
{
GString *out;
gunichar uc;
char utf8[7];
int n;
p = servlist_json_skip_ws (p, end);
if (p >= end || *p != '"')
return NULL;
p++;
out = g_string_new (NULL);
while (p < end)
{
if (*p == '"')
{
p++;
if (after)
*after = p;
return g_string_free (out, FALSE);
}
if (*p != '\\')
{
g_string_append_c (out, *p++);
continue;
}
p++;
if (p >= end)
break;
switch (*p++)
{
case '"': g_string_append_c (out, '"'); break;
case '\\': g_string_append_c (out, '\\'); break;
case '/': g_string_append_c (out, '/'); break;
case 'b': g_string_append_c (out, '\b'); break;
case 'f': g_string_append_c (out, '\f'); break;
case 'n': g_string_append_c (out, '\n'); break;
case 'r': g_string_append_c (out, '\r'); break;
case 't': g_string_append_c (out, '\t'); break;
case 'u':
if (end - p < 4 || !g_ascii_isxdigit (p[0]) || !g_ascii_isxdigit (p[1]) ||
!g_ascii_isxdigit (p[2]) || !g_ascii_isxdigit (p[3]))
goto invalid;
{
char hex[5];
memcpy (hex, p, 4);
hex[4] = 0;
uc = (gunichar) strtoul (hex, NULL, 16);
p += 4;
if (uc >= 0xd800 && uc <= 0xdbff)
{
gunichar low;
if (end - p < 6 || p[0] != '\\' || p[1] != 'u' ||
!g_ascii_isxdigit (p[2]) || !g_ascii_isxdigit (p[3]) ||
!g_ascii_isxdigit (p[4]) || !g_ascii_isxdigit (p[5]))
goto invalid;
memcpy (hex, p + 2, 4);
hex[4] = 0;
low = (gunichar) strtoul (hex, NULL, 16);
if (low < 0xdc00 || low > 0xdfff)
goto invalid;
p += 6;
uc = 0x10000 + ((uc - 0xd800) << 10) + (low - 0xdc00);
}
else if (uc >= 0xdc00 && uc <= 0xdfff)
goto invalid;
n = g_unichar_to_utf8 (uc, utf8);
utf8[n] = 0;
g_string_append (out, utf8);
}
break;
default:
goto invalid;
}
}
invalid:
g_string_free (out, TRUE);
return NULL;
}
static const char *
servlist_json_object_member (const char *object, const char *end, const char *wanted)
{
const char *p;
char *key;
p = servlist_json_skip_ws (object, end);
if (p >= end || *p != '{')
return NULL;
p++;
while (p < end)
{
const char *after_key;
const char *value;
const char *next;
p = servlist_json_skip_ws (p, end);
if (p >= end || *p == '}')
return NULL;
key = servlist_json_dup_string (p, end, &after_key);
if (!key)
return NULL;
p = servlist_json_skip_ws (after_key, end);
if (p >= end || *p != ':')
{
g_free (key);
return NULL;
}
value = servlist_json_skip_ws (p + 1, end);
if (!strcmp (key, wanted))
{
g_free (key);
return value;
}
g_free (key);
next = servlist_json_value_end (value, end);
if (!next)
return NULL;
p = servlist_json_skip_ws (next, end);
if (p < end && *p == ',')
p++;
}
return NULL;
}
static char *
servlist_json_member_string (const char *object, const char *end, const char *key)
{
const char *value = servlist_json_object_member (object, end, key);
return value ? servlist_json_dup_string (value, end, NULL) : NULL;
}
static int
servlist_json_member_int (const char *object, const char *end, const char *key, int fallback)
{
const char *value = servlist_json_object_member (object, end, key);
char *tail;
long parsed;
if (!value)
return fallback;
parsed = strtol (value, &tail, 10);
if (tail == value || parsed < G_MININT || parsed > G_MAXINT)
return fallback;
return (int) parsed;
}
static gboolean
servlist_json_member_bool (const char *object, const char *end, const char *key, gboolean fallback)
{
const char *value = servlist_json_object_member (object, end, key);
if (!value)
return fallback;
if (!strncmp (value, "true", 4) || *value == '1')
return TRUE;
if (!strncmp (value, "false", 5) || *value == '0')
return FALSE;
return fallback;
}
static gboolean
servlist_json_array_contains_string (const char *array, const char *end, const char *wanted)
{
const char *p;
p = servlist_json_skip_ws (array, end);
if (p >= end || *p != '[')
return FALSE;
p++;
while (p < end)
{
const char *after;
char *value;
p = servlist_json_skip_ws (p, end);
if (p >= end || *p == ']')
break;
if (*p == '"')
{
value = servlist_json_dup_string (p, end, &after);
if (!value)
return FALSE;
if (!g_ascii_strcasecmp (value, wanted))
{
g_free (value);
return TRUE;
}
g_free (value);
p = after;
}
else
{
p = servlist_json_value_end (p, end);
if (!p)
return FALSE;
}
p = servlist_json_skip_ws (p, end);
if (p < end && *p == ',')
p++;
}
return FALSE;
}
static gboolean
servlist_json_array_has_prefix_ci (const char *array, const char *end, const char *prefix)
{
const char *p;
gsize prefix_len;
if (!prefix)
return FALSE;
prefix_len = strlen (prefix);
p = servlist_json_skip_ws (array, end);
if (p >= end || *p != '[')
return FALSE;
p++;
while (p < end)
{
const char *after;
char *value;
p = servlist_json_skip_ws (p, end);
if (p >= end || *p == ']')
break;
if (*p == '"')
{
value = servlist_json_dup_string (p, end, &after);
if (!value)
return FALSE;
if (!g_ascii_strncasecmp (value, prefix, prefix_len))
{
g_free (value);
return TRUE;
}
g_free (value);
p = after;
}
else
{
p = servlist_json_value_end (p, end);
if (!p)
return FALSE;
}
p = servlist_json_skip_ws (p, end);
if (p < end && *p == ',')
p++;
}
return FALSE;
}
static gboolean
servlist_json_array_contains_text_ci (const char *array, const char *end, const char *wanted)
{
const char *p;
char *wanted_folded;
gboolean found = FALSE;
if (!wanted)
return FALSE;
wanted_folded = g_ascii_strdown (wanted, -1);
p = servlist_json_skip_ws (array, end);
if (p >= end || *p != '[')
goto done;
p++;
while (p < end)
{
const char *after;
char *value;
char *folded;
p = servlist_json_skip_ws (p, end);
if (p >= end || *p == ']')
break;
if (*p == '"')
{
value = servlist_json_dup_string (p, end, &after);
if (!value)
break;
folded = g_ascii_strdown (value, -1);
found = strstr (folded, wanted_folded) != NULL;
g_free (folded);
g_free (value);
if (found)
break;
p = after;
}
else
{
p = servlist_json_value_end (p, end);
if (!p)
break;
}
p = servlist_json_skip_ws (p, end);
if (p < end && *p == ',')
p++;
}
done:
g_free (wanted_folded);
return found;
}
static void
servlist_json_add_string_array (const char *array, const char *end, ServlistOnboardingChoice *choice)
{
const char *p;
p = servlist_json_skip_ws (array, end);
if (p >= end || *p != '[')
return;
p++;
while (p < end)
{
const char *after;
char *value;
p = servlist_json_skip_ws (p, end);
if (p >= end || *p == ']')
break;
if (*p != '"')
{
p = servlist_json_value_end (p, end);
if (!p)
break;
}
else
{
value = servlist_json_dup_string (p, end, &after);
if (!value)
break;
servlist_onboarding_add_starter_channel (choice, value);
g_free (value);
p = after;
}
p = servlist_json_skip_ws (p, end);
if (p < end && *p == ',')
p++;
}
}
static ServlistOnboardingChoice *
servlist_onboarding_find_choice (ServlistOnboardingState *state, const char *name, const char *slug)
{
guint i;
for (i = 0; i < state->choices->len; i++)
{
ServlistOnboardingChoice *choice = g_ptr_array_index (state->choices, i);
if (slug && *slug && choice->slug && !g_ascii_strcasecmp (choice->slug, slug))
return choice;
if (name && *name && choice->name && !g_ascii_strcasecmp (choice->name, name))
return choice;
}
return NULL;
}
static void
servlist_onboarding_parse_endpoint (ServlistOnboardingChoice *choice, const char *object, const char *end)
{
const char *array;
const char *item;
const char *item_end;
char *host;
int port;
gboolean tls;
array = servlist_json_object_member (object, end, "servers");
if (!array)
array = servlist_json_object_member (object, end, "endpoints");
if (array)
{
item = servlist_json_skip_ws (array, end);
if (*item == '[')
{
item = servlist_json_skip_ws (item + 1, end);
if (item < end && *item == '{')
{
item_end = servlist_json_value_end (item, end);
if (item_end)
{
object = item;
end = item_end;
}
}
}
}
host = servlist_json_member_string (object, end, "host");
if (!host)
host = servlist_json_member_string (object, end, "hostname");
if (!host)
host = servlist_json_member_string (object, end, "server");
if (!host || !servlist_onboarding_endpoint_is_safe (host))
{
g_free (host);
return;
}
#ifdef USE_OPENSSL
port = servlist_json_member_int (object, end, "tls_port", 0);
tls = port > 0;
if (port <= 0)
{
port = servlist_json_member_int (object, end, "plain_port", 0);
tls = FALSE;
}
#else
port = servlist_json_member_int (object, end, "plain_port", 0);
tls = FALSE;
#endif
if (port <= 0)
port = servlist_json_member_int (object, end, "port", 0);
if (port <= 0 || port > 65535)
port = tls ? 6697 : 6667;
#ifdef USE_OPENSSL
tls = servlist_json_member_bool (object, end, "tls", tls);
#endif
g_free (choice->endpoint_host);
choice->endpoint_host = host;
choice->endpoint_port = port;
choice->endpoint_tls = tls;
choice->atlas_approved = TRUE;
}
static void
servlist_onboarding_parse_services (ServlistOnboardingChoice *choice, const char *object, const char *end)
{
const char *services = servlist_json_object_member (object, end, "services");
const char *caps = servlist_json_object_member (object, end, "capabilities");
const char *nicks;
char *family;
if (services && *services == '{')
{
const char *services_end = servlist_json_value_end (services, end);
family = services_end ? servlist_json_member_string (services, services_end, "family") : NULL;
if (family)
{
servlist_onboarding_set_string (&choice->services_family, family);
g_free (family);
}
choice->services_confidence = services_end ? servlist_json_member_int (services, services_end, "confidence", choice->services_confidence) : choice->services_confidence;
choice->services_present = services_end ? servlist_json_member_bool (services, services_end, "services_present", choice->services_present) : choice->services_present;
nicks = services_end ? servlist_json_object_member (services, services_end, "service_nicks") : NULL;
if (!nicks && services_end)
nicks = servlist_json_object_member (services, services_end, "nicks");
if (nicks && servlist_json_array_contains_string (nicks, services_end, "NickServ"))
choice->has_nickserv = TRUE;
}
else
{
family = servlist_json_member_string (object, end, "services_family");
if (family)
{
servlist_onboarding_set_string (&choice->services_family, family);
g_free (family);
}
choice->services_confidence = servlist_json_member_int (object, end, "services_confidence", choice->services_confidence);
choice->services_present = servlist_json_member_bool (object, end, "services_present", choice->services_present);
}
if (caps && servlist_json_array_has_prefix_ci (caps, end, "sasl"))
choice->supports_sasl = TRUE;
}
static void
servlist_onboarding_parse_verified_metadata (ServlistOnboardingChoice *choice, const char *object, const char *end)
{
const char *onboarding = servlist_json_object_member (object, end, "onboarding");
const char *onboarding_end;
const char *starter;
const char *account;
const char *account_end;
const char *registration;
const char *registration_end;
char *value;
if (!onboarding || *onboarding != '{')
return;
onboarding_end = servlist_json_value_end (onboarding, end);
if (!onboarding_end)
return;
choice->onboarding_verified = servlist_json_member_bool (onboarding, onboarding_end, "verified", FALSE);
if (!choice->onboarding_verified)
return;
value = servlist_json_member_string (onboarding, onboarding_end, "help_channel");
if (value && strchr ("#&+!", value[0]))
servlist_onboarding_set_string (&choice->help_channel, value);
g_free (value);
starter = servlist_json_object_member (onboarding, onboarding_end, "starter_channels");
if (starter)
servlist_json_add_string_array (starter, onboarding_end, choice);
account = servlist_json_object_member (onboarding, onboarding_end, "account");
if (!account || *account != '{')
return;
account_end = servlist_json_value_end (account, onboarding_end);
if (!account_end)
return;
value = servlist_json_member_string (account, account_end, "identify_method");
if (value)
{
servlist_onboarding_set_string (&choice->identify_method, value);
if (!g_ascii_strcasecmp (value, "sasl"))
choice->supports_sasl = TRUE;
g_free (value);
}
registration = servlist_json_object_member (account, account_end, "registration");
if (!registration || *registration != '{')
return;
registration_end = servlist_json_value_end (registration, account_end);
if (!registration_end || !servlist_json_member_bool (registration, registration_end, "verified", FALSE))
return;
value = servlist_json_member_string (registration, registration_end, "service");
if (value && servlist_onboarding_service_target_is_safe (value))
servlist_onboarding_set_string (&choice->registration_service, value);
g_free (value);
value = servlist_json_member_string (registration, registration_end, "command");
if (value && servlist_onboarding_registration_template_is_safe (value))
servlist_onboarding_set_string (&choice->registration_template, value);
g_free (value);
choice->registration_email_required = servlist_json_member_bool (registration, registration_end, "requires_email", FALSE);
choice->registration_verified = choice->registration_service && choice->registration_template &&
(!choice->registration_email_required || strstr (choice->registration_template, "{email}"));
}
static void
servlist_onboarding_apply_atlas_document (ServlistOnboardingState *state, const char *json, gboolean client_feed)
{
const char *end;
const char *array;
const char *p;
if (!json || !*json)
return;
end = json + strlen (json);
array = servlist_json_object_member (json, end, "networks");
if (!array)
array = servlist_json_object_member (json, end, "data");
if (!array || *array != '[')
return;
p = array + 1;
while (p < end)
{
const char *object_end;
char *name;
char *slug;
char *description;
char *homepage;
ServlistOnboardingChoice *choice;
gboolean added = FALSE;
p = servlist_json_skip_ws (p, end);
if (p >= end || *p == ']')
break;
if (*p != '{')
{
p = servlist_json_value_end (p, end);
if (!p)
break;
goto next_value;
}
object_end = servlist_json_value_end (p, end);
if (!object_end)
break;
name = servlist_json_member_string (p, object_end, "name");
if (!name)
name = servlist_json_member_string (p, object_end, "network");
slug = servlist_json_member_string (p, object_end, "slug");
if (!slug)
slug = servlist_json_member_string (p, object_end, "id");
choice = servlist_onboarding_find_choice (state, name, slug);
if (!choice && client_feed && name && *name)
{
choice = servlist_onboarding_choice_new (NULL, name);
g_ptr_array_add (state->choices, choice);
added = TRUE;
}
if (choice)
{
if (slug && servlist_onboarding_slug_is_safe (slug))
servlist_onboarding_set_string (&choice->slug, slug);
description = servlist_json_member_string (p, object_end, "description");
homepage = servlist_json_member_string (p, object_end, "homepage");
if (!homepage)
homepage = servlist_json_member_string (p, object_end, "homepage_url");
if (!homepage)
homepage = servlist_json_member_string (p, object_end, "website");
if (description)
servlist_onboarding_set_string (&choice->description, description);
if (homepage)
servlist_onboarding_set_string (&choice->homepage, homepage);
choice->users = servlist_json_member_int (p, object_end, "users", choice->users);
if (choice->users < 0)
choice->users = servlist_json_member_int (p, object_end, "users_total", choice->users);
servlist_onboarding_parse_services (choice, p, object_end);
if (client_feed)
{
servlist_onboarding_parse_endpoint (choice, p, object_end);
servlist_onboarding_parse_verified_metadata (choice, p, object_end);
}
g_free (description);
g_free (homepage);
if (added && !choice->atlas_approved)
{
g_ptr_array_remove (state->choices, choice);
choice = NULL;
}
}
g_free (name);
g_free (slug);
p = object_end;
next_value:
p = servlist_json_skip_ws (p, end);
if (p < end && *p == ',')
p++;
}
}
static void
servlist_onboarding_add_popular_channel (ServlistOnboardingChoice *choice, const char *channel)
{
if (!choice || !channel || !*channel || !strchr ("#&+!", channel[0]))
return;
if (servlist_onboarding_ptr_array_contains (choice->starter_channels, channel) ||
servlist_onboarding_ptr_array_contains (choice->popular_channels, channel))
return;
if (choice->popular_channels->len >= 12)
return;
g_ptr_array_add (choice->popular_channels, g_strdup (channel));
}
static void
servlist_onboarding_apply_atlas_detail (ServlistOnboardingChoice *choice, const char *json)
{
const char *end;
const char *data;
const char *data_end;
const char *evidence;
const char *channels;
const char *p;
char *value;
if (!choice || !json || !*json)
return;
end = json + strlen (json);
data = servlist_json_object_member (json, end, "data");
if (!data || *data != '{')
return;
data_end = servlist_json_value_end (data, end);
if (!data_end)
return;
value = servlist_json_member_string (data, data_end, "description");
if (value)
{
servlist_onboarding_set_string (&choice->description, value);
g_free (value);
}
value = servlist_json_member_string (data, data_end, "homepage_url");
if (value)
{
servlist_onboarding_set_string (&choice->homepage, value);
g_free (value);
}
choice->users = servlist_json_member_int (data, data_end, "users_total", choice->users);
servlist_onboarding_parse_services (choice, data, data_end);
evidence = servlist_json_object_member (data, data_end, "services_evidence");
if (evidence && servlist_json_array_contains_text_ci (evidence, data_end, "nickserv"))
choice->has_nickserv = TRUE;
if (choice->services_family && !g_ascii_strcasecmp (choice->services_family, "Ergo integrated services") &&
choice->services_confidence >= 80)
choice->has_nickserv = TRUE;
servlist_onboarding_parse_verified_metadata (choice, data, data_end);
g_ptr_array_set_size (choice->popular_channels, 0);
channels = servlist_json_object_member (data, data_end, "channels");
if (channels && *channels == '[')
{
p = channels + 1;
while (p < data_end && choice->popular_channels->len < 12)
{
const char *item_end;
char *name;
p = servlist_json_skip_ws (p, data_end);
if (p >= data_end || *p == ']')
break;
if (*p != '{')
{
p = servlist_json_value_end (p, data_end);
if (!p)
break;
}
else
{
item_end = servlist_json_value_end (p, data_end);
if (!item_end)
break;
name = servlist_json_member_string (p, item_end, "name");
if (name)
{
servlist_onboarding_add_popular_channel (choice, name);
g_free (name);
}
p = item_end;
}
p = servlist_json_skip_ws (p, data_end);
if (p < data_end && *p == ',')
p++;
}
}
choice->detail_loaded = TRUE;
}
static gboolean
servlist_onboarding_common_registration (ServlistOnboardingChoice *choice)
{
if (!choice || !choice->has_nickserv || !choice->services_family || choice->services_confidence < 90)
return FALSE;
return !g_ascii_strcasecmp (choice->services_family, "Anope") ||
!g_ascii_strcasecmp (choice->services_family, "Atheme") ||
!g_ascii_strcasecmp (choice->services_family, "Ergo integrated services");
}
static gboolean
servlist_onboarding_can_prepare_registration (ServlistOnboardingChoice *choice)
{
return choice && (choice->registration_verified || servlist_onboarding_common_registration (choice));
}
static char *
servlist_atlas_http_get (const char *path, GCancellable *cancellable, GError **error)
{
GSocketClient *client;
GSocketConnection *connection;
GOutputStream *output;
GInputStream *input;
GByteArray *response;
char request[512];
char buffer[8192];
gsize written;
gssize got;
char *raw;
char *body;
gsize body_offset;
client = g_socket_client_new ();
g_socket_client_set_tls (client, TRUE);
g_socket_client_set_timeout (client, 8);
connection = g_socket_client_connect_to_host (client, ONBOARDING_ATLAS_HOST, 443, cancellable, error);
g_object_unref (client);
if (!connection)
return NULL;
output = g_io_stream_get_output_stream (G_IO_STREAM (connection));
input = g_io_stream_get_input_stream (G_IO_STREAM (connection));
g_snprintf (request, sizeof request,
"GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: ZoiteChat/%s\r\nAccept: application/json\r\nConnection: close\r\n\r\n",
path, ONBOARDING_ATLAS_HOST, PACKAGE_VERSION);
if (!g_output_stream_write_all (output, request, strlen (request), &written, cancellable, error) ||
!g_output_stream_flush (output, cancellable, error))
{
g_object_unref (connection);
return NULL;
}
response = g_byte_array_new ();
for (;;)
{
got = g_input_stream_read (input, buffer, sizeof buffer, cancellable, error);
if (got < 0)
{
g_byte_array_free (response, TRUE);
g_object_unref (connection);
return NULL;
}
if (got == 0)
break;
if (response->len + (guint) got > ONBOARDING_ATLAS_MAX_RESPONSE)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
"Network Atlas response was too large");
g_byte_array_free (response, TRUE);
g_object_unref (connection);
return NULL;
}
g_byte_array_append (response, (const guint8 *) buffer, (guint) got);
}
g_object_unref (connection);
g_byte_array_append (response, (const guint8 *) "\0", 1);
raw = (char *) g_byte_array_free (response, FALSE);
if (!g_str_has_prefix (raw, "HTTP/1.0 200 ") && !g_str_has_prefix (raw, "HTTP/1.1 200 "))
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, "Network Atlas returned a non-success HTTP status");
g_free (raw);
return NULL;
}
body = strstr (raw, "\r\n\r\n");
if (!body)
{
g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA, "Network Atlas returned an invalid HTTP response");
g_free (raw);
return NULL;
}
body += 4;
body_offset = (gsize) (body - raw);
memmove (raw, body, strlen (raw + body_offset) + 1);
return raw;
}
static void
servlist_atlas_detail_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable)
{
ServlistAtlasDetailRequest *request = task_data;
ServlistAtlasDetailResult *result = g_new0 (ServlistAtlasDetailResult, 1);
GError *error = NULL;
char *path;
(void) source_object;
result->slug = g_strdup (request->slug);
result->generation = request->generation;
path = g_strdup_printf (ONBOARDING_ATLAS_PUBLIC_PATH "/%s", request->slug);
result->json = servlist_atlas_http_get (path, cancellable, &error);
g_free (path);
if (!result->json && error && !g_cancellable_is_cancelled (cancellable))
result->error = g_strdup (error->message);
g_clear_error (&error);
g_task_return_pointer (task, result, (GDestroyNotify) servlist_atlas_detail_result_free);
}
static void
servlist_atlas_fetch_thread (GTask *task, gpointer source_object, gpointer task_data, GCancellable *cancellable)
{
ServlistAtlasFetchResult *result = g_new0 (ServlistAtlasFetchResult, 1);
GError *client_error = NULL;
GError *public_error = NULL;
(void) source_object;
(void) task_data;
result->client_json = servlist_atlas_http_get (ONBOARDING_ATLAS_CLIENT_PATH, cancellable, &client_error);
if (!g_cancellable_is_cancelled (cancellable))
result->public_json = servlist_atlas_http_get (ONBOARDING_ATLAS_PUBLIC_PATH, cancellable, &public_error);
if (!result->client_json && !result->public_json && !g_cancellable_is_cancelled (cancellable))
{
result->error = g_strdup (client_error ? client_error->message :
public_error ? public_error->message : "Network Atlas is unavailable");
}
g_clear_error (&client_error);
g_clear_error (&public_error);
g_task_return_pointer (task, result, (GDestroyNotify) servlist_atlas_fetch_result_free);
}
static ServlistOnboardingChoice *
servlist_onboarding_selected_choice (ServlistOnboardingState *state)
{
int index = gtk_combo_box_get_active (GTK_COMBO_BOX (state->network_combo));
if (index < 0 || (guint) index >= state->choices->len)
return NULL;
return g_ptr_array_index (state->choices, (guint) index);
}
static void
servlist_onboarding_update_network_info (ServlistOnboardingState *state)
{
ServlistOnboardingChoice *choice = servlist_onboarding_selected_choice (state);
GString *text = g_string_new (NULL);
if (!choice)
g_string_append (text, _("Choose an IRC network to continue."));
else
{
if (choice->description && *choice->description)
g_string_append (text, choice->description);
else
g_string_append_printf (text, _("%s is available in ZoiteChat's network list."), choice->name);
if (choice->users >= 0)
g_string_append_printf (text, _("\n\nAtlas recently saw about %d users on this network."), choice->users);
if (choice->services_present && choice->services_family)
{
if (choice->services_confidence >= 0)
g_string_append_printf (text, _("\nAccount services: %s (%d%% detection confidence)."),
choice->services_family, choice->services_confidence);
else
g_string_append_printf (text, _("\nAccount services: %s."), choice->services_family);
}
if (choice->onboarding_verified)
g_string_append (text, _("\nThis network publishes verified setup guidance through the ZoiteChat Network Atlas."));
else if (choice->atlas_approved)
g_string_append (text, _("\nConnection endpoint approved by the ZoiteChat Network Atlas."));
}
gtk_label_set_text (GTK_LABEL (state->network_info), text->str);
g_string_free (text, TRUE);
}
static void
servlist_onboarding_detail_ready (GObject *source, GAsyncResult *async_result, gpointer userdata)
{
ServlistOnboardingState *state = userdata;
ServlistAtlasDetailResult *result;
GError *error = NULL;
ServlistOnboardingChoice *choice = NULL;
(void) source;
result = g_task_propagate_pointer (G_TASK (async_result), &error);
if (state->detail_pending > 0)
state->detail_pending--;
if (!state->closing && result && result->slug)
{
choice = servlist_onboarding_find_choice (state, NULL, result->slug);
if (choice && result->json)
servlist_onboarding_apply_atlas_detail (choice, result->json);
else if (choice)
choice->detail_loaded = TRUE;
if (choice && result->generation == state->detail_generation)
{
servlist_onboarding_update_network_info (state);
if (state->account_skip)
servlist_onboarding_account_changed (GTK_TOGGLE_BUTTON (state->account_skip), state);
if (state->channel_suggestions && gtk_notebook_get_current_page (GTK_NOTEBOOK (state->notebook)) == 4)
servlist_onboarding_refresh_channel_suggestions (state);
}
}
if (result)
servlist_atlas_detail_result_free (result);
g_clear_error (&error);
if (state->closing && !state->atlas_pending && state->detail_pending == 0)
servlist_onboarding_state_free (state);
}
static void
servlist_onboarding_start_detail (ServlistOnboardingState *state, ServlistOnboardingChoice *choice)
{
ServlistAtlasDetailRequest *request;
GTask *task;
if (!state || !choice || !servlist_onboarding_slug_is_safe (choice->slug) || choice->detail_loaded ||
!gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (state->use_atlas)))
return;
if (state->detail_cancel)
{
g_cancellable_cancel (state->detail_cancel);
g_object_unref (state->detail_cancel);
}
state->detail_cancel = g_cancellable_new ();
state->detail_generation++;
state->detail_pending++;
request = g_new0 (ServlistAtlasDetailRequest, 1);
request->slug = g_strdup (choice->slug);
request->generation = state->detail_generation;
task = g_task_new (NULL, state->detail_cancel, servlist_onboarding_detail_ready, state);
g_task_set_task_data (task, request, (GDestroyNotify) servlist_atlas_detail_request_free);
g_task_set_return_on_cancel (task, FALSE);
g_task_run_in_thread (task, servlist_atlas_detail_thread);
g_object_unref (task);
}
static void
servlist_onboarding_network_changed (GtkComboBox *combo, gpointer userdata)
{
ServlistOnboardingState *state = userdata;
(void) combo;
servlist_onboarding_update_network_info (state);
if (state->account_skip)
servlist_onboarding_account_changed (GTK_TOGGLE_BUTTON (state->account_skip), state);
servlist_onboarding_start_detail (state, servlist_onboarding_selected_choice (state));
}
static void
servlist_onboarding_refresh_combo (ServlistOnboardingState *state)
{
int old = gtk_combo_box_get_active (GTK_COMBO_BOX (state->network_combo));
guint i;
gtk_combo_box_text_remove_all (GTK_COMBO_BOX_TEXT (state->network_combo));
for (i = 0; i < state->choices->len; i++)
{
ServlistOnboardingChoice *choice = g_ptr_array_index (state->choices, i);
gtk_combo_box_text_append_text (GTK_COMBO_BOX_TEXT (state->network_combo), choice->name);
}
if (old < 0 || (guint) old >= state->choices->len)
old = prefs.hex_gui_slist_select;
if (old < 0 || (guint) old >= state->choices->len)
old = 0;
gtk_combo_box_set_active (GTK_COMBO_BOX (state->network_combo), old);
servlist_onboarding_update_network_info (state);
}
static void
servlist_onboarding_atlas_ready (GObject *source, GAsyncResult *async_result, gpointer userdata)
{
ServlistOnboardingState *state = userdata;
ServlistAtlasFetchResult *result;
GError *error = NULL;
(void) source;
result = g_task_propagate_pointer (G_TASK (async_result), &error);
state->atlas_pending = FALSE;
if (state->closing)
{
if (result)
servlist_atlas_fetch_result_free (result);
g_clear_error (&error);
if (state->detail_pending == 0)
servlist_onboarding_state_free (state);
return;
}
if (error || !result || (!result->client_json && !result->public_json))
{
const char *message = error ? error->message : result && result->error ? result->error : _("Atlas is unavailable");
char *status = g_strdup_printf (_("Network Atlas unavailable (%s). The built-in network list still works normally."), message);
gtk_label_set_text (GTK_LABEL (state->atlas_status), status);
g_free (status);
}
else
{
if (result->client_json)
servlist_onboarding_apply_atlas_document (state, result->client_json, TRUE);
if (result->public_json)
servlist_onboarding_apply_atlas_document (state, result->public_json, FALSE);
servlist_onboarding_refresh_combo (state);
gtk_label_set_text (GTK_LABEL (state->atlas_status),
_("Network Atlas loaded. Current approved endpoints and setup hints are available where published."));
}
if (result)
servlist_atlas_fetch_result_free (result);
g_clear_error (&error);
}
static void
servlist_onboarding_start_atlas (ServlistOnboardingState *state)
{
GTask *task;
if (state->atlas_pending)
return;
state->atlas_pending = TRUE;
gtk_label_set_text (GTK_LABEL (state->atlas_status), _("Checking the ZoiteChat Network Atlas..."));
task = g_task_new (NULL, state->atlas_cancel, servlist_onboarding_atlas_ready, state);
g_task_set_return_on_cancel (task, FALSE);
g_task_run_in_thread (task, servlist_atlas_fetch_thread);
g_object_unref (task);
}
static void
servlist_onboarding_account_changed (GtkToggleButton *button, gpointer userdata)
{
ServlistOnboardingState *state = userdata;
ServlistOnboardingChoice *choice;
gboolean existing;
gboolean creating;
gboolean can_prepare;
gboolean needs_email;
GString *info;
(void) button;
choice = servlist_onboarding_selected_choice (state);
existing = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (state->account_existing));
creating = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (state->account_new));
can_prepare = creating && servlist_onboarding_can_prepare_registration (choice);
needs_email = can_prepare && (!choice->registration_verified || choice->registration_email_required);
gtk_widget_set_visible (state->account_label, existing);
gtk_widget_set_visible (state->account_entry, existing);
gtk_widget_set_visible (state->password_label, existing || can_prepare);
gtk_widget_set_visible (state->password_entry, existing || can_prepare);
gtk_widget_set_visible (state->password_confirm_label, can_prepare);
gtk_widget_set_visible (state->password_confirm_entry, can_prepare);
gtk_widget_set_visible (state->remember_password, existing || can_prepare);
gtk_widget_set_visible (state->email_label, needs_email);
gtk_widget_set_visible (state->email_entry, needs_email);
info = g_string_new (NULL);
if (!choice)
g_string_append (info, _("Choose a network first."));
else if (existing)
{
if (choice->supports_sasl || (choice->net && (choice->net->logintype == LOGIN_SASL ||
choice->net->logintype == LOGIN_SASL_SCRAM_SHA_1 ||
choice->net->logintype == LOGIN_SASL_SCRAM_SHA_256 ||
choice->net->logintype == LOGIN_SASL_SCRAM_SHA_512)))
g_string_append (info, _("ZoiteChat can log in to your network account during connection with SASL, so you are already identified when you enter channels."));
else if (choice->has_nickserv)
g_string_append (info, _("This network appears to use NickServ. ZoiteChat can save the password and identify you after connecting."));
else
g_string_append (info, _("ZoiteChat will preserve this network's existing login method. If it uses an unusual account system, Advanced setup is still available."));
}
else if (creating)
{
if (choice->registration_verified)
{
g_string_append_printf (info, _("This network publishes an operator-verified registration recipe through Atlas. ZoiteChat will connect first and ask before sending the one-time registration request to %s."),
choice->registration_service ? choice->registration_service : "NickServ");
}
else if (servlist_onboarding_common_registration (choice))
{
g_string_append_printf (info, _("Atlas detected %s with %d%% confidence and found NickServ. These services normally register accounts with a password and email address. ZoiteChat can prepare that common registration request, but the network may customize or disable registration, so nothing is sent until you confirm."),
choice->services_family, choice->services_confidence);
}
else if (choice->has_nickserv)
{
g_string_append (info, _("Atlas found NickServ, but ZoiteChat does not have enough verified information to safely guess a password-bearing registration command. After connecting, ZoiteChat can ask NickServ for its registration help instead."));
}
else if (choice->services_present && choice->services_family)
{
g_string_append_printf (info, _("Atlas detected %s account services"), choice->services_family);
if (choice->services_confidence >= 0)
g_string_append_printf (info, _(" with %d%% confidence"), choice->services_confidence);
g_string_append (info, _(". ZoiteChat will connect you first, but will not guess a command containing your password."));
}
else if (state->atlas_pending || (choice->slug && !choice->detail_loaded))
g_string_append (info, _("ZoiteChat is still checking Atlas for this network's account setup information. You can continue while it loads."));
else
g_string_append (info, _("This network has not published enough account-registration information for automatic setup. You can still connect now and use the network's own help."));
}
else
g_string_append (info, _("An IRC account is optional on many networks. You can start chatting with only a nickname and set up an account later."));
gtk_label_set_text (GTK_LABEL (state->account_info), info->str);
g_string_free (info, TRUE);
}
static void
servlist_onboarding_clear_box (GtkWidget *box)
{
GList *children = gtk_container_get_children (GTK_CONTAINER (box));
GList *node;
for (node = children; node; node = node->next)
gtk_widget_destroy (GTK_WIDGET (node->data));
g_list_free (children);
}
static void
servlist_onboarding_refresh_channel_suggestions (ServlistOnboardingState *state)
{
ServlistOnboardingChoice *choice = servlist_onboarding_selected_choice (state);
guint i;
guint shown = 0;
servlist_onboarding_clear_box (state->channel_suggestions);
if (choice && choice->starter_channels->len > 0)
{
GtkWidget *heading = gtk_label_new (_("Recommended starting channels"));
gtk_widget_set_halign (heading, GTK_ALIGN_START);
gtk_box_pack_start (GTK_BOX (state->channel_suggestions), heading, FALSE, FALSE, 0);
for (i = 0; i < choice->starter_channels->len && shown < 8; i++)
{
const char *channel = g_ptr_array_index (choice->starter_channels, i);
GtkWidget *check = gtk_check_button_new_with_label (channel);
g_object_set_data_full (G_OBJECT (check), "zoitechat-onboarding-channel", g_strdup (channel), g_free);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (check), shown == 0);
gtk_box_pack_start (GTK_BOX (state->channel_suggestions), check, FALSE, FALSE, 0);
shown++;
}
}
if (choice && choice->help_channel && !servlist_onboarding_ptr_array_contains (choice->starter_channels, choice->help_channel))
{
GtkWidget *check = gtk_check_button_new_with_label (choice->help_channel);
g_object_set_data_full (G_OBJECT (check), "zoitechat-onboarding-channel", g_strdup (choice->help_channel), g_free);
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (check), shown == 0);
gtk_box_pack_start (GTK_BOX (state->channel_suggestions), check, FALSE, FALSE, 0);
shown++;
}
if (choice && choice->popular_channels->len > 0)
{
GtkWidget *heading = gtk_label_new (_("Popular public channels recently seen by Atlas"));
GtkWidget *note = gtk_label_new (_("These are based on recent channel counts, not endorsements. Nothing here is joined unless you select it."));
gtk_widget_set_halign (heading, GTK_ALIGN_START);
gtk_label_set_line_wrap (GTK_LABEL (note), TRUE);
gtk_label_set_xalign (GTK_LABEL (note), 0.0);
gtk_box_pack_start (GTK_BOX (state->channel_suggestions), heading, FALSE, FALSE, shown ? 8 : 0);
gtk_box_pack_start (GTK_BOX (state->channel_suggestions), note, FALSE, FALSE, 0);
for (i = 0; i < choice->popular_channels->len && i < 8; i++)
{
const char *channel = g_ptr_array_index (choice->popular_channels, i);
GtkWidget *check = gtk_check_button_new_with_label (channel);
g_object_set_data_full (G_OBJECT (check), "zoitechat-onboarding-channel", g_strdup (channel), g_free);
gtk_box_pack_start (GTK_BOX (state->channel_suggestions), check, FALSE, FALSE, 0);
shown++;
}
}
if (!shown)
{
GtkWidget *label = gtk_label_new (choice && choice->slug && !choice->detail_loaded ?
_("Atlas is still loading channel information. You can wait a moment, type a channel below, or connect without joining one yet.") :
_("No beginner channel recommendations are available for this network. You can leave this blank and choose a room after connecting."));
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_box_pack_start (GTK_BOX (state->channel_suggestions), label, FALSE, FALSE, 0);
}
gtk_widget_show_all (state->channel_suggestions);
}
static ServlistOnboardingAccountMode
servlist_onboarding_account_mode (ServlistOnboardingState *state)
{
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (state->account_existing)))
return ONBOARDING_ACCOUNT_EXISTING;
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (state->account_new)))
return ONBOARDING_ACCOUNT_NEW;
return ONBOARDING_ACCOUNT_SKIP;
}
static void
servlist_onboarding_update_summary (ServlistOnboardingState *state)
{
ServlistOnboardingChoice *choice = servlist_onboarding_selected_choice (state);
ServlistOnboardingAccountMode mode = servlist_onboarding_account_mode (state);
const char *nick = gtk_entry_get_text (GTK_ENTRY (state->nick_entry));
GString *summary = g_string_new (NULL);
g_string_append_printf (summary, _("Network: %s\nNickname: %s\n"),
choice ? choice->name : _("(none)"), nick && *nick ? nick : _("(none)"));
if (mode == ONBOARDING_ACCOUNT_EXISTING)
g_string_append (summary, _("Account: use an existing network account\n"));
else if (mode == ONBOARDING_ACCOUNT_NEW)
{
if (choice && choice->registration_verified)
g_string_append (summary, _("Account: connect first, then use operator-verified Atlas registration guidance\n"));
else if (servlist_onboarding_common_registration (choice))
g_string_append (summary, _("Account: connect first, then confirm the common registration flow Atlas detected\n"));
else if (choice && choice->has_nickserv)
g_string_append (summary, _("Account: connect first, then ask NickServ for registration help\n"));
else
g_string_append (summary, _("Account: connect first, then use the network's own account help\n"));
}
else
g_string_append (summary, _("Account: skip for now\n"));
g_string_append (summary, _("Channels: only the rooms you selected or entered\n\n"));
g_string_append (summary, _("ZoiteChat will save this network and nickname locally. Network Atlas lookups, when enabled, contain no nickname or password."));
gtk_label_set_text (GTK_LABEL (state->summary_label), summary->str);
g_string_free (summary, TRUE);
}
static void
servlist_onboarding_update_buttons (ServlistOnboardingState *state)
{
int page = gtk_notebook_get_current_page (GTK_NOTEBOOK (state->notebook));
int pages = gtk_notebook_get_n_pages (GTK_NOTEBOOK (state->notebook));
gtk_widget_set_sensitive (state->back_button, page > 0);
gtk_widget_set_visible (state->next_button, page < pages - 1);
gtk_widget_set_visible (state->connect_button, page == pages - 1);
if (page == pages - 1)
servlist_onboarding_update_summary (state);
}
static GtkWidget *
servlist_onboarding_page (const char *title, const char *intro)
{
GtkWidget *box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 12);
GtkWidget *heading;
GtkWidget *label;
char *markup = g_markup_printf_escaped ("<span size='x-large' weight='bold'>%s</span>", title);
gtk_container_set_border_width (GTK_CONTAINER (box), 18);
heading = gtk_label_new (NULL);
gtk_label_set_markup (GTK_LABEL (heading), markup);
gtk_label_set_xalign (GTK_LABEL (heading), 0.0);
gtk_box_pack_start (GTK_BOX (box), heading, FALSE, FALSE, 0);
g_free (markup);
label = gtk_label_new (intro);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_box_pack_start (GTK_BOX (box), label, FALSE, FALSE, 0);
return box;
}
static GtkWidget *
servlist_onboarding_labeled_entry (GtkWidget *grid, int row, const char *label_text, GtkWidget **out_label)
{
GtkWidget *label = gtk_label_new (label_text);
GtkWidget *entry = gtk_entry_new ();
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_widget_set_halign (label, GTK_ALIGN_START);
gtk_grid_attach (GTK_GRID (grid), label, 0, row, 1, 1);
gtk_grid_attach (GTK_GRID (grid), entry, 1, row, 1, 1);
if (out_label)
*out_label = label;
return entry;
}
static ircnet *
servlist_onboarding_ensure_network (ServlistOnboardingChoice *choice)
{
ircnet *net;
char *server_name;
if (choice->net)
return choice->net;
if (!choice->atlas_approved || !choice->endpoint_host)
return NULL;
net = servlist_net_find (choice->name, NULL, g_ascii_strcasecmp);
if (!net)
{
net = servlist_net_add (choice->name, NULL, FALSE);
net->encoding = g_strdup (IRC_DEFAULT_CHARSET);
if (choice->endpoint_tls)
net->flags |= FLAG_USE_SSL;
else
net->flags &= ~FLAG_USE_SSL;
if (choice->supports_sasl)
net->logintype = LOGIN_SASL;
server_name = g_strdup_printf (choice->endpoint_tls ? "%s/+%d" : "%s/%d",
choice->endpoint_host, choice->endpoint_port);
servlist_server_add (net, server_name);
g_free (server_name);
}
choice->net = net;
return net;
}
static void
servlist_onboarding_set_identity (ircnet *net, const char *nick, const char *realname, const char *account_name)
{
const char *profile = realname && *realname ? realname : nick;
const char *network_user = account_name && *account_name ? account_name : nick;
g_strlcpy (prefs.hex_irc_nick1, nick, sizeof prefs.hex_irc_nick1);
g_strlcpy (prefs.hex_irc_nick2, nick, sizeof prefs.hex_irc_nick2);
g_strlcat (prefs.hex_irc_nick2, "_", sizeof prefs.hex_irc_nick2);
g_strlcpy (prefs.hex_irc_nick3, nick, sizeof prefs.hex_irc_nick3);
g_strlcat (prefs.hex_irc_nick3, "__", sizeof prefs.hex_irc_nick3);
g_strlcpy (prefs.hex_irc_user_name, nick, sizeof prefs.hex_irc_user_name);
g_strlcpy (prefs.hex_irc_real_name, profile, sizeof prefs.hex_irc_real_name);
g_free (net->nick);
net->nick = g_strdup (prefs.hex_irc_nick1);
g_free (net->nick2);
net->nick2 = g_strdup (prefs.hex_irc_nick2);
g_free (net->user);
net->user = g_strdup (network_user);
g_free (net->real);
net->real = g_strdup (profile);
/* The assistant deliberately creates a complete per-network identity.
* This also lets an existing account name differ from the visible nick,
* which ZoiteChat's SASL path represents with the network user field. */
net->flags &= ~FLAG_USE_GLOBAL;
}
static gboolean
servlist_onboarding_store_password (ircnet *net, const char *password)
{
char *stored;
if (!net || !password || !*password)
return TRUE;
if (secretstore_is_keyring_available () && secretstore_set_network_password (net->name, password))
{
if (net->pass)
{
memset (net->pass, 0, strlen (net->pass));
g_free (net->pass);
net->pass = NULL;
}
net->flags |= FLAG_USE_KEYRING;
return TRUE;
}
stored = servlist_password_encrypt_for_storage (password);
if (!stored)
return FALSE;
if (net->pass)
{
memset (net->pass, 0, strlen (net->pass));
g_free (net->pass);
}
net->pass = stored;
net->flags &= ~FLAG_USE_KEYRING;
return TRUE;
}
static void
servlist_onboarding_clear_password (ircnet *net)
{
if (!net)
return;
if (net->flags & FLAG_USE_KEYRING)
secretstore_delete_network_password (net->name);
net->flags &= ~FLAG_USE_KEYRING;
if (net->pass)
{
memset (net->pass, 0, strlen (net->pass));
g_free (net->pass);
net->pass = NULL;
}
}
static void
servlist_onboarding_add_channels_text (ircnet *net, const char *channels)
{
char **parts;
int i;
if (!net || !channels || !*channels)
return;
parts = g_strsplit_set (channels, ", \t", -1);
for (i = 0; parts[i]; i++)
{
if (*parts[i] && strchr ("#&+!", parts[i][0]) && !servlist_favchan_find (net, parts[i], NULL))
servlist_favchan_add (net, parts[i]);
}
g_strfreev (parts);
}
static void
servlist_onboarding_add_selected_channels (ServlistOnboardingState *state, ircnet *net)
{
GList *children = gtk_container_get_children (GTK_CONTAINER (state->channel_suggestions));
GList *node;
for (node = children; node; node = node->next)
{
GtkWidget *widget = node->data;
const char *channel = g_object_get_data (G_OBJECT (widget), "zoitechat-onboarding-channel");
if (channel && GTK_IS_TOGGLE_BUTTON (widget) && gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (widget)) &&
!servlist_favchan_find (net, (char *) channel, NULL))
servlist_favchan_add (net, (char *) channel);
}
g_list_free (children);
servlist_onboarding_add_channels_text (net, gtk_entry_get_text (GTK_ENTRY (state->channel_entry)));
}
static int
servlist_onboarding_preferred_login (ServlistOnboardingChoice *choice, ircnet *net)
{
if (choice && choice->identify_method)
{
if (!g_ascii_strcasecmp (choice->identify_method, "sasl"))
return LOGIN_SASL;
if (!g_ascii_strcasecmp (choice->identify_method, "nickserv"))
return LOGIN_MSG_NICKSERV;
if (!g_ascii_strcasecmp (choice->identify_method, "server_password"))
return LOGIN_PASS;
}
if (choice && choice->supports_sasl)
return LOGIN_SASL;
if (net && net->logintype)
return net->logintype;
if (choice && choice->has_nickserv)
return LOGIN_MSG_NICKSERV;
return LOGIN_DEFAULT_REAL;
}
static void
servlist_onboarding_connect_password_once (session *sess, ircnet *net, const char *password)
{
char *old_pass = net->pass;
guint32 old_flags = net->flags;
char *temporary = g_strdup (password ? password : "");
net->pass = temporary;
net->flags &= ~(FLAG_USE_KEYRING | FLAG_PROMPT_PASSWORD);
servlist_connect (sess, net, TRUE);
net->flags = old_flags;
net->pass = old_pass;
memset (temporary, 0, strlen (temporary));
g_free (temporary);
}
static char *
servlist_onboarding_replace_all (const char *source, const char *needle, const char *replacement)
{
GString *out = g_string_new (NULL);
const char *p = source;
const char *hit;
gsize needle_len = strlen (needle);
while ((hit = strstr (p, needle)) != NULL)
{
g_string_append_len (out, p, hit - p);
g_string_append (out, replacement ? replacement : "");
p = hit + needle_len;
}
g_string_append (out, p);
return g_string_free (out, FALSE);
}
static char *
servlist_onboarding_expand_registration (ServlistRegistrationGuide *guide)
{
char *step1;
char *step2;
char *step3;
char *step4;
char *step5;
if (!guide || !guide->command_template || !servlist_onboarding_registration_template_is_safe (guide->command_template))
return NULL;
step1 = servlist_onboarding_replace_all (guide->command_template, "{nick}", guide->nick ? guide->nick : "");
step2 = servlist_onboarding_replace_all (step1, "{nickname}", guide->nick ? guide->nick : "");
g_free (step1);
step3 = servlist_onboarding_replace_all (step2, "{account}", guide->nick ? guide->nick : "");
g_free (step2);
step4 = servlist_onboarding_replace_all (step3, "{password}", guide->password ? guide->password : "");
g_free (step3);
step5 = servlist_onboarding_replace_all (step4, "{email}", guide->email ? guide->email : "");
g_free (step4);
if (strchr (step5, '{') || strchr (step5, '}') || strchr (step5, '\r') || strchr (step5, '\n') || strlen (step5) > 350)
{
memset (step5, 0, strlen (step5));
g_free (step5);
return NULL;
}
return step5;
}
static void
servlist_registration_guide_free (ServlistRegistrationGuide *guide)
{
if (!guide)
return;
g_free (guide->network_name);
g_free (guide->nick);
if (guide->password)
{
memset (guide->password, 0, strlen (guide->password));
g_free (guide->password);
}
if (guide->email)
{
memset (guide->email, 0, strlen (guide->email));
g_free (guide->email);
}
g_free (guide->service);
g_free (guide->command_template);
g_free (guide->homepage);
g_free (guide->services_family);
g_free (guide->identify_method);
g_free (guide);
}
static gboolean
servlist_onboarding_login_uses_promptable_sasl (int login)
{
return login == LOGIN_SASL || login == LOGIN_SASL_SCRAM_SHA_1 ||
login == LOGIN_SASL_SCRAM_SHA_256 || login == LOGIN_SASL_SCRAM_SHA_512;
}
static void
servlist_registration_configure_future_login (ServlistRegistrationGuide *guide)
{
guide->net->logintype = guide->preferred_login;
guide->net->flags &= ~FLAG_PROMPT_PASSWORD;
if (guide->remember_password)
{
if (!servlist_onboarding_store_password (guide->net, guide->password))
fe_message (_("The registration request was sent, but ZoiteChat could not save the password for future logins."), FE_MSG_WARN);
}
else
{
servlist_onboarding_clear_password (guide->net);
if (servlist_onboarding_login_uses_promptable_sasl (guide->preferred_login))
guide->net->flags |= FLAG_PROMPT_PASSWORD;
}
if (!servlist_save ())
fe_message (_("Could not save the updated network login settings."), FE_MSG_WARN);
}
static gboolean
servlist_onboarding_persist_success (ServlistRegistrationGuide *guide)
{
gboolean ok = TRUE;
if (!guide || !guide->net)
return FALSE;
if (guide->account_mode == ONBOARDING_ACCOUNT_EXISTING)
{
guide->net->logintype = guide->preferred_login;
guide->net->flags &= ~FLAG_PROMPT_PASSWORD;
if (guide->remember_password)
{
if (!servlist_onboarding_store_password (guide->net, guide->password))
{
fe_message (_("You are connected, but ZoiteChat could not save the account password. Your network settings will still be kept."), FE_MSG_WARN);
ok = FALSE;
}
}
else
{
servlist_onboarding_clear_password (guide->net);
if (servlist_onboarding_login_uses_promptable_sasl (guide->preferred_login))
guide->net->flags |= FLAG_PROMPT_PASSWORD;
}
}
prefs.hex_gui_onboarding_pending = FALSE;
if (!servlist_save ())
{
fe_message (_("You are connected, but ZoiteChat could not save the server list."), FE_MSG_WARN);
ok = FALSE;
}
if (!save_config ())
{
fe_message (_("You are connected, but ZoiteChat could not save zoitechat.conf."), FE_MSG_WARN);
ok = FALSE;
}
return ok;
}
static void
servlist_onboarding_connected_show (ServlistRegistrationGuide *guide)
{
GtkWidget *dialog;
GtkWidget *area;
GtkWidget *label;
GString *text;
if (!guide)
return;
dialog = gtk_dialog_new_with_buttons (_("You're connected to IRC"), NULL, GTK_DIALOG_MODAL,
_("Start chatting"), GTK_RESPONSE_OK,
NULL);
if (current_sess && current_sess->gui && current_sess->gui->window)
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (current_sess->gui->window));
gtk_window_set_default_size (GTK_WINDOW (dialog), 560, -1);
area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
text = g_string_new (NULL);
g_string_append_printf (text, _("You're online on %s as %s.\n\n"), guide->network_name, guide->nick);
if (guide->account_mode == ONBOARDING_ACCOUNT_EXISTING)
g_string_append (text, _("ZoiteChat used the account details you supplied. Watch the server tab for any login warning from the network.\n\n"));
else if (guide->account_mode == ONBOARDING_ACCOUNT_NEW)
g_string_append (text, _("Your first connection is working. If you sent a registration request, keep the server tab open long enough to read NickServ's reply and finish any verification it requested.\n\n"));
else
g_string_append (text, _("You can use IRC without registering an account. You can set one up later from the network's help or Network List.\n\n"));
g_string_append (text, _("If you selected channels, ZoiteChat is joining them now. Click a channel in the left-hand list, type in the box at the bottom, and press Enter to talk. A private conversation with one person is called a query or direct message.\n\nYou can always open Network List later to add another network, change channels, or edit account login settings."));
label = gtk_label_new (text->str);
g_string_free (text, TRUE);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_container_set_border_width (GTK_CONTAINER (label), 16);
gtk_box_pack_start (GTK_BOX (area), label, TRUE, TRUE, 0);
gtk_widget_show_all (dialog);
gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
}
static gboolean
servlist_registration_confirm_automatic_login (ServlistRegistrationGuide *guide)
{
GtkWidget *dialog;
GtkWidget *area;
GtkWidget *label;
int response;
dialog = gtk_dialog_new_with_buttons (_("Confirm your IRC account"), NULL, GTK_DIALOG_MODAL,
_("I'll finish this later"), GTK_RESPONSE_CANCEL,
_("Set up automatic login"), GTK_RESPONSE_OK,
NULL);
if (current_sess && current_sess->gui && current_sess->gui->window)
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (current_sess->gui->window));
gtk_window_set_default_size (GTK_WINDOW (dialog), 560, -1);
area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
label = gtk_label_new (_("Read NickServ's reply in the server tab first. If it says the account was registered and asks you to confirm an email or code, complete that step before continuing.\n\nWhen the account is ready, ZoiteChat can save the password and configure normal automatic login. If you are unsure whether registration succeeded, choose 'I'll finish this later'. Your password will not be stored automatically."));
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_container_set_border_width (GTK_CONTAINER (label), 16);
gtk_box_pack_start (GTK_BOX (area), label, TRUE, TRUE, 0);
gtk_widget_show_all (dialog);
response = gtk_dialog_run (GTK_DIALOG (dialog));
gtk_widget_destroy (dialog);
return response == GTK_RESPONSE_OK;
}
static void
servlist_registration_show (ServlistRegistrationGuide *guide)
{
GtkWidget *dialog;
GtkWidget *area;
GtkWidget *label;
GString *text;
int response;
gboolean can_send;
gboolean can_ask_help;
can_send = guide->verified_recipe || guide->common_recipe;
can_ask_help = guide->has_nickserv && guide->serv && guide->serv->p_message;
dialog = gtk_dialog_new_with_buttons (_("Finish setting up your IRC account"), NULL, GTK_DIALOG_MODAL,
_("Not now"), GTK_RESPONSE_CANCEL,
NULL);
if (can_send)
gtk_dialog_add_button (GTK_DIALOG (dialog), _("Send registration request"), GTK_RESPONSE_OK);
else if (can_ask_help)
gtk_dialog_add_button (GTK_DIALOG (dialog), _("Ask NickServ how to register"), GTK_RESPONSE_APPLY);
if (guide->homepage)
gtk_dialog_add_button (GTK_DIALOG (dialog), _("Open network website"), GTK_RESPONSE_HELP);
if (current_sess && current_sess->gui && current_sess->gui->window)
gtk_window_set_transient_for (GTK_WINDOW (dialog), GTK_WINDOW (current_sess->gui->window));
gtk_window_set_default_size (GTK_WINDOW (dialog), 560, -1);
area = gtk_dialog_get_content_area (GTK_DIALOG (dialog));
text = g_string_new (NULL);
g_string_append_printf (text, _("You are connected to %s as %s.\n\n"), guide->network_name, guide->nick);
if (guide->verified_recipe)
{
g_string_append_printf (text, _("This network publishes an operator-verified account registration recipe through the ZoiteChat Network Atlas. If you continue, ZoiteChat will send one registration request to %s. The command is never added to reconnect commands."), guide->service);
if (guide->email_required)
g_string_append (text, _("\n\nThe network requires an email address. Read NickServ's reply and complete any email verification it requests."));
}
else if (guide->common_recipe)
{
g_string_append_printf (text, _("Atlas detected %s account services with %d%% confidence and found NickServ. This services family normally uses REGISTER with a password and email address. The network can customize or disable registration, so ZoiteChat will only send the request if you choose to continue."),
guide->services_family, guide->services_confidence);
g_string_append (text, _("\n\nAfter sending it, read NickServ's reply. If it asks for email verification, complete that before relying on automatic login."));
}
else
{
if (guide->services_family)
{
if (guide->services_confidence >= 0)
g_string_append_printf (text, _("Atlas detected %s account services with %d%% confidence, but ZoiteChat does not have enough verified information to send your password in a registration command."),
guide->services_family, guide->services_confidence);
else
g_string_append_printf (text, _("Atlas detected %s account services, but ZoiteChat does not have enough verified information to send your password in a registration command."), guide->services_family);
}
else
g_string_append (text, _("Atlas does not currently have enough account-registration information for this network. ZoiteChat will not guess a password-bearing command."));
if (can_ask_help)
g_string_append (text, _("\n\nZoiteChat can safely ask NickServ for its REGISTER help without sending a password."));
}
label = gtk_label_new (text->str);
g_string_free (text, TRUE);
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_container_set_border_width (GTK_CONTAINER (label), 16);
gtk_box_pack_start (GTK_BOX (area), label, TRUE, TRUE, 0);
gtk_widget_show_all (dialog);
for (;;)
{
response = gtk_dialog_run (GTK_DIALOG (dialog));
if (response == GTK_RESPONSE_HELP && guide->homepage)
{
fe_open_url (guide->homepage);
continue;
}
if (response == GTK_RESPONSE_APPLY && can_ask_help)
{
guide->serv->p_message (guide->serv, "NickServ", "HELP REGISTER");
fe_message (_("Asked NickServ for registration help. Read its reply in the server tab."), FE_MSG_INFO);
break;
}
if (response == GTK_RESPONSE_OK && can_send)
{
char *expanded = servlist_onboarding_expand_registration (guide);
if (!expanded || !servlist_onboarding_service_target_is_safe (guide->service))
{
if (expanded)
{
memset (expanded, 0, strlen (expanded));
g_free (expanded);
}
fe_message (_("The Atlas registration instructions could not be expanded safely."), FE_MSG_ERROR);
break;
}
guide->serv->p_message (guide->serv, guide->service, expanded);
memset (expanded, 0, strlen (expanded));
g_free (expanded);
fe_message (_("Registration request sent. Read NickServ's reply in the server tab before confirming automatic login."), FE_MSG_INFO);
if (servlist_registration_confirm_automatic_login (guide))
{
servlist_registration_configure_future_login (guide);
fe_message (_("Automatic account login is now configured for future connections."), FE_MSG_INFO);
}
break;
}
break;
}
gtk_widget_destroy (dialog);
}
static int
servlist_registration_wait_cb (gpointer data)
{
ServlistRegistrationGuide *guide = data;
if (!is_server (guide->serv))
{
servlist_registration_guide_free (guide);
return 0;
}
if (guide->serv->end_of_motd)
{
servlist_onboarding_persist_success (guide);
if (guide->account_mode == ONBOARDING_ACCOUNT_NEW)
{
servlist_registration_show (guide);
servlist_onboarding_connected_show (guide);
}
else
servlist_onboarding_connected_show (guide);
servlist_registration_guide_free (guide);
return 0;
}
if (g_get_monotonic_time () >= guide->deadline)
{
fe_message (_("The first connection did not finish in time, so ZoiteChat did not mark onboarding complete. You can retry the connection or reopen Network List."), FE_MSG_WARN);
servlist_registration_guide_free (guide);
return 0;
}
return 1;
}
static gboolean
servlist_onboarding_validate_page (ServlistOnboardingState *state, int page)
{
ServlistOnboardingChoice *choice;
ServlistOnboardingAccountMode mode;
const char *nick;
const char *password;
const char *password_confirm;
const char *email;
if (page == 0)
{
if (gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (state->use_atlas)))
servlist_onboarding_start_atlas (state);
return TRUE;
}
if (page == 1)
{
choice = servlist_onboarding_selected_choice (state);
if (!choice)
{
fe_message (_("Choose an IRC network before continuing."), FE_MSG_ERROR);
return FALSE;
}
return TRUE;
}
if (page == 2)
{
nick = gtk_entry_get_text (GTK_ENTRY (state->nick_entry));
if (!nick || !*nick || strpbrk (nick, " \t\r\n,:"))
{
fe_message (_("Choose a nickname without spaces, commas, or colons."), FE_MSG_ERROR);
gtk_widget_grab_focus (state->nick_entry);
return FALSE;
}
return TRUE;
}
if (page == 3)
{
choice = servlist_onboarding_selected_choice (state);
mode = servlist_onboarding_account_mode (state);
password = gtk_entry_get_text (GTK_ENTRY (state->password_entry));
password_confirm = gtk_entry_get_text (GTK_ENTRY (state->password_confirm_entry));
email = gtk_entry_get_text (GTK_ENTRY (state->email_entry));
if (mode == ONBOARDING_ACCOUNT_NEW && choice && choice->slug && !choice->detail_loaded &&
gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (state->use_atlas)))
{
servlist_onboarding_start_detail (state, choice);
fe_message (_("ZoiteChat is still checking this network's Atlas account information. Give it a moment, or choose 'Skip account setup for now' to connect immediately."), FE_MSG_INFO);
return FALSE;
}
if (mode == ONBOARDING_ACCOUNT_EXISTING)
{
const char *account = gtk_entry_get_text (GTK_ENTRY (state->account_entry));
if (!account || !*account || strpbrk (account, " \t\r\n"))
{
fe_message (_("Enter the account name you registered on this network. It cannot contain spaces."), FE_MSG_ERROR);
return FALSE;
}
if (!password || !*password)
{
fe_message (_("Enter the password for your existing network account, or choose 'Skip account setup for now'."), FE_MSG_ERROR);
return FALSE;
}
}
if (mode == ONBOARDING_ACCOUNT_NEW && servlist_onboarding_can_prepare_registration (choice))
{
gboolean needs_email = !choice->registration_verified || choice->registration_email_required;
if (!password || !*password || strpbrk (password, " \t\r\n") || strlen (password) < 10)
{
fe_message (_("Choose a registration password of at least 10 characters with no spaces. Do not reuse an important password from another service."), FE_MSG_ERROR);
return FALSE;
}
if (!password_confirm || strcmp (password, password_confirm))
{
fe_message (_("The two registration password entries do not match."), FE_MSG_ERROR);
gtk_widget_grab_focus (state->password_confirm_entry);
return FALSE;
}
if (needs_email && (!email || !*email || !strchr (email, '@') || strpbrk (email, " \t\r\n")))
{
fe_message (_("Enter a valid email address for account registration."), FE_MSG_ERROR);
return FALSE;
}
}
return TRUE;
}
return TRUE;
}
static void
servlist_open_networks_window (session *sess)
{
servlist_sess = sess;
serverlist_win = servlist_open_networks ();
gtkutil_set_icon (serverlist_win);
servlist_networks_populate (networks_tree, network_list);
g_signal_connect (G_OBJECT (serverlist_win), "delete-event", G_CALLBACK (servlist_delete_cb), 0);
g_signal_connect (G_OBJECT (serverlist_win), "configure-event", G_CALLBACK (servlist_configure_cb), 0);
g_signal_connect (G_OBJECT (gtk_tree_view_get_selection (GTK_TREE_VIEW (networks_tree))),
"changed", G_CALLBACK (servlist_network_row_cb), NULL);
g_signal_connect (G_OBJECT (networks_tree), "key-press-event",
G_CALLBACK (servlist_net_keypress_cb), networks_tree);
gtk_widget_show (serverlist_win);
}
static gboolean
servlist_open_onboarding (session *sess)
{
ServlistOnboardingState *state;
GtkWidget *area;
GtkWidget *page;
GtkWidget *grid;
GtkWidget *label;
GtkWidget *scroll;
GSList *group;
GSList *list;
int response;
int page_index;
ircnet *net;
state = g_new0 (ServlistOnboardingState, 1);
state->sess = sess;
state->choices = g_ptr_array_new_with_free_func ((GDestroyNotify) servlist_onboarding_choice_free);
state->atlas_cancel = g_cancellable_new ();
for (list = network_list; list; list = list->next)
{
net = list->data;
g_ptr_array_add (state->choices, servlist_onboarding_choice_new (net, net->name));
}
state->dialog = gtk_dialog_new_with_buttons (_("Welcome to ZoiteChat"), NULL, GTK_DIALOG_MODAL,
_("_Advanced setup..."), GTK_RESPONSE_HELP,
_("_Cancel"), GTK_RESPONSE_CANCEL,
NULL);
gtk_window_set_default_size (GTK_WINDOW (state->dialog), 650, 520);
if (sess && sess->gui && sess->gui->window)
gtk_window_set_transient_for (GTK_WINDOW (state->dialog), GTK_WINDOW (sess->gui->window));
state->back_button = gtk_dialog_add_button (GTK_DIALOG (state->dialog), _("_Back"), GTK_RESPONSE_NO);
state->next_button = gtk_dialog_add_button (GTK_DIALOG (state->dialog), _("_Next"), GTK_RESPONSE_YES);
state->connect_button = gtk_dialog_add_button (GTK_DIALOG (state->dialog), _("C_onnect"), GTK_RESPONSE_OK);
gtk_widget_set_can_default (state->next_button, TRUE);
gtk_widget_set_can_default (state->connect_button, TRUE);
area = gtk_dialog_get_content_area (GTK_DIALOG (state->dialog));
state->notebook = gtk_notebook_new ();
gtk_notebook_set_show_tabs (GTK_NOTEBOOK (state->notebook), FALSE);
gtk_notebook_set_show_border (GTK_NOTEBOOK (state->notebook), FALSE);
gtk_box_pack_start (GTK_BOX (area), state->notebook, TRUE, TRUE, 0);
page = servlist_onboarding_page (_("Welcome to IRC"),
_("IRC is a collection of independent chat communities called networks. You do not need to know server addresses, commands, or IRC terminology to get started. This assistant will choose a community, set your public chat name, explain accounts, pick rooms, and connect you."));
label = gtk_label_new (_("ZoiteChat can use the public Network Atlas for current network information and verified setup guidance. This sends a normal HTTPS request to zoitechat.org. Your nickname and password are never included in that request."));
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_box_pack_start (GTK_BOX (page), label, FALSE, FALSE, 0);
state->use_atlas = gtk_check_button_new_with_label (_("Use the ZoiteChat Network Atlas for current setup information"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (state->use_atlas), TRUE);
gtk_box_pack_start (GTK_BOX (page), state->use_atlas, FALSE, FALSE, 0);
gtk_notebook_append_page (GTK_NOTEBOOK (state->notebook), page, NULL);
page = servlist_onboarding_page (_("Choose a network"),
_("A network is an independent IRC community. Pick one that sounds useful to you. You can connect to several networks later."));
state->network_combo = gtk_combo_box_text_new ();
gtk_box_pack_start (GTK_BOX (page), state->network_combo, FALSE, FALSE, 0);
state->atlas_status = gtk_label_new (_("Using ZoiteChat's built-in network list."));
gtk_label_set_line_wrap (GTK_LABEL (state->atlas_status), TRUE);
gtk_label_set_xalign (GTK_LABEL (state->atlas_status), 0.0);
gtk_box_pack_start (GTK_BOX (page), state->atlas_status, FALSE, FALSE, 0);
state->network_info = gtk_label_new ("");
gtk_label_set_line_wrap (GTK_LABEL (state->network_info), TRUE);
gtk_label_set_xalign (GTK_LABEL (state->network_info), 0.0);
gtk_box_pack_start (GTK_BOX (page), state->network_info, TRUE, TRUE, 0);
gtk_notebook_append_page (GTK_NOTEBOOK (state->notebook), page, NULL);
g_signal_connect (state->network_combo, "changed", G_CALLBACK (servlist_onboarding_network_changed), state);
servlist_onboarding_refresh_combo (state);
page = servlist_onboarding_page (_("Choose your chat name"),
_("Your nickname is the public name people see in chat. It does not need to be your real name. If your first choice is already in use, ZoiteChat keeps two automatic fallback versions."));
grid = gtk_grid_new ();
gtk_grid_set_row_spacing (GTK_GRID (grid), 10);
gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
gtk_box_pack_start (GTK_BOX (page), grid, FALSE, FALSE, 0);
state->nick_entry = servlist_onboarding_labeled_entry (grid, 0, _("Nickname:"), NULL);
/* Leave room for the automatic _ and __ fallback nicknames. */
gtk_entry_set_max_length (GTK_ENTRY (state->nick_entry), NICKLEN - 3);
gtk_entry_set_text (GTK_ENTRY (state->nick_entry), prefs.hex_irc_nick1);
label = gtk_label_new (_("Examples: alex, MapleLeaf, codecat. Spaces are not allowed."));
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_grid_attach (GTK_GRID (grid), label, 1, 1, 1, 1);
state->realname_entry = servlist_onboarding_labeled_entry (grid, 2, _("Profile name:"), NULL);
gtk_entry_set_max_length (GTK_ENTRY (state->realname_entry), sizeof prefs.hex_irc_real_name - 1);
gtk_entry_set_text (GTK_ENTRY (state->realname_entry), prefs.hex_irc_nick1);
label = gtk_label_new (_("This IRC 'real name' field can be visible in WHOIS. ZoiteChat starts it with your nickname instead of exposing your computer account's real name; change it if you want."));
gtk_label_set_line_wrap (GTK_LABEL (label), TRUE);
gtk_label_set_xalign (GTK_LABEL (label), 0.0);
gtk_grid_attach (GTK_GRID (grid), label, 1, 3, 1, 1);
gtk_notebook_append_page (GTK_NOTEBOOK (state->notebook), page, NULL);
page = servlist_onboarding_page (_("Do you want a network account?"),
_("A nickname lets you chat immediately. A network account can reserve that nickname and lets the network recognize you when you return. Accounts belong to the IRC network, not to ZoiteChat."));
state->account_skip = gtk_radio_button_new_with_label (NULL, _("Skip account setup for now"));
group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (state->account_skip));
state->account_existing = gtk_radio_button_new_with_label (group, _("I already have an account on this network"));
group = gtk_radio_button_get_group (GTK_RADIO_BUTTON (state->account_existing));
state->account_new = gtk_radio_button_new_with_label (group, _("I'm new here. Help me reserve this nickname"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (state->account_skip), TRUE);
gtk_box_pack_start (GTK_BOX (page), state->account_skip, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (page), state->account_existing, FALSE, FALSE, 0);
gtk_box_pack_start (GTK_BOX (page), state->account_new, FALSE, FALSE, 0);
state->account_info = gtk_label_new ("");
gtk_label_set_line_wrap (GTK_LABEL (state->account_info), TRUE);
gtk_label_set_xalign (GTK_LABEL (state->account_info), 0.0);
gtk_box_pack_start (GTK_BOX (page), state->account_info, FALSE, FALSE, 0);
grid = gtk_grid_new ();
gtk_grid_set_row_spacing (GTK_GRID (grid), 8);
gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
gtk_box_pack_start (GTK_BOX (page), grid, FALSE, FALSE, 0);
state->account_entry = servlist_onboarding_labeled_entry (grid, 0, _("Account name:"), &state->account_label);
gtk_entry_set_text (GTK_ENTRY (state->account_entry), prefs.hex_irc_nick1);
state->password_entry = servlist_onboarding_labeled_entry (grid, 1, _("Account password:"), &state->password_label);
gtk_entry_set_visibility (GTK_ENTRY (state->password_entry), FALSE);
gtk_entry_set_input_purpose (GTK_ENTRY (state->password_entry), GTK_INPUT_PURPOSE_PASSWORD);
gtk_entry_set_max_length (GTK_ENTRY (state->password_entry), 200);
state->password_confirm_entry = servlist_onboarding_labeled_entry (grid, 2, _("Confirm password:"), &state->password_confirm_label);
gtk_entry_set_visibility (GTK_ENTRY (state->password_confirm_entry), FALSE);
gtk_entry_set_input_purpose (GTK_ENTRY (state->password_confirm_entry), GTK_INPUT_PURPOSE_PASSWORD);
gtk_entry_set_max_length (GTK_ENTRY (state->password_confirm_entry), 200);
state->email_entry = servlist_onboarding_labeled_entry (grid, 3, _("Email address:"), &state->email_label);
gtk_entry_set_max_length (GTK_ENTRY (state->email_entry), 254);
state->remember_password = gtk_check_button_new_with_label (_("Remember this password for future connections"));
gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (state->remember_password), TRUE);
gtk_grid_attach (GTK_GRID (grid), state->remember_password, 1, 4, 1, 1);
gtk_notebook_append_page (GTK_NOTEBOOK (state->notebook), page, NULL);
g_signal_connect (state->account_skip, "toggled", G_CALLBACK (servlist_onboarding_account_changed), state);
g_signal_connect (state->account_existing, "toggled", G_CALLBACK (servlist_onboarding_account_changed), state);
g_signal_connect (state->account_new, "toggled", G_CALLBACK (servlist_onboarding_account_changed), state);
servlist_onboarding_account_changed (GTK_TOGGLE_BUTTON (state->account_skip), state);
page = servlist_onboarding_page (_("Choose where to start chatting"),
_("Channels are chat rooms inside a network. Networks can publish beginner-friendly starter rooms through Atlas. You can also type a channel name if someone invited you to one."));
scroll = gtk_scrolled_window_new (NULL, NULL);
gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scroll), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_widget_set_size_request (scroll, -1, 180);
state->channel_suggestions = gtk_box_new (GTK_ORIENTATION_VERTICAL, 6);
gtk_container_add (GTK_CONTAINER (scroll), state->channel_suggestions);
gtk_box_pack_start (GTK_BOX (page), scroll, TRUE, TRUE, 0);
grid = gtk_grid_new ();
gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
gtk_box_pack_start (GTK_BOX (page), grid, FALSE, FALSE, 0);
state->channel_entry = servlist_onboarding_labeled_entry (grid, 0, _("Other channels (optional):"), NULL);
gtk_entry_set_placeholder_text (GTK_ENTRY (state->channel_entry), _("#channel, #another"));
gtk_notebook_append_page (GTK_NOTEBOOK (state->notebook), page, NULL);
page = servlist_onboarding_page (_("Ready to connect"),
_("That's enough setup to get onto IRC. Nothing here is permanent: the Network List can change networks, nicknames, account login, servers, and channels later."));
state->summary_label = gtk_label_new ("");
gtk_label_set_line_wrap (GTK_LABEL (state->summary_label), TRUE);
gtk_label_set_xalign (GTK_LABEL (state->summary_label), 0.0);
gtk_box_pack_start (GTK_BOX (page), state->summary_label, TRUE, TRUE, 0);
gtk_notebook_append_page (GTK_NOTEBOOK (state->notebook), page, NULL);
gtk_widget_show_all (state->dialog);
gtk_notebook_set_current_page (GTK_NOTEBOOK (state->notebook), 0);
servlist_onboarding_update_buttons (state);
for (;;)
{
response = gtk_dialog_run (GTK_DIALOG (state->dialog));
page_index = gtk_notebook_get_current_page (GTK_NOTEBOOK (state->notebook));
if (response == GTK_RESPONSE_HELP)
{
prefs.hex_gui_onboarding_disable = TRUE;
prefs.hex_gui_onboarding_pending = FALSE;
save_config ();
gtk_widget_destroy (state->dialog);
servlist_onboarding_close_state (state);
servlist_open_networks_window (sess);
return TRUE;
}
if (response == GTK_RESPONSE_CANCEL || response == GTK_RESPONSE_DELETE_EVENT)
{
gtk_widget_destroy (state->dialog);
servlist_onboarding_close_state (state);
return TRUE;
}
if (response == GTK_RESPONSE_NO)
{
if (page_index > 0)
gtk_notebook_set_current_page (GTK_NOTEBOOK (state->notebook), page_index - 1);
servlist_onboarding_update_buttons (state);
continue;
}
if (response == GTK_RESPONSE_YES)
{
if (!servlist_onboarding_validate_page (state, page_index))
continue;
if (page_index == 1)
servlist_onboarding_account_changed (GTK_TOGGLE_BUTTON (state->account_skip), state);
if (page_index == 2 && state->account_entry)
gtk_entry_set_text (GTK_ENTRY (state->account_entry), gtk_entry_get_text (GTK_ENTRY (state->nick_entry)));
if (page_index == 3)
servlist_onboarding_refresh_channel_suggestions (state);
gtk_notebook_set_current_page (GTK_NOTEBOOK (state->notebook), page_index + 1);
servlist_onboarding_update_buttons (state);
continue;
}
if (response == GTK_RESPONSE_OK)
{
ServlistOnboardingChoice *choice = servlist_onboarding_selected_choice (state);
ServlistOnboardingAccountMode mode = servlist_onboarding_account_mode (state);
const char *nick = gtk_entry_get_text (GTK_ENTRY (state->nick_entry));
const char *password = gtk_entry_get_text (GTK_ENTRY (state->password_entry));
const char *email = gtk_entry_get_text (GTK_ENTRY (state->email_entry));
gboolean remember = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (state->remember_password));
int preferred_login;
ServlistRegistrationGuide *guide = NULL;
server *target_server;
if (!choice || !servlist_onboarding_validate_page (state, 2) || !servlist_onboarding_validate_page (state, 3))
continue;
net = servlist_onboarding_ensure_network (choice);
if (!net)
{
fe_message (_("ZoiteChat could not create a usable connection for the selected network. Try Advanced setup."), FE_MSG_ERROR);
continue;
}
servlist_onboarding_set_identity (net, nick, gtk_entry_get_text (GTK_ENTRY (state->realname_entry)),
mode == ONBOARDING_ACCOUNT_EXISTING ? gtk_entry_get_text (GTK_ENTRY (state->account_entry)) : nick);
servlist_onboarding_add_selected_channels (state, net);
preferred_login = servlist_onboarding_preferred_login (choice, net);
prefs.hex_gui_slist_select = g_slist_index (network_list, net);
guide = g_new0 (ServlistRegistrationGuide, 1);
guide->account_mode = mode;
guide->network_name = g_strdup (choice->name);
guide->nick = g_strdup (nick);
guide->homepage = g_strdup (choice->homepage);
guide->services_family = g_strdup (choice->services_family);
guide->identify_method = g_strdup (choice->identify_method);
guide->services_confidence = choice->services_confidence;
guide->preferred_login = preferred_login;
guide->remember_password = remember;
guide->has_nickserv = choice->has_nickserv;
guide->net = net;
if (mode == ONBOARDING_ACCOUNT_EXISTING)
{
/* Connect with the supplied credential once, but do not persist it until
* the network has actually accepted the connection. */
guide->password = g_strdup (password);
net->logintype = preferred_login;
}
else if (mode == ONBOARDING_ACCOUNT_NEW)
{
gboolean common_recipe = servlist_onboarding_common_registration (choice);
gboolean can_prepare = choice->registration_verified || common_recipe;
guide->password = can_prepare ? g_strdup (password) : NULL;
guide->email = can_prepare ? g_strdup (email) : NULL;
guide->service = g_strdup (choice->registration_verified ? choice->registration_service :
common_recipe ? "NickServ" : NULL);
guide->command_template = g_strdup (choice->registration_verified ? choice->registration_template :
common_recipe ? "REGISTER {password} {email}" : NULL);
guide->email_required = choice->registration_verified ? choice->registration_email_required : common_recipe;
guide->verified_recipe = choice->registration_verified;
guide->common_recipe = common_recipe;
/* A brand-new account cannot authenticate before it exists. Keep the
* first connection unauthenticated; the guide switches the saved
* login method only after a registration request is sent. */
net->logintype = LOGIN_PASS;
servlist_onboarding_clear_password (net);
net->flags &= ~FLAG_PROMPT_PASSWORD;
}
if (!is_session (sess))
sess = current_sess;
if (!sess)
sess = new_ircwindow (NULL, NULL, SESS_SERVER, TRUE);
target_server = sess->server;
if (mode == ONBOARDING_ACCOUNT_EXISTING)
servlist_onboarding_connect_password_once (sess, net, password);
else
servlist_connect (sess, net, TRUE);
guide->serv = target_server;
guide->deadline = g_get_monotonic_time () + (gint64) ONBOARDING_REGISTRATION_TIMEOUT_SECONDS * G_USEC_PER_SEC;
fe_timeout_add (500, servlist_registration_wait_cb, guide);
gtk_entry_set_text (GTK_ENTRY (state->password_entry), "");
gtk_entry_set_text (GTK_ENTRY (state->password_confirm_entry), "");
gtk_entry_set_text (GTK_ENTRY (state->email_entry), "");
gtk_widget_destroy (state->dialog);
servlist_onboarding_close_state (state);
return TRUE;
}
}
}
void
fe_serverlist_open (session *sess)
{
if (serverlist_win)
{
gtk_window_present (GTK_WINDOW (serverlist_win));
return;
}
if (!prefs.hex_gui_onboarding_disable &&
(servlist_needs_onboarding () || prefs.hex_gui_onboarding_pending))
{
if (servlist_needs_onboarding () && !prefs.hex_gui_onboarding_pending)
{
/* Persist a small pending marker immediately. The normal shutdown path
* can create servlist.conf even after a cancelled or failed first
* connection; without this marker a newcomer would lose the assistant
* on the next launch. */
prefs.hex_gui_onboarding_pending = TRUE;
if (!save_config ())
fe_message (_("Could not save the first-run assistant state."), FE_MSG_WARN);
}
if (servlist_open_onboarding (sess))
return;
}
servlist_open_networks_window (sess);
}