protocols/ircv3/purpleircv3connection.c

Thu, 24 Apr 2025 22:19:39 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Thu, 24 Apr 2025 22:19:39 -0500
changeset 43242
a9bc19e23c36
parent 43238
58bca4467196
child 43246
0e9bbe9b4da8
permissions
-rw-r--r--

IRCv3: check if a member is in a conversation before adding them

Awhile ago we update the NAMREPLY handler to build a separate
Purple.ConversationMembers and then splice that onto the existing one to help
with sorting and other performance issues. However, we didn't check if the
users already existed in the existing list, so we would get duplicates. This
address that.

Also fixed a reference leak.

Testing Done:
Joined a channel and sent `/quote names #channel` multiple times and verified that the member list did not grow to include a bunch of duplicates.

Reviewed at https://reviews.imfreedom.org/r/3987/

/*
 * Purple - Internet Messaging Library
 * Copyright (C) Pidgin Developers <devel@pidgin.im>
 *
 * Purple is the legal property of its developers, whose names are too numerous
 * to list here.  Please refer to the COPYRIGHT file distributed with this
 * source distribution.
 *
 * This library 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 library 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 library; if not, see <https://www.gnu.org/licenses/>.
 */

#include <glib/gi18n-lib.h>

#include <birb.h>

#include <hasl.h>

#include "purpleircv3connection.h"

#include "purpleircv3core.h"
#include "purpleircv3messagehandlers.h"

#define IRCV3_STATUS_CONVERSATION_ID "@ircv3-status@"

enum {
	PROP_0,
	PROP_CLIENT,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

struct _PurpleIRCv3Connection {
	PurpleConnection parent;

	IbisClient *client;

	char *server_name;

	PurpleConversation *status_conversation;
};

G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleIRCv3Connection,
                               purple_ircv3_connection,
                               PURPLE_TYPE_CONNECTION,
                               G_TYPE_FLAG_FINAL,
                               {})

static gboolean
purple_ircv3_connection_unknown_message_cb(IbisClient *client,
                                           const char *command,
                                           IbisMessage *message,
                                           gpointer data);

static gboolean
purple_ircv3_connection_saslsuccess(IbisClient *client, const char *command,
                                    IbisMessage *message, gpointer data);

/******************************************************************************
 * Helpers
 *****************************************************************************/
static void
purple_ircv3_connection_rejoin_channels(PurpleIRCv3Connection *connection) {
	PurpleAccount *account = NULL;
	PurpleConversationManager *manager = NULL;
	GList *conversations = NULL;

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	manager = purple_conversation_manager_get_default();

	conversations = purple_conversation_manager_get_all(manager);
	while(conversations != NULL) {
		PurpleConversation *conversation = conversations->data;
		PurpleAccount *conv_account = NULL;

		conv_account = purple_conversation_get_account(conversation);
		if(conv_account == account) {
			IbisMessage *message = NULL;
			const char *id = purple_conversation_get_id(conversation);

			/* We set the online status and clear the error on the conversation
			 * so that we can get any updated value if the join fails.
			 */
			purple_conversation_set_online(conversation, TRUE);
			purple_conversation_set_error(conversation, NULL);

			/* If this is not the status conversation we need to rejoin it. */
			if(conversation != connection->status_conversation) {
				message = ibis_message_new(IBIS_MSG_JOIN);
				ibis_message_set_params(message, id, NULL);
				ibis_client_write(connection->client, message);
			}
		}

		conversations = g_list_delete_link(conversations, conversations);
	}
}

static inline void
purple_ircv3_connection_setup_sasl(PurpleIRCv3Connection *connection,
                                   PurpleAccount *account)
{
	HaslContext *hasl_context = NULL;
	const char *value = NULL;
	gboolean clear_text = FALSE;
	gboolean tls = FALSE;

	hasl_context = hasl_context_new();

	value = purple_account_get_string(account, "sasl-login-name", NULL);
	if(!purple_strempty(value)) {
		hasl_context_set_username(hasl_context, value);
	} else {
		hasl_context_set_username(hasl_context,
		                          ibis_client_get_nick(connection->client));

		/* Since the user doesn't have a SASL login name set, we'll listen for
		 * IBIS_RPL_SASLSUCCESS and use that to set the login name to the
		 * username in HASL which worked if the signal handler gets called.
		 */
		g_signal_connect_object(connection->client,
		                        "message::" IBIS_RPL_SASLSUCCESS,
		                        G_CALLBACK(purple_ircv3_connection_saslsuccess),
		                        connection, G_CONNECT_DEFAULT);
	}

	value = purple_connection_get_password(PURPLE_CONNECTION(connection));
	hasl_context_set_password(hasl_context, value);

	value = purple_account_get_string(account, "sasl-mechanisms", NULL);
	if(!purple_strempty(value)) {
		hasl_context_set_allowed_mechanisms(hasl_context, value);
	}

	tls = purple_account_get_bool(account, "use-tls", TRUE);
	hasl_context_set_tls(hasl_context, tls);

	clear_text = purple_account_get_bool(account, "plain-sasl-in-clear",
	                                     FALSE);
	hasl_context_set_allow_clear_text(hasl_context, clear_text);

	ibis_client_set_hasl_context(connection->client, hasl_context);
	g_clear_object(&hasl_context);
}

/**
 * purple_ircv3_write_server_status_message: (skip)
 * @connection: The instance.
 * @message: The message to write to the status window.
 * @show_command: Whether or not to display the command.
 *
 * Like purple_ircv3_connection_write_status_message() but removes the first
 * parameter which is the user's nick for server messages.
 */
static void
purple_ircv3_write_server_status_message(PurpleIRCv3Connection *connection,
                                         IbisMessage *message,
                                         gboolean show_command)
{
	GStrv original = NULL;
	GStrv extra_crispy = NULL;

	original = ibis_message_get_params(message);
	extra_crispy = g_strdupv(original + 1);
	ibis_message_set_paramsv(message, extra_crispy);
	g_strfreev(extra_crispy);

	purple_ircv3_connection_write_status_message(connection, message,
	                                             show_command, FALSE);
}

/******************************************************************************
 * Message Handlers
 *****************************************************************************/
static void
purple_ircv3_wrote_message_echo_cb(G_GNUC_UNUSED IbisClient *client,
                                   IbisMessage *message, gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleAccount *account = NULL;
	PurpleContactInfo *info = NULL;
	PurpleConversationMember *author = NULL;
	PurpleMessage *purple_message = NULL;
	GStrv params = NULL;
	char *body = NULL;
	const char *command = NULL;

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	info = purple_account_get_contact_info(account);

	command = ibis_message_get_command(message);
	params = ibis_message_get_params(message);
	if(params != NULL) {
		char *paramsv = g_strjoinv(" ", params);
		body = g_strdup_printf("%s %s", command, paramsv);
		g_free(paramsv);
	} else {
		body = g_strdup(command);
	}

	author = purple_conversation_find_or_add_member(connection->status_conversation,
	                                                info, FALSE, NULL);
	purple_message = purple_message_new(author, body);
	purple_message_set_event(purple_message, TRUE);
	g_clear_pointer(&body, g_free);

	purple_conversation_write_message(connection->status_conversation,
	                                  purple_message);
	g_clear_object(&purple_message);
}

static gboolean
purple_ircv3_message_handler_ignore(G_GNUC_UNUSED IbisClient *client,
                                    G_GNUC_UNUSED const char *command,
                                    G_GNUC_UNUSED IbisMessage *message,
                                    G_GNUC_UNUSED gpointer data)
{
	return TRUE;
}

static gboolean
purple_ircv3_connection_saslsuccess(IbisClient *client,
                                    G_GNUC_UNUSED const char *command,
                                    G_GNUC_UNUSED IbisMessage *message,
                                    gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleAccount *account = NULL;
	const char *value = NULL;

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	value = purple_account_get_string(account, "sasl-login-name", NULL);

	/* If the sasl-login-name is empty, we set it to the current username in
	 * our hasl context that was used to login.
	 */
	if(purple_strempty(value)) {
		HaslContext *hasl_context = NULL;

		hasl_context = ibis_client_get_hasl_context(client);
		if(HASL_IS_CONTEXT(hasl_context)) {
			purple_account_set_string(account, "sasl-login-name",
			                          hasl_context_get_username(hasl_context));

		}
	}

	/* We don't actually handle SASLSUCCESS, but we just needed to know if it
	 * was sent.
	 */
	return FALSE;
}

static gboolean
purple_ircv3_server_message_echo(G_GNUC_UNUSED IbisClient *client,
                                 G_GNUC_UNUSED const char *command,
                                 IbisMessage *message,
                                 gpointer data)
{
	purple_ircv3_connection_write_status_message(data, message, TRUE, TRUE);

	return FALSE;
}

static gboolean
purple_ircv3_server_message_handler(G_GNUC_UNUSED IbisClient *client,
                                    G_GNUC_UNUSED const char *command,
                                    IbisMessage *message, gpointer data)
{
	GStrv original = NULL;
	GStrv extra_crispy = NULL;

	original = ibis_message_get_params(message);
	extra_crispy = g_strdupv(original + 1);
	ibis_message_set_paramsv(message, extra_crispy);
	g_strfreev(extra_crispy);

	purple_ircv3_connection_write_status_message(data, message, FALSE, FALSE);

	return TRUE;
}

static gboolean
purple_ircv3_rpl_welcome_handler(G_GNUC_UNUSED IbisClient *client,
                                 G_GNUC_UNUSED const char *command,
                                 IbisMessage *message, gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleAccount *account = NULL;
	PurpleContactInfo *info = NULL;
	GStrv params = NULL;

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	info = purple_account_get_contact_info(account);

	params = ibis_message_get_params(message);
	if(params != NULL) {
		/* Per https://modern.ircdocs.horse/#connection-registration the first
		 * parameter of RPL_WELCOME is the nick that the server assigned us.
		 */
		purple_contact_info_set_id(info, params[0]);
		purple_contact_info_set_username(info, params[0]);
	}

	purple_ircv3_write_server_status_message(connection, message, FALSE);

	return TRUE;
}

static gboolean
purple_ircv3_server_rpl_isupport(G_GNUC_UNUSED IbisClient *client,
                                 G_GNUC_UNUSED const char *command,
                                 IbisMessage *message, gpointer data)
{
	PurpleIRCv3Connection *connection = data;

	purple_ircv3_write_server_status_message(connection, message, FALSE);

	/* We want the default handler to run which will populate the features
	 * object on the client, so we return false here.
	 */

	return FALSE;
}

static gboolean
purple_ircv3_server_no_motd_handler(G_GNUC_UNUSED IbisClient *client,
                                    G_GNUC_UNUSED const char *command,
                                    IbisMessage *ibis_message,
                                    gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleContact *contact = NULL;
	PurpleConversationMember *author = NULL;
	PurpleMessage *purple_message = NULL;

	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         ibis_message);
	author = purple_conversation_find_or_add_member(connection->status_conversation,
	                                                PURPLE_CONTACT_INFO(contact),
	                                                FALSE, NULL);
	purple_message = purple_message_new(author,
	                                    _("no message of the day found"));
	purple_conversation_write_message(connection->status_conversation,
	                                  purple_message);
	g_clear_object(&purple_message);

	return TRUE;
}

static void
purple_ircv3_connection_add_message_handlers(PurpleIRCv3Connection *connection,
                                             IbisClient *client)
{
	g_signal_connect_object(client, "wrote-message::" IBIS_MSG_PING,
	                        G_CALLBACK(purple_ircv3_wrote_message_echo_cb),
	                        connection, 0);
	g_signal_connect_object(client, "wrote-message::" IBIS_MSG_PONG,
	                        G_CALLBACK(purple_ircv3_wrote_message_echo_cb),
	                        connection, 0);

	g_signal_connect_object(client, "message::" IBIS_MSG_PING,
	                        G_CALLBACK(purple_ircv3_server_message_echo),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_PONG,
	                        G_CALLBACK(purple_ircv3_server_message_echo),
	                        connection, G_CONNECT_DEFAULT);

	g_signal_connect_object(client, "message::" IBIS_RPL_WELCOME,
	                        G_CALLBACK(purple_ircv3_rpl_welcome_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_YOURHOST,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_CREATED,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_MYINFO,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_ISUPPORT,
	                        G_CALLBACK(purple_ircv3_server_rpl_isupport),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_LUSERCLIENT,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_LUSEROP,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_LUSERUNKNOWN,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_LUSERCHANNELS,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_LUSERME,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_LOCALUSERS,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_GLOBALUSERS,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_MOTD,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_MOTDSTART,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_UMODEIS,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_ERR_YOUREBANNEDCREEP,
	                        G_CALLBACK(purple_ircv3_server_message_handler),
	                        connection, G_CONNECT_DEFAULT);

	g_signal_connect_object(client, "message::" IBIS_ERR_NOMOTD,
	                        G_CALLBACK(purple_ircv3_server_no_motd_handler),
	                        connection, G_CONNECT_DEFAULT);

	g_signal_connect_object(client, "message::" IBIS_MSG_TOPIC,
	                        G_CALLBACK(purple_ircv3_message_handler_topic),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_NOTOPIC,
	                        G_CALLBACK(purple_ircv3_message_handler_topic),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_TOPIC,
	                        G_CALLBACK(purple_ircv3_message_handler_topic),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_TOPICWHOTIME,
	                        G_CALLBACK(purple_ircv3_message_handler_whotopic),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_CHANNEL_URL,
	                        G_CALLBACK(purple_ircv3_message_handler_channel_url),
	                        connection, G_CONNECT_DEFAULT);

	g_signal_connect_object(client, "message::" IBIS_MSG_PRIVMSG,
	                        G_CALLBACK(purple_ircv3_message_handler_privmsg),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_NOTICE,
	                        G_CALLBACK(purple_ircv3_message_handler_privmsg),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_TAGMSG,
	                        G_CALLBACK(purple_ircv3_message_handler_tagmsg),
	                        connection, G_CONNECT_DEFAULT);

	g_signal_connect_object(client, "message::" IBIS_MSG_AWAY,
	                        G_CALLBACK(purple_ircv3_message_handler_away),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_JOIN,
	                        G_CALLBACK(purple_ircv3_message_handler_join),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_PART,
	                        G_CALLBACK(purple_ircv3_message_handler_part),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_NICK,
	                        G_CALLBACK(purple_ircv3_message_handler_nick),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_QUIT,
	                        G_CALLBACK(purple_ircv3_message_handler_quit),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_ERROR,
	                        G_CALLBACK(purple_ircv3_message_handler_error),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_NAMREPLY,
	                        G_CALLBACK(purple_ircv3_message_handler_namreply),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_ENDOFNAMES,
	                        G_CALLBACK(purple_ircv3_message_handler_ignore),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_WALLOPS,
	                        G_CALLBACK(purple_ircv3_message_handler_wallops),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_KICK,
	                        G_CALLBACK(purple_ircv3_message_handler_kick),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_MSG_MODE,
	                        G_CALLBACK(purple_ircv3_message_handler_mode),
	                        connection, G_CONNECT_DEFAULT);

	g_signal_connect_object(client, "message::" IBIS_RPL_WHOREPLY,
	                        G_CALLBACK(purple_ircv3_message_handler_whoreply),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(client, "message::" IBIS_RPL_ENDOFWHO,
	                        G_CALLBACK(purple_ircv3_message_handler_ignore),
	                        connection, G_CONNECT_DEFAULT);

	g_signal_connect_object(client, "message",
	                        G_CALLBACK(purple_ircv3_connection_unknown_message_cb),
	                        connection, G_CONNECT_AFTER);
}

/******************************************************************************
 * Callbacks
 *****************************************************************************/
static gboolean
purple_ircv3_connection_unknown_message_cb(G_GNUC_UNUSED IbisClient *client,
                                           G_GNUC_UNUSED const char *command,
                                           IbisMessage *ibis_message,
                                           gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	PurpleContact *contact = NULL;
	PurpleConversationMember *author = NULL;
	PurpleMessage *purple_message = NULL;
	char *contents = NULL;

	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         ibis_message);
	author = purple_conversation_find_or_add_member(connection->status_conversation,
	                                                PURPLE_CONTACT_INFO(contact),
	                                                FALSE, NULL);
	contents = g_strdup_printf(_("unhandled message: '%s'"),
	                           ibis_message_get_raw_message(ibis_message));

	purple_message = purple_message_new(author, contents);
	purple_conversation_write_message(connection->status_conversation,
	                                  purple_message);
	g_clear_object(&purple_message);

	g_free(contents);

	return TRUE;
}

static void
purple_ircv3_connection_connect_cb(GObject *source,
                                   G_GNUC_UNUSED GParamSpec *pspec,
                                   gpointer data)
{
	PurpleConnection *connection = data;
	IbisClient *client = IBIS_CLIENT(source);

	if(!ibis_client_get_connected(client)) {
		PurpleAccount *account = purple_connection_get_account(connection);

		purple_account_disconnect(account);
	}
}

static void
purple_ircv3_connection_registered_cb(GObject *source,
                                      G_GNUC_UNUSED GParamSpec *pspec,
                                      gpointer data)
{
	PurpleConnection *connection = data;
	IbisClient *client = IBIS_CLIENT(source);

	if(ibis_client_get_registered(client)) {
		PurpleAccount *account = NULL;

		account = purple_connection_get_account(connection);
		if(!purple_account_is_connected(account)) {
			purple_account_connected(account);
		}

		/* Once reconnected, we need to rejoin any channels that the
		 * conversation manager has for us.
		 */
		purple_ircv3_connection_rejoin_channels(PURPLE_IRCV3_CONNECTION(connection));
	}
}

static void
purple_ircv3_connection_error_cb(GObject *source,
                                 G_GNUC_UNUSED GParamSpec *pspec,
                                 gpointer data)
{
	IbisClient *client = IBIS_CLIENT(source);
	PurpleConnection *connection = data;
	GError *error = NULL;

	error = ibis_client_get_error(client);
	if(error != NULL) {
		PurpleAccount *account = purple_connection_get_account(connection);

		purple_account_set_error(account, g_error_copy(error));
	}
}

static void
purple_ircv3_connection_capabilities_ready_cb(IbisCapabilities *capabilities,
                                              G_GNUC_UNUSED gpointer data)
{
	/* account-tag just adds an account tag to everything if it's available.
	 * The account-tag is the user's username for authentication for all users
	 * not just the one using libpurple.
	 */
	ibis_capabilities_lookup_and_request(capabilities,
	                                     IBIS_CAPABILITY_ACCOUNT_TAG);

	/* away-notify tells us when users in a channel go away or come back. */
	ibis_capabilities_lookup_and_request(capabilities,
	                                     IBIS_CAPABILITY_AWAY_NOTIFY);

	/* no-implicit-names tells the server to not send us the namreply message
	 * when joining a channel. These messages are not useful to use since we
	 * immediately send a WHO command on the channel when we join which has a
	 * super set of the information in namreply.
	 */
	ibis_capabilities_lookup_and_request(capabilities,
	                                     IBIS_CAPABILITY_NO_IMPLICIT_NAMES);
}

static void
purple_ircv3_connection_update_status_title_cb(G_GNUC_UNUSED GObject *obj,
                                               G_GNUC_UNUSED GParamSpec *pspec,
                                               gpointer data)
{
	PurpleIRCv3Connection *connection = data;
	char *title = NULL;
	const char *nick = NULL;
	const char *network = NULL;

	nick = ibis_client_get_active_nick(connection->client);

	network = ibis_client_get_network(connection->client);
	if(purple_strempty(network)) {
		network = connection->server_name;
	}

	title = g_strdup_printf(_("status %s on %s"), nick, network);
	purple_conversation_set_title(connection->status_conversation, title);
	g_free(title);
}

/******************************************************************************
 * PurpleConnection Implementation
 *****************************************************************************/
static gboolean
purple_ircv3_connection_connect(PurpleConnection *purple_connection,
                                GError **error)
{
	PurpleIRCv3Connection *connection = NULL;
	PurpleAccount *account = NULL;
	GCancellable *cancellable = NULL;
	IbisCapabilities *capabilities = NULL;
	GError *local_error = NULL;
	GProxyResolver *resolver = NULL;
	GStrv userparts = NULL;
	const char *password = NULL;
	const char *username = NULL;
	const char *value = NULL;
	int default_port = PURPLE_IRCV3_DEFAULT_TLS_PORT;
	int port = 0;
	gboolean tls = TRUE;
	gboolean require_password = FALSE;

	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(purple_connection), FALSE);

	connection = PURPLE_IRCV3_CONNECTION(purple_connection);
	account = purple_connection_get_account(purple_connection);

	connection->client = ibis_client_new();
	g_signal_connect_object(connection->client, "notify::connected",
	                        G_CALLBACK(purple_ircv3_connection_connect_cb),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(connection->client, "notify::registered",
	                        G_CALLBACK(purple_ircv3_connection_registered_cb),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(connection->client, "notify::error",
	                        G_CALLBACK(purple_ircv3_connection_error_cb),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(connection->client, "notify::active-nick",
	                        G_CALLBACK(purple_ircv3_connection_update_status_title_cb),
	                        connection, G_CONNECT_DEFAULT);
	g_signal_connect_object(connection->client, "notify::network",
	                        G_CALLBACK(purple_ircv3_connection_update_status_title_cb),
	                        connection, G_CONNECT_DEFAULT);
	purple_ircv3_connection_add_message_handlers(connection,
	                                             connection->client);

	/* We need to split the username to get the nick. */
	username = purple_account_get_username(account);
	userparts = g_strsplit(username, "@", 2);
	ibis_client_set_nick(connection->client, userparts[0]);
	g_strfreev(userparts);

	value = purple_account_get_string(account, "ident", NULL);
	ibis_client_set_username(connection->client, value);

	value = purple_account_get_string(account, "real-name", NULL);
	ibis_client_set_realname(connection->client, value);

	password = purple_account_get_string(account, "server-password", NULL);

	/* Turn on TLS if requested. */
	tls = purple_account_get_bool(account, "use-tls", TRUE);

	/* If TLS is not being used, set the default port to the plain port. */
	if(!tls) {
		default_port = PURPLE_IRCV3_DEFAULT_PLAIN_PORT;
	}
	port = purple_account_get_int(account, "port", default_port);

	require_password = purple_account_get_require_password(account);
	if(require_password) {
		purple_ircv3_connection_setup_sasl(connection, account);
	}

	cancellable = purple_connection_get_cancellable(purple_connection);

	/* Connect to the ready signal of capabilities. */
	capabilities = ibis_client_get_capabilities(connection->client);
	g_signal_connect_object(capabilities, "ready",
	                        G_CALLBACK(purple_ircv3_connection_capabilities_ready_cb),
	                        connection, G_CONNECT_DEFAULT);

	resolver = purple_proxy_get_proxy_resolver(account, &local_error);
	if(local_error != NULL) {
		g_propagate_error(error, local_error);

		g_clear_object(&resolver);
		g_clear_object(&connection->client);

		return FALSE;
	}

	ibis_client_connect(connection->client, connection->server_name, port,
	                    password, tls, cancellable, resolver);

	return TRUE;
}

static gboolean
purple_ircv3_connection_disconnect(PurpleConnection *purple_connection,
                                   G_GNUC_UNUSED GError **error)
{
	PurpleIRCv3Connection *connection = NULL;

	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(purple_connection), FALSE);

	connection = PURPLE_IRCV3_CONNECTION(purple_connection);

	/* TODO: send QUIT command. */

	g_clear_object(&connection->client);

	return TRUE;
}

/******************************************************************************
 * GObject Implementation
 *****************************************************************************/
static void
purple_ircv3_connection_get_property(GObject *obj, guint param_id,
                                     GValue *value, GParamSpec *pspec)
{
	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);

	switch(param_id) {
	case PROP_CLIENT:
		g_value_set_object(value,
		                   purple_ircv3_connection_get_client(connection));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
		break;
	}
}

static void
purple_ircv3_connection_dispose(GObject *obj) {
	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);

	g_clear_object(&connection->client);
	g_clear_object(&connection->status_conversation);

	G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->dispose(obj);
}

static void
purple_ircv3_connection_finalize(GObject *obj) {
	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);

	g_clear_pointer(&connection->server_name, g_free);

	G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->finalize(obj);
}

static void
purple_ircv3_connection_constructed(GObject *obj) {
	PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
	PurpleAccount *account = NULL;
	PurpleConversationManager *conversation_manager = NULL;
	char **userparts = NULL;
	const char *username = NULL;

	G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->constructed(obj);

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));

	/* Split the username into nick and server and store the values. */
	username = purple_account_get_username(account);
	userparts = g_strsplit(username, "@", 2);
	connection->server_name = g_strdup(userparts[1]);

	/* Free the userparts vector. */
	g_strfreev(userparts);

	/* Check if we have an existing status conversation. */
	conversation_manager = purple_conversation_manager_get_default();
	connection->status_conversation = purple_conversation_manager_find_with_id(conversation_manager,
	                                                                           account,
	                                                                           IRCV3_STATUS_CONVERSATION_ID);

	if(!PURPLE_IS_CONVERSATION(connection->status_conversation)) {
		/* Create our status conversation. */
		connection->status_conversation = g_object_new(
			PURPLE_TYPE_CONVERSATION,
			"account", account,
			"id", IRCV3_STATUS_CONVERSATION_ID,
			"online", TRUE,
			NULL);

			purple_conversation_manager_add(conversation_manager,
			                                connection->status_conversation);
	} else {
		/* The conversation existed, so add a reference to it. */
		g_object_ref(connection->status_conversation);

		purple_conversation_set_online(connection->status_conversation, TRUE);
		purple_conversation_set_error(connection->status_conversation, NULL);
	}
}

static void
purple_ircv3_connection_init(G_GNUC_UNUSED PurpleIRCv3Connection *connection) {
}

static void
purple_ircv3_connection_class_finalize(G_GNUC_UNUSED PurpleIRCv3ConnectionClass *klass) {
}

static void
purple_ircv3_connection_class_init(PurpleIRCv3ConnectionClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
	PurpleConnectionClass *connection_class = PURPLE_CONNECTION_CLASS(klass);

	obj_class->get_property = purple_ircv3_connection_get_property;
	obj_class->constructed = purple_ircv3_connection_constructed;
	obj_class->dispose = purple_ircv3_connection_dispose;
	obj_class->finalize = purple_ircv3_connection_finalize;

	connection_class->connect = purple_ircv3_connection_connect;
	connection_class->disconnect = purple_ircv3_connection_disconnect;

	/**
	 * PurpleIRCv3Connection:client:
	 *
	 * The [class@Ibis.Client] that this connection is using.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CLIENT] = g_param_spec_object(
		"client", NULL, NULL,
		IBIS_TYPE_CLIENT,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
}

/******************************************************************************
 * Internal API
 *****************************************************************************/
void
purple_ircv3_connection_register(GPluginNativePlugin *plugin) {
	purple_ircv3_connection_register_type(G_TYPE_MODULE(plugin));
}

/******************************************************************************
 * Public API
 *****************************************************************************/
IbisClient *
purple_ircv3_connection_get_client(PurpleIRCv3Connection *connection) {
	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);

	return connection->client;
}

void
purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection,
                                           IbisMessage *ibis_message)
{
	PurpleContact *contact = NULL;
	PurpleConversationMember *author = NULL;
	PurpleMessage *purple_message = NULL;
	GString *str = NULL;
	GStrv params = NULL;
	char *stripped = NULL;
	const char *command = NULL;

	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
	g_return_if_fail(IBIS_IS_MESSAGE(ibis_message));

	command = ibis_message_get_command(ibis_message);

	str = g_string_new(command);

	params = ibis_message_get_params(ibis_message);
	if(params != NULL && params[0] != NULL) {
		char *joined = g_strjoinv(" ", params);

		g_string_append_printf(str, " %s", joined);

		g_free(joined);
	}

	stripped = ibis_formatting_strip(str->str);
	g_string_free(str, TRUE);

	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         ibis_message);
	author = purple_conversation_find_or_add_member(connection->status_conversation,
	                                                PURPLE_CONTACT_INFO(contact),
	                                                FALSE, NULL);
	purple_message = purple_message_new(author, stripped);
	g_free(stripped);

	purple_conversation_write_message(connection->status_conversation,
	                                  purple_message);

	g_clear_object(&purple_message);
}

PurpleConversation *
purple_ircv3_connection_find_or_create_conversation(PurpleIRCv3Connection *connection,
                                                    const char *id)
{
	PurpleAccount *account = NULL;
	PurpleConversation *conversation = NULL;
	PurpleConversationManager *manager = NULL;
	char *normalized_id = NULL;

	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);
	g_return_val_if_fail(id != NULL, NULL);

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	manager = purple_conversation_manager_get_default();

	normalized_id = ibis_client_normalize(connection->client, id);
	conversation = purple_conversation_manager_find_with_id(manager, account,
	                                                        normalized_id);

	if(!PURPLE_IS_CONVERSATION(conversation)) {
		PurpleConversationType type = PURPLE_CONVERSATION_TYPE_DM;

		if(ibis_client_is_channel(connection->client, id)) {
			type = PURPLE_CONVERSATION_TYPE_CHANNEL;
		}

		conversation = g_object_new(
			PURPLE_TYPE_CONVERSATION,
			"account", account,
			"id", normalized_id,
			"title", id,
			"type", type,
			"online", TRUE,
			NULL);

		purple_conversation_manager_add(manager, conversation);

		/* The manager creates its own reference on our new conversation, so we
		 * borrow it like we do above if it already exists.
		 */
		g_object_unref(conversation);
	}

	g_free(normalized_id);

	return conversation;
}

PurpleContact *
purple_ircv3_connection_find_or_create_contact(PurpleIRCv3Connection *connection,
                                               IbisMessage *message)
{
	PurpleAccount *account = NULL;
	PurpleContact *contact = NULL;
	PurpleContactManager *manager = NULL;
	PurplePresence *presence = NULL;
	IbisTags *tags = NULL;
	const char *source = NULL;
	char *nick = NULL;

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	manager = purple_contact_manager_get_default();

	tags = ibis_message_get_tags(message);
	if(IBIS_IS_TAGS(tags)) {
		const char *account_tag = NULL;

		account_tag = ibis_tags_lookup(tags, IBIS_TAG_ACCOUNT);
		if(!purple_strempty(account_tag)) {
			contact = purple_contact_manager_find_with_id(manager, account,
			                                              account_tag);
		}
	}

	source = ibis_message_get_source(message);
	ibis_source_parse(source, &nick, NULL, NULL);

	/* If we don't have a contact yet, use the source (Luke) to search next. */
	if(!PURPLE_IS_CONTACT(contact)) {
		contact = purple_contact_manager_find_with_id(manager, account, nick);
	}

	/* If we _still_ don't have a contact, create it. */
	if(!PURPLE_IS_CONTACT(contact)) {
		contact = purple_contact_new(account, nick);
		purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact), nick);

		purple_contact_manager_add(manager, contact);

		/* We don't return a reference and the manager add its own. */
		g_object_unref(G_OBJECT(contact));
	}

	purple_contact_info_set_sid(PURPLE_CONTACT_INFO(contact), source);
	purple_contact_info_set_display_name(PURPLE_CONTACT_INFO(contact), nick);

	/* Grab the presence and set it as online right away. */
	presence = purple_contact_info_get_presence(PURPLE_CONTACT_INFO(contact));
	purple_presence_set_primitive(presence,
	                              PURPLE_PRESENCE_PRIMITIVE_AVAILABLE);

	g_free(nick);

	return contact;
}

PurpleContact *
purple_ircv3_connection_find_or_create_contact_from_nick(PurpleIRCv3Connection *connection,
                                                         const char *nick)
{
	PurpleAccount *account = NULL;
	PurpleContact *contact = NULL;
	PurpleContactManager *manager = NULL;

	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);
	g_return_val_if_fail(!purple_strempty(nick), NULL);

	account = purple_connection_get_account(PURPLE_CONNECTION(connection));
	manager = purple_contact_manager_get_default();

	contact = purple_contact_manager_find_with_id(manager, account, nick);
	if(!PURPLE_IS_CONTACT(contact)) {
		contact = purple_contact_manager_find_with_username(manager, account,
		                                                    nick);
	}

	if(!PURPLE_IS_CONTACT(contact)) {
		PurpleAccount *account = NULL;

		account = purple_connection_get_account(PURPLE_CONNECTION(connection));

		contact = purple_contact_new(account, nick);
		purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact), nick);
		purple_contact_info_set_display_name(PURPLE_CONTACT_INFO(contact),
		                                     nick);

		purple_contact_manager_add(manager, contact);
		g_object_unref(G_OBJECT(contact));
	}

	return contact;
}

PurpleConversation *
purple_ircv3_connection_get_status_conversation(PurpleIRCv3Connection *connection)
{
	g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);

	return connection->status_conversation;
}

void
purple_ircv3_connection_write_status_message(PurpleIRCv3Connection *connection,
                                             IbisMessage *ibis_message,
                                             gboolean show_command,
                                             gboolean event)
{
	PurpleContact *contact = NULL;
	PurpleConversationMember *author = NULL;
	PurpleMessage *purple_message = NULL;
	GString *str = NULL;
	GStrv params = NULL;
	char *body = NULL;
	char *stripped = NULL;

	str = g_string_new("");

	if(show_command) {
		const char *command = NULL;

		command = ibis_message_get_command(ibis_message);

		if(!purple_strempty(command)) {
			g_string_append_printf(str, "%s ", command);
		}
	}

	params = ibis_message_get_params(ibis_message);
	body = g_strjoinv(" ", params);
	stripped = ibis_formatting_strip(body);
	g_free(body);
	g_string_append(str, stripped);
	g_free(stripped);

	contact = purple_ircv3_connection_find_or_create_contact(connection,
	                                                         ibis_message);
	author = purple_conversation_find_or_add_member(connection->status_conversation,
	                                                PURPLE_CONTACT_INFO(contact),
	                                                FALSE, NULL);

	purple_message = purple_message_new(author, str->str);
	g_string_free(str, TRUE);

	purple_message_set_event(purple_message, event);

	purple_conversation_write_message(connection->status_conversation,
	                                  purple_message);
	g_clear_object(&purple_message);
}

mercurial