Sun, 10 Aug 2025 23:03:27 +0800
satoriformat.{c,h}: Add message parsing support
/* * 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 "purplesatoriplugin.h" #include "satorimessage.h" #include "satoritypes.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, 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; 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); purple_satori_add_person_from_user(con, &user); } 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); } /****************************************************************************** * 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); }