# HG changeset patch
# Node ID 10bf263e8138
# Parent  cc9493cdb323
diff --git a/libpurple/meson.build b/libpurple/meson.build
--- a/libpurple/meson.build
+++ b/libpurple/meson.build
@@ -55,6 +55,7 @@
 	'purplecredentialmanager.c',
 	'purplecredentialprovider.c',
 	'purpledebugui.c',
+	'purplefiletransfer.c',
 	'purplegdkpixbuf.c',
 	'purplegio.c',
 	'purplehistoryadapter.c',
@@ -176,6 +177,7 @@
 	'purplecredentialmanager.h',
 	'purplecredentialprovider.h',
 	'purpledebugui.h',
+	'purplefiletransfer.h',
 	'purplegdkpixbuf.h',
 	'purplegio.h',
 	'purplehistoryadapter.h',
@@ -300,6 +302,7 @@
 	'purpleconnectionerrorinfo.h',
 	'purplecontactinfo.h',
 	'purpleconversation.h',
+	'purplefiletransfer.h',
 	'purpleimconversation.h',
 	'purplemessage.h',
 	'purplenotification.h',
diff --git a/libpurple/purplefiletransfer.h b/libpurple/purplefiletransfer.h
new file mode 100644
--- /dev/null
+++ b/libpurple/purplefiletransfer.h
@@ -0,0 +1,302 @@
+/*
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; 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_FILE_TRANSFER_H
+#define PURPLE_FILE_TRANSFER_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/**
+ * PurpleFileTransferState:
+ * @PURPLE_FILE_TRANSFER_STATE_UNKNOWN: The transfer is in an unknown state.
+ * @PURPLE_FILE_TRANSFER_STATE_NEGOTIATING: The transfer is still being
+ *                                          negotiated. This includes
+ *                                          determining whose actually sending
+ *                                          as well as waiting for the
+ *                                          receiving user to select an
+ *                                          destination file.
+ * @PURPLE_FILE_TRANSFER_STATE_STARTED: The transfer is in progress.
+ * @PURPLE_FILE_TRANSFER_STATE_FINISHED: The transfer has completed
+ *                                       successfully.
+ * @PURPLE_FILE_TRANSFER_STATE_CANCELLED_FAILED: The transfer failed.
+ */
+typedef enum {
+	PURPLE_FILE_TRANSFER_STATE_UNKNOWN,
+	PURPLE_FILE_TRANSFER_STATE_NEGOTIATING,
+	PURPLE_FILE_TRANSFER_STATE_STARTED,
+	PURPLE_FILE_TRANSFER_STATE_FINISHED,
+	PURPLE_FILE_TRANSFER_STATE_FAILED,
+} PurpleFileTransferState;
+
+/**
+ * PurpleFileTransfer:
+ *
+ * A peer to peer file transfer object.
+ *
+ * Since: 3.0.0
+ */
+
+#define PURPLE_TYPE_FILE_TRANSFER purple_file_transfer_get_type()
+G_DECLARE_FINAL_TYPE(PurpleFileTransfer, purple_file_transfer, PURPLE,
+                     FILE_TRANSFER, GObject)
+
+/**
+ * purple_file_transfer_new_send:
+ * @account: The account this file transfer is from.
+ * @remote: The [class@ContactInfo] of the remote user.
+ * @local_file: The [iface@Gio.File] of the file to send.
+ *
+ * Creates a new [class@FileTransfer] which will be used to send @local_file to
+ * @remote.
+ *
+ * This will also set [property@FileTransfer:filename] and
+ * [property@FileTransfer:file-size] to the proper values
+ * for @local_file.
+ *
+ * Returns: (transfer full): The new file transfer.
+ *
+ * Since: 3.0.0
+ */
+PurpleFileTransfer *purple_file_transfer_new_send(PurpleAccount *account, PurpleContactInfo *remote, GFile *local_file);
+
+/**
+ * purple_file_transfer_new_receieve:
+ * @account: The account this file transfer is to.
+ * @remote: The [class@ContactInfo] of the user sending the file.
+ * @filename: (non-nullable): The base filename.
+ * @file_size: The size of the file.
+ *
+ * Creates a new [class@FileTransfer] which will be used to negotiate a file
+ * transfer from @remote to @account with the given @filename and @file_size.
+ *
+ * > Note: This function should only be called by a protocol plugin
+ * implementation.
+ *
+ * Returns: (transfer full): The new file transfer.
+ *
+ * Since: 3.0.0
+ */
+PurpleFileTransfer *purple_file_transfer_new_receive(PurpleAccount *account, PurpleContactInfo *remote, const char *filename, guint64 file_size);
+
+/**
+ * purple_file_transfer_get_account:
+ * @transfer: The instance.
+ *
+ * Gets the [class@Account] that @transfer was created with to transfer a file.
+ *
+ * Returns: (transfer none): The account.
+ *
+ * Since: 3.0.0
+ */
+PurpleAccount *purple_file_transfer_get_account(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_get_remote:
+ * @transfer: The instance.
+ *
+ * Gets the [class@ContactInfo] that @transfer was created with to transfer a
+ * file with.
+ *
+ * Returns: (transfer none): The remote contact info.
+ *
+ * Since: 3.0.0
+ */
+PurpleContactInfo *purple_file_transfer_get_remote(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_get_initiator:
+ * @transfer: The instance.
+ *
+ * Gets the [class@ContactInfo] of the user that initiated @transfer.
+ *
+ * Returns: (transfer none): The contact info who initiated the file transfer
+ *          transfer.
+ *
+ * Since: 3.0.0
+ */
+PurpleContactInfo *purple_file_transfer_get_initiator(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_get_cancellable:
+ * @transfer: The instance.
+ *
+ * The [class@Gio.Cancellable] for @transfer that can be used to cancel the
+ * transfer.
+ *
+ * Returns: (transfer none): The cancellable.
+ *
+ * Since: 3.0.0
+ */
+GCancellable *purple_file_transfer_get_cancellable(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_get_state:
+ * @transfer: The instance.
+ *
+ * Gets the [enum@FileTransferState] of the transfer.
+ *
+ * Returns: The state of @transfer.
+ *
+ * Since: 3.0.0
+ */
+PurpleFileTransferState purple_file_transfer_get_state(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_set_state:
+ * @transfer: The instance.
+ * @state: The new state.
+ *
+ * Sets the state of @transfer to @state.
+ *
+ * This method should only be called by protocol plugin to match what it is
+ * doing.
+ *
+ * Since: 3.0.0
+ */
+void purple_file_transfer_set_state(PurpleFileTransfer *transfer, PurpleFileTransferState state);
+
+/**
+ * purple_file_transfer_get_error:
+ * @transfer: The instance.
+ *
+ * Gets the #GError from @transfer.
+ *
+ * Returns: (transfer none) (nullable): The error for the transfer.
+ *
+ * Since: 3.0.0
+ */
+GError *purple_file_transfer_get_error(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_set_error:
+ * @transfer: The instance.
+ * @error: (transfer full) (nullable): The new error.
+ *
+ * Sets the error of @transfer to @error.
+ *
+ * Since: 3.0.0
+ */
+void purple_file_transfer_set_error(PurpleFileTransfer *transfer, GError *error);
+
+/**
+ * purple_file_transfer_get_local_file:
+ * @transfer: The instance.
+ *
+ * Gets the local file from @transfer.
+ *
+ * Returns: (transfer none): The local file.
+ *
+ * Since: 3.0.0
+ */
+GFile *purple_file_transfer_get_local_file(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_set_local_file:
+ * @transfer: The instance.
+ * @local_file: The new local file.
+ *
+ * Sets the local file of @transfer to @local_file.
+ *
+ * Since: 3.0.0
+ */
+void purple_file_transfer_set_local_file(PurpleFileTransfer *transfer, GFile *local_file);
+
+/**
+ * purple_file_transfer_get_filename:
+ * @transfer: The instance.
+ *
+ * Gets the base filename from @transfer.
+ *
+ * Returns: (transfer none): The base filename.
+ *
+ * Since: 3.0.0
+ */
+const char *purple_file_transfer_get_filename(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_get_file_size:
+ * @transfer: The instance.
+ *
+ * Gets the size of the file that is being transferred.
+ *
+ * Returns: The size of the file in bytes or %0 if the size is unknown.
+ *
+ * Since: 3.0.0
+ */
+guint64 purple_file_transfer_get_file_size(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_get_content_type:
+ * @transfer: The instance.
+ *
+ * Gets the content type of the file being transferred.
+ *
+ * Returns: (nullable): The content type.
+ *
+ * Since: 3.0.0
+ */
+const char *purple_file_transfer_get_content_type(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_set_content_type:
+ * @transfer: The instance.
+ * @content_type: (nullable): The new content type.
+ *
+ * Sets the content type of @transfer to @content_type.
+ *
+ * Since: 3.0.0
+ */
+void purple_file_transfer_set_content_type(PurpleFileTransfer *transfer, const char *content_type);
+
+/**
+ * purple_file_transfer_get_message:
+ * @transfer: The instance.
+ *
+ * The message that was sent with the file transfer if the protocol supports
+ * sending one.
+ *
+ * Returns: The message sent with the transfer.
+ *
+ * Since: 3.0.0
+ */
+const char *purple_file_transfer_get_message(PurpleFileTransfer *transfer);
+
+/**
+ * purple_file_transfer_set_message:
+ * @transfer: The instance.
+ * @message: (nullable): The new message.
+ *
+ * Sets the message to send with @transfer to @message.
+ *
+ * Since: 3.0.0
+ */
+void purple_file_transfer_set_message(PurpleFileTransfer *transfer, const char *message);
+
+G_END_DECLS
+
+#endif /* PURPLE_FILE_TRANSFER_H */
diff --git a/libpurple/purplefiletransfer.c b/libpurple/purplefiletransfer.c
new file mode 100644
--- /dev/null
+++ b/libpurple/purplefiletransfer.c
@@ -0,0 +1,684 @@
+/*
+ * 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 Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "account.h"
+#include "purplecontactinfo.h"
+#include "purpleenums.h"
+#include "purplefiletransfer.h"
+#include "util.h"
+
+enum {
+	PROP_0,
+	PROP_ACCOUNT,
+	PROP_REMOTE,
+	PROP_INITIATOR,
+	PROP_CANCELLABLE,
+	PROP_STATE,
+	PROP_ERROR,
+	PROP_LOCAL_FILE,
+	PROP_FILENAME,
+	PROP_FILE_SIZE,
+	PROP_CONTENT_TYPE,
+	PROP_MESSAGE,
+	N_PROPERTIES,
+};
+static GParamSpec *properties[N_PROPERTIES] = {NULL, };
+
+struct _PurpleFileTransfer {
+	GObject parent;
+
+	PurpleAccount *account;
+	PurpleContactInfo *remote;
+	PurpleContactInfo *initiator;
+
+	GCancellable *cancellable;
+
+	PurpleFileTransferState state;
+	GError *error;
+
+	GFile *local_file;
+	char *filename;
+	guint64 file_size;
+	char *content_type;
+
+	char *message;
+};
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_file_transfer_set_account(PurpleFileTransfer *transfer,
+                                 PurpleAccount *account)
+{
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+	g_return_if_fail(PURPLE_IS_ACCOUNT(account));
+
+	if(g_set_object(&transfer->account, account)) {
+		g_object_notify_by_pspec(G_OBJECT(transfer), properties[PROP_ACCOUNT]);
+	}
+}
+
+static void
+purple_file_transfer_set_remote(PurpleFileTransfer *transfer,
+                                PurpleContactInfo *remote)
+{
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+	g_return_if_fail(PURPLE_IS_CONTACT_INFO(remote));
+
+	if(g_set_object(&transfer->remote, remote)) {
+		g_object_notify_by_pspec(G_OBJECT(transfer), properties[PROP_REMOTE]);
+	}
+}
+
+static void
+purple_file_transfer_set_initiator(PurpleFileTransfer *transfer,
+                                   PurpleContactInfo *initiator)
+{
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+	g_return_if_fail(PURPLE_IS_CONTACT_INFO(initiator));
+
+	if(g_set_object(&transfer->initiator, initiator)) {
+		g_object_notify_by_pspec(G_OBJECT(transfer),
+		                         properties[PROP_INITIATOR]);
+	}
+}
+
+static void
+purple_file_transfer_set_filename(PurpleFileTransfer *transfer,
+                                  const char *filename)
+{
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+	g_return_if_fail(!purple_strempty(filename));
+
+	if(!purple_strequal(transfer->filename, filename)) {
+		g_free(transfer->filename);
+		transfer->filename = g_strdup(filename);
+
+		g_object_notify_by_pspec(G_OBJECT(transfer),
+		                         properties[PROP_FILENAME]);
+	}
+}
+
+static void
+purple_file_transfer_set_file_size(PurpleFileTransfer *transfer, guint64 size)
+{
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+
+	if(transfer->file_size != size) {
+		transfer->file_size = size;
+
+		g_object_notify_by_pspec(G_OBJECT(transfer),
+		                         properties[PROP_FILE_SIZE]);
+	}
+}
+
+static char *
+purple_file_transfer_get_local_file_name_and_size(GFile *local_file,
+                                                  guint64 *file_size)
+{
+	GError *error = NULL;
+	GFileInfo *info = NULL;
+	const char *display_name = NULL;
+	char *ret = NULL;
+
+	info = g_file_query_info(local_file,
+	                         G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
+	                         G_FILE_ATTRIBUTE_STANDARD_SIZE,
+	                         G_FILE_QUERY_INFO_NONE,
+	                         NULL,
+	                         &error);
+
+	if(error != NULL) {
+		char *path = g_file_get_path(local_file);
+
+		g_warning("failed to query %s: %s", path, error->message);
+
+		g_clear_pointer(&path, g_free);
+		g_clear_error(&error);
+		g_clear_object(&info);
+
+		return NULL;
+	}
+
+	display_name = g_file_info_get_attribute_string(info,
+	                                                G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME);
+	/* display_name is only valid as long as the GFileInfo object is around,
+	 * but it is freed before this function returns, so we need to dupe the
+	 * display name attribute to return it.
+	 */
+	ret = g_strdup(display_name);
+
+	if(file_size != NULL) {
+		*file_size = g_file_info_get_attribute_uint64(info,
+		                                              G_FILE_ATTRIBUTE_STANDARD_SIZE);
+	}
+
+	g_clear_object(&info);
+
+	return ret;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_TYPE(PurpleFileTransfer, purple_file_transfer, G_TYPE_OBJECT)
+
+static void
+purple_file_transfer_finalize(GObject *obj) {
+	PurpleFileTransfer *transfer = PURPLE_FILE_TRANSFER(obj);
+
+	g_clear_object(&transfer->account);
+	g_clear_object(&transfer->remote);
+	g_clear_object(&transfer->initiator);
+
+	g_clear_object(&transfer->cancellable);
+	g_clear_error(&transfer->error);
+
+	g_clear_object(&transfer->local_file);
+	g_clear_pointer(&transfer->filename, g_free);
+	g_clear_pointer(&transfer->content_type, g_free);
+
+	g_clear_pointer(&transfer->message, g_free);
+
+	G_OBJECT_CLASS(purple_file_transfer_parent_class)->finalize(obj);
+}
+
+static void
+purple_file_transfer_get_property(GObject *obj, guint param_id, GValue *value,
+                                  GParamSpec *pspec)
+{
+	PurpleFileTransfer *transfer = PURPLE_FILE_TRANSFER(obj);
+
+	switch(param_id) {
+	case PROP_ACCOUNT:
+		g_value_set_object(value, purple_file_transfer_get_account(transfer));
+		break;
+	case PROP_REMOTE:
+		g_value_set_object(value, purple_file_transfer_get_remote(transfer));
+		break;
+	case PROP_INITIATOR:
+		g_value_set_object(value,
+		                   purple_file_transfer_get_initiator(transfer));
+		break;
+	case PROP_CANCELLABLE:
+		g_value_set_object(value,
+		                   purple_file_transfer_get_cancellable(transfer));
+		break;
+	case PROP_STATE:
+		g_value_set_enum(value, purple_file_transfer_get_state(transfer));
+		break;
+	case PROP_ERROR:
+		g_value_set_boxed(value, purple_file_transfer_get_error(transfer));
+		break;
+	case PROP_LOCAL_FILE:
+		g_value_set_object(value,
+		                   purple_file_transfer_get_local_file(transfer));
+		break;
+	case PROP_FILENAME:
+		g_value_set_string(value,
+		                   purple_file_transfer_get_filename(transfer));
+		break;
+	case PROP_FILE_SIZE:
+		g_value_set_uint64(value,
+		                   purple_file_transfer_get_file_size(transfer));
+		break;
+	case PROP_CONTENT_TYPE:
+		g_value_set_string(value,
+		                   purple_file_transfer_get_content_type(transfer));
+		break;
+	case PROP_MESSAGE:
+		g_value_set_string(value, purple_file_transfer_get_message(transfer));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+purple_file_transfer_set_property(GObject *obj, guint param_id,
+                                  const GValue *value, GParamSpec *pspec)
+{
+	PurpleFileTransfer *transfer = PURPLE_FILE_TRANSFER(obj);
+
+	switch(param_id) {
+	case PROP_ACCOUNT:
+		purple_file_transfer_set_account(transfer, g_value_get_object(value));
+		break;
+	case PROP_REMOTE:
+		purple_file_transfer_set_remote(transfer, g_value_get_object(value));
+		break;
+	case PROP_INITIATOR:
+		purple_file_transfer_set_initiator(transfer,
+		                                   g_value_get_object(value));
+		break;
+	case PROP_STATE:
+		purple_file_transfer_set_state(transfer, g_value_get_enum(value));
+		break;
+	case PROP_ERROR:
+		purple_file_transfer_set_error(transfer, g_value_get_boxed(value));
+		break;
+	case PROP_LOCAL_FILE:
+		purple_file_transfer_set_local_file(transfer,
+		                                    g_value_get_object(value));
+		break;
+	case PROP_FILENAME:
+		purple_file_transfer_set_filename(transfer,
+		                                  g_value_get_string(value));
+		break;
+	case PROP_FILE_SIZE:
+		purple_file_transfer_set_file_size(transfer,
+		                                   g_value_get_uint64(value));
+		break;
+	case PROP_CONTENT_TYPE:
+		purple_file_transfer_set_content_type(transfer,
+		                                      g_value_get_string(value));
+		break;
+	case PROP_MESSAGE:
+		purple_file_transfer_set_message(transfer, g_value_get_string(value));
+		break;
+	default:
+		G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
+		break;
+	}
+}
+
+static void
+purple_file_transfer_init(PurpleFileTransfer *transfer) {
+	transfer->cancellable = g_cancellable_new();
+}
+
+static void
+purple_file_transfer_class_init(PurpleFileTransferClass *klass) {
+	GObjectClass *obj_class = G_OBJECT_CLASS(klass);
+
+	obj_class->finalize = purple_file_transfer_finalize;
+	obj_class->get_property = purple_file_transfer_get_property;
+	obj_class->set_property = purple_file_transfer_set_property;
+
+	/**
+	 * PurpleFileTransfer:account:
+	 *
+	 * The account that this file transfer is for.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ACCOUNT] = g_param_spec_object(
+		"account", "account",
+		"The account that this file transfer is for.",
+		PURPLE_TYPE_ACCOUNT,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:remote:
+	 *
+	 * The [class@ContactInfo] for the remote user of this file transfer.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_REMOTE] = g_param_spec_object(
+		"remote", "remote",
+		"The contact info for the remote user.",
+		PURPLE_TYPE_CONTACT_INFO,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:initiator:
+	 *
+	 * The [class@ContactInfo] that initiated this file transfer.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_INITIATOR] = g_param_spec_object(
+		"initiator", "initiator",
+		"The contact info of the user that initiated the file transfer.",
+		PURPLE_TYPE_CONTACT_INFO,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:cancellable:
+	 *
+	 * The [class@Gio.Cancellable] for this transfer. It may be used to cancel
+	 * the file transfer at any time.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_CANCELLABLE] = g_param_spec_object(
+		"cancellable", "cancellable",
+		"The cancellable for this transfer.",
+		G_TYPE_CANCELLABLE,
+		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:state:
+	 *
+	 * The state of the transfer.
+	 *
+	 * This is typically only set by the protocol plugin that is performing the
+	 * file transfer.
+	 *
+	 * If the state is set to error then [property@FileTransfer:error]
+	 * should be set.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_STATE] = g_param_spec_enum(
+		"state", "state",
+		"The state of the transfer for this transfer.",
+		PURPLE_TYPE_FILE_TRANSFER_STATE,
+		PURPLE_FILE_TRANSFER_STATE_UNKNOWN,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:error:
+	 *
+	 * A #GError representing the failure of the transfer. This should only be
+	 * set if [property@FileTransfer:state] is set to error.
+	 *
+	 * This should be used to tell the user about network issues or if the
+	 * transfer was cancelled and so on.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_ERROR] = g_param_spec_boxed(
+		"error", "error",
+		"The error that caused the transfer to fail.",
+		G_TYPE_ERROR,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:local-file:
+	 *
+	 * The local file that is being sent or received.
+	 *
+	 * When sending a file, this is the file that's being sent. When receiving
+	 * a file, this is the file where the transfer is being written.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_LOCAL_FILE] = g_param_spec_object(
+		"local-file", "local-file",
+		"The local file for the transfer.",
+		G_TYPE_FILE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:filename:
+	 *
+	 * The base filename for the transfer. This is used as the default filename
+	 * for the receiving side.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_FILENAME] = g_param_spec_string(
+		"filename", "filename",
+		"The base filename for the transfer.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:file-size:
+	 *
+	 * The size of the file in bytes. A value of %0 typically means the size is
+	 * unknown, but it is possible to transfer empty files as well.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_FILE_SIZE] = g_param_spec_uint64(
+		"file-size", "file-size",
+		"The size of the file in bytes.",
+		0, G_MAXUINT64, 0,
+		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:content-type:
+	 *
+	 * The content or media type of the file that's being transferred. This is
+	 * meant to be used as a hint to user interfaces so they can provide
+	 * previews or appropriate actions for the files.
+	 *
+	 * See the [Media Types page](https://www.iana.org/assignments/media-types/media-types.xhtml)
+	 * for more information.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_CONTENT_TYPE] = g_param_spec_string(
+		"content-type", "content-type",
+		"The content type of the file being transferred.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleFileTransfer:message:
+	 *
+	 * Some protocols support sending a message with the file transfer. This
+	 * field is to hold that message.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_MESSAGE] = g_param_spec_string(
+		"message", "message",
+		"The message to send with the transfer.",
+		NULL,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
+}
+
+/******************************************************************************
+ * Public API
+ *****************************************************************************/
+PurpleFileTransfer *
+purple_file_transfer_new_send(PurpleAccount *account,
+                              PurpleContactInfo *remote,
+                              GFile *local_file)
+{
+	PurpleFileTransfer *transfer = NULL;
+	gchar *filename = NULL;
+	guint64 file_size = 0;
+
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
+	g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(remote), NULL);
+
+	filename = purple_file_transfer_get_local_file_name_and_size(local_file,
+	                                                             &file_size);
+
+	transfer = g_object_new(
+		PURPLE_TYPE_FILE_TRANSFER,
+		"account", account,
+		"remote", remote,
+		"initiator", PURPLE_CONTACT_INFO(account),
+		"local-file", local_file,
+		"filename", filename,
+		"file-size", file_size,
+		NULL);
+
+	g_clear_pointer(&filename, g_free);
+
+	return transfer;
+}
+
+PurpleFileTransfer *
+purple_file_transfer_new_receive(PurpleAccount *account,
+                                 PurpleContactInfo *remote,
+                                 const char *filename,
+                                 guint64 file_size)
+{
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
+	g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(remote), NULL);
+	g_return_val_if_fail(!purple_strempty(filename), NULL);
+
+	return g_object_new(
+		PURPLE_TYPE_FILE_TRANSFER,
+		"account", account,
+		"remote", remote,
+		"initiator", remote,
+		"filename", filename,
+		"file-size", file_size,
+		NULL);
+}
+
+PurpleAccount *
+purple_file_transfer_get_account(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), NULL);
+
+	return transfer->account;
+}
+
+PurpleContactInfo *
+purple_file_transfer_get_remote(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), NULL);
+
+	return transfer->remote;
+}
+
+PurpleContactInfo *
+purple_file_transfer_get_initiator(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), NULL);
+
+	return transfer->initiator;
+}
+
+GCancellable *
+purple_file_transfer_get_cancellable(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), NULL);
+
+	return transfer->cancellable;
+}
+
+PurpleFileTransferState
+purple_file_transfer_get_state(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer),
+	                     PURPLE_FILE_TRANSFER_STATE_UNKNOWN);
+
+	return transfer->state;
+}
+
+void
+purple_file_transfer_set_state(PurpleFileTransfer *transfer,
+                               PurpleFileTransferState state)
+{
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+
+	if(transfer->state != state) {
+		transfer->state = state;
+
+		g_object_notify_by_pspec(G_OBJECT(transfer), properties[PROP_STATE]);
+	}
+}
+
+GError *
+purple_file_transfer_get_error(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), NULL);
+
+	return transfer->error;
+}
+
+void
+purple_file_transfer_set_error(PurpleFileTransfer *transfer, GError *error) {
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+
+	if(transfer->error != error) {
+		g_clear_error(&transfer->error);
+		transfer->error = error;
+
+		g_object_notify_by_pspec(G_OBJECT(transfer), properties[PROP_ERROR]);
+	}
+}
+
+GFile *
+purple_file_transfer_get_local_file(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), NULL);
+
+	return transfer->local_file;
+}
+
+void
+purple_file_transfer_set_local_file(PurpleFileTransfer *transfer,
+                                    GFile *local_file)
+{
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+	g_return_if_fail(G_IS_FILE(local_file));
+
+	if(g_set_object(&transfer->local_file, local_file)) {
+		g_object_notify_by_pspec(G_OBJECT(transfer),
+		                         properties[PROP_LOCAL_FILE]);
+	}
+}
+
+const char *
+purple_file_transfer_get_filename(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), NULL);
+
+	return transfer->filename;
+}
+
+guint64
+purple_file_transfer_get_file_size(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), 0);
+
+	return transfer->file_size;
+}
+
+const char *
+purple_file_transfer_get_content_type(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), NULL);
+
+	return transfer->content_type;
+}
+
+void
+purple_file_transfer_set_content_type(PurpleFileTransfer *transfer,
+                                      const char *content_type)
+{
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+
+	if(!purple_strequal(transfer->content_type, content_type)) {
+		g_free(transfer->content_type);
+		transfer->content_type = g_strdup(content_type);
+
+		g_object_notify_by_pspec(G_OBJECT(transfer),
+		                         properties[PROP_CONTENT_TYPE]);
+	}
+}
+
+const char *
+purple_file_transfer_get_message(PurpleFileTransfer *transfer) {
+	g_return_val_if_fail(PURPLE_IS_FILE_TRANSFER(transfer), NULL);
+
+	return transfer->message;
+}
+
+void
+purple_file_transfer_set_message(PurpleFileTransfer *transfer,
+                                 const char *message)
+{
+	g_return_if_fail(PURPLE_IS_FILE_TRANSFER(transfer));
+
+	if(!purple_strequal(transfer->message, message)) {
+		g_free(transfer->message);
+		transfer->message = g_strdup(message);
+
+		g_object_notify_by_pspec(G_OBJECT(transfer), properties[PROP_MESSAGE]);
+	}
+}
diff --git a/libpurple/tests/meson.build b/libpurple/tests/meson.build
--- a/libpurple/tests/meson.build
+++ b/libpurple/tests/meson.build
@@ -12,6 +12,7 @@
     'conversation_member',
     'credential_manager',
     'credential_provider',
+    'file_transfer',
     'history_adapter',
     'history_manager',
     'image',
diff --git a/libpurple/tests/test_file_transfer.c b/libpurple/tests/test_file_transfer.c
new file mode 100644
--- /dev/null
+++ b/libpurple/tests/test_file_transfer.c
@@ -0,0 +1,199 @@
+/*
+ * Purple - Internet Messaging Library
+ * Copyright (C) Pidgin Developers <devel@pidgin.im>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include <purple.h>
+
+#include "test_ui.h"
+
+/******************************************************************************
+ * Tests
+ *****************************************************************************/
+static void
+test_purple_file_transfer_new_send(gconstpointer data) {
+	PurpleAccount *account = NULL;
+	PurpleFileTransfer *transfer = NULL;
+	PurpleContactInfo *initiator = NULL;
+	PurpleContactInfo *remote = NULL;
+	GFile *local_file = NULL;
+	GStatBuf st;
+	char *basename = NULL;
+	const char *executable = data;
+	const char *filename = NULL;
+
+	account = purple_account_new("test", "test");
+	remote = purple_contact_info_new(NULL);
+	local_file = g_file_new_for_path(executable);
+
+	transfer = purple_file_transfer_new_send(account, remote, local_file);
+	g_assert_true(PURPLE_IS_FILE_TRANSFER(transfer));
+
+	initiator = purple_file_transfer_get_initiator(transfer);
+	g_assert_true(initiator == PURPLE_CONTACT_INFO(account));
+
+	filename = purple_file_transfer_get_filename(transfer);
+	basename = g_path_get_basename(executable);
+	g_assert_cmpstr(filename, ==, basename);
+	g_clear_pointer(&basename, g_free);
+
+	g_stat(executable, &st);
+	g_assert_cmpuint(purple_file_transfer_get_file_size(transfer), ==,
+	                 st.st_size);
+
+	g_clear_object(&local_file);
+	g_clear_object(&account);
+	g_clear_object(&remote);
+	g_clear_object(&transfer);
+}
+
+static void
+test_purple_file_transfer_new_receive(void) {
+	PurpleAccount *account = NULL;
+	PurpleFileTransfer *transfer = NULL;
+	PurpleContactInfo *initiator = NULL;
+	PurpleContactInfo *remote = NULL;
+
+	account = purple_account_new("test", "test");
+	remote = purple_contact_info_new(NULL);
+
+	transfer = purple_file_transfer_new_receive(account, remote, "foo", 0);
+	g_assert_true(PURPLE_IS_FILE_TRANSFER(transfer));
+
+	initiator = purple_file_transfer_get_initiator(transfer);
+	g_assert_true(initiator == remote);
+
+	g_clear_object(&account);
+	g_clear_object(&remote);
+	g_clear_object(&transfer);
+}
+
+static void
+test_file_transfer_properties(gconstpointer data) {
+	PurpleAccount *account = NULL;
+	PurpleAccount *account1 = NULL;
+	PurpleFileTransfer *transfer = NULL;
+	PurpleContactInfo *initiator = NULL;
+	PurpleContactInfo *initiator1 = NULL;
+	PurpleContactInfo *remote = NULL;
+	PurpleContactInfo *remote1 = NULL;
+	GCancellable *cancellable = NULL;
+	GFile *local_file = NULL;
+	GFile *local_file1 = NULL;
+	char *content_type = NULL;
+	char *filename = NULL;
+	char *filename1 = NULL;
+	char *message = NULL;
+	const char *executable = data;
+	guint64 file_size = 1337;
+	guint64 file_size1 = 0;
+
+	account = purple_account_new("test", "test");
+	remote = purple_contact_info_new(NULL);
+	initiator = purple_contact_info_new(NULL);
+	local_file = g_file_new_for_path(executable);
+	filename = g_path_get_basename(executable);
+
+	/* Create the instance. */
+	transfer = g_object_new(
+		PURPLE_TYPE_FILE_TRANSFER,
+		"account", account,
+		"remote", remote,
+		"initiator", initiator,
+		"local-file", local_file,
+		"filename", filename,
+		"file-size", file_size,
+		"content-type", "application/octet-stream",
+		"message", "have you heard the word?",
+		NULL);
+
+	/* Get all the property values. */
+	g_object_get(
+		G_OBJECT(transfer),
+		"account", &account1,
+		"remote", &remote1,
+		"initiator", &initiator1,
+		"local-file", &local_file1,
+		"filename", &filename1,
+		"file-size", &file_size1,
+		"content-type", &content_type,
+		"message", &message,
+		"cancellable", &cancellable,
+		NULL);
+
+	/* Validate and clean up all of the properties. */
+	g_assert_true(account == account1);
+	g_clear_object(&account1);
+
+	g_assert_true(remote == remote1);
+	g_clear_object(&remote1);
+
+	g_assert_true(initiator == initiator1);
+	g_clear_object(&initiator1);
+
+	g_assert_true(local_file == local_file1);
+	g_clear_object(&local_file1);
+
+	g_assert_cmpstr(filename, ==, filename1);
+	g_clear_pointer(&filename1, g_free);
+
+	g_assert_cmpuint(file_size, ==, file_size1);
+
+	g_assert_cmpstr(content_type, ==, "application/octet-stream");
+	g_clear_pointer(&content_type, g_free);
+
+	g_assert_cmpstr(message, ==, "have you heard the word?");
+	g_clear_pointer(&message, g_free);
+
+	g_assert_true(G_IS_CANCELLABLE(cancellable));
+	g_clear_object(&cancellable);
+
+	/* Clean up or initial objects. */
+	g_clear_pointer(&filename, g_free);
+	g_clear_object(&local_file);
+	g_clear_object(&initiator);
+	g_clear_object(&account);
+	g_clear_object(&remote);
+	g_clear_object(&transfer);
+}
+
+/******************************************************************************
+ * Main
+ *****************************************************************************/
+gint
+main(gint argc, gchar *argv[]) {
+	gint ret = 0;
+
+	g_test_init(&argc, &argv, NULL);
+
+	test_ui_purple_init();
+
+	g_test_add_data_func("/file-transfer/new/send", argv[0],
+	                     test_purple_file_transfer_new_send);
+	g_test_add_func("/file-transfer/new/receive",
+	                test_purple_file_transfer_new_receive);
+	g_test_add_data_func("/file-transfer/properties", argv[0],
+	                     test_file_transfer_properties);
+
+	ret = g_test_run();
+
+	test_ui_purple_uninit();
+
+	return ret;
+}
