pidgin/pidgin

Parents 5b864b9b22aa
Children b37a9a8ad04a
Add the ability to create conversations to PurpleProtocolConversation

Testing Done:
Ran the unit tests under valgrind and had the turtles check on everything as well.

Bugs closed: PIDGIN-17855

Reviewed at https://reviews.imfreedom.org/r/3045/
--- a/libpurple/purpleprotocolconversation.c Wed Apr 10 00:17:04 2024 -0500
+++ b/libpurple/purpleprotocolconversation.c Wed Apr 10 00:37:20 2024 -0500
@@ -36,6 +36,118 @@
/******************************************************************************
* Public API
*****************************************************************************/
+gboolean
+purple_protocol_conversation_implements_create_conversation(PurpleProtocolConversation *protocol)
+{
+ PurpleProtocolConversation *protocol_conversation = NULL;
+ PurpleProtocolConversationInterface *iface = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol), FALSE);
+
+ protocol_conversation = PURPLE_PROTOCOL_CONVERSATION(protocol);
+ iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol_conversation);
+
+ if(iface->get_create_conversation_details == NULL) {
+ return FALSE;
+ }
+
+ if(iface->create_conversation_async == NULL) {
+ return FALSE;
+ }
+
+ if(iface->create_conversation_finish == NULL) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+PurpleCreateConversationDetails *
+purple_protocol_conversation_get_create_conversation_details(PurpleProtocolConversation *protocol,
+ PurpleAccount *account)
+{
+ PurpleProtocolConversationInterface *iface = NULL;
+
+ g_return_val_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol), NULL);
+ g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
+
+ iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol);
+ if(iface != NULL && iface->get_create_conversation_details != NULL) {
+ return iface->get_create_conversation_details(protocol, account);
+ }
+
+ g_warning("%s does not implement get_create_conversation_details",
+ G_OBJECT_TYPE_NAME(protocol));
+
+ return NULL;
+}
+
+void
+purple_protocol_conversation_create_conversation_async(PurpleProtocolConversation *protocol,
+ PurpleAccount *account,
+ PurpleCreateConversationDetails *details,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer data)
+{
+ PurpleProtocolConversationInterface *iface = NULL;
+
+ g_return_if_fail(PURPLE_IS_PROTOCOL_CONVERSATION(protocol));
+ g_return_if_fail(PURPLE_IS_ACCOUNT(account));
+ g_return_if_fail(PURPLE_IS_CREATE_CONVERSATION_DETAILS(details));
+
+ iface = PURPLE_PROTOCOL_CONVERSATION_GET_IFACE(protocol);
+ if(iface != NULL && iface->create_conversation_async != NULL) {
+ iface->create_conversation_async(protocol, account, details,
+ cancellable, callback, data);
+ } else {
+ GTask *task = NULL;
+
+ task = g_task_new(protocol, cancellable, callback, data);
+ g_task_return_new_error(task, PURPLE_PROTOCOL_CONVERSATION_DOMAIN, 0,
+ "%s does not implement create_conversation_async",
+ G_OBJECT_TYPE_NAME(protocol));
+ g_task_set_source_tag(task,
+ purple_protocol_conversation_create_conversation_async);
+
+ /* details is transfer full and has already been null checked. */
+ g_object_unref(details);
+
+ g_clear_object(&task);
+ }
+}
+
+PurpleConversation *
+purple_protocol_conversation_create_conversation_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->create_conversation_finish != NULL) {
+ return iface->create_conversation_finish(protocol, result, error);
+ }
+
+ if(G_IS_TASK(result)) {
+ GTask *task = G_TASK(result);
+ gpointer source = NULL;
+
+ source = g_task_get_source_tag(task);
+ if(source == purple_protocol_conversation_create_conversation_async) {
+ return g_task_propagate_pointer(task, error);
+ } else {
+ g_warning("%s does not implement create_conversation_finish",
+ G_OBJECT_TYPE_NAME(protocol));
+ }
+ }
+
+ return FALSE;
+}
+
void
purple_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol,
PurpleConversation *conversation,
--- a/libpurple/purpleprotocolconversation.h Wed Apr 10 00:17:04 2024 -0500
+++ b/libpurple/purpleprotocolconversation.h Wed Apr 10 00:37:20 2024 -0500
@@ -34,6 +34,7 @@
#include "purpleavatar.h"
#include "purplechanneljoindetails.h"
#include "purpleconversation.h"
+#include "purplecreateconversationdetails.h"
#include "purplemessage.h"
#include "purpleprotocol.h"
#include "purpletyping.h"
@@ -41,6 +42,17 @@
G_BEGIN_DECLS
+/**
+ * PURPLE_PROTOCOL_CONVERSATION_DOMAIN:
+ *
+ * An error domain for [iface@ProtocolConversation] errors.
+ *
+ * Since: 3.0
+ */
+#define PURPLE_PROTOCOL_CONVERSATION_DOMAIN \
+ (g_quark_from_static_string("purple-protocol-conversation")) \
+ PURPLE_AVAILABLE_MACRO_IN_3_0
+
#define PURPLE_TYPE_PROTOCOL_CONVERSATION (purple_protocol_conversation_get_type())
PURPLE_AVAILABLE_IN_3_0
@@ -61,6 +73,10 @@
GTypeInterface parent;
/*< public >*/
+ PurpleCreateConversationDetails *(*get_create_conversation_details)(PurpleProtocolConversation *protocol, PurpleAccount *account);
+ void (*create_conversation_async)(PurpleProtocolConversation *protocol, PurpleAccount *account, PurpleCreateConversationDetails *details, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+ PurpleConversation *(*create_conversation_finish)(PurpleProtocolConversation *protocol, GAsyncResult *result, GError **error);
+
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);
@@ -81,6 +97,81 @@
};
/**
+ * purple_protocol_conversation_implements_create_conversation:
+ * @protocol: The instance.
+ *
+ * Checks if @protocol implements
+ * [vfunc@ProtocolConversation.get_create_conversation_details],
+ * [vfunc@ProtocolConversation.create_conversation_async], and
+ * [vfunc@ProtocolConversation.create_conversation_finish].
+ *
+ * Returns: %TRUE if everything is implemented, otherwise %FALSE.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+gboolean purple_protocol_conversation_implements_create_conversation(PurpleProtocolConversation *protocol);
+
+/**
+ * purple_protocol_conversation_get_create_conversation_details:
+ * @protocol: The instance.
+ * @account: The account.
+ *
+ * Gets a [class@CreateConversationDetails] from @protocol for @account.
+ *
+ * This can then be used in calls to
+ * [method@ProtocolConversation.create_conversation_async] to create the
+ * conversation.
+ *
+ * Returns: (transfer full): The details to create a conversation with
+ * @account.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleCreateConversationDetails *purple_protocol_conversation_get_create_conversation_details(PurpleProtocolConversation *protocol, PurpleAccount *account);
+
+/**
+ * purple_protocol_conversation_create_conversation_async:
+ * @protocol: The instance.
+ * @account: The account.
+ * @details: (transfer full): The create conversation details.
+ * @cancellable: (nullable): optional GCancellable object, %NULL to ignore.
+ * @callback: (nullable) (scope async): The callback to call after the
+ * conversation has been created.
+ * @data: (nullable): Optional user data to pass to @callback.
+ *
+ * Starts the process of creating a dm or group dm conversation on @account.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+void purple_protocol_conversation_create_conversation_async(PurpleProtocolConversation *protocol, PurpleAccount *account, PurpleCreateConversationDetails *details, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+
+/**
+ * purple_protocol_conversation_create_conversation_finish:
+ * @protocol: The instance.
+ * @result: The [iface@Gio.AsyncResult] from the previous
+ * [method@ProtocolConversation.create_conversation_async] call.
+ * @error: Return address for a #GError, or %NULL.
+ *
+ * Finishes a previous call to
+ * [method@ProtocolConversation.create_conversation_async]. This should be
+ * called from the callback of that function to get the result of whether or
+ * not the conversation was created successfully.
+ *
+ * It is the responsibility of the protocol to register the conversation with
+ * the default [class@ConversationManager].
+ *
+ * Returns: (transfer full): The new conversation if successful, otherwise
+ * %NULL with @error possibly set.
+ *
+ * Since: 3.0
+ */
+PURPLE_AVAILABLE_IN_3_0
+PurpleConversation *purple_protocol_conversation_create_conversation_finish(PurpleProtocolConversation *protocol, GAsyncResult *result, GError **error);
+
+/**
* purple_protocol_conversation_send_message_async:
* @protocol: The instance.
* @conversation: The conversation to send a message to.
--- a/libpurple/tests/test_protocol_conversation.c Wed Apr 10 00:17:04 2024 -0500
+++ b/libpurple/tests/test_protocol_conversation.c Wed Apr 10 00:37:20 2024 -0500
@@ -87,6 +87,109 @@
* TestProtocolConversationEmpty Tests
*****************************************************************************/
static void
+test_purple_protocol_conversation_empty_implements_create_conversation(void) {
+ PurpleProtocolConversation *protocol = NULL;
+
+ protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+ NULL);
+
+ g_assert_false(purple_protocol_conversation_implements_create_conversation(protocol));
+
+ g_assert_finalize_object(protocol);
+}
+
+static void
+test_purple_protocol_conversation_empty_get_create_conversation_details(void) {
+ if(g_test_subprocess()) {
+ PurpleAccount *account = NULL;
+ PurpleCreateConversationDetails *details = NULL;
+ PurpleProtocolConversation *protocol = NULL;
+
+ protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+ NULL);
+ account = purple_account_new("test", "test");
+ details = purple_protocol_conversation_get_create_conversation_details(protocol,
+ account);
+
+ g_assert_null(details);
+ g_assert_finalize_object(account);
+ g_assert_finalize_object(protocol);
+ }
+
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_stderr("*Purple-WARNING*TestPurpleProtocolConversationEmpty*create_conversation_details*");
+}
+
+static void
+test_purple_protocol_conversation_empty_create_conversation_async_cb(GObject *source,
+ GAsyncResult *result,
+ G_GNUC_UNUSED gpointer data)
+{
+ PurpleConversation *conversation = NULL;
+ GError *error = NULL;
+
+ g_assert_true(PURPLE_IS_PROTOCOL_CONVERSATION(source));
+
+ conversation = purple_protocol_conversation_create_conversation_finish(PURPLE_PROTOCOL_CONVERSATION(source),
+ result,
+ &error);
+ g_assert_error(error, PURPLE_PROTOCOL_CONVERSATION_DOMAIN, 0);
+ g_clear_error(&error);
+ g_assert_null(conversation);
+}
+
+static void
+test_purple_protocol_conversation_empty_create_conversation_async(void) {
+ PurpleAccount *account = NULL;
+ PurpleCreateConversationDetails *details = NULL;
+ PurpleProtocolConversation *protocol = NULL;
+
+ protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+ NULL);
+ account = purple_account_new("test", "test");
+ details = purple_create_conversation_details_new(0);
+
+ purple_protocol_conversation_create_conversation_async(protocol,
+ account,
+ details,
+ NULL,
+ test_purple_protocol_conversation_empty_create_conversation_async_cb,
+ NULL);
+
+ g_main_context_iteration(NULL, FALSE);
+
+ g_assert_finalize_object(account);
+ g_assert_finalize_object(protocol);
+}
+
+static void
+test_purple_protocol_conversation_empty_create_conversation_finish(void) {
+ if(g_test_subprocess()) {
+ PurpleProtocolConversation *protocol = NULL;
+ PurpleConversation *conversation = NULL;
+ GError *error = NULL;
+ GTask *task = NULL;
+
+ protocol = g_object_new(test_purple_protocol_conversation_empty_get_type(),
+ NULL);
+
+ task = g_task_new(protocol, NULL, NULL, NULL);
+
+ conversation = purple_protocol_conversation_create_conversation_finish(protocol,
+ G_ASYNC_RESULT(task),
+ &error);
+ g_assert_no_error(error);
+ g_assert_null(conversation);
+
+ g_assert_finalize_object(task);
+ g_assert_finalize_object(protocol);
+ }
+
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_stderr("*Purple-WARNING*TestPurpleProtocolConversationEmpty*create_conversation_finish*");
+}
+
+static void
test_purple_protocol_conversation_empty_send_message_async(void) {
if(g_test_subprocess()) {
PurpleAccount *account = NULL;
@@ -376,6 +479,10 @@
gboolean should_error;
+ guint get_create_conversation_details;
+ guint create_conversation_async;
+ guint create_conversation_finish;
+
guint send_message_async;
guint send_message_finish;
@@ -392,6 +499,73 @@
guint send_typing;
};
+static PurpleCreateConversationDetails *
+test_purple_protocol_conversation_get_create_conversation_details(PurpleProtocolConversation *protocol,
+ PurpleAccount *account)
+{
+ TestPurpleProtocolConversation *test_protocol = NULL;
+
+ test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+ test_protocol->get_create_conversation_details += 1;
+
+ g_assert_true(PURPLE_IS_PROTOCOL_CONVERSATION(protocol));
+ g_assert_true(PURPLE_IS_ACCOUNT(account));
+
+ return purple_create_conversation_details_new(10);
+}
+
+static void
+test_purple_protocol_conversation_create_conversation_async(PurpleProtocolConversation *protocol,
+ PurpleAccount *account,
+ PurpleCreateConversationDetails *details,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer data)
+{
+ TestPurpleProtocolConversation *test_protocol = NULL;
+ GTask *task = NULL;
+
+ g_assert_true(PURPLE_IS_ACCOUNT(account));
+ g_assert_true(PURPLE_IS_CREATE_CONVERSATION_DETAILS(details));
+
+ /* details is transfer full and we're done with it. */
+ g_object_unref(details);
+
+ test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+ test_protocol->create_conversation_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 {
+ PurpleConversation *conversation = NULL;
+
+ conversation = g_object_new(
+ PURPLE_TYPE_CONVERSATION,
+ "account", account,
+ "name", "test",
+ NULL);
+ g_task_return_pointer(task, conversation, g_object_unref);
+ }
+
+ g_clear_object(&task);
+}
+
+static PurpleConversation *
+test_purple_protocol_conversation_create_conversation_finish(PurpleProtocolConversation *protocol,
+ GAsyncResult *result,
+ GError **error)
+{
+ TestPurpleProtocolConversation *test_protocol = NULL;
+
+ test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(protocol);
+ test_protocol->create_conversation_finish += 1;
+
+ return g_task_propagate_pointer(G_TASK(result), error);
+}
+
static void
test_purple_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol,
PurpleConversation *conversation,
@@ -590,6 +764,10 @@
static void
test_purple_protocol_conversation_iface_init(PurpleProtocolConversationInterface *iface) {
+ iface->get_create_conversation_details = test_purple_protocol_conversation_get_create_conversation_details;
+ iface->create_conversation_async = test_purple_protocol_conversation_create_conversation_async;
+ iface->create_conversation_finish = test_purple_protocol_conversation_create_conversation_finish;
+
iface->send_message_async = test_purple_protocol_conversation_send_message_async;
iface->send_message_finish = test_purple_protocol_conversation_send_message_finish;
@@ -615,6 +793,10 @@
static void
test_purple_protocol_conversation_init(TestPurpleProtocolConversation *protocol)
{
+ protocol->get_create_conversation_details = 0;
+ protocol->create_conversation_async = 0;
+ protocol->create_conversation_finish = 0;
+
protocol->send_message_async = 0;
protocol->send_message_finish = 0;
@@ -637,6 +819,112 @@
}
/******************************************************************************
+ * TestProtocolConversation CreateConversation Tests
+ *****************************************************************************/
+static void
+test_purple_protocol_conversation_implements_create_conversation(void) {
+ PurpleProtocolConversation *protocol = NULL;
+
+ protocol = g_object_new(test_purple_protocol_conversation_get_type(),
+ NULL);
+
+ g_assert_true(purple_protocol_conversation_implements_create_conversation(protocol));
+
+ g_assert_finalize_object(protocol);
+}
+
+static void
+test_purple_protocol_conversation_get_create_conversation_details_normal(void)
+{
+ TestPurpleProtocolConversation *test_protocol = NULL;
+ PurpleAccount *account = NULL;
+ PurpleCreateConversationDetails *details = NULL;
+ PurpleProtocolConversation *protocol = NULL;
+
+ test_protocol = g_object_new(test_purple_protocol_conversation_get_type(),
+ NULL);
+ protocol = PURPLE_PROTOCOL_CONVERSATION(test_protocol);
+
+ account = purple_account_new("test", "test");
+
+ details = purple_protocol_conversation_get_create_conversation_details(protocol,
+ account);
+
+ g_assert_true(PURPLE_IS_CREATE_CONVERSATION_DETAILS(details));
+
+ g_assert_cmpuint(test_protocol->get_create_conversation_details, ==, 1);
+
+ g_assert_finalize_object(details);
+ g_assert_finalize_object(account);
+ g_assert_finalize_object(test_protocol);
+}
+
+static void
+test_purple_protocol_conversation_create_conversation_cb(GObject *obj,
+ GAsyncResult *result,
+ G_GNUC_UNUSED gpointer data)
+{
+ TestPurpleProtocolConversation *test_protocol = NULL;
+ PurpleConversation *conversation = NULL;
+ PurpleProtocolConversation *protocol = NULL;
+ GError *error = NULL;
+
+ protocol = PURPLE_PROTOCOL_CONVERSATION(obj);
+ test_protocol = TEST_PURPLE_PROTOCOL_CONVERSATION(obj);
+
+ conversation = purple_protocol_conversation_create_conversation_finish(protocol,
+ result,
+ &error);
+
+ if(test_protocol->should_error) {
+ g_assert_error(error, TEST_PURPLE_PROTOCOL_CONVERSATION_DOMAIN, 0);
+ g_clear_error(&error);
+ g_assert_null(conversation);
+ } else {
+ g_assert_no_error(error);
+ g_assert_true(PURPLE_IS_CONVERSATION(conversation));
+ g_clear_object(&conversation);
+ }
+}
+
+static void
+test_purple_protocol_conversation_create_conversation_normal(gconstpointer data)
+{
+ TestPurpleProtocolConversation *protocol = NULL;
+ PurpleAccount *account = NULL;
+ PurpleCreateConversationDetails *details = NULL;
+
+ protocol = g_object_new(test_purple_protocol_conversation_get_type(),
+ NULL);
+ protocol->should_error = GPOINTER_TO_INT(data);
+
+ account = purple_account_new("test", "test");
+ details = purple_create_conversation_details_new(10);
+
+ purple_protocol_conversation_create_conversation_async(PURPLE_PROTOCOL_CONVERSATION(protocol),
+ account,
+ details,
+ NULL,
+ test_purple_protocol_conversation_create_conversation_cb,
+ NULL);
+
+ while(g_main_context_iteration(NULL, FALSE));
+
+ g_assert_cmpuint(protocol->create_conversation_async, ==, 1);
+ g_assert_cmpuint(protocol->create_conversation_finish, ==, 1);
+
+ g_assert_finalize_object(protocol);
+
+ /* We can't use g_assert_finalize_object as current, PurpleAccount's
+ * presence is a PurpleAccountPresence that holds a reference to the
+ * account and we have no way to break that cycle. Trying to use dispose
+ * doesn't work because the account and the presence can't even reach a
+ * reference count of zero.
+ */
+ g_clear_object(&account);
+}
+
+/******************************************************************************
* TestProtocolConversation SendMessage Tests
*****************************************************************************/
static void
@@ -1017,6 +1305,16 @@
loop = g_main_loop_new(NULL, FALSE);
+ /* Empty create conversation tests. */
+ g_test_add_func("/protocol-conversation/empty/implements-create-conversation",
+ test_purple_protocol_conversation_empty_implements_create_conversation);
+ g_test_add_func("/protocol-conversation/empty/get-create-conversation-details",
+ test_purple_protocol_conversation_empty_get_create_conversation_details);
+ g_test_add_func("/protocol-conversation/empty/create-conversation-async",
+ test_purple_protocol_conversation_empty_create_conversation_async);
+ g_test_add_func("/protocol-conversation/empty/create-conversation-finish",
+ test_purple_protocol_conversation_empty_create_conversation_finish);
+
/* Empty send message tests. */
g_test_add_func("/protocol-conversation/empty/send-message-async",
test_purple_protocol_conversation_empty_send_message_async);
@@ -1043,6 +1341,18 @@
g_test_add_func("/protocol-conversation/empty/send-typing",
test_purple_protocol_conversation_empty_send_typing);
+ /* Normal create conversation tests. */
+ g_test_add_func("/protocol-conversation/normal/implements-create-conversation",
+ test_purple_protocol_conversation_implements_create_conversation);
+ g_test_add_func("/protocol-conversation/normal/get-create-conversation-details-normal",
+ test_purple_protocol_conversation_get_create_conversation_details_normal);
+ g_test_add_data_func("/protocol-conversation/normal/create-conversation-normal",
+ GINT_TO_POINTER(FALSE),
+ test_purple_protocol_conversation_create_conversation_normal);
+ g_test_add_data_func("/protocol-conversation/normal/create-conversation-error",
+ GINT_TO_POINTER(TRUE),
+ test_purple_protocol_conversation_create_conversation_normal);
+
/* Normal send message tests. */
g_test_add_data_func("/protocol-conversation/normal/send-message-normal",
GINT_TO_POINTER(FALSE),