libpurple/purpleconversation.c

Sat, 13 Apr 2024 21:21:59 -0500

author
Gary Kramlich <grim@reaperworld.com>
date
Sat, 13 Apr 2024 21:21:59 -0500
changeset 42715
f886f74847b0
parent 42708
36610b5bd662
child 42720
22c42a8405ad
permissions
-rw-r--r--

Remove PurpleIMConversation and PurpleProtocolIM

Both of these have been replaced and needed to be removed.

Testing Done:
Checked in with our turtle buddies.

Reviewed at https://reviews.imfreedom.org/r/3113/

/*
 * 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 "purpleconversation.h"

#include "conversations.h"
#include "debug.h"
#include "notify.h"
#include "purpleconversationmanager.h"
#include "purpleconversationmember.h"
#include "purpleenums.h"
#include "purplehistorymanager.h"
#include "purplemarkup.h"
#include "purpleprivate.h"
#include "purpleprotocolconversation.h"
#include "purpletags.h"
#include "server.h"

typedef struct {
	char *id;
	PurpleConversationType type;
	PurpleAccount *account;

	PurpleAvatar *avatar;
	char *name;
	char *title;

	PurpleConnectionFlags features;

	gboolean age_restricted;
	char *description;
	char *topic;
	PurpleContactInfo *topic_author;
	GDateTime *topic_updated;
	char *user_nickname;
	gboolean favorite;
	GDateTime *created_on;
	PurpleContactInfo *creator;
	gboolean online;
	gboolean federated;
	PurpleTags *tags;
	GListStore *members;

	GListStore *messages;
} PurpleConversationPrivate;

enum {
	PROP_0,
	PROP_ID,
	PROP_TYPE,
	PROP_ACCOUNT,
	PROP_AVATAR,
	PROP_NAME,
	PROP_TITLE,
	PROP_FEATURES,
	PROP_AGE_RESTRICTED,
	PROP_DESCRIPTION,
	PROP_TOPIC,
	PROP_TOPIC_AUTHOR,
	PROP_TOPIC_UPDATED,
	PROP_USER_NICKNAME,
	PROP_FAVORITE,
	PROP_CREATED_ON,
	PROP_CREATOR,
	PROP_ONLINE,
	PROP_FEDERATED,
	PROP_TAGS,
	PROP_MEMBERS,
	PROP_MESSAGES,
	N_PROPERTIES,
};
static GParamSpec *properties[N_PROPERTIES] = {NULL, };

enum {
	SIG_MEMBER_ADDED,
	SIG_MEMBER_REMOVED,
	N_SIGNALS,
};
static guint signals[N_SIGNALS] = {0, };

G_DEFINE_TYPE_WITH_PRIVATE(PurpleConversation, purple_conversation,
                           G_TYPE_OBJECT);

static void purple_conversation_account_connected_cb(GObject *obj,
                                                     GParamSpec *pspec,
                                                     gpointer data);

/**************************************************************************
 * Helpers
 **************************************************************************/
static void
purple_conversation_set_id(PurpleConversation *conversation, const char *id) {
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(!purple_strequal(id, priv->id)) {
		g_free(priv->id);
		priv->id = g_strdup(id);

		g_object_notify_by_pspec(G_OBJECT(conversation), properties[PROP_ID]);
	}
}

static void
purple_conversation_set_account(PurpleConversation *conv,
                                PurpleAccount *account)
{
	PurpleConversationMember *member = NULL;
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));
	priv = purple_conversation_get_instance_private(conv);

	/* Remove the account from the conversation if it's a member. */
	if(PURPLE_IS_ACCOUNT(priv->account)) {
		if(PURPLE_IS_CONVERSATION_MEMBER(member)) {
			purple_conversation_remove_member(conv,
			                                  PURPLE_CONTACT_INFO(priv->account),
			                                  FALSE, NULL);
		}
	}

	if(g_set_object(&priv->account, account)) {
		if(PURPLE_IS_ACCOUNT(priv->account)) {
			purple_conversation_add_member(conv, PURPLE_CONTACT_INFO(account),
			                               FALSE, NULL);

			g_signal_connect_object(account, "notify::connected",
			                        G_CALLBACK(purple_conversation_account_connected_cb),
			                        conv, 0);
		}

		purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_ACCOUNT);

		g_object_notify_by_pspec(G_OBJECT(conv), properties[PROP_ACCOUNT]);
	}
}

static void
purple_conversation_set_federated(PurpleConversation *conversation,
                                  gboolean federated)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);
	if(priv->federated != federated) {
		priv->federated = federated;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_FEDERATED]);
	}
}

static gboolean
purple_conversation_check_member_equal(gconstpointer a, gconstpointer b) {
	PurpleConversationMember *member_a = (PurpleConversationMember *)a;
	PurpleConversationMember *member_b = (PurpleConversationMember *)b;
	PurpleContactInfo *info_a = NULL;
	PurpleContactInfo *info_b = NULL;

	info_a = purple_conversation_member_get_contact_info(member_a);
	info_b = purple_conversation_member_get_contact_info(member_b);

	return (purple_contact_info_compare(info_a, info_b) == 0);
}

static void
purple_conversation_send_message_async_cb(GObject *protocol,
                                          GAsyncResult *result,
                                          gpointer data)
{
	PurpleProtocolConversation *protocol_conversation = NULL;
	PurpleMessage *message = data;
	GError *error = NULL;

	protocol_conversation = PURPLE_PROTOCOL_CONVERSATION(protocol);
	purple_protocol_conversation_send_message_finish(protocol_conversation,
	                                                 result, &error);

	if(error != NULL) {
		g_warning("failed to send message: %s", error->message);

		g_clear_error(&error);
	}

	g_clear_object(&message);
}

static void
common_send(PurpleConversation *conv, const gchar *message,
            PurpleMessageFlags msgflags)
{
	PurpleAccount *account;
	PurpleConnection *gc;
	PurpleConversationPrivate *priv = NULL;
	PurpleProtocol *protocol = NULL;
	gchar *displayed = NULL;
	const gchar *sent, *me;
	gint err = 0;

	if(*message == '\0') {
		return;
	}

	priv = purple_conversation_get_instance_private(conv);

	account = purple_conversation_get_account(conv);
	g_return_if_fail(PURPLE_IS_ACCOUNT(account));

	gc = purple_account_get_connection(account);
	g_return_if_fail(PURPLE_IS_CONNECTION(gc));

	protocol = purple_account_get_protocol(account);

	me = purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(account));

	/* Always linkify the text for display, unless we're explicitly asked to do
	 * otherwise. */
	if(!(msgflags & PURPLE_MESSAGE_INVISIBLE)) {
		if(msgflags & PURPLE_MESSAGE_NO_LINKIFY) {
			displayed = g_strdup(message);
		} else {
			displayed = purple_markup_linkify(message);
		}
	}

	if(displayed && (priv->features & PURPLE_CONNECTION_FLAG_HTML) &&
	   !(msgflags & PURPLE_MESSAGE_RAW))
	{
		sent = displayed;
	} else {
		sent = message;
	}

	msgflags |= PURPLE_MESSAGE_SEND;

	if(PURPLE_IS_PROTOCOL_CONVERSATION(protocol)) {
		PurpleMessage *msg = NULL;
		PurpleProtocolConversation *protocol_conversation = NULL;

		msg = purple_message_new_outgoing(me, NULL, sent, msgflags);

		protocol_conversation = PURPLE_PROTOCOL_CONVERSATION(protocol);
		purple_protocol_conversation_send_message_async(protocol_conversation,
		                                                conv, msg, NULL,
		                                                purple_conversation_send_message_async_cb,
		                                                msg);

		g_clear_pointer(&displayed, g_free);
	}

	if(err < 0) {
		const gchar *who;
		const gchar *msg;

		who = purple_conversation_get_name(conv);

		if(err == -E2BIG) {
			msg = _("Unable to send message: The message is too large.");

			if(!purple_conversation_present_error(who, account, msg)) {
				gchar *msg2 = g_strdup_printf(_("Unable to send message to %s."),
				                              who);
				purple_notify_error(gc, NULL, msg2,
				                    _("The message is too large."),
				                    purple_request_cpar_from_connection(gc));
				g_free(msg2);
			}
		} else if(err == -ENOTCONN) {
			purple_debug_error("conversation", "Not yet connected.");
		} else {
			msg = _("Unable to send message.");

			if(!purple_conversation_present_error(who, account, msg)) {
				gchar *msg2 = g_strdup_printf(_("Unable to send message to %s."),
				                              who);
				purple_notify_error(gc, NULL, msg2, NULL,
				                    purple_request_cpar_from_connection(gc));
				g_free(msg2);
			}
		}
	}

	g_free(displayed);
}

static void
purple_conversation_send_confirm_cb(gpointer *data) {
	PurpleConversation *conv = data[0];
	gchar *message = data[1];

	g_free(data);

	if(!PURPLE_IS_CONVERSATION(conv)) {
		/* Maybe it was closed before this callback was called. */
		return;
	}

	common_send(conv, message, 0);
}

/**************************************************************************
 * Callbacks
 **************************************************************************/
static void
purple_conversation_account_connected_cb(GObject *obj,
                                         G_GNUC_UNUSED GParamSpec *pspec,
                                         gpointer data)
{
	PurpleConversation *conversation = data;
	PurpleConversationPrivate *priv = NULL;
	gboolean connected = purple_account_is_connected(PURPLE_ACCOUNT(obj));

	priv = purple_conversation_get_instance_private(conversation);

	if(priv->federated) {
		/* If the account changed to connected and the conversation is
		 * federated we do nothing. But if the account went offline, we can
		 * safely set the conversation to offline.
		 */
		if(!connected) {
			purple_conversation_set_online(conversation, FALSE);
		}
	} else {
		purple_conversation_set_online(conversation, connected);
	}
}

/**************************************************************************
 * GObject Implementation
 **************************************************************************/
static void
purple_conversation_set_property(GObject *obj, guint param_id,
                                 const GValue *value, GParamSpec *pspec)
{
	PurpleConversation *conv = PURPLE_CONVERSATION(obj);
	PurpleConversationPrivate *priv = NULL;

	priv = purple_conversation_get_instance_private(conv);

	switch (param_id) {
		case PROP_ID:
			purple_conversation_set_id(conv, g_value_get_string(value));
			break;
		case PROP_TYPE:
			purple_conversation_set_conversation_type(conv, g_value_get_enum(value));
			break;
		case PROP_ACCOUNT:
			purple_conversation_set_account(conv, g_value_get_object(value));
			break;
		case PROP_AVATAR:
			purple_conversation_set_avatar(conv, g_value_get_object(value));
			break;
		case PROP_NAME:
			g_free(priv->name);
			priv->name = g_value_dup_string(value);
			break;
		case PROP_TITLE:
			purple_conversation_set_title(conv, g_value_get_string(value));
			break;
		case PROP_FEATURES:
			purple_conversation_set_features(conv, g_value_get_flags(value));
			break;
		case PROP_AGE_RESTRICTED:
			purple_conversation_set_age_restricted(conv,
			                                       g_value_get_boolean(value));
			break;
		case PROP_DESCRIPTION:
			purple_conversation_set_description(conv,
			                                    g_value_get_string(value));
			break;
		case PROP_TOPIC:
			purple_conversation_set_topic(conv, g_value_get_string(value));
			break;
		case PROP_TOPIC_AUTHOR:
			purple_conversation_set_topic_author(conv,
			                                     g_value_get_object(value));
			break;
		case PROP_TOPIC_UPDATED:
			purple_conversation_set_topic_updated(conv,
			                                      g_value_get_boxed(value));
			break;
		case PROP_USER_NICKNAME:
			purple_conversation_set_user_nickname(conv,
			                                      g_value_get_string(value));
			break;
		case PROP_FAVORITE:
			purple_conversation_set_favorite(conv, g_value_get_boolean(value));
			break;
		case PROP_CREATED_ON:
			purple_conversation_set_created_on(conv, g_value_get_boxed(value));
			break;
		case PROP_CREATOR:
			purple_conversation_set_creator(conv, g_value_get_object(value));
			break;
		case PROP_ONLINE:
			purple_conversation_set_online(conv, g_value_get_boolean(value));
			break;
		case PROP_FEDERATED:
			purple_conversation_set_federated(conv,
			                                  g_value_get_boolean(value));
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
			break;
	}
}

static void
purple_conversation_get_property(GObject *obj, guint param_id, GValue *value,
                                 GParamSpec *pspec)
{
	PurpleConversation *conv = PURPLE_CONVERSATION(obj);

	switch(param_id) {
		case PROP_ID:
			g_value_set_string(value, purple_conversation_get_id(conv));
			break;
		case PROP_TYPE:
			g_value_set_enum(value,
			                 purple_conversation_get_conversation_type(conv));
			break;
		case PROP_ACCOUNT:
			g_value_set_object(value, purple_conversation_get_account(conv));
			break;
		case PROP_AVATAR:
			g_value_set_object(value, purple_conversation_get_avatar(conv));
			break;
		case PROP_NAME:
			g_value_set_string(value, purple_conversation_get_name(conv));
			break;
		case PROP_TITLE:
			g_value_set_string(value, purple_conversation_get_title(conv));
			break;
		case PROP_FEATURES:
			g_value_set_flags(value, purple_conversation_get_features(conv));
			break;
		case PROP_AGE_RESTRICTED:
			g_value_set_boolean(value,
			                    purple_conversation_get_age_restricted(conv));
			break;
		case PROP_DESCRIPTION:
			g_value_set_string(value,
			                   purple_conversation_get_description(conv));
			break;
		case PROP_TOPIC:
			g_value_set_string(value, purple_conversation_get_topic(conv));
			break;
		case PROP_TOPIC_AUTHOR:
			g_value_set_object(value,
			                   purple_conversation_get_topic_author(conv));
			break;
		case PROP_TOPIC_UPDATED:
			g_value_set_boxed(value,
			                  purple_conversation_get_topic_updated(conv));
			break;
		case PROP_USER_NICKNAME:
			g_value_set_string(value,
			                   purple_conversation_get_user_nickname(conv));
			break;
		case PROP_FAVORITE:
			g_value_set_boolean(value, purple_conversation_get_favorite(conv));
			break;
		case PROP_CREATED_ON:
			g_value_set_boxed(value, purple_conversation_get_created_on(conv));
			break;
		case PROP_CREATOR:
			g_value_set_object(value, purple_conversation_get_creator(conv));
			break;
		case PROP_ONLINE:
			g_value_set_boolean(value, purple_conversation_get_online(conv));
			break;
		case PROP_FEDERATED:
			g_value_set_boolean(value,
			                    purple_conversation_get_federated(conv));
			break;
		case PROP_TAGS:
			g_value_set_object(value, purple_conversation_get_tags(conv));
			break;
		case PROP_MEMBERS:
			g_value_set_object(value, purple_conversation_get_members(conv));
			break;
		case PROP_MESSAGES:
			g_value_set_object(value, purple_conversation_get_messages(conv));
			break;
		default:
			G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
			break;
	}
}

static void
purple_conversation_init(PurpleConversation *conv) {
	PurpleConversationPrivate *priv = NULL;

	priv = purple_conversation_get_instance_private(conv);

	priv->tags = purple_tags_new();
	priv->members = g_list_store_new(PURPLE_TYPE_CONVERSATION_MEMBER);
	priv->messages = g_list_store_new(PURPLE_TYPE_MESSAGE);
}

static void
purple_conversation_constructed(GObject *object) {
	PurpleConversation *conv = PURPLE_CONVERSATION(object);
	PurpleAccount *account;
	PurpleConnection *gc;

	G_OBJECT_CLASS(purple_conversation_parent_class)->constructed(object);

	g_object_get(object, "account", &account, NULL);
	gc = purple_account_get_connection(account);

	/* Check if we have a connection before we use it. The unit tests are one
	 * case where we will not have a connection.
	 */
	if(PURPLE_IS_CONNECTION(gc)) {
		purple_conversation_set_features(conv,
		                                 purple_connection_get_flags(gc));
	}

	/* Auto-set the title. */
	purple_conversation_autoset_title(conv);

	purple_signal_emit(purple_conversations_get_handle(),
	                   "conversation-created", conv);

	g_object_unref(account);
}

static void
purple_conversation_dispose(GObject *obj) {
	g_object_set_data(obj, "is-finalizing", GINT_TO_POINTER(TRUE));
}

static void
purple_conversation_finalize(GObject *object) {
	PurpleConversation *conv = PURPLE_CONVERSATION(object);
	PurpleConversationPrivate *priv =
			purple_conversation_get_instance_private(conv);

	purple_request_close_with_handle(conv);

	purple_signal_emit(purple_conversations_get_handle(),
	                   "deleting-conversation", conv);

	g_clear_pointer(&priv->id, g_free);
	g_clear_object(&priv->avatar);
	g_clear_pointer(&priv->name, g_free);
	g_clear_pointer(&priv->title, g_free);

	g_clear_pointer(&priv->description, g_free);
	g_clear_pointer(&priv->topic, g_free);
	g_clear_object(&priv->topic_author);
	g_clear_pointer(&priv->topic_updated, g_date_time_unref);
	g_clear_pointer(&priv->user_nickname, g_free);
	g_clear_pointer(&priv->created_on, g_date_time_unref);
	g_clear_object(&priv->creator);
	g_clear_object(&priv->tags);
	g_clear_object(&priv->members);
	g_clear_object(&priv->messages);

	G_OBJECT_CLASS(purple_conversation_parent_class)->finalize(object);
}

static void
purple_conversation_class_init(PurpleConversationClass *klass) {
	GObjectClass *obj_class = G_OBJECT_CLASS(klass);

	klass->write_message = _purple_conversation_write_common;

	obj_class->constructed = purple_conversation_constructed;
	obj_class->dispose = purple_conversation_dispose;
	obj_class->finalize = purple_conversation_finalize;
	obj_class->get_property = purple_conversation_get_property;
	obj_class->set_property = purple_conversation_set_property;

	/**
	 * PurpleConversation:id:
	 *
	 * An opaque identifier for this conversation. Generally speaking this is
	 * protocol dependent and should only be used as a unique identifier.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ID] = g_param_spec_string(
		"id", "id",
		"The identifier for the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:type:
	 *
	 * A type hint for the conversation. This may be useful for protocols, but
	 * libpurple treats all conversations the same.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TYPE] = g_param_spec_enum(
		"type", "type",
		"The type of the conversation.",
		PURPLE_TYPE_CONVERSATION_TYPE,
		PURPLE_CONVERSATION_TYPE_UNSET,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:account:
	 *
	 * The account this conversation belongs to.
	 *
	 * Since: 3.0
	 */
	properties[PROP_ACCOUNT] = g_param_spec_object(
		"account", "Account",
		"The account for the conversation.",
		PURPLE_TYPE_ACCOUNT,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:avatar:
	 *
	 * The [class@Avatar] for the conversation.
	 *
	 * Not all protocols support this and most user interfaces will use the
	 * avatar of the remote contact for direct messages.
	 *
	 * Since: 3.0
	 */
	properties[PROP_AVATAR] = g_param_spec_object(
		"avatar", "avatar",
		"The avatar for this conversation.",
		PURPLE_TYPE_AVATAR,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:name:
	 *
	 * The name of the conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_NAME] = g_param_spec_string(
		"name", "Name",
		"The name of the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:title:
	 *
	 * The title of the conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TITLE] = g_param_spec_string(
		"title", "Title",
		"The title of the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:features:
	 *
	 * The features that this conversation supports.
	 *
	 * Since: 3.0
	 */
	properties[PROP_FEATURES] = g_param_spec_flags(
		"features", "Connection features",
		"The connection features of the conversation.",
		PURPLE_TYPE_CONNECTION_FLAGS,
		0,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:age-restricted:
	 *
	 * Whether or not the conversation is age restricted.
	 *
	 * This is typically set only by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_AGE_RESTRICTED] = g_param_spec_boolean(
		"age-restricted", "age-restricted",
		"Whether or not the conversation is age restricted.",
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:description:
	 *
	 * Sets the description of the conversation. This field is typically used
	 * to give more information about a conversation than that which would fit
	 * in [property@Conversation:topic].
	 *
	 * This is typically set only by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_DESCRIPTION] = g_param_spec_string(
		"description", "description",
		"The description for the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:topic:
	 *
	 * The topic of the conversation.
	 *
	 * This is normally controlled by the protocol plugin and often times
	 * requires permission for the user to set.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TOPIC] = g_param_spec_string(
		"topic", "topic",
		"The topic for the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:topic-author:
	 *
	 * Sets the author of the topic for the conversation.
	 *
	 * This should typically only be set by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TOPIC_AUTHOR] = g_param_spec_object(
		"topic-author", "topic-author",
		"The author of the topic for the conversation.",
		PURPLE_TYPE_CONTACT_INFO,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:topic-updated:
	 *
	 * Set to the time that the topic was last updated.
	 *
	 * This should typically only be set by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TOPIC_UPDATED] = g_param_spec_boxed(
		"topic-updated", "topic-updated",
		"The time when the topic was last updated for the conversation.",
		G_TYPE_DATE_TIME,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:user-nickname:
	 *
	 * The user's nickname in this conversation.
	 *
	 * Some protocols allow the user to use a nickname rather than their normal
	 * contact information when joining a conversation. This field holds that
	 * value.
	 *
	 * Since: 3.0
	 */
	properties[PROP_USER_NICKNAME] = g_param_spec_string(
		"user-nickname", "user-nickname",
		"The nickname for the user in the conversation.",
		NULL,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:favorite:
	 *
	 * Whether or not the conversation has been marked as favorite by the user.
	 *
	 * Since: 3.0
	 */
	properties[PROP_FAVORITE] = g_param_spec_boolean(
		"favorite", "favorite",
		"Whether or not the conversation is a favorite.",
		FALSE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:created-on:
	 *
	 * The [struct@GLib.DateTime] when this conversation was created. This can
	 * be %NULL if the value is not known or supported.
	 *
	 * This should typically only be set by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CREATED_ON] = g_param_spec_boxed(
		"created-on", "created-on",
		"When the conversation was created.",
		G_TYPE_DATE_TIME,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:creator:
	 *
	 * The [class@ContactInfo] that created the conversation.
	 *
	 * This should typically only be set by a protocol plugin.
	 *
	 * Since: 3.0
	 */
	properties[PROP_CREATOR] = g_param_spec_object(
		"creator", "creator",
		"The contact info of who created the conversation.",
		PURPLE_TYPE_CONTACT_INFO,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:online:
	 *
	 * Whether or not the conversation is able to send and receive messages.
	 *
	 * This is typically tied to whether or not the account that this
	 * conversation belongs is online or not.
	 *
	 * However, if a protocol supports federated conversation, it is possible
	 * for a conversation to be offline if the server it is on is currently
	 * unreachable.
	 *
	 * See also [property@Conversation:federated].
	 *
	 * Since: 3.0
	 */
	properties[PROP_ONLINE] = g_param_spec_boolean(
		"online", "online",
		"Whether or not the conversation can send and receive messages.",
		TRUE,
		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:federated:
	 *
	 * Whether or this conversation is federated.
	 *
	 * This should only be set by protocols that support federated
	 * conversations.
	 *
	 * When this is %TRUE the [property@Conversation:online] property will not
	 * be automatically set to match the [property@Account:connected] property
	 * of the account that this conversation belongs to. It is the
	 * responsibility of the protocol to manage the online property in this
	 * case.
	 *
	 * Since: 3.0
	 */
	properties[PROP_FEDERATED] = g_param_spec_boolean(
		"federated", "federated",
		"Whether or not this conversation is federated.",
		FALSE,
		G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:tags:
	 *
	 * [class@Tags] for the conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_TAGS] = g_param_spec_object(
		"tags", "tags",
		"The tags for the conversation.",
		PURPLE_TYPE_TAGS,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:members:
	 *
	 * The members that are currently in this conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_MEMBERS] = g_param_spec_object(
		"members", "members",
		"The members that are currently in this conversation",
		G_TYPE_LIST_MODEL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	/**
	 * PurpleConversation:messages:
	 *
	 * A [iface.Gio.ListModel] of all the messages in this conversation.
	 *
	 * Since: 3.0
	 */
	properties[PROP_MESSAGES] = g_param_spec_object(
		"messages", "messages",
		"All of the messages in this conversation's history.",
		G_TYPE_LIST_MODEL,
		G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);

	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);

	/**
	 * PurpleConversation::member-added:
	 * @conversation: The instance.
	 * @member: The [class@Purple.ConversationMember] instance.
	 * @announce: Whether or not this addition should be announced.
	 * @message: (nullable): An optional message to use in the announcement.
	 *
	 * Emitted when a new member is added to this conversation.
	 *
	 * Since: 3.0
	 */
	signals[SIG_MEMBER_ADDED] = g_signal_new_class_handler(
		"member-added",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		3,
		PURPLE_TYPE_CONVERSATION_MEMBER,
		G_TYPE_BOOLEAN,
		G_TYPE_STRING);

	/**
	 * PurpleConversation::member-removed:
	 * @conversation: The instance.
	 * @member: The [class@Purple.ConversationMember] instance.
	 * @announce: Whether or not this removal should be announced.
	 * @message: (nullable): An optional message to use in the announcement.
	 *
	 * Emitted when member is removed from this conversation.
	 *
	 * Since: 3.0
	 */
	signals[SIG_MEMBER_REMOVED] = g_signal_new_class_handler(
		"member-removed",
		G_OBJECT_CLASS_TYPE(klass),
		G_SIGNAL_RUN_LAST,
		NULL,
		NULL,
		NULL,
		NULL,
		G_TYPE_NONE,
		3,
		PURPLE_TYPE_CONVERSATION_MEMBER,
		G_TYPE_BOOLEAN,
		G_TYPE_STRING);
}

/******************************************************************************
 * Public API
 *****************************************************************************/
gboolean
purple_conversation_is_dm(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->type == PURPLE_CONVERSATION_TYPE_DM;
}

gboolean
purple_conversation_is_group_dm(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->type == PURPLE_CONVERSATION_TYPE_GROUP_DM;
}

gboolean
purple_conversation_is_channel(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->type == PURPLE_CONVERSATION_TYPE_CHANNEL;
}

gboolean
purple_conversation_is_thread(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->type == PURPLE_CONVERSATION_TYPE_THREAD;
}

void
purple_conversation_present(G_GNUC_UNUSED PurpleConversation *conv) {
}

void
purple_conversation_set_features(PurpleConversation *conv,
                                 PurpleConnectionFlags features)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));

	priv = purple_conversation_get_instance_private(conv);
	priv->features = features;

	g_object_notify_by_pspec(G_OBJECT(conv), properties[PROP_FEATURES]);

	purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_FEATURES);
}

PurpleConnectionFlags
purple_conversation_get_features(PurpleConversation *conv) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conv), 0);

	priv = purple_conversation_get_instance_private(conv);

	return priv->features;
}

const char *
purple_conversation_get_id(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->id;
}

PurpleConversationType
purple_conversation_get_conversation_type(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation),
	                     PURPLE_CONVERSATION_TYPE_UNSET);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->type;
}

void
purple_conversation_set_conversation_type(PurpleConversation *conversation,
                                          PurpleConversationType type)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(type != priv->type) {
		priv->type = type;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TYPE]);
	}
}

PurpleAccount *
purple_conversation_get_account(PurpleConversation *conv) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conv), NULL);

	priv = purple_conversation_get_instance_private(conv);

	return priv->account;
}

PurpleConnection *
purple_conversation_get_connection(PurpleConversation *conv) {
	PurpleAccount *account;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conv), NULL);

	account = purple_conversation_get_account(conv);

	if(account == NULL) {
		return NULL;
	}

	return purple_account_get_connection(account);
}

void
purple_conversation_set_title(PurpleConversation *conv, const gchar *title) {
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));
	g_return_if_fail(title != NULL);

	priv = purple_conversation_get_instance_private(conv);
	g_free(priv->title);
	priv->title = g_strdup(title);

	if(!g_object_get_data(G_OBJECT(conv), "is-finalizing")) {
		g_object_notify_by_pspec(G_OBJECT(conv), properties[PROP_TITLE]);
	}

	purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_TITLE);
}

const gchar *
purple_conversation_get_title(PurpleConversation *conv) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conv), NULL);

	priv = purple_conversation_get_instance_private(conv);

	return priv->title;
}

void
purple_conversation_autoset_title(PurpleConversation *conv) {
	const gchar *name = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));

	name = purple_conversation_get_name(conv);

	purple_conversation_set_title(conv, name);
}

void
purple_conversation_set_name(PurpleConversation *conv, const gchar *name) {
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));
	priv = purple_conversation_get_instance_private(conv);

	g_free(priv->name);
	priv->name = g_strdup(name);

	g_object_notify_by_pspec(G_OBJECT(conv), properties[PROP_NAME]);

	purple_conversation_autoset_title(conv);
	purple_conversation_update(conv, PURPLE_CONVERSATION_UPDATE_NAME);
}

const gchar *
purple_conversation_get_name(PurpleConversation *conv) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conv), NULL);

	priv = purple_conversation_get_instance_private(conv);

	return priv->name;
}

void
_purple_conversation_write_common(PurpleConversation *conv,
                                  PurpleMessage *pmsg)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));
	g_return_if_fail(pmsg != NULL);

	priv = purple_conversation_get_instance_private(conv);

	if(purple_message_is_empty(pmsg)) {
		return;
	}

	if(!(purple_message_get_flags(pmsg) & PURPLE_MESSAGE_NO_LOG))
	{
		GError *error = NULL;
		PurpleHistoryManager *manager = NULL;

		manager = purple_history_manager_get_default();
		/* We should probably handle this error somehow, but I don't think that
		 * spamming purple_debug_warning is necessarily the right call.
		 */
		if(!purple_history_manager_write(manager, conv, pmsg, &error)){
			purple_debug_info("conversation", "history manager write returned error: %s", error->message);

			g_clear_error(&error);
		}
	}

	g_list_store_append(priv->messages, pmsg);
}

void
purple_conversation_write_message(PurpleConversation *conv,
                                  PurpleMessage *msg)
{
	PurpleConversationClass *klass = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));

	klass = PURPLE_CONVERSATION_GET_CLASS(conv);

	if(klass && klass->write_message) {
		klass->write_message(conv, msg);
	}
}

void
purple_conversation_write_system_message(PurpleConversation *conv,
                                         const gchar *message,
                                         PurpleMessageFlags flags)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));

	_purple_conversation_write_common(conv,
	                                  purple_message_new_system(message,
	                                                            flags));
}

void
purple_conversation_send(PurpleConversation *conv, const gchar *message) {
	purple_conversation_send_with_flags(conv, message, 0);
}

void
purple_conversation_send_with_flags(PurpleConversation *conv,
                                    const gchar *message,
                                    PurpleMessageFlags flags)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));
	g_return_if_fail(message != NULL);

	common_send(conv, message, flags);
}

gboolean
purple_conversation_has_focus(PurpleConversation *conv) {
	gboolean ret = FALSE;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conv), FALSE);

	return ret;
}

/*
 * TODO: Need to make sure calls to this function happen in the core
 * instead of the UI.  That way UIs have less work to do, and the
 * core/UI split is cleaner.  Also need to make sure this is called
 * when chats are added/removed from the blist.
 */
void
purple_conversation_update(PurpleConversation *conv,
                           PurpleConversationUpdateType type)
{
	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));

	purple_signal_emit(purple_conversations_get_handle(),
	                   "conversation-updated", conv, type);
}

gboolean
purple_conversation_present_error(const gchar *who, PurpleAccount *account,
                                  const gchar *what)
{
	PurpleConversation *conv;
	PurpleConversationManager *manager;

	g_return_val_if_fail(who != NULL, FALSE);
	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), FALSE);
	g_return_val_if_fail(what != NULL, FALSE);

	manager = purple_conversation_manager_get_default();
	conv = purple_conversation_manager_find(manager, account, who);
	if(PURPLE_IS_CONVERSATION(conv)) {
		purple_conversation_write_system_message(conv, what,
		                                         PURPLE_MESSAGE_ERROR);
		return TRUE;
	}

	return FALSE;
}

void
purple_conversation_send_confirm(PurpleConversation *conv,
                                 const gchar *message)
{
	gchar *text;
	gpointer *data;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conv));
	g_return_if_fail(message != NULL);

	text = g_strdup_printf("You are about to send the following message:\n%s",
	                       message);
	data = g_new0(gpointer, 2);
	data[0] = conv;
	data[1] = (gpointer)message;

	purple_request_action(conv, NULL, _("Send Message"), text, 0,
		purple_request_cpar_from_account(
			purple_conversation_get_account(conv)),
		data, 2, _("_Send Message"),
		G_CALLBACK(purple_conversation_send_confirm_cb), _("Cancel"), NULL);
}

GList *
purple_conversation_get_extended_menu(PurpleConversation *conv) {
	GList *menu = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conv), NULL);

	purple_signal_emit(purple_conversations_get_handle(),
	                   "conversation-extended-menu", conv, &menu);

	return menu;
}

gboolean
purple_conversation_get_age_restricted(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->age_restricted;
}

void
purple_conversation_set_age_restricted(PurpleConversation *conversation,
                                       gboolean age_restricted)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(priv->age_restricted != age_restricted) {
		priv->age_restricted = age_restricted;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_AGE_RESTRICTED]);
	}
}

const char *
purple_conversation_get_description(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->description;
}

void
purple_conversation_set_description(PurpleConversation *conversation,
                                    const char *description)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(!purple_strequal(priv->description, description)) {
		g_free(priv->description);
		priv->description = g_strdup(description);

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_DESCRIPTION]);
	}
}

const char *
purple_conversation_get_topic(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->topic;
}

void
purple_conversation_set_topic(PurpleConversation *conversation,
                              const char *topic)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(!purple_strequal(priv->topic, topic)) {
		g_free(priv->topic);
		priv->topic = g_strdup(topic);

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TOPIC]);
	}
}

void
purple_conversation_set_topic_full(PurpleConversation *conversation,
                                   const char *topic,
                                   PurpleContactInfo *author,
                                   GDateTime *updated)
{
	GObject *obj = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	obj = G_OBJECT(conversation);
	g_object_freeze_notify(obj);

	purple_conversation_set_topic(conversation, topic);
	purple_conversation_set_topic_author(conversation, author);
	purple_conversation_set_topic_updated(conversation, updated);

	g_object_thaw_notify(obj);
}


PurpleContactInfo *
purple_conversation_get_topic_author(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->topic_author;
}

void
purple_conversation_set_topic_author(PurpleConversation *conversation,
                                     PurpleContactInfo *author)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(g_set_object(&priv->topic_author, author)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TOPIC_AUTHOR]);
	}
}

GDateTime *
purple_conversation_get_topic_updated(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->topic_updated;
}

void
purple_conversation_set_topic_updated(PurpleConversation *conversation,
                                      GDateTime *updated)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(priv->topic_updated != updated) {
		g_clear_pointer(&priv->topic_updated, g_date_time_unref);

		if(updated != NULL) {
			priv->topic_updated = g_date_time_ref(updated);
		}

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_TOPIC_UPDATED]);
	}
}

const char *
purple_conversation_get_user_nickname(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->user_nickname;
}

void
purple_conversation_set_user_nickname(PurpleConversation *conversation,
                                      const char *nickname)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(!purple_strequal(priv->user_nickname, nickname)) {
		g_free(priv->user_nickname);
		priv->user_nickname = g_strdup(nickname);

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_USER_NICKNAME]);
	}
}

gboolean
purple_conversation_get_favorite(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->favorite;
}

void
purple_conversation_set_favorite(PurpleConversation *conversation,
                                 gboolean favorite)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(priv->favorite != favorite) {
		priv->favorite = favorite;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_FAVORITE]);
	}
}

GDateTime *
purple_conversation_get_created_on(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->created_on;
}

void
purple_conversation_set_created_on(PurpleConversation *conversation,
                                   GDateTime *created_on)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);

	if(priv->created_on != created_on) {
		g_clear_pointer(&priv->created_on, g_date_time_unref);

		if(created_on != NULL) {
			priv->created_on = g_date_time_ref(created_on);
		}

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_CREATED_ON]);
	}
}

PurpleContactInfo *
purple_conversation_get_creator(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->creator;
}

void
purple_conversation_set_creator(PurpleConversation *conversation,
                                PurpleContactInfo *creator)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);
	if(g_set_object(&priv->creator, creator)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_CREATOR]);
	}
}

gboolean
purple_conversation_get_online(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->online;
}

void
purple_conversation_set_online(PurpleConversation *conversation,
                               gboolean online)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);
	if(priv->online != online) {
		priv->online = online;

		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_ONLINE]);
	}
}

gboolean
purple_conversation_get_federated(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->federated;
}

PurpleTags *
purple_conversation_get_tags(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->tags;
}

GListModel *
purple_conversation_get_members(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return G_LIST_MODEL(priv->members);
}

gboolean
purple_conversation_has_member(PurpleConversation *conversation,
                               PurpleContactInfo *info, guint *position)
{
	PurpleConversationPrivate *priv = NULL;
	PurpleConversationMember *needle = NULL;
	gboolean found = FALSE;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
	g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(info), FALSE);

	priv = purple_conversation_get_instance_private(conversation);

	needle = purple_conversation_member_new(info);
	found = g_list_store_find_with_equal_func(priv->members, needle,
	                                          purple_conversation_check_member_equal,
	                                          position);

	g_clear_object(&needle);

	return found;
}

PurpleConversationMember *
purple_conversation_find_member(PurpleConversation *conversation,
                                PurpleContactInfo *info)
{
	PurpleConversationMember *member = NULL;
	guint position = 0;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);
	g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(info), NULL);

	if(purple_conversation_has_member(conversation, info, &position)) {
		PurpleConversationPrivate *priv = NULL;

		priv = purple_conversation_get_instance_private(conversation);

		member = g_list_model_get_item(G_LIST_MODEL(priv->members), position);

		/* We don't return a reference, but get_item does, so we need to get
		 * rid of that.
		 */
		g_object_unref(member);
	}

	return member;
}

PurpleConversationMember *
purple_conversation_add_member(PurpleConversation *conversation,
                               PurpleContactInfo *info, gboolean announce,
                               const char *message)
{
	PurpleConversationMember *member = NULL;
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);
	g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(info), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	member = purple_conversation_find_member(conversation, info);
	if(PURPLE_IS_CONVERSATION_MEMBER(member)) {
		return member;
	}

	member = purple_conversation_member_new(info);
	g_list_store_append(priv->members, member);

	g_signal_emit(conversation, signals[SIG_MEMBER_ADDED], 0, member, announce,
	              message);

	g_object_unref(member);

	return member;
}

gboolean
purple_conversation_remove_member(PurpleConversation *conversation,
                                  PurpleContactInfo *info, gboolean announce,
                                  const char *message)
{
	PurpleConversationMember *member = NULL;
	PurpleConversationPrivate *priv = NULL;
	guint position = 0;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
	g_return_val_if_fail(PURPLE_IS_CONTACT_INFO(info), FALSE);

	if(!purple_conversation_has_member(conversation, info, &position)) {
		return FALSE;
	}

	priv = purple_conversation_get_instance_private(conversation);
	member = g_list_model_get_item(G_LIST_MODEL(priv->members), position);

	g_list_store_remove(priv->members, position);

	g_signal_emit(conversation, signals[SIG_MEMBER_REMOVED], 0, member,
	              announce, message);

	g_clear_object(&member);

	return TRUE;
}

GListModel *
purple_conversation_get_messages(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	if(G_IS_LIST_MODEL(priv->messages)) {
		return G_LIST_MODEL(priv->messages);
	}

	return NULL;
}

PurpleAvatar *
purple_conversation_get_avatar(PurpleConversation *conversation) {
	PurpleConversationPrivate *priv = NULL;

	g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), NULL);

	priv = purple_conversation_get_instance_private(conversation);

	return priv->avatar;
}

void
purple_conversation_set_avatar(PurpleConversation *conversation,
                               PurpleAvatar *avatar)
{
	PurpleConversationPrivate *priv = NULL;

	g_return_if_fail(PURPLE_IS_CONVERSATION(conversation));

	priv = purple_conversation_get_instance_private(conversation);
	if(g_set_object(&priv->avatar, avatar)) {
		g_object_notify_by_pspec(G_OBJECT(conversation),
		                         properties[PROP_AVATAR]);
	}
}

mercurial