# HG changeset patch
# Node ID c24f21fd9578
# Parent  8a8e85e9e114
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -78,6 +78,7 @@
 	'purpleprotocolclient.c',
 	'purpleprotocolcontacts.c',
 	'purpleprotocolcontactsearch.c',
+	'purpleprotocolconversation.c',
 	'purpleprotocolim.c',
 	'purpleprotocolmanager.c',
 	'purpleprotocolmedia.c',
@@ -198,6 +199,7 @@
 	'purpleprotocolclient.h',
 	'purpleprotocolcontacts.h',
 	'purpleprotocolcontactsearch.h',
+	'purpleprotocolconversation.h',
 	'purpleprotocolim.h',
 	'purpleprotocolmedia.h',
 	'purpleprotocolmanager.h',
diff --git a/libpurple/purpleprotocolconversation.h b/libpurple/purpleprotocolconversation.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleprotocolconversation.h
@@ -0,0 +1,101 @@
+/*
+ * 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 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#if !defined(PURPLE_GLOBAL_HEADER_INSIDE) && !defined(PURPLE_COMPILATION)
+# error "only <purple.h> may be included directly"
+#endif
+
+#ifndef PURPLE_PROTOCOL_CONVERSATION_H
+#define PURPLE_PROTOCOL_CONVERSATION_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include "purpleconversation.h"
+#include "purplemessage.h"
+#include "purpleprotocol.h"
+
+G_BEGIN_DECLS
+
+#define PURPLE_TYPE_PROTOCOL_CONVERSATION (purple_protocol_conversation_get_type())
+G_DECLARE_INTERFACE(PurpleProtocolConversation, purple_protocol_conversation,
+                    PURPLE, PROTOCOL_CONVERSATION, PurpleProtocol)
+
+/**
+ * PurpleProtocolConversationInterface:
+ *
+ * This interface defines the behavior for interacting with conversations at
+ * the protocol layer. These methods will primarily be called by the user
+ * interface.
+ *
+ * Since: 3.0.0
+ */
+struct _PurpleProtocolConversationInterface {
+	/*< private >*/
+	GTypeInterface parent;
+
+	/*< public >*/
+	void (*send_message_async)(PurpleProtocolConversation *protocol, PurpleConversation *conversation, PurpleMessage *message, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+	gboolean (*send_message_finish)(PurpleProtocolConversation *protocol, GAsyncResult *result, GError **error);
+
+	/*< private >*/
+	gpointer reserved[8];
+};
+
+/**
+ * purple_protocol_conversation_send_message_async:
+ * @protocol: The instance.
+ * @conversation: The conversation to send a message to.
+ * @message: The message to send.
+ * @cancellable: (nullable): optional GCancellable object, %NULL to ignore.
+ * @callback: (nullable) (scope async): The callback to call after the message
+ *            has been sent.
+ * @data: (nullable): Optional user data to pass to @callback.
+ *
+ * Starts the process of sending @message to @conversation.
+ *
+ * Since: 3.0.0
+ */
+void purple_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol, PurpleConversation *conversation, PurpleMessage *message, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+
+/**
+ * purple_protocol_conversation_send_message_finish:
+ * @protocol: The instance.
+ * @result: The [iface@Gio.AsyncResult] from the previous
+ *          [method@ProtocolConversation.send_message_async] call.
+ * @error: Return address for a #GError, or %NULL.
+ *
+ * Finishes a previous call to
+ * [method@ProtocolConversation.send_message_async]. This should be called from
+ * the callback of that function to get the result of whether or not the
+ * message was sent successfully.
+ *
+ * Returns: %TRUE if the message was sent successfully, otherwise %FALSE with
+ *          @error possibly set.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_protocol_conversation_send_message_finish(PurpleProtocolConversation *protocol, GAsyncResult *result, GError **error);
+
+G_END_DECLS
+
+#endif /* PURPLE_PROTOCOL_CONVERSATION_H */
diff --git a/libpurple/purpleprotocolconversation.c b/libpurple/purpleprotocolconversation.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purpleprotocolconversation.c
@@ -0,0 +1,82 @@
+/*
+ * 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 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "purpleprotocolconversation.h"
+
+/******************************************************************************
+ * GInterface Implementation
+ *****************************************************************************/
+G_DEFINE_INTERFACE(PurpleProtocolConversation, purple_protocol_conversation,
+                   PURPLE_TYPE_PROTOCOL)
+
+static void
+purple_protocol_conversation_default_init(G_GNUC_UNUSED PurpleProtocolConversationInterface *iface)
+{
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+void
+purple_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol,
+                                                PurpleConversation *conversation,
+                                                PurpleMessage *message,
+                                                GCancellable *cancellable,
+                                                GAsyncReadyCallback callback,
+                                                gpointer data)
+{
+	PurpleProtocolConversationInterface *iface = NULL;
+
+	g_return_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol));
+	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));
+	g_return_if_fail(PURPLE_IS_MESSAGE(message));
+
+	iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol);
+	if(iface != NULL && iface->send_message_async != NULL) {
+		iface->send_message_async(protocol, conversation, message, cancellable,
+		                          callback, data);
+	} else {
+		g_warning("%s does not implement send_message_async",
+		          G_OBJECT_TYPE_NAME(protocol));
+	}
+}
+
+gboolean
+purple_protocol_conversation_send_message_finish(PurpleProtocolConversation *protocol,
+                                                 GAsyncResult *result,
+                                                 GError **error)
+{
+	PurpleProtocolConversationInterface *iface = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol), FALSE);
+	g_return_val_if_fail(G_IS_ASYNC_RESULT(result), FALSE);
+
+	iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol);
+	if(iface != NULL && iface->send_message_finish != NULL) {
+		return iface->send_message_finish(protocol, result, error);
+	} else {
+		g_warning("%s does not implement send_message_finish",
+		          G_OBJECT_TYPE_NAME(protocol));
+	}
+
+	return FALSE;
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -26,6 +26,7 @@
     'protocol_action',
     'protocol_contacts',
     'protocol_contact_search',
+    'protocol_conversation',
     'protocol_roster',
     'protocol_xfer',
     'purplepath',
diff --git a/libpurple/tests/test_protocol_conversation.c b/libpurple/tests/test_protocol_conversation.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_protocol_conversation.c
@@ -0,0 +1,342 @@
+/*
+ * 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 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, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+/******************************************************************************
+ * Globals
+ *****************************************************************************/
+
+/* Since we're using GTask to test asynchronous functions, we need to use a
+ * main loop.
+ */
+static GMainLoop *loop = NULL;
+
+#define TEST_PURPLE_PROTOCOL_CONVERSATION_DOMAIN (g_quark_from_static_string("test-protocol-conversation"))
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static gboolean
+test_purple_protocol_conversation_timeout_cb(gpointer data) {
+	g_main_loop_quit(data);
+
+	g_warning("timed out waiting for the callback function to be called");
+
+	return G_SOURCE_REMOVE;
+}
+
+/******************************************************************************
+ * TestProtocolConversationEmpty implementation
+ *****************************************************************************/
+G_DECLARE_FINAL_TYPE(TestPurpleProtocolConversationEmpty,
+                     test_purple_protocol_conversation_empty,
+                     TEST_PURPLE, PROTOCOL_CONVERSATION_EMPTY, PurpleProtocol)
+
+struct _TestPurpleProtocolConversationEmpty {
+	PurpleProtocol parent;
+};
+
+static void
+test_purple_protocol_conversation_empty_iface_init(G_GNUC_UNUSED PurpleProtocolConversationInterface *iface)
+{
+}
+
+G_DEFINE_TYPE_WITH_CODE(TestPurpleProtocolConversationEmpty,
+                        test_purple_protocol_conversation_empty,
+                        PURPLE_TYPE_PROTOCOL,
+                        G_IMPLEMENT_INTERFACE(PURPLE_TYPE_PROTOCOL_CONVERSATION,
+                                              test_purple_protocol_conversation_empty_iface_init))
+
+static void
+test_purple_protocol_conversation_empty_init(G_GNUC_UNUSED TestPurpleProtocolConversationEmpty *empty)
+{
+}
+
+static void
+test_purple_protocol_conversation_empty_class_init(G_GNUC_UNUSED TestPurpleProtocolConversationEmptyClass *klass)
+{
+}
+
+/******************************************************************************
+ * TestProtocolConversationEmpty Tests
+ *****************************************************************************/
+static void
+test_purple_protocol_conversation_empty_send_message_async(void) {
+	if(g_test_subprocess()) {
+		PurpleAccount *account = NULL;
+		PurpleConversation *conversation = NULL;
+		PurpleMessage *message = NULL;
+		PurpleProtocolConversation *protocol = NULL;
+
+		protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+		                        NULL);
+		account = purple_account_new("test", "test");
+		conversation = g_object_new(
+			PURPLE_TYPE_CONVERSATION,
+			"account", account,
+			"name", "this is required at the moment",
+			"type", PurpleConversationTypeDM,
+			NULL);
+		message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
+
+		purple_protocol_conversation_send_message_async(protocol, conversation,
+		                                                message, NULL, NULL,
+		                                                NULL);
+
+		g_clear_object(&account);
+		g_clear_object(&conversation);
+		g_clear_object(&message);
+		g_clear_object(&protocol);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-WARNING*TestPurpleProtocolConversationEmpty*send_message_async*");
+}
+
+static void
+test_purple_protocol_conversation_empty_send_message_finish(void) {
+	if(g_test_subprocess()) {
+		PurpleProtocolConversation *protocol = NULL;
+		GError *error = NULL;
+		GTask *task = NULL;
+		gboolean result = FALSE;
+
+		protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+		                        NULL);
+
+		task = g_task_new(protocol, NULL, NULL, NULL);
+
+		result = purple_protocol_conversation_send_message_finish(protocol,
+		                                                          G_ASYNC_RESULT(task),
+		                                                          &error);
+		g_assert_no_error(error);
+		g_assert_false(result);
+
+		g_clear_object(&task);
+		g_clear_object(&protocol);
+	}
+
+	g_test_trap_subprocess(NULL, 0, 0);
+	g_test_trap_assert_stderr("*Purple-WARNING*TestPurpleProtocolConversationEmpty*send_message_finish*");
+}
+
+/******************************************************************************
+ * TestProtocolConversation Implementation
+ *****************************************************************************/
+G_DECLARE_FINAL_TYPE(TestPurpleProtocolConversation,
+                     test_purple_protocol_conversation, TEST_PURPLE,
+                     PROTOCOL_CONVERSATION, PurpleProtocol)
+
+struct _TestPurpleProtocolConversation {
+	PurpleProtocol parent;
+
+	gboolean should_error;
+
+	guint send_message_async;
+	guint send_message_finish;
+};
+
+static void
+test_purple_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol,
+                                                     PurpleConversation *conversation,
+                                                     PurpleMessage *message,
+                                                     GCancellable *cancellable,
+                                                     GAsyncReadyCallback callback,
+                                                     gpointer data)
+{
+	TestPurpleProtocolConversation *test_protocol = NULL;
+	GTask *task = NULL;
+
+	g_assert_true(PURPLE_IS_CONVERSATION(conversation));
+	g_assert_true(PURPLE_IS_MESSAGE(message));
+
+	test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+	test_protocol->send_message_async += 1;
+
+	task = g_task_new(protocol, cancellable, callback, data);
+	if(test_protocol->should_error) {
+		GError *error = g_error_new_literal(TEST_PURPLE_PROTOCOL_CONVERSATION_DOMAIN,
+		                                    0, "error");
+		g_task_return_error(task, error);
+	} else {
+		g_task_return_boolean(task, TRUE);
+	}
+
+	g_clear_object(&task);
+}
+
+static gboolean
+test_purple_protocol_conversation_send_message_finish(PurpleProtocolConversation *protocol,
+                                                      GAsyncResult *result,
+                                                      GError **error)
+{
+	TestPurpleProtocolConversation *test_protocol = NULL;
+
+	test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+	test_protocol->send_message_finish += 1;
+
+	return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+static void
+test_purple_protocol_conversation_iface_init(PurpleProtocolConversationInterface *iface) {
+	iface->send_message_async = test_purple_protocol_conversation_send_message_async;
+	iface->send_message_finish = test_purple_protocol_conversation_send_message_finish;
+}
+
+G_DEFINE_TYPE_WITH_CODE(TestPurpleProtocolConversation, test_purple_protocol_conversation,
+                        PURPLE_TYPE_PROTOCOL,
+                        G_IMPLEMENT_INTERFACE(PURPLE_TYPE_PROTOCOL_CONVERSATION,
+                                              test_purple_protocol_conversation_iface_init))
+
+static void
+test_purple_protocol_conversation_init(TestPurpleProtocolConversation *protocol)
+{
+	protocol->send_message_async = 0;
+	protocol->send_message_finish = 0;
+}
+
+static void
+test_purple_protocol_conversation_class_init(G_GNUC_UNUSED TestPurpleProtocolConversationClass *klass)
+{
+}
+
+/******************************************************************************
+ * TestProtocolConversation SendMessage Tests
+ *****************************************************************************/
+static void
+test_purple_protocol_conversation_send_message_cb(GObject *obj,
+                                                  GAsyncResult *res,
+                                                  G_GNUC_UNUSED gpointer data)
+{
+	TestPurpleProtocolConversation *test_protocol = NULL;
+	PurpleProtocolConversation *protocol = NULL;
+	GError *error = NULL;
+	gboolean result = FALSE;
+
+	protocol = PURPLE_PROTOCOL_CONVERSATION(obj);
+	test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(obj);
+
+	result = purple_protocol_conversation_send_message_finish(protocol, res,
+	                                                          &error);
+
+	if(test_protocol->should_error) {
+		g_assert_error(error, TEST_PURPLE_PROTOCOL_CONVERSATION_DOMAIN, 0);
+		g_clear_error(&error);
+		g_assert_false(result);
+	} else {
+		g_assert_no_error(error);
+		g_assert_true(result);
+	}
+
+	g_main_loop_quit(loop);
+}
+
+static gboolean
+test_purple_protocol_conversation_send_message_idle(gpointer data) {
+	PurpleAccount *account = NULL;
+	PurpleConversation *conversation = NULL;
+	PurpleMessage *message = NULL;
+	PurpleProtocolConversation *protocol = data;
+
+	account = purple_account_new("test", "test");
+	g_object_set_data_full(G_OBJECT(protocol), "account", account, g_object_unref);
+
+	conversation = g_object_new(
+		PURPLE_TYPE_CONVERSATION,
+		"account", account,
+		"name", "this is required at the moment",
+		"type", PurpleConversationTypeDM,
+		NULL);
+	g_object_set_data_full(G_OBJECT(protocol), "conversation", conversation,
+	                       g_object_unref);
+
+	message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
+	g_object_set_data_full(G_OBJECT(protocol), "message", message,
+	                       g_object_unref);
+
+	purple_protocol_conversation_send_message_async(protocol, conversation,
+	                                                message, NULL,
+	                                                test_purple_protocol_conversation_send_message_cb,
+	                                                NULL);
+
+	return G_SOURCE_REMOVE;
+}
+
+static void
+test_purple_protocol_conversation_send_message_normal(gconstpointer data) {
+	TestPurpleProtocolConversation *protocol = NULL;
+
+	protocol = g_object_new(test_purple_protocol_conversation_get_type(),
+	                        NULL);
+	protocol->should_error = GPOINTER_TO_INT(data);
+
+	g_idle_add(test_purple_protocol_conversation_send_message_idle, protocol);
+	g_timeout_add_seconds(10, test_purple_protocol_conversation_timeout_cb,
+	                      loop);
+
+	g_main_loop_run(loop);
+
+	g_assert_cmpuint(protocol->send_message_async, ==, 1);
+	g_assert_cmpuint(protocol->send_message_finish, ==, 1);
+
+	g_clear_object(&protocol);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(int argc, char **argv) {
+	int ret = 0;
+
+	g_test_init(&argc, &argv, NULL);
+
+	test_ui_purple_init();
+
+	loop = g_main_loop_new(NULL, FALSE);
+
+	g_test_add_func("/protocol-conversation/empty/send-message-async",
+	                test_purple_protocol_conversation_empty_send_message_async);
+	g_test_add_func("/protocol-conversation/empty/send-message-finish",
+	                test_purple_protocol_conversation_empty_send_message_finish);
+
+	g_test_add_data_func("/protocol-contacts/normal/send-message-normal",
+	                     GINT_TO_POINTER(FALSE),
+	                     test_purple_protocol_conversation_send_message_normal);
+	g_test_add_data_func("/protocol-contacts/normal/send-message-error",
+	                     GINT_TO_POINTER(TRUE),
+	                     test_purple_protocol_conversation_send_message_normal);
+
+	ret = g_test_run();
+
+	g_main_loop_unref(loop);
+
+	test_ui_purple_uninit();
+
+	return ret;
+}
