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 <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 <time.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, {}); /****************************************************************************** * 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 *****************************************************************************/ 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); 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; } default: /* ignored */ break; } 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); }