diff --git a/libpurple/purplecontact.h b/libpurple/purplecontact.h
--- a/libpurple/purplecontact.h
+++ b/libpurple/purplecontact.h
@@ -37,6 +37,17 @@
 
 G_BEGIN_DECLS
 
+/**
+ * PURPLE_CONTACT_DOMAIN:
+ *
+ * An error domain for [class@Contact].
+ *
+ * Since: 3.0
+ */
+#define PURPLE_CONTACT_DOMAIN \
+    (g_quark_from_static_string("purple-contact")) \
+    PURPLE_AVAILABLE_MACRO_IN_3_0
+
 #define PURPLE_TYPE_CONTACT (purple_contact_get_type())
 
 PURPLE_AVAILABLE_IN_3_0
@@ -103,6 +114,37 @@
 PURPLE_AVAILABLE_IN_3_0
 PurpleConversation *purple_contact_find_dm(PurpleContact *contact, gboolean create);
 
+/**
+ * purple_contact_create_dm_async:
+ * @contact: The instance.
+ * @cancellable: (nullable): optional GCancellable object, %NULL to ignore.
+ * @callback: (scope async): a #GAsyncReadyCallback to call when the request is
+ *            satisfied.
+ * @data: User data to pass to @callback.
+ *
+ * Asks the protocol to create a direct message with @contact.
+ *
+ * The protocol may return an existing conversation.
+ *
+ * Since: 3.0
+ */
+void purple_contact_create_dm_async(PurpleContact *contact, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer data);
+
+/**
+ * purple_contact_create_dm_finish:
+ * @contact: The instance.
+ * @result: The result.
+ * @error: The return address for a #GError.
+ *
+ * Finishes a previous call to [method@Contact.create_dm_async].
+ *
+ * Returns: (transfer full): The conversation if successful, otherwise %NULL
+ *          with @error set.
+ *
+ * Since: 3.0
+ */
+PurpleConversation *purple_contact_create_dm_finish(PurpleContact *contact, GAsyncResult *result, GError **error);
+
 G_END_DECLS
 
 #endif /* PURPLE_CONTACT_H */
# HG changeset patch
# Node ID 0b2f3c874d0c
# Parent  62ab40b0f152
diff --git a/libpurple/purplecontact.c b/libpurple/purplecontact.c
--- a/libpurple/purplecontact.c
+++ b/libpurple/purplecontact.c
@@ -23,6 +23,7 @@
 #include "purplecontact.h"
 
 #include "purpleconversationmanager.h"
+#include "purpleprotocolconversation.h"
 
 struct _PurpleContact {
 	PurpleContactInfo parent;
@@ -53,6 +54,39 @@
 }
 
 /******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static void
+purple_contact_create_dm_cb(GObject *obj, GAsyncResult *result, gpointer data)
+{
+	PurpleProtocolConversation *protocol = PURPLE_PROTOCOL_CONVERSATION(obj);
+	PurpleConversation *conversation = NULL;
+	GError *error = NULL;
+	GTask *task = data;
+
+	/* task and result share a cancellable, so we just need to clear task to
+	 * make sure it's callback gets called.
+	 */
+	if(g_task_return_error_if_cancelled(G_TASK(result))) {
+		g_clear_object(&task);
+
+		return;
+	}
+
+	conversation = purple_protocol_conversation_create_conversation_finish(protocol,
+	                                                                       result,
+	                                                                       &error);
+
+	if(error != NULL) {
+		g_task_return_error(task, error);
+	} else {
+		g_task_return_pointer(task, conversation, g_object_unref);
+	}
+
+	g_clear_object(&task);
+}
+
+/******************************************************************************
  * GObject Implementation
  *****************************************************************************/
 static void
@@ -178,3 +212,76 @@
 
 	return conversation;
 }
+
+void
+purple_contact_create_dm_async(PurpleContact *contact,
+                               GCancellable *cancellable,
+                               GAsyncReadyCallback callback,
+                               gpointer data)
+{
+	PurpleProtocol *protocol = NULL;
+	PurpleProtocolConversation *protocol_conversation = NULL;
+	PurpleCreateConversationDetails *details = NULL;
+	GListStore *participants = NULL;
+	GTask *task = NULL;
+	gboolean implements = FALSE;
+
+	task = g_task_new(contact, cancellable, callback, data);
+	g_task_set_source_tag(task, purple_contact_create_dm_async);
+
+	protocol = purple_account_get_protocol(contact->account);
+	if(!PURPLE_IS_PROTOCOL_CONVERSATION(protocol)) {
+		g_task_return_new_error(task, PURPLE_CONTACT_DOMAIN, 0,
+		                        "protocol %s does not implement the "
+		                        "ProtocolConversation interface.",
+		                        purple_account_get_protocol_id(contact->account));
+		g_clear_object(&task);
+
+		return;
+	}
+
+	protocol_conversation = PURPLE_PROTOCOL_CONVERSATION(protocol);
+	implements = purple_protocol_conversation_implements_create_conversation(protocol_conversation);
+	if(!implements) {
+		g_task_return_new_error(task, PURPLE_CONTACT_DOMAIN, 0,
+		                        "protocol %s does not implement creating "
+		                        "conversations.",
+		                        purple_account_get_protocol_id(contact->account));
+		g_clear_object(&task);
+
+		return;
+	}
+
+	details = purple_protocol_conversation_get_create_conversation_details(protocol_conversation,
+	                                                                       contact->account);
+
+	participants = g_list_store_new(PURPLE_TYPE_CONTACT);
+	g_list_store_append(participants, contact);
+	purple_create_conversation_details_set_participants(details,
+	                                                    G_LIST_MODEL(participants));
+	g_clear_object(&participants);
+
+	purple_protocol_conversation_create_conversation_async(protocol_conversation,
+	                                                       contact->account,
+	                                                       details,
+	                                                       cancellable,
+	                                                       purple_contact_create_dm_cb,
+	                                                       task);
+}
+
+PurpleConversation *
+purple_contact_create_dm_finish(PurpleContact *contact, GAsyncResult *result,
+                                GError **error)
+{
+	GTask *task = NULL;
+
+	g_return_val_if_fail(PURPLE_IS_CONTACT(contact), NULL);
+	g_return_val_if_fail(G_IS_TASK(result), NULL);
+
+	task = G_TASK(result);
+
+	g_return_val_if_fail(g_task_get_source_tag(task) == purple_contact_create_dm_async,
+	                     NULL);
+
+	return g_task_propagate_pointer(task, error);
+}
