Initial Commit

Fri, 08 Aug 2025 09:46:55 +0800

author
William Goodspeed <goodspeed@mailo.cat>
date
Fri, 08 Aug 2025 09:46:55 +0800
changeset 0
cc7c1f9d20f7
child 1
98bcf06036b8

Initial Commit

.hgignore file | annotate | diff | comparison | revisions
meson.build file | annotate | diff | comparison | revisions
purplesatori.c file | annotate | diff | comparison | revisions
purplesatoriconnection.c file | annotate | diff | comparison | revisions
purplesatoriconnection.h file | annotate | diff | comparison | revisions
purplesatoriplugin.h file | annotate | diff | comparison | revisions
purplesatoriprotocol.c file | annotate | diff | comparison | revisions
purplesatoriprotocol.h file | annotate | diff | comparison | revisions
purplesatoriprotocolcontacts.c file | annotate | diff | comparison | revisions
purplesatoriprotocolcontacts.h file | annotate | diff | comparison | revisions
purplesatoriprotocolconversation.c file | annotate | diff | comparison | revisions
purplesatoriprotocolconversation.h file | annotate | diff | comparison | revisions
satoriapi.c file | annotate | diff | comparison | revisions
satoriapi.h file | annotate | diff | comparison | revisions
satorimessage.h file | annotate | diff | comparison | revisions
satoritypes.h file | annotate | diff | comparison | revisions
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,42 @@
+syntax: glob
+
+# Meson build directories
+build*/
+meson-private/
+meson-logs/
+compile_commands.json
+install_manifest.txt
+
+# Object and binary files
+*.o
+*.a
+*.so
+*.so.*
+*.dylib
+*.dll
+*.exe
+
+# Debug symbols
+*.pdb
+*.dSYM/
+*.stackdump
+
+# Generated source files
+*.generated.c
+*.generated.h
+
+# Backup / temporary files
+*~
+*.swp
+*.tmp
+*.bak
+
+# Editor-specific
+.vscode/
+.idea/
+
+# OS junk
+.DS_Store
+Thumbs.db
+
+.cache/
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/meson.build	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,26 @@
+project('purplesatori', 'c')
+
+libpurple_dep = dependency('purple-3')
+libsoup_dep = dependency('libsoup-3.0')
+libjson_dep = dependency('json-glib-1.0')
+
+PURPLE_PLUGINDIR = libpurple_dep.get_pkgconfig_variable('plugindir')
+PURPLE_SATORI_SRCS = [
+  'purplesatori.c',
+  'purplesatoriconnection.c',
+  'purplesatoriprotocol.c',
+  'purplesatoriprotocolcontacts.c',
+  'purplesatoriprotocolconversation.c',
+  'satoriapi.c',
+]
+
+library('libpurplesatori', PURPLE_SATORI_SRCS,
+    c_args : [
+        '-DG_LOG_USE_STRUCTURED',
+        '-DG_LOG_DOMAIN="Purple-Satori"',
+        '-DGETTEXT_PACKAGE="pidgin3"'
+    ],
+    dependencies : [libpurple_dep, libsoup_dep, libjson_dep],
+    name_prefix : '',
+    install : true,
+    install_dir : PURPLE_PLUGINDIR)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatori.c	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,106 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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 "purplesatoriconnection.h"
+#include "purplesatoriplugin.h"
+#include "purplesatoriprotocol.h"
+
+/******************************************************************************
+ * Globals
+ *****************************************************************************/
+static PurpleProtocol *satori_protocol = NULL;
+
+/******************************************************************************
+ * GPlugin Implementation
+ *****************************************************************************/
+static GPluginPluginInfo *
+purple_satori_plugin_query(G_GNUC_UNUSED GError **error) {
+	const gchar *authors[] = {
+		"Gong Zhile <gongzl@stu.hebust.edu.cn>",
+		NULL
+	};
+
+	return purple_plugin_info_new(
+		"id", "prpl-satori",
+		"name", "Satori Protocol Plugin",
+		"authors", authors,
+		"version", "0.0.1",
+		"category", N_("Protocol"),
+		"summary", N_("Satori protocol plugin for purple3"),
+		"description", N_("Satori protocol plugin for purple3"),
+		"website", "https://example.org/",
+		"abi-version", PURPLE_ABI_VERSION,
+		NULL
+        );		
+}
+
+static gboolean
+purple_satori_plugin_load(GPluginPlugin *plugin, GError **error) {
+	PurpleProtocolManager *manager = NULL;
+
+	if(PURPLE_IS_PROTOCOL(satori_protocol)) {
+		g_set_error_literal(error, PURPLE_SATORI_DOMAIN, 0,
+		                    "plugin was not cleaned up properly");
+
+		return FALSE;
+	}
+
+	purple_satori_connection_register(GPLUGIN_NATIVE_PLUGIN(plugin));
+	purple_satori_protocol_register(GPLUGIN_NATIVE_PLUGIN(plugin));
+
+	manager = purple_protocol_manager_get_default();
+
+	satori_protocol = purple_satori_protocol_new();
+	if (!purple_protocol_manager_add(manager, satori_protocol, error)) {
+		g_clear_object(&satori_protocol);
+		return FALSE;
+	}
+
+	return TRUE;
+}
+
+static gboolean
+purple_satori_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(satori_protocol)) {
+		g_set_error_literal(error, PURPLE_SATORI_DOMAIN, 0,
+				    "plugin was not setup properly");
+		return FALSE;
+	}
+
+	if (!purple_protocol_manager_remove(manager, satori_protocol, error)) {
+		return FALSE;
+	}
+
+	g_clear_object(&satori_protocol);
+	return TRUE;
+}
+
+GPLUGIN_NATIVE_PLUGIN_DECLARE(purple_satori_plugin)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatoriconnection.c	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,229 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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-object.h>
+#include <glib/gi18n-lib.h>
+#include <json-glib/json-glib.h>
+
+#include <gio/gio.h>
+#include <libsoup/soup.h>
+#include <libsoup/soup-session.h>
+#include <libsoup/soup-message.h>
+#include <libsoup/soup-types.h>
+#include <libsoup/soup-websocket-connection.h>
+
+#include "purplesatoriconnection.h"
+#include "purplesatoriprotocolcontacts.h"
+#include "satorimessage.h"
+#include "satoritypes.h"
+#include "satoriapi.h"
+
+struct _PurpleSatoriConnection {
+	PurpleConnection	 parent;
+	SoupSession		*session;
+
+	SoupWebsocketConnection *wscon;
+	gboolean		 wsidented;
+};
+
+G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleSatoriConnection, purple_satori_connection,
+                               PURPLE_TYPE_CONNECTION, G_TYPE_FLAG_FINAL, {});
+
+/******************************************************************************
+ * PurpleConnection WS Callbacks
+ *****************************************************************************/
+
+static void
+satori_ws_on_closed(SoupWebsocketConnection *wscon, PurpleSatoriConnection *data)
+{
+	PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION(data);
+	PurpleAccount *acc = purple_connection_get_account(PURPLE_CONNECTION(con));
+	purple_account_disconnect(acc);
+
+	con->wscon = NULL;
+}
+
+static void
+satori_ws_on_message(SoupWebsocketConnection *wscon, gint type,
+		     GBytes *message, PurpleSatoriConnection *con)
+{
+	PurpleAccount *acc = purple_connection_get_account(
+		PURPLE_CONNECTION(con));
+
+	if (type != SOUP_WEBSOCKET_DATA_TEXT) {
+		purple_debug_warning("satori", "unexpected data recv from ws");
+		return;
+	}
+
+	gsize sz;
+        const gchar *ptr = g_bytes_get_data(message, &sz);
+
+        /* g_print("Received text data: %s\n", ptr); */
+
+	JsonParser *parser = json_parser_new();
+	if (!json_parser_load_from_data(parser, ptr, sz, NULL)) {
+		purple_debug_warning("satori", "bad json received from ws");
+		g_object_unref(parser);
+		return;
+	}
+
+	JsonObject *root = json_node_get_object(json_parser_get_root(parser));
+	JsonObject *body = json_object_get_object_member(root, "body");
+	SatoriWebsocketOpcode op = json_object_get_int_member(root, "op");
+
+	switch (op) {
+
+	case SATORI_WEBSOCKET_OP_READY:
+	{
+		purple_account_connected(acc);
+
+		JsonArray *logins = json_object_get_array_member(body, "logins");
+		JsonObject *user_obj = json_object_get_object_member(
+			json_array_get_object_element(logins, 0), "user");
+		SatoriUser user = { 0 };
+
+		if (!user_obj) break;
+		satori_user_from_json(user_obj, &user);
+
+		PurpleContactInfo *ci = purple_account_get_contact_info(acc);
+		purple_contact_info_set_id(ci, user.id);
+		purple_contact_info_set_display_name(ci, user.nick
+						     ? user.nick : user.name);
+
+		satori_refresh_buddy_contacts(con, NULL);
+		break;
+	}
+
+	default:		/* ignored */
+		break;
+
+	}
+
+	g_print("op = %d\n", op);
+
+	g_object_unref(parser);
+}
+
+static void
+satori_ws_on_connection(SoupSession *session, GAsyncResult *res, gpointer data)
+{
+	PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION(data);
+	PurpleAccount *acc = purple_connection_get_account(PURPLE_CONNECTION(con));
+	GError *err = NULL;
+
+	con->wscon = soup_session_websocket_connect_finish(session, res, &err);
+	if (err) {
+		purple_account_disconnect_with_error(acc, err);
+		return;
+	}
+
+	g_signal_connect(con->wscon, "message",
+			 G_CALLBACK(satori_ws_on_message), con);
+	g_signal_connect(con->wscon, "closed",
+			 G_CALLBACK(satori_ws_on_closed), con);
+
+	GBytes *frame = satori_message_gen_ident(NULL, 0);
+	soup_websocket_connection_send_text(con->wscon,
+					    g_bytes_get_data(frame, NULL));
+	g_bytes_unref(frame);
+}
+
+/******************************************************************************
+ * PurpleConnection Implementation
+ *****************************************************************************/
+static gboolean
+purple_satori_connection_connect(PurpleConnection *connection,
+				 G_GNUC_UNUSED GError **error)
+{
+	PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION(connection);
+
+	if (con->wscon)
+		g_object_unref(con->wscon);
+
+	SoupMessage *svmsg = satori_message_new("GET", PURPLE_SATORI_WSURL);
+	soup_session_websocket_connect_async(
+		con->session, svmsg,
+		NULL, NULL, 0, NULL,
+		(GAsyncReadyCallback) satori_ws_on_connection,
+		con);
+
+	/* purple_account_connected(account); */
+	/* purple_satori_contacts_load(account); */
+
+	return TRUE;
+}
+
+static gboolean
+purple_satori_connection_disconnect(PurpleConnection *connection,
+				    G_GNUC_UNUSED GError **error)
+{
+	PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION(connection);
+	if (!con->wscon) return TRUE;
+
+	g_signal_handlers_disconnect_by_data(con->wscon, con);
+	soup_websocket_connection_close(con->wscon,
+					SOUP_WEBSOCKET_CLOSE_NO_STATUS, NULL);
+	con->wscon = NULL;
+	return TRUE;
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+static void
+purple_satori_connection_init(PurpleSatoriConnection *connection) {
+	connection->session = soup_session_new();
+	connection->wscon   = NULL;
+}
+
+static void
+purple_satori_connection_class_finalize(G_GNUC_UNUSED PurpleSatoriConnectionClass *klass) {
+}
+
+static void
+purple_satori_connection_class_init(PurpleSatoriConnectionClass *klass) {
+	PurpleConnectionClass *connection_class = PURPLE_CONNECTION_CLASS(klass);
+
+	connection_class->connect = purple_satori_connection_connect;
+	connection_class->disconnect = purple_satori_connection_disconnect;
+}
+
+/******************************************************************************
+ * Internal API
+ *****************************************************************************/
+void
+purple_satori_connection_register(GPluginNativePlugin *plugin) {
+	purple_satori_connection_register_type(G_TYPE_MODULE(plugin));
+}
+
+/******************************************************************************
+ * Public API Implementation
+ *****************************************************************************/
+
+void
+purple_satori_connection_send_and_read_async(PurpleSatoriConnection	*con,
+					     SoupMessage		*msg,
+					     int			 io_priority,
+					     GCancellable		*cancellable,
+					     GAsyncReadyCallback	 callback,
+					     gpointer			 user_data)
+{
+	soup_session_send_and_read_async(con->session, msg, io_priority,
+					 cancellable, callback, user_data);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatoriconnection.h	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,50 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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_SATORI_CONNECTION_H
+#define PURPLE_SATORI_CONNECTION_H
+
+#include <glib.h>
+#include <purple.h>
+#include <libsoup/soup.h>
+
+G_BEGIN_DECLS
+
+#define PURPLE_SATORI_HOST	"192.168.1.154:5600"
+#define PURPLE_SATORI_WSURL	"ws://" PURPLE_SATORI_HOST "/v1/events"
+
+#define PURPLE_SATORI_PLATFORM	"QQ"
+#define PURPLE_SATORI_USER_ID	"3751531667"
+
+#define PURPLE_SATORI_TYPE_CONNECTION (purple_satori_connection_get_type())
+G_DECLARE_FINAL_TYPE(PurpleSatoriConnection, purple_satori_connection, PURPLE_SATORI,
+                     CONNECTION, PurpleConnection)
+
+G_GNUC_INTERNAL void purple_satori_connection_register(GPluginNativePlugin *plugin);
+
+void
+purple_satori_connection_send_and_read_async(PurpleSatoriConnection	*con,
+					     SoupMessage		*msg,
+					     int			 io_priority,
+					     GCancellable		*cancellable,
+					     GAsyncReadyCallback	 callback,
+					     gpointer			 user_data);
+
+G_END_DECLS
+
+#endif /* PURPLE_SATORI_CONNECTION_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatoriplugin.h	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,30 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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_SATORI_PLUGIN_H
+#define PURPLE_SATORI_PLUGIN_H
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+#define PURPLE_SATORI_DOMAIN (g_quark_from_static_string("satori-plugin"))
+
+G_END_DECLS
+
+#endif	/* PURPLE_SATORI_PLUGIN_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatoriprotocol.c	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,98 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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/gi18n-lib.h>
+
+#include "purplesatoriprotocol.h"
+
+#include "purplesatoriconnection.h"
+#include "purplesatoriprotocolcontacts.h"
+#include "purplesatoriprotocolconversation.h"
+
+struct _PurpleSatoriProtocol {
+	PurpleProtocol parent;
+};
+
+/******************************************************************************
+ * PurpleProtocol Implementation
+ *****************************************************************************/
+static PurpleConnection *
+purple_satori_protocol_create_connection(PurpleProtocol *protocol,
+                                       PurpleAccount *account,
+                                       const char *password,
+                                       G_GNUC_UNUSED GError **error)
+{
+	g_return_val_if_fail(PURPLE_IS_PROTOCOL(protocol), NULL);
+	g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
+
+	return g_object_new(PURPLE_SATORI_TYPE_CONNECTION,
+			    "protocol", protocol,
+			    "account", account,
+			    "password", password,
+			    NULL);
+
+}
+
+/******************************************************************************
+ * GObject Implementation
+ *****************************************************************************/
+G_DEFINE_DYNAMIC_TYPE_EXTENDED(
+	PurpleSatoriProtocol,
+	purple_satori_protocol,
+	PURPLE_TYPE_PROTOCOL,
+	G_TYPE_FLAG_FINAL,
+	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONTACTS,
+	                              purple_satori_protocol_contacts_init)
+	G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONVERSATION,
+	                              purple_satori_protocol_conversation_init))
+
+static void
+purple_satori_protocol_init(G_GNUC_UNUSED PurpleSatoriProtocol *protocol) {
+}
+
+static void
+purple_satori_protocol_class_finalize(G_GNUC_UNUSED PurpleSatoriProtocolClass *klass) {
+}
+
+static void
+purple_satori_protocol_class_init(PurpleSatoriProtocolClass *klass) {
+	PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass);
+
+	protocol_class->create_connection = purple_satori_protocol_create_connection;
+}
+
+/******************************************************************************
+ * Local Exports
+ *****************************************************************************/
+void
+purple_satori_protocol_register(GPluginNativePlugin *plugin) {
+	purple_satori_protocol_register_type(G_TYPE_MODULE(plugin));
+}
+
+PurpleProtocol *
+purple_satori_protocol_new(void) {
+	return g_object_new(
+		PURPLE_SATORI_TYPE_PROTOCOL,
+		"id", "prpl-satori",
+		"name", "Satori",
+		"description", "Satori Protocol Plugin for Purple 3",
+		"icon-name", "im-purple-satori",
+		"icon-resource-path", "/im/pidgin/libpurple/protocols/satori/icons",
+		"options", OPT_PROTO_NO_PASSWORD,
+		NULL);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatoriprotocol.h	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,41 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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_SATORI_PROTOCOL_H
+#define PURPLE_SATORI_PROTOCOL_H
+
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gplugin-native.h>
+
+#include <purple.h>
+
+G_BEGIN_DECLS
+
+#define PURPLE_SATORI_TYPE_PROTOCOL (purple_satori_protocol_get_type())
+G_DECLARE_FINAL_TYPE(PurpleSatoriProtocol, purple_satori_protocol, PURPLE_SATORI,
+                     PROTOCOL, PurpleProtocol)
+
+G_GNUC_INTERNAL void purple_satori_protocol_register(GPluginNativePlugin *plugin);
+
+G_GNUC_INTERNAL PurpleProtocol *purple_satori_protocol_new(void);
+
+G_END_DECLS
+
+#endif /* PURPLE_SATORI_PROTOCOL_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatoriprotocolcontacts.c	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,57 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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/gi18n-lib.h>
+
+#include "purplesatoriprotocolcontacts.h"
+
+/******************************************************************************
+ * PurpleProtocolContacts Implementation
+ *****************************************************************************/
+static char *
+purple_satori_protocol_contacts_get_profile_finish(G_GNUC_UNUSED PurpleProtocolContacts *contacts,
+                                                 GAsyncResult *result,
+                                                 GError **error)
+{
+	g_return_val_if_fail(G_IS_TASK(result), NULL);
+
+	return g_task_propagate_pointer(G_TASK(result), error);
+}
+
+static void
+purple_satori_protocol_contacts_get_profile_async(PurpleProtocolContacts *contacts,
+                                                PurpleContactInfo *info,
+                                                GCancellable *cancellable,
+                                                GAsyncReadyCallback callback,
+                                                gpointer data)
+{
+	GTask *task = NULL;
+	const char *profile = NULL;
+
+	task = g_task_new(contacts, cancellable, callback, data);
+
+	profile = g_object_get_data(G_OBJECT(info), "satori-profile");
+	g_task_return_pointer(task, g_strdup(profile), g_free);
+	g_clear_object(&task);
+}
+
+void
+purple_satori_protocol_contacts_init(PurpleProtocolContactsInterface *iface) {
+	iface->get_profile_async = purple_satori_protocol_contacts_get_profile_async;
+	iface->get_profile_finish = purple_satori_protocol_contacts_get_profile_finish;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatoriprotocolcontacts.h	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,28 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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_SATORI_PROTOCOL_CONTACTS_H
+#define PURPLE_SATORI_PROTOCOL_CONTACTS_H
+
+#include <glib.h>
+
+#include <purple.h>
+
+G_GNUC_INTERNAL void purple_satori_protocol_contacts_init(PurpleProtocolContactsInterface *iface);
+
+#endif /* PURPLE_SATORI_PROTOCOL_CONTACTS_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatoriprotocolconversation.c	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,373 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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/gi18n-lib.h>
+
+#include "purplesatoriprotocolconversation.h"
+
+#include "purplesatoriplugin.h"
+#include "purplesatoriprotocol.h"
+
+typedef struct {
+	PurpleConversation *conversation;
+	PurpleMessage *message;
+} PurpleSatoriProtocolIMInfo;
+
+/******************************************************************************
+ * Helpers
+ *****************************************************************************/
+static void
+purple_satori_protocol_im_info_free(PurpleSatoriProtocolIMInfo *info) {
+	g_clear_object(&info->conversation);
+	g_clear_object(&info->message);
+	g_free(info);
+}
+
+static gint
+purple_satori_protocol_contact_sort(gconstpointer a, gconstpointer b,
+                                  G_GNUC_UNUSED gpointer data)
+{
+	return purple_contact_info_compare(PURPLE_CONTACT_INFO((gpointer)a),
+	                                   PURPLE_CONTACT_INFO((gpointer)b));
+}
+
+static char *
+purple_satori_protocol_generate_conversation_id(PurpleAccount *account,
+                                              PurpleCreateConversationDetails *details)
+{
+	GChecksum *checksum = NULL;
+	GListModel *participants = NULL;
+	GListStore *sorted = NULL;
+	char *ret = NULL;
+	const char *id = NULL;
+
+	/* Sort the participants. */
+	sorted = g_list_store_new(PURPLE_TYPE_CONTACT);
+	participants = purple_create_conversation_details_get_participants(details);
+	for(guint i = 0; i < g_list_model_get_n_items(participants); i++) {
+		PurpleContactInfo *info = NULL;
+
+		info = g_list_model_get_item(participants, i);
+		g_list_store_insert_sorted(sorted, info,
+		                           purple_satori_protocol_contact_sort,
+		                           NULL);
+		g_clear_object(&info);
+	}
+
+	/* Build a checksum of the account and the sorted participants. */
+	checksum = g_checksum_new(G_CHECKSUM_SHA256);
+
+	id = purple_account_get_id(account);
+	g_checksum_update(checksum, (guchar *)id, -1);
+
+	for(guint i = 0; i < g_list_model_get_n_items(G_LIST_MODEL(sorted)); i++) {
+		PurpleContactInfo *info = NULL;
+
+		info = g_list_model_get_item(G_LIST_MODEL(sorted), i);
+		id = purple_contact_info_get_id(info);
+		g_checksum_update(checksum, (guchar *)id, -1);
+		g_clear_object(&info);
+	}
+
+	ret = g_strdup(g_checksum_get_string(checksum));
+
+	g_clear_pointer(&checksum, g_checksum_free);
+	g_clear_object(&sorted);
+
+	return ret;
+}
+
+/******************************************************************************
+ * Callbacks
+ *****************************************************************************/
+static gboolean
+purple_satori_protocol_echo_im_cb(gpointer data) {
+	PurpleSatoriProtocolIMInfo *info = data;
+
+	purple_conversation_write_message(info->conversation, info->message);
+
+	return G_SOURCE_REMOVE;
+}
+
+/******************************************************************************
+ * PurpleProtocolConversation Implementation
+ *****************************************************************************/
+static PurpleCreateConversationDetails *
+purple_satori_protocol_get_create_conversation_details(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
+                                                     G_GNUC_UNUSED PurpleAccount *account)
+{
+	return purple_create_conversation_details_new(9);
+}
+
+static void
+purple_satori_protocol_create_conversation_async(PurpleProtocolConversation *protocol,
+                                               PurpleAccount *account,
+                                               PurpleCreateConversationDetails *details,
+                                               GCancellable *cancellable,
+                                               GAsyncReadyCallback callback,
+                                               gpointer data)
+{
+	PurpleConversation *conversation = NULL;
+	PurpleConversationManager *manager = NULL;
+	PurpleConversationType type = PURPLE_CONVERSATION_TYPE_UNSET;
+	GListModel *participants = NULL;
+	GTask *task = NULL;
+	char *id = NULL;
+	guint n_participants = 0;
+
+	task = g_task_new(protocol, cancellable, callback, data);
+	g_task_set_source_tag(task,
+	                      purple_satori_protocol_create_conversation_async);
+
+	participants = purple_create_conversation_details_get_participants(details);
+	n_participants = g_list_model_get_n_items(participants);
+	if(n_participants == 0) {
+		g_task_return_new_error_literal(task, PURPLE_SATORI_DOMAIN, 0,
+		                                _("no participants were provided"));
+		g_clear_object(&task);
+
+		return;
+	}
+
+	if(n_participants == 1) {
+		type = PURPLE_CONVERSATION_TYPE_DM;
+	} else {
+		type = PURPLE_CONVERSATION_TYPE_GROUP_DM;
+	}
+	id = purple_satori_protocol_generate_conversation_id(account, details);
+
+	conversation = g_object_new(
+		PURPLE_TYPE_CONVERSATION,
+		"account", account,
+		"id", id,
+		"type", type,
+		"online", TRUE,
+		NULL);
+	g_clear_pointer(&id, g_free);
+
+	for(guint i = 0; i < g_list_model_get_n_items(participants); i++) {
+		PurpleContactInfo *info = NULL;
+		PurpleConversationMember *member = NULL;
+		PurpleConversationMembers *members = NULL;
+		PurpleTags *tags = NULL;
+		const char *badge_id = NULL;
+
+		info = g_list_model_get_item(participants, i);
+		members = purple_conversation_get_members(conversation);
+		member = purple_conversation_members_add_member(members, info, FALSE,
+		                                                NULL);
+
+		tags = purple_contact_info_get_tags(info);
+		badge_id = purple_tags_get(tags, "satori-badge");
+		if(!purple_strempty(badge_id)) {
+			PurpleBadge *badge = NULL;
+			PurpleBadgeManager *manager = NULL;
+			PurpleBadges *badges = NULL;
+
+			badges = purple_conversation_member_get_badges(member);
+
+			manager = purple_badge_manager_get_default();
+			badge = purple_badge_manager_find(manager, badge_id);
+			if(PURPLE_IS_BADGE(badge)) {
+				purple_badges_add_badge(badges, badge);
+			} else {
+				char *icon_name = NULL;
+				char *id = NULL;
+
+				id = g_strdup_printf("satori-badge-%s", badge_id);
+				icon_name = g_strdup_printf("satori-badge-%s", badge_id);
+				badge = purple_badge_new(id, 0, icon_name, " ");
+				purple_badge_set_description(badge, badge_id);
+				g_free(id);
+				g_free(icon_name);
+
+				purple_badge_manager_add(manager, badge);
+				purple_badges_add_badge(badges, badge);
+				g_clear_object(&badge);
+			}
+		}
+
+		g_clear_object(&info);
+	}
+	g_clear_object(&details);
+
+	manager = purple_conversation_manager_get_default();
+	if(!purple_conversation_manager_add(manager, conversation)) {
+		g_task_return_new_error(task, PURPLE_SATORI_DOMAIN, 0,
+		                        _("This conversation already exists."));
+		g_clear_object(&task);
+
+		return;
+	}
+
+	g_task_return_pointer(task, conversation, g_object_unref);
+
+	g_clear_object(&task);
+}
+
+static PurpleConversation *
+purple_satori_protocol_create_conversation_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
+                                                GAsyncResult *result,
+                                                GError **error)
+{
+	GTask *task = G_TASK(result);
+
+	g_return_val_if_fail(g_task_get_source_tag(task) ==
+	                     purple_satori_protocol_create_conversation_async,
+	                     NULL);
+
+	return g_task_propagate_pointer(task, error);
+}
+
+static void
+purple_satori_protocol_conversation_leave_conversation_async(PurpleProtocolConversation *protocol,
+                                                           G_GNUC_UNUSED PurpleConversation *conversation,
+                                                           GCancellable *cancellable,
+                                                           GAsyncReadyCallback callback,
+                                                           gpointer data)
+{
+	GTask *task = NULL;
+
+	task = g_task_new(protocol, cancellable, callback, data);
+	g_task_set_source_tag(task,
+	                      purple_satori_protocol_conversation_leave_conversation_async);
+
+	g_task_return_boolean(task, TRUE);
+	g_clear_object(&task);
+}
+
+static gboolean
+purple_satori_protocol_conversation_leave_conversation_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
+                                                            GAsyncResult *result,
+                                                            GError **error)
+{
+	gpointer tag = purple_satori_protocol_conversation_leave_conversation_async;
+
+	g_return_val_if_fail(g_async_result_is_tagged(result, tag), FALSE);
+
+	return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+static void
+purple_satori_protocol_send_message_async(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
+                                        PurpleConversation *conversation,
+                                        PurpleMessage *message,
+                                        GCancellable *cancellable,
+                                        GAsyncReadyCallback callback,
+                                        gpointer data)
+{
+	GTask *task = NULL;
+
+	if(purple_conversation_is_dm(conversation)) {
+		PurpleAccount *account = NULL;
+		PurpleContact *contact = NULL;
+		PurpleContactInfo *contact_info = NULL;
+		PurpleContactManager *manager = NULL;
+		PurpleConversationMember *member = NULL;
+		PurpleConversationMembers *members = NULL;
+
+		account = purple_conversation_get_account(conversation);
+		members = purple_conversation_get_members(conversation);
+
+		manager = purple_contact_manager_get_default();
+
+		/* Check if this dm is with echo. */
+		contact = purple_contact_manager_find_with_id(manager, account,
+		                                              "echo");
+		contact_info = PURPLE_CONTACT_INFO(contact);
+		member = purple_conversation_members_find_member(members, contact_info);
+		if(PURPLE_IS_CONVERSATION_MEMBER(member)) {
+			PurpleSatoriProtocolIMInfo *info = NULL;
+			const char *contents = purple_message_get_contents(message);
+
+			info = g_new(PurpleSatoriProtocolIMInfo, 1);
+			info->conversation = g_object_ref(conversation);
+			info->message = purple_message_new(member, contents);
+			purple_message_set_edited(info->message,
+			                          purple_message_get_edited(message));
+
+			g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
+			                purple_satori_protocol_echo_im_cb, info,
+			                (GDestroyNotify)purple_satori_protocol_im_info_free);
+		}
+
+		/* Check if this dm is with aegina. */
+		contact = purple_contact_manager_find_with_id(manager, account,
+		                                              "aegina");
+		contact_info = PURPLE_CONTACT_INFO(contact);
+		member = purple_conversation_members_find_member(members, contact_info);
+		if(PURPLE_IS_CONVERSATION_MEMBER(member)) {
+			PurpleSatoriProtocolIMInfo *info = g_new(PurpleSatoriProtocolIMInfo, 1);
+			PurpleConversationMember *author = purple_message_get_author(message);
+			PurpleContactInfo *author_info = NULL;
+			const char *contents = NULL;
+			const char *author_id = NULL;
+
+			author_info = purple_conversation_member_get_contact_info(author);
+			author_id = purple_contact_info_get_id(author_info);
+			if(purple_strequal(author_id, "hades")) {
+				contents = "🫥️";
+			} else {
+				/* TRANSLATORS: This is a reference to the Cap of Invisibility owned by
+				 * various Greek gods, such as Hades, as mentioned. */
+				contents = _("Don't tell Hades I have his Cap");
+			}
+
+			info->conversation = g_object_ref(conversation);
+			info->message = purple_message_new(member, contents);
+
+			g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, purple_satori_protocol_echo_im_cb,
+			                info, (GDestroyNotify)purple_satori_protocol_im_info_free);
+		}
+	}
+
+	purple_conversation_write_message(conversation, message);
+
+	task = g_task_new(protocol, cancellable, callback, data);
+	g_task_return_boolean(task, TRUE);
+
+	g_clear_object(&task);
+}
+
+static gboolean
+purple_satori_protocol_send_message_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
+                                         GAsyncResult *result,
+                                         GError **error)
+{
+	g_return_val_if_fail(G_IS_TASK(result), FALSE);
+
+	return g_task_propagate_boolean(G_TASK(result), error);
+}
+
+void
+purple_satori_protocol_conversation_init(PurpleProtocolConversationInterface *iface) {
+	iface->get_create_conversation_details =
+		purple_satori_protocol_get_create_conversation_details;
+	iface->create_conversation_async =
+		purple_satori_protocol_create_conversation_async;
+	iface->create_conversation_finish =
+		purple_satori_protocol_create_conversation_finish;
+
+	iface->leave_conversation_async =
+		purple_satori_protocol_conversation_leave_conversation_async;
+	iface->leave_conversation_finish =
+		purple_satori_protocol_conversation_leave_conversation_finish;
+
+	iface->send_message_async = purple_satori_protocol_send_message_async;
+	iface->send_message_finish = purple_satori_protocol_send_message_finish;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/purplesatoriprotocolconversation.h	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,28 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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_SATORI_PROTOCOL_CONVERSATION_H
+#define PURPLE_SATORI_PROTOCOL_CONVERSATION_H
+
+#include <glib.h>
+
+#include <purple.h>
+
+G_GNUC_INTERNAL void purple_satori_protocol_conversation_init(PurpleProtocolConversationInterface *iface);
+
+#endif /* PURPLE_SATORI_PROTOCOL_CONVERSATION_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/satoriapi.c	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,151 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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 <gio/gio.h>
+#include <glib-object.h>
+#include <json-glib/json-glib.h>
+#include <libsoup/soup-message.h>
+#include <libsoup/soup-session.h>
+
+
+#include "purplesatoriconnection.h"
+#include "satorimessage.h"
+#include "satoriapi.h"
+
+static void
+satori_on_buddy_contacts_resp(SoupSession *session,
+			      GAsyncResult *res,
+			      PurpleSatoriConnection *con)
+{
+	GError *error = NULL;
+	GBytes *resp = soup_session_send_and_read_finish(session, res, &error);
+
+	if (error) {
+		purple_debug_error("satori",
+				   "refresh_buddy_contacts failed: %s",
+				   error->message);
+		if (resp)
+			g_bytes_unref(resp);
+		g_error_free(error);
+		return;
+	}
+
+	gsize sz;
+	const gchar *ptr = g_bytes_get_data(resp, &sz), *next = NULL;
+
+	JsonParser *parser = json_parser_new();
+	if (!json_parser_load_from_data(parser, ptr, sz, NULL)) {
+		purple_debug_warning("satori", "bad json received from ws");
+		goto finish;
+	}
+
+	JsonObject *root = json_node_get_object(json_parser_get_root(parser));
+	JsonArray *data = json_object_get_array_member(root, "data");
+	next = json_object_get_string_member_with_default(
+		root, "next", NULL);
+
+	if (!data) goto finish;
+
+	PurpleAccount *acc =						\
+		purple_connection_get_account(PURPLE_CONNECTION(con));
+	PurpleContactManager *manager =			\
+		purple_contact_manager_get_default();
+
+	for (guint i = 0; i < json_array_get_length(data); i++) {
+		JsonObject *user_obj = json_array_get_object_element(data, i);
+
+		SatoriUser user = { 0 };
+		satori_user_from_json(user_obj, &user);
+
+		PurpleContact		*contact  = NULL;
+		PurpleContactInfo	*info	  = NULL;
+		PurplePresence		*presence = NULL;
+		PurplePerson		*person	  = NULL;
+
+		gboolean new_contact = FALSE, new_person = FALSE;
+
+		contact = purple_contact_manager_find_with_id(manager, acc, user.id);
+		if (!PURPLE_IS_CONTACT(contact)) {
+			contact = purple_contact_new(acc, user.id);
+			new_contact = TRUE;
+		}
+
+		/* Initialize PurpleContactInfo */
+		info = PURPLE_CONTACT_INFO(contact);
+		purple_contact_info_set_username(info, user.id);
+		purple_contact_info_set_display_name(info, user.nick ?
+						     user.nick : user.name);
+
+		/* Initialize PurplePerson */
+		person = purple_contact_info_get_person(info);
+		if (!PURPLE_IS_PERSON(person)) {
+			person = g_object_new(PURPLE_TYPE_PERSON,
+					      "id", user.id, NULL);
+			new_person = TRUE;
+		}
+
+		if (new_person) {
+			purple_person_add_contact_info(person, info);
+			purple_contact_info_set_person(info, person);
+			g_clear_object(&person);
+		}
+
+		/* Initialize PurplePresence */
+		presence = purple_contact_info_get_presence(info);
+		purple_presence_set_primitive(presence,
+					      PURPLE_PRESENCE_PRIMITIVE_AVAILABLE);
+
+		if (new_contact) {
+			purple_contact_manager_add(manager, contact);
+			g_clear_object(&contact);
+		}
+	}
+
+finish:
+	if (next)
+		satori_refresh_buddy_contacts(con, next);
+
+        g_bytes_unref(resp);
+	g_object_unref(parser);
+}
+
+void
+satori_refresh_buddy_contacts(PurpleSatoriConnection *con, const gchar *next)
+{
+	GBytes *data = NULL;
+
+	{
+		JB_BEGIN_OBJ(b);
+		if (next)
+			JBA(b, "next", next);
+		JB_END_OBJ(data, b);
+	}
+
+	SoupMessage *msg = satori_message_new(
+		"POST", SATORI_ENDPOINT("/v1/friend.list"));
+	soup_message_set_request_body_from_bytes(msg, "application/json", data);
+
+	purple_satori_connection_send_and_read_async(
+		con, msg, 0, NULL,
+		(GAsyncReadyCallback) satori_on_buddy_contacts_resp,
+		PURPLE_SATORI_CONNECTION(con));
+
+	g_object_unref(msg);
+	g_bytes_unref(data);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/satoriapi.h	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,26 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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 SATORI_API_H
+#define SATORI_API_H
+
+#include "purplesatoriconnection.h"
+
+void satori_refresh_buddy_contacts(PurpleSatoriConnection *con, const gchar *next);
+
+#endif	/* SATORI_API_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/satorimessage.h	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,113 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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 SATORI_MESSAGE_H
+#define SATORI_MESSAGE_H
+
+#include <glib.h>
+#include <glib-object.h>
+#include <libsoup/soup.h>
+#include <libsoup/soup-message.h>
+#include <json-glib/json-glib.h>
+
+#include "satoritypes.h"
+#include "purplesatoriconnection.h"
+
+#define SATORI_ENDPOINT(path) "http://" PURPLE_SATORI_HOST path
+
+#define JBO(b) json_builder_begin_object(b)
+#define JEO(b) json_builder_end_object(b)
+#define JBSN(b, name) json_builder_set_member_name(b, name)
+#define JBSI(b, val) json_builder_add_int_value(b, val)
+#define JBSS(b, val) json_builder_add_string_value(b, val)
+#define JBA(b, name, val) (JBSN(b, name), JBSS(b, val))
+#define JBAI(b, name, val) (JBSN(b, name), JBSI(b, val))
+
+static inline GBytes *
+JB2GBYTES(JsonBuilder *builder)
+{
+	JsonNode *root = json_builder_get_root(builder);
+	JsonGenerator *gen = json_generator_new();
+	json_generator_set_root(gen, root);
+
+	gchar *json_str = json_generator_to_data(gen, NULL);
+	GBytes *bytes = g_bytes_new_take(json_str, strlen(json_str));
+
+	json_node_free(root);
+	g_object_unref(gen);
+
+	return bytes;
+}
+
+#define JB_BEGIN_OBJ(name) \
+	JsonBuilder *name = json_builder_new(); \
+	JBO(name);
+
+#define JB_END_OBJ(lval, name) \
+	do { \
+		JEO(b); \
+		lval = JB2GBYTES(name); \
+		g_object_unref(b); \
+	} while (0)
+
+static inline SoupMessage *
+satori_message_new(const gchar *method, const gchar *url)
+{
+	SoupMessage *msg = soup_message_new(method, url);
+	SoupMessageHeaders *headers = soup_message_get_request_headers(msg);
+	soup_message_headers_append(headers, "Satori-Platform",
+				    PURPLE_SATORI_PLATFORM);
+	soup_message_headers_append(headers, "Satori-User-ID",
+				    PURPLE_SATORI_USER_ID);
+	return msg;
+}
+
+static inline GBytes *
+satori_message_gen_ident(const gchar *token, gint sn)
+{
+	GBytes *msg = NULL;
+
+	JB_BEGIN_OBJ(b);
+	JBAI(b, "op", SATORI_WEBSOCKET_OP_IDENTIFY);
+	JBSN(b, "body");
+
+	JBO(b);
+	if (token)
+		JBA(b, "token", token);
+	if (sn)
+		JBAI(b, "sn", sn);
+	JEO(b);
+
+	JB_END_OBJ(msg, b);
+	return msg;
+}
+
+static inline void
+satori_user_from_json(JsonObject *user_obj, SatoriUser *out_user) {
+	out_user->id = json_object_get_string_member(user_obj, "id");
+	out_user->name = json_object_get_string_member_with_default(
+		user_obj, "name", NULL);
+	out_user->nick = json_object_get_string_member_with_default(
+		user_obj, "nick", NULL);
+	out_user->avatar = json_object_get_string_member_with_default(
+		user_obj, "avatar", NULL);
+	out_user->is_bot = json_object_get_boolean_member_with_default(
+		user_obj, "is_bot", FALSE);
+}
+
+#endif	/* SATORI_MESSAGE_H */
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/satoritypes.h	Fri Aug 08 09:46:55 2025 +0800
@@ -0,0 +1,41 @@
+/*
+ * Purple Satori Plugin - Satori Protocol Plugin for Purple3
+ * Copyright (C) 2025 Gong Zhile
+ *
+ * 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 SATORI_TYPES_H
+#define SATORI_TYPES_H
+
+#include <glib.h>
+
+typedef enum {
+	SATORI_WEBSOCKET_OP_EVENT = 0,
+	SATORI_WEBSOCKET_OP_PING,
+	SATORI_WEBSOCKET_OP_PONG,
+	SATORI_WEBSOCKET_OP_IDENTIFY,
+	SATORI_WEBSOCKET_OP_READY,
+	SATORI_WEBSOCKET_OP_META,
+} SatoriWebsocketOpcode;
+
+typedef struct {
+	const gchar *id;
+	const gchar *name;
+	const gchar *nick;
+	const gchar *avatar;
+	gboolean is_bot;
+} SatoriUser;
+
+#endif	/* SATORI_TYPES_H */

mercurial