# HG changeset patch
# Node ID 412e52daac6b
# Parent  48baac6c8989
diff --git a/libpurple/protocols/demo/meson.build b/libpurple/protocols/demo/meson.build
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/meson.build
@@ -0,0 +1,24 @@
+DEMO_SOURCES = [
+	'purpledemocontacts.c',
+	'purpledemocontacts.h',
+	'purpledemoplugin.c',
+	'purpledemoplugin.h',
+	'purpledemoprotocol.c',
+	'purpledemoprotocol.h',
+]
+
+if DYNAMIC_DEMO
+	demo_resources = gnome.compile_resources('purpledemoresource',
+		'resources/purpledemo.gresource.xml',
+		source_dir : 'resources',
+		c_name : 'purple_demo')
+	DEMO_SOURCES += demo_resources
+
+	demo_prpl = shared_library('demo', DEMO_SOURCES,
+		c_args : ['-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Purple-Demo"'],
+		dependencies : [glib, json, libpurple_dep],
+		install : false,
+		install_dir : PURPLE_PLUGINDIR)
+
+	devenv.append('PURPLE_PLUGIN_PATH', meson.current_build_dir())
+endif
diff --git a/libpurple/protocols/demo/purpledemocontacts.h b/libpurple/protocols/demo/purpledemocontacts.h
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/purpledemocontacts.h
@@ -0,0 +1,32 @@
+/*
+ * 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/>.
+ */
+
+#ifndef PURPLE_DEMO_CONTACTS_H
+#define PURPLE_DEMO_CONTACTS_H
+
+#include <glib.h>
+
+#include <purple.h>
+
+G_BEGIN_DECLS
+
+G_GNUC_INTERNAL void purple_demo_contacts_load(PurpleAccount *account);
+
+G_END_DECLS
+
+#endif /* PURPLE_DEMO_CONTACTS_H */
diff --git a/libpurple/protocols/demo/purpledemocontacts.c b/libpurple/protocols/demo/purpledemocontacts.c
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/purpledemocontacts.c
@@ -0,0 +1,224 @@
+/*
+ * 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 <json-glib/json-glib.h>
+
+#include "purpledemocontacts.h"
+
+#include "purpledemoresource.h"
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_demo_protocol_load_status(PurpleAccount *account, PurpleGroup *group,
+                                 PurpleContact *contact, PurpleBuddy *buddy,
+                                 JsonObject *buddy_object)
+{
+	JsonObject *status_object = NULL;
+	const gchar *id = NULL;
+
+	if(!json_object_has_member(buddy_object, "status")) {
+		return;
+	}
+
+	status_object = json_object_get_object_member(buddy_object, "status");
+
+	if(json_object_has_member(status_object, "id")) {
+		id = json_object_get_string_member(status_object, "id");
+	}
+
+	if(id != NULL) {
+		if(json_object_has_member(status_object, "message")) {
+			const gchar *message = NULL;
+
+			message = json_object_get_string_member(status_object, "message");
+
+			purple_protocol_got_user_status(account,
+			                                purple_buddy_get_name(buddy),
+			                                id,
+			                                "message", message,
+			                                NULL);
+		} else {
+			purple_protocol_got_user_status(account,
+			                                purple_buddy_get_name(buddy),
+			                                id,
+			                                NULL);
+		}
+
+		if(json_object_has_member(status_object, "idle")) {
+			gint idle_seconds = 0;
+			time_t now;
+
+			idle_seconds = json_object_get_int_member(status_object, "idle");
+			now = time(NULL);
+
+			purple_protocol_got_user_idle(account,
+			                              purple_buddy_get_name(buddy), TRUE,
+			                              now - idle_seconds);
+		}
+	}
+}
+
+static void
+purple_demo_protocol_load_buddies(PurpleAccount *account, PurpleGroup *group,
+                                  PurpleContact *contact, GList *buddies)
+{
+	while(buddies != NULL) {
+		JsonNode *buddy_node = NULL;
+		JsonObject *buddy_object = NULL;
+		const gchar *name = NULL, *alias = NULL;
+
+		buddy_node = (JsonNode *)buddies->data;
+		buddy_object = json_node_get_object(buddy_node);
+
+		if(json_object_has_member(buddy_object, "name")) {
+			name = json_object_get_string_member(buddy_object, "name");
+		}
+
+		if(json_object_has_member(buddy_object, "alias")) {
+			alias = json_object_get_string_member(buddy_object, "alias");
+		}
+
+		if(name != NULL) {
+			PurpleBuddy *buddy = NULL;
+
+			buddy = purple_blist_find_buddy(account, name);
+			if(buddy == NULL) {
+				buddy = purple_buddy_new(account, name, alias);
+				purple_blist_add_buddy(buddy, contact, group, NULL);
+			}
+
+			purple_demo_protocol_load_status(account, group, contact, buddy,
+			                                 buddy_object);
+		}
+
+		buddies = g_list_delete_link(buddies, buddies);
+	}
+}
+
+static void
+purple_demo_protocol_load_contacts(PurpleAccount *account, PurpleGroup *group,
+                                   GList *contacts)
+{
+	while(contacts != NULL) {
+		PurpleContact *contact = NULL;
+		JsonNode *contact_node = NULL;
+		JsonObject *contact_object = NULL;
+
+		contact_node = (JsonNode *)contacts->data;
+		contact_object = json_node_get_object(contact_node);
+
+		contact = purple_contact_new();
+
+		/* Set the contact's alias if one was specified. */
+		if(json_object_has_member(contact_object, "alias")) {
+			const gchar *alias = NULL;
+
+			alias = json_object_get_string_member(contact_object, "alias");
+
+			purple_contact_set_alias(contact, alias);
+		}
+
+		/* Add the contact to the group. */
+		purple_blist_add_contact(contact, group, NULL);
+
+		/* Finally add the buddies */
+		if(json_object_has_member(contact_object, "buddies")) {
+			JsonArray *buddies_array = NULL;
+			GList *buddies = NULL;
+
+			buddies_array = json_object_get_array_member(contact_object,
+			                                             "buddies");
+			buddies = json_array_get_elements(buddies_array);
+
+			purple_demo_protocol_load_buddies(account, group, contact,
+			                                  buddies);
+		}
+
+		contacts = g_list_delete_link(contacts, contacts);
+	}
+}
+
+static void
+purple_demo_protocol_load_groups(PurpleAccount *account,
+                                 JsonObject *root_object)
+{
+	PurpleGroup *last = NULL;
+	GList *groups = NULL;
+
+	/* Get the members of the root object, this is our list of group names. */
+	groups = json_object_get_members(root_object);
+
+	while(groups != NULL) {
+		PurpleGroup *group = NULL;
+		JsonArray *group_array = NULL;
+		GList *contacts = NULL;
+		const gchar *group_name = (const gchar *)groups->data;
+
+		/* Add each group according to the json file. */
+		group = purple_group_new(group_name);
+		purple_blist_add_group(group, PURPLE_BLIST_NODE(last));
+
+		/* Now get the contacts and add them. */
+		group_array = json_object_get_array_member(root_object, group_name);
+		contacts = json_array_get_elements(group_array);
+
+		purple_demo_protocol_load_contacts(account, group, contacts);
+
+		groups = g_list_delete_link(groups, groups);
+
+		last = group;
+	}
+}
+
+/******************************************************************************
+ * Local Exports
+ *****************************************************************************/
+void
+purple_demo_contacts_load(PurpleAccount *account) {
+	GInputStream *istream = NULL;
+	GError *error = NULL;
+	JsonParser *parser = NULL;
+	JsonNode *root_node = NULL;
+	JsonObject *root_object = NULL;
+
+	/* get a stream to the contacts.json resource */
+	istream = g_resource_open_stream(purple_demo_get_resource(),
+	                                 "/im/pidgin/purple/demo/contacts.json",
+	                                 G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
+
+	/* create our parser */
+	parser = json_parser_new();
+
+	if(!json_parser_load_from_stream(parser, istream, NULL, &error)) {
+		g_critical("%s", error->message);
+	}
+
+	/* Load our data */
+	root_node = json_parser_get_root(parser);
+	root_object = json_node_get_object(root_node);
+
+	/* Load the groups! */
+	purple_demo_protocol_load_groups(account, root_object);
+
+	/* Clean up everything else... */
+	g_clear_object(&parser);
+
+	g_input_stream_close(istream, NULL, NULL);
+}
diff --git a/libpurple/protocols/demo/purpledemoplugin.h b/libpurple/protocols/demo/purpledemoplugin.h
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/purpledemoplugin.h
@@ -0,0 +1,29 @@
+/*
+ * 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/>.
+ */
+#ifndef PURPLE_DEMO_PLUGIN_H
+#define PURPLE_DEMO_PLUGIN_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#define PURPLE_DEMO_DOMAIN (g_quark_from_static_string("demo-plugin"))
+
+G_BEGIN_DECLS
+
+#endif /* PURPLE_DEMO_PLUGIN_H */
diff --git a/libpurple/protocols/demo/purpledemoplugin.c b/libpurple/protocols/demo/purpledemoplugin.c
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/purpledemoplugin.c
@@ -0,0 +1,110 @@
+/*
+ * 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/gi18n-lib.h>
+
+#include <gplugin.h>
+#include <gplugin-native.h>
+
+#include <purple.h>
+
+#include "purpledemoplugin.h"
+#include "purpledemoprotocol.h"
+
+/******************************************************************************
+ * Globals
+ *****************************************************************************/
+static PurpleProtocol *demo_protocol = NULL;
+
+/******************************************************************************
+ * GPlugin Implementation
+ *****************************************************************************/
+static GPluginPluginInfo *
+purple_demo_plugin_query(G_GNUC_UNUSED GError **error) {
+	const gchar *authors[] = {
+		"Pidgin Developers <devel@pidgin.im>",
+		NULL
+	};
+
+	return purple_plugin_info_new(
+		"id", "prpl-demo",
+		"name", "Demo Protocol Plugin",
+		"authors", authors,
+		"version", PURPLE_VERSION,
+		"category", N_("Protocol"),
+		"summary", N_("A protocol plugin used for demos."),
+		"description", N_("A protocol plugin that helps to demonstrate "
+		                  "features of libpurple and clients."),
+		"website", PURPLE_WEBSITE,
+		"abi-version", PURPLE_ABI_VERSION,
+		"flags", PURPLE_PLUGIN_INFO_FLAGS_INTERNAL |
+		         PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD,
+		NULL
+	);
+}
+
+static gboolean
+purple_demo_plugin_load(GPluginPlugin *plugin, GError **error) {
+	PurpleProtocolManager *manager = NULL;
+
+	if(PURPLE_IS_PROTOCOL(demo_protocol)) {
+		g_set_error_literal(error, PURPLE_DEMO_DOMAIN, 0,
+		                    "plugin was not cleaned up properly");
+
+		return FALSE;
+	}
+
+	purple_demo_protocol_register(GPLUGIN_NATIVE_PLUGIN(plugin));
+
+	manager = purple_protocol_manager_get_default();
+
+	demo_protocol = purple_demo_protocol_new();
+	if(!purple_protocol_manager_register(manager, demo_protocol, error)) {
+		g_clear_object(&demo_protocol);
+
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static gboolean
+purple_demo_plugin_unload(G_GNUC_UNUSED GPluginPlugin *plugin,
+                          G_GNUC_UNUSED gboolean shutdown,
+                          GError **error)
+{
+	PurpleProtocolManager *manager = purple_protocol_manager_get_default();
+
+	if(!PURPLE_IS_PROTOCOL(demo_protocol)) {
+		g_set_error_literal(error, PURPLE_DEMO_DOMAIN, 0,
+		                    "plugin was not setup properly");
+
+		return FALSE;
+	}
+
+	if(!purple_protocol_manager_unregister(manager, demo_protocol, error)) {
+		return FALSE;
+	}
+
+	g_clear_object(&demo_protocol);
+
+	return TRUE;
+}
+
+GPLUGIN_NATIVE_PLUGIN_DECLARE(purple_demo_plugin)
diff --git a/libpurple/protocols/demo/purpledemoprotocol.h b/libpurple/protocols/demo/purpledemoprotocol.h
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/purpledemoprotocol.h
@@ -0,0 +1,41 @@
+/*
+ * 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/>.
+ */
+
+#ifndef PURPLE_DEMO_PROTOCOL_H
+#define PURPLE_DEMO_PROTOCOL_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gplugin-native.h>
+
+#include <purple.h>
+
+G_BEGIN_DECLS
+
+#define PURPLE_DEMO_TYPE_PROTOCOL (purple_demo_protocol_get_type())
+G_DECLARE_FINAL_TYPE(PurpleDemoProtocol, purple_demo_protocol, PURPLE_DEMO,
+                     PROTOCOL, PurpleProtocol)
+
+G_GNUC_INTERNAL void purple_demo_protocol_register(GPluginNativePlugin *plugin);
+
+G_GNUC_INTERNAL PurpleProtocol *purple_demo_protocol_new(void);
+
+G_END_DECLS
+
+#endif /* PURPLE_DEMO_PROTOCOL_H */
diff --git a/libpurple/protocols/demo/purpledemoprotocol.c b/libpurple/protocols/demo/purpledemoprotocol.c
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/purpledemoprotocol.c
@@ -0,0 +1,148 @@
+/*
+ * 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 <time.h>
+
+#include <glib/gi18n-lib.h>
+
+#include "purpledemoprotocol.h"
+
+#include "purpledemocontacts.h"
+
+struct _PurpleDemoProtocol {
+	PurpleProtocol parent;
+};
+
+/******************************************************************************
+ * PurpleProtocolClient Implementation
+ *****************************************************************************/
+static gchar *
+purple_demo_protocol_status_text(PurpleProtocolClient *client,
+                                 PurpleBuddy *buddy)
+{
+	PurplePresence *presence = NULL;
+	PurpleStatus *status = NULL;
+	const gchar *message = NULL;
+	gchar *ret = NULL;
+
+	presence = purple_buddy_get_presence(buddy);
+	status = purple_presence_get_active_status(presence);
+
+	message = purple_status_get_attr_string(status, "message");
+	if(message != NULL) {
+		ret = g_markup_escape_text(message, -1);
+		purple_util_chrreplace(ret, '\n', ' ');
+	}
+
+	return ret;
+}
+
+static void
+purple_demo_protocol_client_init(PurpleProtocolClientInterface *iface) {
+	iface->status_text = purple_demo_protocol_status_text;
+}
+
+/******************************************************************************
+ * PurpleProtocol Implementation
+ *****************************************************************************/
+static void
+purple_demo_protocol_login(PurpleAccount *account) {
+	PurpleConnection *connection = NULL;
+
+	connection = purple_account_get_connection(account);
+	purple_connection_set_state(connection, PURPLE_CONNECTION_CONNECTED);
+
+	purple_demo_contacts_load(account);
+}
+
+static GList *
+purple_demo_protocol_status_types(PurpleAccount *account) {
+	PurpleStatusType *type = NULL;
+	GList *status_types = NULL;
+
+	type = purple_status_type_new_with_attrs(
+		PURPLE_STATUS_AVAILABLE, "available", NULL,
+		TRUE, TRUE, FALSE,
+		"message", _("Message"), purple_value_new(G_TYPE_STRING),
+		NULL);
+	status_types = g_list_append(status_types, type);
+
+	type = purple_status_type_new_with_attrs(
+		PURPLE_STATUS_AWAY, "away", NULL,
+		TRUE, TRUE, FALSE,
+		"message", _("Message"), purple_value_new(G_TYPE_STRING),
+		NULL);
+	status_types = g_list_append(status_types, type);
+
+	type = purple_status_type_new_full(
+		PURPLE_STATUS_OFFLINE, NULL, NULL,
+		TRUE, TRUE, FALSE);
+	status_types = g_list_append(status_types, type);
+
+	return status_types;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_DYNAMIC_TYPE_EXTENDED(
+	PurpleDemoProtocol,
+	purple_demo_protocol,
+	PURPLE_TYPE_PROTOCOL,
+	0,
+	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CLIENT,
+	                              purple_demo_protocol_client_init)
+)
+
+static void
+purple_demo_protocol_init(PurpleDemoProtocol *protocol) {
+}
+
+static void
+purple_demo_protocol_class_finalize(PurpleDemoProtocolClass *klass) {
+}
+
+static void
+purple_demo_protocol_class_init(PurpleDemoProtocolClass *klass) {
+	PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass);
+
+	protocol_class->login = purple_demo_protocol_login;
+	protocol_class->status_types = purple_demo_protocol_status_types;
+}
+
+/******************************************************************************
+ * Local Exports
+ *****************************************************************************/
+void
+purple_demo_protocol_register(GPluginNativePlugin *plugin) {
+	purple_demo_protocol_register_type(G_TYPE_MODULE(plugin));
+}
+
+PurpleProtocol *
+purple_demo_protocol_new(void) {
+	return g_object_new(
+		PURPLE_DEMO_TYPE_PROTOCOL,
+		"id", "prpl-demo",
+		"name", "Demo",
+		"description", "A protocol plugin with static data to be used in "
+		               "screen shots.",
+		"icon-name", "im-purple-demo",
+		"icon-resource-path", "/im/pidgin/purple/demo/icons",
+		"options", OPT_PROTO_NO_PASSWORD,
+		NULL);
+}
diff --git a/libpurple/protocols/demo/resources/contacts.json b/libpurple/protocols/demo/resources/contacts.json
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/resources/contacts.json
@@ -0,0 +1,47 @@
+{
+	"Family": [{
+		"alias": "Grim",
+		"buddies": [{
+			"name": "Gary",
+			"status": {
+				"id": "available",
+				"message": "💤 Sleeping... 😴",
+				"idle": 1337
+			}
+		}]
+	}],
+	"Friends": [{
+		"buddies": [{
+			"name": "John",
+			"alias": "Rekkanoryo",
+			"status": {
+				"id": "away"
+			}
+		}]
+	}],
+	"Work": [{
+		"buddies": [{
+			"name": "Elliot",
+			"status": {
+				"id": "away",
+				"message": "Coding..."
+			}
+		}]
+	}, {
+		"buddies": [{
+			"name": "Richard",
+			"status": {
+				"id": "available"
+			}
+		}]
+	}],
+	"School": [{
+		"buddies": [{
+			"name": "Eion",
+			"status": {
+				"id": "available",
+				"message": "Writing crazy patches!"
+			}
+		}]
+	}]
+}
diff --git a/libpurple/protocols/demo/resources/icons/16x16/apps/im-purple-demo.png b/libpurple/protocols/demo/resources/icons/16x16/apps/im-purple-demo.png
new file mode 100644
index 0000000000000000000000000000000000000000..3839033cd72dc2ac336b6f48770b250c62052c33
GIT binary patch
diff --git a/libpurple/protocols/demo/resources/icons/22x22/apps/im-purple-demo.png b/libpurple/protocols/demo/resources/icons/22x22/apps/im-purple-demo.png
new file mode 100644
index 0000000000000000000000000000000000000000..3cbd650a7ac430498864a325f5eacc4830649ebe
GIT binary patch
diff --git a/libpurple/protocols/demo/resources/icons/48x48/apps/im-purple-demo.png b/libpurple/protocols/demo/resources/icons/48x48/apps/im-purple-demo.png
new file mode 100644
index 0000000000000000000000000000000000000000..a659038fe5e49ba6a2f34b93913a94c59a1cb955
GIT binary patch
diff --git a/libpurple/protocols/demo/resources/icons/scalable/apps/im-purple-demo.svg b/libpurple/protocols/demo/resources/icons/scalable/apps/im-purple-demo.svg
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/resources/icons/scalable/apps/im-purple-demo.svg
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="37.851284mm"
+   height="39.057049mm"
+   viewBox="0 0 37.851284 39.057049"
+   version="1.1"
+   id="svg31537"
+   inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
+   sodipodi:docname="im-purple-demo.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <defs
+     id="defs31531" />
+  <sodipodi:namedview
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="2.8"
+     inkscape:cx="25.178571"
+     inkscape:cy="33.571429"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     showgrid="false"
+     fit-margin-top="0"
+     fit-margin-left="0"
+     fit-margin-right="0"
+     fit-margin-bottom="0"
+     inkscape:window-width="1916"
+     inkscape:window-height="1017"
+     inkscape:window-x="1080"
+     inkscape:window-y="493"
+     inkscape:window-maximized="1"
+     inkscape:pagecheckerboard="0" />
+  <metadata
+     id="metadata31534">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1"
+     transform="translate(75.835419,-56.26127)">
+    <g
+       id="g1812">
+      <path
+         style="isolation:isolate;fill:#4b2854;fill-opacity:1;stroke:#fdfdfd;stroke-width:11.7132;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
+         inkscape:connector-curvature="0"
+         id="path2938"
+         d="m 137.45776,361.78346 q 9.2531,10.42501 11.91562,19.37813 a 19.687501,19.687501 0 0 0 -9.08437,2.24061 17.090626,17.090626 0 0 0 -6.84376,6.38437 19.312501,19.312501 0 0 0 -2.74688,9.76876 c -0.15944,5.625 1.64063,10.15313 5.44688,13.54687 a 20.831251,20.831251 0 0 0 13.70626,5.33438 19.687501,19.687501 0 0 0 11.61563,-3.02811 20.690626,20.690626 0 0 0 7.36874,-8.22189 23.690626,23.690626 0 0 0 2.65312,-10.14374 39.196877,39.196877 0 0 0 -6.79688,-22.98749 87.590631,87.590631 0 0 0 -18.20626,-19.98751 z"
+         class="cls-9"
+         transform="matrix(0.37919911,0,0,0.38886781,-123.17216,-79.14694)" />
+      <path
+         style="isolation:isolate;fill:#5c3566;stroke:#fdfdfd;stroke-width:11.7132;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
+         inkscape:connector-curvature="0"
+         id="path2940"
+         d="m 184.75465,435.07723 q 9.25309,-10.42501 11.90622,-19.37814 a 19.687501,19.687501 0 0 1 -9.08437,-2.24064 17.062501,17.062501 0 0 1 -6.83437,-6.38437 19.312501,19.312501 0 0 1 -2.75626,-9.75936 q -0.23457,-8.4375 5.45626,-13.55627 a 20.812501,20.812501 0 0 1 13.70626,-5.33438 19.687501,19.687501 0 0 1 11.6156,3.0375 20.709376,20.709376 0 0 1 7.36877,8.20315 23.765626,23.765626 0 0 1 2.65312,10.15313 39.281254,39.281254 0 0 1 -6.79688,22.98749 87.590631,87.590631 0 0 1 -18.20626,19.98751 z"
+         class="cls-9"
+         transform="matrix(0.37919911,0,0,0.38886781,-123.17216,-79.14694)" />
+    </g>
+  </g>
+</svg>
diff --git a/libpurple/protocols/demo/resources/purpledemo.gresource.xml b/libpurple/protocols/demo/resources/purpledemo.gresource.xml
new file mode 100644
--- /dev/null
+++ b/libpurple/protocols/demo/resources/purpledemo.gresource.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<gresources>
+  <gresource prefix="/im/pidgin/purple/demo">
+    <file compressed="true">contacts.json</file>
+    <file>icons/16x16/apps/im-purple-demo.png</file>
+    <file>icons/22x22/apps/im-purple-demo.png</file>
+    <file>icons/48x48/apps/im-purple-demo.png</file>
+    <file>icons/scalable/apps/im-purple-demo.svg</file>
+  </gresource>
+</gresources>
diff --git a/libpurple/protocols/meson.build b/libpurple/protocols/meson.build
--- a/libpurple/protocols/meson.build
+++ b/libpurple/protocols/meson.build
@@ -1,4 +1,5 @@
 subdir('bonjour')
+subdir('demo')
 subdir('facebook')
 subdir('gg')
 subdir('irc')
diff --git a/meson.build b/meson.build
--- a/meson.build
+++ b/meson.build
@@ -408,8 +408,8 @@
 endif
 
 
-DEFAULT_PRPLS = ['bonjour', 'facebook', 'gg', 'irc', 'jabber', 'novell',
-                 'null', 'sametime', 'silc', 'zephyr']
+DEFAULT_PRPLS = ['bonjour', 'demo', 'facebook', 'gg', 'irc', 'jabber',
+                 'novell', 'null', 'sametime', 'silc', 'zephyr']
 ALL_PRPLS = DEFAULT_PRPLS + ['null']
 
 dynamic_list = get_option('dynamic-prpls').split(',')
@@ -436,6 +436,7 @@
 endforeach
 
 DYNAMIC_BONJOUR = DYNAMIC_PRPLS.contains('bonjour')
+DYNAMIC_DEMO = DYNAMIC_PRPLS.contains('demo')
 DYNAMIC_FACEBOOK = DYNAMIC_PRPLS.contains('facebook')
 DYNAMIC_GG  = DYNAMIC_PRPLS.contains('gg')
 DYNAMIC_IRC = DYNAMIC_PRPLS.contains('irc')
diff --git a/po/POTFILES.in b/po/POTFILES.in
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -69,6 +69,7 @@
 libpurple/plugins/statenotify.c
 libpurple/plugins/wincred/wincred.c
 libpurple/prefs.c
+libpurple/protocols.c
 libpurple/protocols/bonjour/bonjour.c
 libpurple/protocols/bonjour/bonjour_ft.c
 libpurple/protocols/bonjour/buddy.c
@@ -79,7 +80,9 @@
 libpurple/protocols/bonjour/mdns_win32.c
 libpurple/protocols/bonjour/parser.c
 libpurple/protocols/bonjour/xmpp.c
-libpurple/protocols.c
+libpurple/protocols/demo/purpledemocontacts.c
+libpurple/protocols/demo/purpledemoplugin.c
+libpurple/protocols/demo/purpledemoprotocol.c
 libpurple/protocols/facebook/api.c
 libpurple/protocols/facebook/data.c
 libpurple/protocols/facebook/facebook.c
