protocols/ircv3/purpleircv3protocolconversation.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 43235
42e7b89033fe
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 <ibis.h>

#include "purpleircv3protocolconversation.h"

#include "purpleircv3connection.h"
#include "purpleircv3core.h"

/******************************************************************************
 * PurpleProtocolConversation Implementation
 *****************************************************************************/
static void
purple_ircv3_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol,
                                                      PurpleConversation *conversation,
                                                      PurpleMessage *message,
                                                      GCancellable *cancellable,
                                                      GAsyncReadyCallback callback,
                                                      gpointer data)
{
	PurpleIRCv3Connection *v3_connection = NULL;
	PurpleAccount *account = NULL;
	PurpleConnection *connection = NULL;
	PurpleConversation *status_conversation = NULL;
	IbisClient *client = NULL;
	IbisMessage *ibis_message = NULL;
	GTask *task = NULL;
	const char *contents = NULL;
	const char *id = NULL;

	account = purple_conversation_get_account(conversation);
	connection = purple_account_get_connection(account);
	v3_connection = PURPLE_IRCV3_CONNECTION(connection);

	/* We ignore non commands to the status conversation. */
	status_conversation = purple_ircv3_connection_get_status_conversation(v3_connection);
	if(conversation == status_conversation) {
		task = g_task_new(protocol, cancellable, callback, data);
		g_task_return_boolean(task, TRUE);
		g_clear_object(&task);

		return;
	}

	id = purple_conversation_get_id(conversation);
	contents = purple_message_get_contents(message);

	ibis_message = ibis_message_new(IBIS_MSG_PRIVMSG);
	if(purple_message_get_action(message)) {
		IbisCTCPMessage *ctcp_message = NULL;

		ibis_message_set_params(ibis_message, id, NULL);

		ctcp_message = ibis_ctcp_message_new(IBIS_CTCP_ACTION);
		ibis_ctcp_message_set_params(ctcp_message, contents, NULL);
		ibis_message_set_ctcp_message(ibis_message, ctcp_message);
	} else {
		ibis_message_set_params(ibis_message, id, contents, NULL);
	}

	client = purple_ircv3_connection_get_client(v3_connection);
	ibis_client_write(client, ibis_message);

	task = g_task_new(protocol, cancellable, callback, data);
	g_task_return_boolean(task, TRUE);
	g_clear_object(&task);

	/* This will be made conditional when we add echo-message support.
	 * https://ircv3.net/specs/extensions/echo-message
	 */
	purple_conversation_write_message(conversation, message);
}

static gboolean
purple_ircv3_protocol_conversation_send_message_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
                                                       GAsyncResult *result,
                                                       GError **error)
{
	return g_task_propagate_boolean(G_TASK(result), error);
}

static PurpleChannelJoinDetails *
purple_ircv3_protocol_conversation_get_channel_join_details(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
                                                            G_GNUC_UNUSED PurpleAccount *account)
{
	return purple_channel_join_details_new(0, FALSE, 0, TRUE, 0);
}

static void
purple_ircv3_protocol_conversation_join_channel_async(PurpleProtocolConversation *protocol,
                                                      PurpleAccount *account,
                                                      PurpleChannelJoinDetails* details,
                                                      GCancellable* cancellable,
                                                      GAsyncReadyCallback callback,
                                                      gpointer data)
{
	PurpleIRCv3Connection *v3_connection = NULL;
	PurpleConnection *connection = NULL;
	PurpleConversation *conversation = NULL;
	PurpleConversationManager *manager = NULL;
	IbisClient *client = NULL;
	IbisMessage *message = NULL;
	GTask *task = NULL;
	char *normalized_name = NULL;
	const char *name = NULL;
	const char *password = NULL;

	connection = purple_account_get_connection(account);
	v3_connection = PURPLE_IRCV3_CONNECTION(connection);

	task = g_task_new(protocol, cancellable, callback, data);

	/* Validate that the name isn't empty. */
	/* TODO: check that name match the ISUPPORT channel prefixes. */
	name = purple_channel_join_details_get_name(details);
	if(purple_strempty(name)) {
		g_task_return_new_error_literal(task, PURPLE_IRCV3_DOMAIN, 0,
		                                _("channel name is empty"));
		g_clear_object(&task);

		return;
	}

	if(strchr(name, ',') != NULL) {
		g_task_return_new_error_literal(task, PURPLE_IRCV3_DOMAIN, 0,
		                                _("only one channel may be joined at a time"));
		g_clear_object(&task);

		return;
	}

	client = purple_ircv3_connection_get_client(v3_connection);
	normalized_name = ibis_client_normalize(client, name);

	manager = purple_conversation_manager_get_default();
	conversation = purple_conversation_manager_find_with_id(manager, account,
	                                                        normalized_name);

	/* If the conversation already exists, just return TRUE. */
	if(PURPLE_IS_CONVERSATION(conversation)) {
		g_free(normalized_name);

		g_task_return_boolean(task, TRUE);
		g_clear_object(&task);

		return;
	}

	message = ibis_message_new(IBIS_MSG_JOIN);

	password = purple_channel_join_details_get_password(details);
	if(!purple_strempty(password)) {
		ibis_message_set_params(message, normalized_name, password, NULL);
	} else {
		ibis_message_set_params(message, normalized_name, NULL);
	}

	conversation = g_object_new(
		PURPLE_TYPE_CONVERSATION,
		"account", account,
		"type", PURPLE_CONVERSATION_TYPE_CHANNEL,
		"id", normalized_name,
		"title", name,
		"online", TRUE,
		NULL);
	purple_conversation_manager_add(manager, conversation);

	ibis_client_write(client, message);

	g_free(normalized_name);

	g_task_return_pointer(task, conversation, g_object_unref);
	g_clear_object(&task);
}

static PurpleConversation *
purple_ircv3_protocol_conversation_join_channel_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
                                                       GAsyncResult *result,
                                                       GError **error)
{
	return g_task_propagate_pointer(G_TASK(result), error);
}

static void
purple_ircv3_protocol_conversation_leave_conversation_async(PurpleProtocolConversation *protocol,
                                                            PurpleConversation *conversation,
                                                            GCancellable *cancellable,
                                                            GAsyncReadyCallback callback,
                                                            gpointer data)
{
	GTask *task = NULL;

	if(purple_conversation_is_channel(conversation)) {
		PurpleIRCv3Connection *v3_connection = NULL;
		PurpleAccount *account = NULL;
		PurpleConnection *connection = NULL;
		IbisClient *client = NULL;
		IbisMessage *message = NULL;

		account = purple_conversation_get_account(conversation);
		connection = purple_account_get_connection(account);
		v3_connection = PURPLE_IRCV3_CONNECTION(connection);
		client = purple_ircv3_connection_get_client(v3_connection);

		message = ibis_message_new(IBIS_MSG_PART);
		ibis_message_set_params(message,
		                        purple_conversation_get_id(conversation),
		                        NULL);
		ibis_client_write(client, message);
	}

	task = g_task_new(protocol, cancellable, callback, data);
	g_task_set_source_tag(task,
	                      purple_ircv3_protocol_conversation_leave_conversation_async);
	g_task_return_boolean(task, TRUE);
	g_clear_object(&task);
}

static gboolean
purple_ircv3_protocol_conversation_leave_conversation_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
                                                             GAsyncResult *result,
                                                             GError **error)
{
	g_return_val_if_fail(G_IS_ASYNC_RESULT(result), FALSE);

	if(!g_async_result_is_tagged(result,
	                             purple_ircv3_protocol_conversation_leave_conversation_async))
	{
		g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
		                    "result has unexpected tag");
		return FALSE;
	}

	return g_task_propagate_boolean(G_TASK(result), error);
}

static void
purple_ircv3_protocol_conversation_send_typing(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
                                               PurpleConversation *conversation,
                                               PurpleTypingState state)
{
	PurpleIRCv3Connection *connection;
	PurpleAccount *account = NULL;
	PurpleConnection *purple_connection = NULL;
	PurpleConversation *status_conversation = NULL;
	IbisClient *client = NULL;
	IbisMessage *message = NULL;
	IbisTags *tags = NULL;
	const char *value = NULL;

	if(state == PURPLE_TYPING_STATE_TYPING) {
		value = IBIS_TYPING_ACTIVE;
	} else if(state == PURPLE_TYPING_STATE_PAUSED) {
		value = IBIS_TYPING_PAUSED;
	} else if(state == PURPLE_TYPING_STATE_NONE) {
		value = IBIS_TYPING_DONE;
	}

	if(value == NULL) {
		return;
	}

	account = purple_conversation_get_account(conversation);
	purple_connection = purple_account_get_connection(account);
	connection = PURPLE_IRCV3_CONNECTION(purple_connection);

	status_conversation = purple_ircv3_connection_get_status_conversation(connection);
	if(conversation == status_conversation) {
		return;
	}

	message = ibis_message_new(IBIS_MSG_TAGMSG);
	ibis_message_set_params(message, purple_conversation_get_id(conversation),
	                        NULL);

	tags = ibis_message_get_tags(message);
	ibis_tags_add(tags, IBIS_TAG_TYPING, value);

	client = purple_ircv3_connection_get_client(connection);
	ibis_client_write(client, message);
}

static void
purple_ircv3_protocol_conversation_refresh(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
                                           PurpleConversation *conversation)
{
	PurpleIRCv3Connection *connection;
	PurpleAccount *account = NULL;
	PurpleConnection *purple_connection = NULL;
	IbisClient *client = NULL;
	IbisMessage *message = NULL;

	if(purple_conversation_get_online(conversation)) {
		return;
	}

	if(!purple_conversation_is_channel(conversation)) {
		/* We need to mark DM's as online again. */
		purple_conversation_set_online(conversation, TRUE);

		return;
	}

	account = purple_conversation_get_account(conversation);
	purple_connection = purple_account_get_connection(account);
	connection = PURPLE_IRCV3_CONNECTION(purple_connection);

	message = ibis_message_new(IBIS_MSG_JOIN);
	ibis_message_set_params(message, purple_conversation_get_id(conversation),
	                        NULL);

	client = purple_ircv3_connection_get_client(connection);
	ibis_client_write(client, message);

	/* TODO: We need to validate the JOIN by handling the join from the server,
	 * but right now we just assume it was successful.
	 */
	purple_conversation_set_online(conversation, TRUE);
}

void
purple_ircv3_protocol_conversation_init(PurpleProtocolConversationInterface *iface) {
	iface->send_message_async =
		purple_ircv3_protocol_conversation_send_message_async;
	iface->send_message_finish =
		purple_ircv3_protocol_conversation_send_message_finish;

	iface->get_channel_join_details =
		purple_ircv3_protocol_conversation_get_channel_join_details;
	iface->join_channel_async =
		purple_ircv3_protocol_conversation_join_channel_async;
	iface->join_channel_finish =
		purple_ircv3_protocol_conversation_join_channel_finish;

	iface->leave_conversation_async =
		purple_ircv3_protocol_conversation_leave_conversation_async;
	iface->leave_conversation_finish =
		purple_ircv3_protocol_conversation_leave_conversation_finish;

	iface->send_typing  =
		purple_ircv3_protocol_conversation_send_typing;
	iface->refresh =
		purple_ircv3_protocol_conversation_refresh;
}

mercurial