diff --git a/libpurple/purplemessage.h b/libpurple/purplemessage.h
--- a/libpurple/purplemessage.h
+++ b/libpurple/purplemessage.h
@@ -561,6 +561,60 @@
  */
 void purple_message_set_delivered_at(PurpleMessage *message, GDateTime *datetime);
 
+/**
+ * purple_message_get_edited:
+ * @message: The instance.
+ *
+ * Gets whether or not @message has been edited.
+ *
+ * Returns: %TRUE if edited, otherwise %FALSE.
+ *
+ * Since: 3.0.0
+ */
+gboolean purple_message_get_edited(PurpleMessage *message);
+
+/**
+ * purple_message_set_edited:
+ * @message: The instance.
+ * @edited: The new edited state.
+ *
+ * Sets the edited state of @message to @edited.
+ *
+ * > Note: Setting this will also set [property@Message:edited-at]. If
+ * @edited is %TRUE it will be set to the current time, otherwise it will be
+ * unset.
+ *
+ * Since: 3.0.0
+ */
+void purple_message_set_edited(PurpleMessage *message, gboolean edited);
+
+/**
+ * purple_message_get_edited_at:
+ * @message: The instance.
+ *
+ * Gets the time that @message was last edited. If @message has never been
+ * edited this will be %NULL.
+ *
+ * Returns: (transfer none) (nullable): The last edit time of @message.
+ *
+ * Since: 3.0.0
+ */
+GDateTime *purple_message_get_edited_at(PurpleMessage *message);
+
+/**
+ * purple_message_set_edited_at:
+ * @message: The instance.
+ * @datetime: (nullable): The time of the last edit.
+ *
+ * Sets the last edit time of @message to @datetime.
+ *
+ * > Note: Setting this will also set [property@Message:edited]. If
+ * @datetime is %NULL it will be set to %FALSE, otherwise %TRUE.
+ *
+ * Since: 3.0.0
+ */
+void purple_message_set_edited_at(PurpleMessage *message, GDateTime *datetime);
+
 G_END_DECLS
 
 #endif /* PURPLE_MESSAGE_H */
# HG changeset patch
# Node ID 654133d4d983
# Parent  45d2756d2a14
diff --git a/libpurple/purplemessage.c b/libpurple/purplemessage.c
--- a/libpurple/purplemessage.c
+++ b/libpurple/purplemessage.c
@@ -46,6 +46,7 @@
 	GError *error;
 
 	GDateTime *delivered_at;
+	GDateTime *edited_at;
 
 	GHashTable *attachments;
 };
@@ -65,6 +66,8 @@
 	PROP_ERROR,
 	PROP_DELIVERED,
 	PROP_DELIVERED_AT,
+	PROP_EDITED,
+	PROP_EDITED_AT,
 	N_PROPERTIES
 };
 static GParamSpec *properties[N_PROPERTIES];
@@ -132,6 +135,12 @@
 		case PROP_DELIVERED_AT:
 			g_value_set_boxed(value, purple_message_get_delivered_at(message));
 			break;
+		case PROP_EDITED:
+			g_value_set_boolean(value, purple_message_get_edited(message));
+			break;
+		case PROP_EDITED_AT:
+			g_value_set_boxed(value, purple_message_get_edited_at(message));
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
 			break;
@@ -185,6 +194,12 @@
 		case PROP_DELIVERED_AT:
 			purple_message_set_delivered_at(message, g_value_get_boxed(value));
 			break;
+		case PROP_EDITED:
+			purple_message_set_edited(message, g_value_get_boolean(value));
+			break;
+		case PROP_EDITED_AT:
+			purple_message_set_edited_at(message, g_value_get_boxed(value));
+			break;
 		default:
 			G_OBJECT_WARN_INVALID_PROPERTY_ID(object, param_id, pspec);
 			break;
@@ -204,11 +219,9 @@
 
 	g_clear_error(&message->error);
 
-	if(message->timestamp != NULL) {
-		g_date_time_unref(message->timestamp);
-	}
-
+	g_clear_pointer(&message->timestamp, g_date_time_unref);
 	g_clear_pointer(&message->delivered_at, g_date_time_unref);
+	g_clear_pointer(&message->edited_at, g_date_time_unref);
 
 	g_hash_table_destroy(message->attachments);
 
@@ -408,6 +421,36 @@
 		G_TYPE_DATE_TIME,
 		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
 
+	/**
+	 * PurpleMessaged:edited:
+	 *
+	 * Whether or not this message has been edited.
+	 *
+	 * This should typically only be set by a protocol plugin.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_EDITED] = g_param_spec_boolean(
+		"edited", "edited",
+		"Whether or not this message has been edited.",
+		FALSE,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+	/**
+	 * PurpleMessage:edit-at:
+	 *
+	 * The time that the message was last edited at. This is protocol dependent
+	 * and possibly client dependent as well. So if this is %NULL that doesn't
+	 * necessarily mean the message was not edited.
+	 *
+	 * Since: 3.0.0
+	 */
+	properties[PROP_EDITED_AT] = g_param_spec_boxed(
+		"edited-at", "edited-at",
+		"The time that the message was last edited.",
+		G_TYPE_DATE_TIME,
+		G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
 	g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
 }
 
@@ -830,3 +873,69 @@
 	g_object_notify_by_pspec(obj, properties[PROP_DELIVERED_AT]);
 	g_object_thaw_notify(obj);
 }
+
+gboolean
+purple_message_get_edited(PurpleMessage *message) {
+	g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
+
+	return (message->edited_at != NULL);
+}
+
+void
+purple_message_set_edited(PurpleMessage *message, gboolean edited) {
+	GDateTime *datetime = NULL;
+
+	g_return_if_fail(PURPLE_IS_MESSAGE(message));
+
+	if(edited) {
+		datetime = g_date_time_new_now_utc();
+	}
+
+	purple_message_set_edited_at(message, datetime);
+	g_clear_pointer(&datetime, g_date_time_unref);
+}
+
+GDateTime *
+purple_message_get_edited_at(PurpleMessage *message) {
+	g_return_val_if_fail(PURPLE_IS_MESSAGE(message), NULL);
+
+	return message->edited_at;
+}
+
+void
+purple_message_set_edited_at(PurpleMessage *message, GDateTime *datetime) {
+	GObject *obj = NULL;
+
+	g_return_if_fail(PURPLE_IS_MESSAGE(message));
+
+	obj = G_OBJECT(message);
+
+	/* If there are any changes here, we need to manually change both edited
+	 * and edited-at because we can't call purple_message_set_edited as
+	 * it would need to call us, making an infinite loop.
+	 */
+
+	if(datetime == NULL) {
+		if(message->edited_at == NULL) {
+			return;
+		}
+
+		g_clear_pointer(&message->edited_at, g_date_time_unref);
+	} else {
+		if(message->edited_at != NULL) {
+			if(g_date_time_equal(message->edited_at, datetime)) {
+				return;
+			}
+
+			g_clear_pointer(&message->edited_at, g_date_time_unref);
+			message->edited_at = g_date_time_ref(datetime);
+		} else {
+			message->edited_at = g_date_time_ref(datetime);
+		}
+	}
+
+	g_object_freeze_notify(obj);
+	g_object_notify_by_pspec(obj, properties[PROP_EDITED]);
+	g_object_notify_by_pspec(obj, properties[PROP_EDITED_AT]);
+	g_object_thaw_notify(obj);
+}
diff --git a/libpurple/tests/test_message.c b/libpurple/tests/test_message.c
--- a/libpurple/tests/test_message.c
+++ b/libpurple/tests/test_message.c
@@ -44,6 +44,7 @@
 	GDateTime *timestamp = NULL;
 	GDateTime *timestamp1 = NULL;
 	GDateTime *delivered_at1 = NULL;
+	GDateTime *edited_at1 = NULL;
 	GError *error = NULL;
 	GError *error1 = NULL;
 	char *id = NULL;
@@ -54,6 +55,7 @@
 	char *contents = NULL;
 	gboolean action = FALSE;
 	gboolean delivered = FALSE;
+	gboolean edited = FALSE;
 
 	timestamp = g_date_time_new_from_unix_utc(911347200);
 	error = g_error_new(g_quark_from_static_string("test-message"), 0,
@@ -71,6 +73,7 @@
 		"author-alias", "alias",
 		"author-name-color", "purple",
 		"delivered", TRUE,
+		"edited", TRUE,
 		"recipient", "pidgy",
 		"contents", "Now that is a big door",
 		"content-type", PURPLE_MESSAGE_CONTENT_TYPE_MARKDOWN,
@@ -88,6 +91,8 @@
 		"author-name-color", &author_name_color,
 		"delivered", &delivered,
 		"delivered-at", &delivered_at1,
+		"edited", &edited,
+		"edited-at", &edited_at1,
 		"recipient", &recipient,
 		"contents", &contents,
 		"content-type", &content_type,
@@ -103,6 +108,8 @@
 	g_assert_cmpstr(author_name_color, ==, "purple");
 	g_assert_true(delivered);
 	g_assert_nonnull(delivered_at1);
+	g_assert_true(edited);
+	g_assert_nonnull(edited_at1);
 	g_assert_cmpstr(recipient, ==, "pidgy");
 	g_assert_cmpstr(contents, ==, "Now that is a big door");
 	g_assert_cmpint(content_type, ==, PURPLE_MESSAGE_CONTENT_TYPE_MARKDOWN);
@@ -115,6 +122,7 @@
 	g_clear_pointer(&author_alias, g_free);
 	g_clear_pointer(&author_name_color, g_free);
 	g_clear_pointer(&delivered_at1, g_date_time_unref);
+	g_clear_pointer(&edited_at1, g_date_time_unref);
 	g_clear_pointer(&recipient, g_free);
 	g_clear_pointer(&contents, g_free);
 	g_clear_pointer(&timestamp, g_date_time_unref);
@@ -200,6 +208,83 @@
 	g_clear_object(&message);
 }
 
+static void
+test_purple_message_edited_set_edited_at(void) {
+	PurpleMessage *message = NULL;
+	guint edited_counter = 0;
+	guint edited_at_counter = 0;
+
+	message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
+	g_signal_connect(message, "notify::edited",
+	                 G_CALLBACK(test_purple_message_notify_counter_cb),
+	                 &edited_counter);
+	g_signal_connect(message, "notify::edited-at",
+	                 G_CALLBACK(test_purple_message_notify_counter_cb),
+	                 &edited_at_counter);
+
+	/* The default edit state is FALSE, so setting it to true, should call
+	 * the notify signals and set edited-at.
+	 */
+	purple_message_set_edited(message, TRUE);
+	g_assert_true(purple_message_get_edited(message));
+	g_assert_nonnull(purple_message_get_edited_at(message));
+	g_assert_cmpuint(edited_counter, ==, 1);
+	g_assert_cmpuint(edited_at_counter, ==, 1);
+
+	/* Now clear everything and verify it's empty. */
+	edited_counter = 0;
+	edited_at_counter = 0;
+
+	purple_message_set_edited(message, FALSE);
+	g_assert_false(purple_message_get_edited(message));
+	g_assert_null(purple_message_get_edited_at(message));
+	g_assert_cmpuint(edited_counter, ==, 1);
+	g_assert_cmpuint(edited_at_counter, ==, 1);
+
+	g_clear_object(&message);
+}
+
+static void
+test_purple_message_edited_at_set_edited(void) {
+	PurpleMessage *message = NULL;
+	GDateTime *edited_at = NULL;
+	GDateTime *edited_at1 = NULL;
+	guint edited_counter = 0;
+	guint edited_at_counter = 0;
+
+	message = g_object_new(PURPLE_TYPE_MESSAGE, NULL);
+	g_signal_connect(message, "notify::edited",
+	                 G_CALLBACK(test_purple_message_notify_counter_cb),
+	                 &edited_counter);
+	g_signal_connect(message, "notify::edited-at",
+	                 G_CALLBACK(test_purple_message_notify_counter_cb),
+	                 &edited_at_counter);
+
+	/* The default value for edited-at is NULL, so setting it to non-null
+	 * should emit the signals and everything.
+	 */
+	edited_at = g_date_time_new_now_utc();
+	purple_message_set_edited_at(message, edited_at);
+	g_assert_true(purple_message_get_edited(message));
+	edited_at1 = purple_message_get_edited_at(message);
+	g_assert_nonnull(edited_at1);
+	g_assert_true(g_date_time_equal(edited_at1, edited_at));
+	g_assert_cmpuint(edited_counter, ==, 1);
+	g_assert_cmpuint(edited_at_counter, ==, 1);
+
+	/* Now clear everything and make sure it's all good. */
+	edited_counter = 0;
+	edited_at_counter = 0;
+	purple_message_set_edited_at(message, NULL);
+	g_assert_false(purple_message_get_edited(message));
+	g_assert_null(purple_message_get_edited_at(message));
+	g_assert_cmpuint(edited_counter, ==, 1);
+	g_assert_cmpuint(edited_at_counter, ==, 1);
+
+	g_clear_pointer(&edited_at, g_date_time_unref);
+	g_clear_object(&message);
+}
+
 /******************************************************************************
  * Main
  *****************************************************************************/
@@ -215,5 +300,10 @@
 	g_test_add_func("/message/delivered-at-sets-delivered",
 	                test_purple_message_delivered_at_set_delivered);
 
+	g_test_add_func("/message/edited-sets-edited-at",
+	                test_purple_message_edited_set_edited_at);
+	g_test_add_func("/message/edited-at-sets-edited",
+	                test_purple_message_edited_at_set_edited);
+
 	return g_test_run();
 }
