--- /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); +}