Another Minor Milestone Reached, Conversation Creation & Recv works now

Sat, 09 Aug 2025 00:19:03 +0800

author
William Goodspeed <goodspeed@mailo.cat>
date
Sat, 09 Aug 2025 00:19:03 +0800
changeset 1
98bcf06036b8
parent 0
cc7c1f9d20f7
child 2
efafd19ab2fe

Another Minor Milestone Reached, Conversation Creation & Recv works now

.hgignore file | annotate | diff | comparison | revisions
purplesatoriconnection.c file | annotate | diff | comparison | revisions
purplesatoriprotocolconversation.c 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
--- a/.hgignore	Fri Aug 08 09:46:55 2025 +0800
+++ b/.hgignore	Sat Aug 09 00:19:03 2025 +0800
@@ -39,4 +39,4 @@
 .DS_Store
 Thumbs.db
 
-.cache/
\ No newline at end of file
+.cache/
--- a/purplesatoriconnection.c	Fri Aug 08 09:46:55 2025 +0800
+++ b/purplesatoriconnection.c	Sat Aug 09 00:19:03 2025 +0800
@@ -27,6 +27,7 @@
 #include <libsoup/soup-message.h>
 #include <libsoup/soup-types.h>
 #include <libsoup/soup-websocket-connection.h>
+#include <time.h>
 
 #include "purplesatoriconnection.h"
 #include "purplesatoriprotocolcontacts.h"
@@ -46,6 +47,43 @@
                                PURPLE_TYPE_CONNECTION, G_TYPE_FLAG_FINAL, {});
 
 /******************************************************************************
+ * PurpleConversation Helpers
+ *****************************************************************************/
+
+static void
+purple_satori_handle_pending_message(PurpleSatoriConnection *con,
+				     SatoriUser *user,
+				     SatoriChannel *chan,
+				     JsonObject *msg_obj)
+{
+	PurpleConversation *conversation = \
+		purple_satori_add_conversation_from_chan(con, chan);
+	PurpleConversationMember *mbr = \
+		purple_satori_add_conversation_member_from_user(
+			con, conversation, user);
+
+	const gchar *id = json_object_get_string_member_with_default(
+		msg_obj, "id", NULL);
+	const gchar *text = json_object_get_string_member_with_default(
+		msg_obj, "content", "Invalid Message");
+
+	time_t created_at = json_object_get_int_member_with_default(
+		msg_obj, "created_at", 0) / 1000; /* timestamp in mS */
+
+	PurpleMessage *message = purple_message_new(mbr, text);
+	if (id) purple_message_set_id(message, id);
+
+	if (created_at) {
+		GDateTime *ts = g_date_time_new_from_unix_local(created_at);
+		purple_message_set_timestamp(message, ts);
+		g_date_time_unref(ts);
+	} else purple_message_set_timestamp_now(message);
+
+	purple_conversation_write_message(conversation, message);
+	g_clear_object(&message);
+}
+
+/******************************************************************************
  * PurpleConnection WS Callbacks
  *****************************************************************************/
 
@@ -107,6 +145,39 @@
 						     ? user.nick : user.name);
 
 		satori_refresh_buddy_contacts(con, NULL);
+		satori_refresh_conversations(con, NULL);
+		break;
+	}
+
+	case SATORI_WEBSOCKET_OP_EVENT:
+	{
+		const gchar *type = json_object_get_string_member(body, "type");
+		if (purple_strequal(type, "message-created")) {
+			JsonObject *obj = json_object_get_object_member(
+				body, "message");
+
+			JsonObject *usr_obj = json_object_get_object_member(
+				json_object_get_object_member(body, "member"),
+				"user");
+
+			if (!usr_obj)
+				usr_obj = \
+					json_object_get_object_member(body, "user");
+
+			if (!usr_obj) break;
+
+			JsonObject *chan_obj = \
+				json_object_get_object_member(body, "channel");
+
+			if (!chan_obj) break;
+
+			SatoriChannel chan;
+			SatoriUser usr;
+			satori_channel_from_json(chan_obj, &chan);
+			satori_user_from_json(usr_obj, &usr);
+
+			purple_satori_handle_pending_message(con, &usr, &chan, obj);
+		}
 		break;
 	}
 
@@ -115,8 +186,6 @@
 
 	}
 
-	g_print("op = %d\n", op);
-
 	g_object_unref(parser);
 }
 
--- a/purplesatoriprotocolconversation.c	Fri Aug 08 09:46:55 2025 +0800
+++ b/purplesatoriprotocolconversation.c	Sat Aug 09 00:19:03 2025 +0800
@@ -20,8 +20,11 @@
 
 #include "purplesatoriprotocolconversation.h"
 
+#include "purplesatoriconnection.h"
 #include "purplesatoriplugin.h"
 #include "purplesatoriprotocol.h"
+#include "satoriapi.h"
+#include "satoritypes.h"
 
 typedef struct {
 	PurpleConversation *conversation;
@@ -38,60 +41,6 @@
 	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
  *****************************************************************************/
@@ -109,26 +58,22 @@
  *****************************************************************************/
 static PurpleCreateConversationDetails *
 purple_satori_protocol_get_create_conversation_details(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
-                                                     G_GNUC_UNUSED PurpleAccount *account)
+						       G_GNUC_UNUSED PurpleAccount *account)
 {
-	return purple_create_conversation_details_new(9);
+	return purple_create_conversation_details_new(0);
 }
 
 static void
 purple_satori_protocol_create_conversation_async(PurpleProtocolConversation *protocol,
-                                               PurpleAccount *account,
-                                               PurpleCreateConversationDetails *details,
-                                               GCancellable *cancellable,
-                                               GAsyncReadyCallback callback,
-                                               gpointer data)
+						 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;
+	GListModel		*participants	= NULL;
+	GTask			*task		= NULL;
+	guint			 n_participants = 0;
 
 	task = g_task_new(protocol, cancellable, callback, data);
 	g_task_set_source_tag(task,
@@ -136,88 +81,36 @@
 
 	participants = purple_create_conversation_details_get_participants(details);
 	n_participants = g_list_model_get_n_items(participants);
-	if(n_participants == 0) {
+	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;
+	if (n_participants > 1) {
+		g_task_return_new_error_literal(task, PURPLE_SATORI_DOMAIN, 0,
+		                                _("only dm conversation supported"));
+		g_clear_object(&task);
+		return;		/* not implimented */
 	}
-	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;
+		SatoriUser user = { 0 };
 
 		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);
+		satori_user_from_contactinfo(info, &user);
 
-			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);
-			}
-		}
+		satori_create_dm_channel(
+			PURPLE_SATORI_CONNECTION(
+				purple_account_get_connection(account)),
+			&user, task);
 
 		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);
-
+		g_clear_object(&details);
 		return;
 	}
-
-	g_task_return_pointer(task, conversation, g_object_unref);
-
-	g_clear_object(&task);
 }
 
 static PurpleConversation *
--- a/satoriapi.c	Fri Aug 08 09:46:55 2025 +0800
+++ b/satoriapi.c	Sat Aug 09 00:19:03 2025 +0800
@@ -23,10 +23,171 @@
 #include <libsoup/soup-message.h>
 #include <libsoup/soup-session.h>
 
+#include "purplesatoriconnection.h"
+#include "purplesatoriplugin.h"
+#include "satorimessage.h"
+#include "satoritypes.h"
+#include "satoriapi.h"
 
-#include "purplesatoriconnection.h"
-#include "satorimessage.h"
-#include "satoriapi.h"
+/******************************************************************************
+ * Purple Integration Helpers
+ *****************************************************************************/
+
+PurpleConversationType
+satoir_channel_type_to_conversation_type(SatoriChannelType t)
+{
+	switch (t) {
+	case SATORI_CHANNEL_DIRECT:
+		return PURPLE_CONVERSATION_TYPE_DM;
+	case SATORI_CHANNEL_TEXT:
+		return PURPLE_CONVERSATION_TYPE_CHANNEL;
+	case SATORI_CHANNEL_VOICE:
+	case SATORI_CHANNEL_CATEGORY:
+		return 0;	/* Unsupported */
+	default:
+		return 0;
+	}
+}
+
+void
+purple_satori_add_person_from_user(PurpleSatoriConnection *con, SatoriUser *user)
+{
+	PurpleAccount *acc = \
+		purple_connection_get_account(PURPLE_CONNECTION(con));
+	PurpleContactManager	*manager  = purple_contact_manager_get_default();
+
+	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_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);
+	}
+}
+
+PurpleConversation *
+purple_satori_add_conversation_from_chan(PurpleSatoriConnection *con,
+					 SatoriChannel *chan)
+{
+	PurpleAccount *acc =						\
+		purple_connection_get_account(PURPLE_CONNECTION(con));
+	PurpleConversationManager *manager =	\
+		purple_conversation_manager_get_default();
+	PurpleConversation *conversation = NULL;
+
+	conversation = purple_conversation_manager_find_with_id(manager, acc,
+								chan->id);
+
+	if (!PURPLE_IS_CONVERSATION(conversation)) {
+		PurpleConversationType type = \
+			satoir_channel_type_to_conversation_type(chan->type);
+		if (!type) return NULL;
+
+		conversation = g_object_new(
+			PURPLE_TYPE_CONVERSATION,
+			"account", acc,
+			"id", chan->id,
+			"title", chan->name ? chan->name : "Unnamed Channel",
+			"type", type,
+			"online", TRUE,
+			NULL);
+
+		purple_conversation_manager_add(manager, conversation);
+		g_object_unref(conversation);
+	}
+
+	purple_conversation_set_online(conversation, TRUE);
+	return conversation;
+}
+
+PurpleConversationMember *
+purple_satori_add_conversation_member_from_user(PurpleSatoriConnection *con,
+						PurpleConversation *conversation,
+						SatoriUser *user)
+{
+	PurpleAccount *acc = \
+		purple_connection_get_account(PURPLE_CONNECTION(con));
+	PurpleContactManager *manager = \
+		purple_contact_manager_get_default();
+	PurpleContact *contact = NULL;
+
+	gboolean found = TRUE;
+	contact = purple_contact_manager_find_with_id(manager, acc, user->id);
+
+	if (!PURPLE_IS_CONTACT(contact)) {
+		contact = purple_contact_new(acc, user->id);
+		found = FALSE;
+	}
+
+	purple_contact_info_set_display_name(
+		PURPLE_CONTACT_INFO(contact),
+		user->nick ? user->nick : user->name);
+
+	PurplePresence *presence = purple_contact_info_get_presence(
+		PURPLE_CONTACT_INFO(contact));
+	purple_presence_set_primitive(presence,
+				      PURPLE_PRESENCE_PRIMITIVE_AVAILABLE);
+
+	if (!found)
+		purple_contact_manager_add(manager, contact);
+
+	PurpleConversationMembers *existing_members = \
+		purple_conversation_get_members(conversation);
+	PurpleConversationMembers *new_members = NULL;
+	PurpleConversationMember *member = NULL;
+
+	member = purple_conversation_members_find_member(
+		existing_members, PURPLE_CONTACT_INFO(contact));
+
+	if (!PURPLE_IS_CONVERSATION_MEMBER(member)) {
+		new_members = purple_conversation_members_new();
+		member = purple_conversation_members_add_member(
+			new_members, PURPLE_CONTACT_INFO(contact), FALSE, NULL);
+
+		purple_conversation_members_extend(existing_members, new_members);
+	}
+
+	g_clear_object(&contact);
+	return member;
+}
+
+/******************************************************************************
+ * Buddy/Friend List Synchronization
+ *****************************************************************************/
 
 static void
 satori_on_buddy_contacts_resp(SoupSession *session,
@@ -62,59 +223,13 @@
 
 	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);
-		}
+		purple_satori_add_person_from_user(con, &user);
 	}
 
 finish:
@@ -149,3 +264,254 @@
 	g_object_unref(msg);
 	g_bytes_unref(data);
 }
+
+/******************************************************************************
+ * Group/Room List (Recursive) Synchronization
+ *****************************************************************************/
+
+static void satori_refresh_guild_channels(PurpleSatoriConnection *,
+					  const gchar *,
+					  const gchar *);
+
+static void
+satori_on_guild_channels_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_conversations 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);
+
+	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");
+
+	if (!data) goto finish;
+
+	for (guint i = 0; i < json_array_get_length(data); i++) {
+		JsonObject *chan_obj = json_array_get_object_element(data, i);
+
+		SatoriChannel chan = { 0 };
+		satori_channel_from_json(chan_obj, &chan);
+
+		purple_satori_add_conversation_from_chan(con, &chan);
+	}
+
+finish:
+	/* if (next) */
+	/* 	satori_refresh_guild_channels(con, next); */
+
+        g_bytes_unref(resp);
+	g_object_unref(parser);	
+}
+
+static void
+satori_refresh_guild_channels(PurpleSatoriConnection *con,
+			      const gchar *guild_id,
+			      const gchar *next)
+{
+	GBytes *data = NULL;
+
+	{
+		JB_BEGIN_OBJ(b);
+		JBA(b, "guild_id", guild_id);
+		if (next)
+			JBA(b, "next", next);
+		JB_END_OBJ(data, b);
+	}
+
+	SoupMessage *msg = satori_message_new(
+		"POST", SATORI_ENDPOINT("/v1/channel.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_guild_channels_resp,
+		PURPLE_SATORI_CONNECTION(con));
+
+	g_object_unref(msg);
+	g_bytes_unref(data);
+}
+
+static void
+satori_on_guild_list_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_conversations 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;
+
+	for (guint i = 0; i < json_array_get_length(data); i++) {
+		JsonObject *guild_obj = json_array_get_object_element(data, i);
+		satori_refresh_guild_channels(
+			con,
+			json_object_get_string_member(guild_obj, "id"),
+			NULL);
+	}
+
+finish:
+	if (next)
+		satori_refresh_conversations(con, next);
+
+        g_bytes_unref(resp);
+	g_object_unref(parser);
+}
+
+void
+satori_refresh_conversations(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/guild.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_guild_list_resp,
+		PURPLE_SATORI_CONNECTION(con));
+
+	g_object_unref(msg);
+	g_bytes_unref(data);
+}
+
+typedef struct {
+	PurpleSatoriConnection *con;
+	SatoriUser user;
+	GTask *task;
+} SatoriOnDmChannelData;
+
+static void
+satori_on_dm_channel(SoupSession *session,
+		     GAsyncResult *res,
+		     SatoriOnDmChannelData *dptr)
+{
+	GError *error = NULL;
+	GBytes *resp = soup_session_send_and_read_finish(session, res, &error);
+
+	if (error) {
+		purple_debug_error("satori",
+				   "create_dm_channel failed: %s",
+				   error->message);
+		if (resp)
+			g_bytes_unref(resp);
+
+		g_task_return_error(dptr->task, error);
+		goto cleanup;
+	}
+
+	gsize sz;
+	const gchar *ptr = g_bytes_get_data(resp, &sz);
+
+	JsonParser *parser = json_parser_new();
+	if (!json_parser_load_from_data(parser, ptr, sz, NULL)) {
+		purple_debug_warning("satori", "bad json received from api");
+		g_task_return_new_error_literal(dptr->task, PURPLE_SATORI_DOMAIN, 0,
+						"bad json received from api");
+		goto cleanup;
+	}
+
+	JsonObject *root = json_node_get_object(json_parser_get_root(parser));
+
+	SatoriChannel chan;
+	satori_channel_from_json(root, &chan);
+
+	PurpleConversation *conversation = \
+		purple_satori_add_conversation_from_chan(dptr->con, &chan);
+
+	if (!chan.name)
+		purple_conversation_set_title(conversation, dptr->user.name);
+
+	purple_satori_add_conversation_member_from_user(dptr->con, conversation,
+							&dptr->user);
+
+	g_task_return_pointer(dptr->task, conversation, g_object_unref);
+	g_object_unref(parser);
+cleanup:
+	g_clear_object(&dptr->task);
+	g_free((gchar *) dptr->user.name);
+	g_free((gchar *) dptr->user.id);
+	g_free(dptr);
+}
+
+void
+satori_create_dm_channel(PurpleSatoriConnection *con,
+			 SatoriUser *user,
+			 GTask *task)
+{
+	GBytes *data = NULL;
+
+	{
+		JB_BEGIN_OBJ(b);
+		JBA(b, "user_id", user->id);
+		JB_END_OBJ(data, b);
+	}
+
+	SoupMessage *msg = satori_message_new(
+		"POST", SATORI_ENDPOINT("/v1/user.channel.create"));
+	soup_message_set_request_body_from_bytes(msg, "application/json", data);
+
+	SatoriOnDmChannelData *dptr = g_new0(SatoriOnDmChannelData, 1);
+	dptr->user.name = dptr->user.nick = g_strdup(user->name);
+	dptr->user.id = g_strdup(user->id);
+	dptr->task = task;
+	dptr->con = con;
+
+	purple_satori_connection_send_and_read_async(
+		con, msg, 0, NULL,
+		(GAsyncReadyCallback) satori_on_dm_channel,
+	        dptr);
+
+	g_object_unref(msg);
+	g_bytes_unref(data);
+}
--- a/satoriapi.h	Fri Aug 08 09:46:55 2025 +0800
+++ b/satoriapi.h	Sat Aug 09 00:19:03 2025 +0800
@@ -19,8 +19,16 @@
 #ifndef SATORI_API_H
 #define SATORI_API_H
 
+#include "satoritypes.h"
 #include "purplesatoriconnection.h"
 
+PurpleConversationType satoir_channel_type_to_conversation_type(SatoriChannelType);
+void purple_satori_add_person_from_user(PurpleSatoriConnection *con, SatoriUser *user);
+PurpleConversation *purple_satori_add_conversation_from_chan(PurpleSatoriConnection *con, SatoriChannel *chan);
+PurpleConversationMember *purple_satori_add_conversation_member_from_user(PurpleSatoriConnection *con, PurpleConversation *conversation, SatoriUser *user);
+
 void satori_refresh_buddy_contacts(PurpleSatoriConnection *con, const gchar *next);
+void satori_refresh_conversations(PurpleSatoriConnection *con, const gchar *next);
+void satori_create_dm_channel(PurpleSatoriConnection *con, SatoriUser *user, GTask *task);
 
 #endif	/* SATORI_API_H */
--- a/satorimessage.h	Fri Aug 08 09:46:55 2025 +0800
+++ b/satorimessage.h	Sat Aug 09 00:19:03 2025 +0800
@@ -97,17 +97,4 @@
 	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 */
--- a/satoritypes.h	Fri Aug 08 09:46:55 2025 +0800
+++ b/satoritypes.h	Sat Aug 09 00:19:03 2025 +0800
@@ -19,7 +19,9 @@
 #ifndef SATORI_TYPES_H
 #define SATORI_TYPES_H
 
+#include "purplesatoriprotocolconversation.h"
 #include <glib.h>
+#include <json-glib/json-glib.h>
 
 typedef enum {
 	SATORI_WEBSOCKET_OP_EVENT = 0,
@@ -30,12 +32,59 @@
 	SATORI_WEBSOCKET_OP_META,
 } SatoriWebsocketOpcode;
 
-typedef struct {
-	const gchar *id;
-	const gchar *name;
-	const gchar *nick;
-	const gchar *avatar;
-	gboolean is_bot;
+typedef struct satori_user {
+	const gchar	*id;
+	const gchar	*name;
+	const gchar	*nick;
+	const gchar	*avatar;
+	gboolean	 is_bot;
 } SatoriUser;
 
+typedef enum {
+	SATORI_CHANNEL_TEXT = 0,
+	SATORI_CHANNEL_DIRECT,
+	SATORI_CHANNEL_CATEGORY,
+	SATORI_CHANNEL_VOICE,
+} SatoriChannelType;
+
+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);
+}
+
+static inline void
+satori_user_from_contactinfo(PurpleContactInfo *info, SatoriUser *out_user) {
+	out_user->nick = out_user->name = \
+		purple_contact_info_get_display_name(info);
+	out_user->id = \
+		purple_contact_info_get_id(info);
+}
+
+typedef struct satori_channel {
+	const gchar		*id;
+	SatoriChannelType	 type;
+	const gchar		*name;
+	const gchar		*parent_id;
+} SatoriChannel;
+
+static inline void
+satori_channel_from_json(JsonObject *obj, SatoriChannel *out_chan) {
+	out_chan->id = json_object_get_string_member(obj, "id");
+	out_chan->type = (SatoriChannelType) json_object_get_int_member(
+		obj, "type");
+	out_chan->name = json_object_get_string_member_with_default(
+		obj, "name", NULL);
+	out_chan->parent_id = json_object_get_string_member_with_default(
+		obj, "parent_id", NULL);
+}
+
+
 #endif	/* SATORI_TYPES_H */

mercurial