Fri, 08 Aug 2025 09:46:55 +0800
Initial Commit
| 0 | 1 | /* |
| 2 | * Purple Satori Plugin - Satori Protocol Plugin for Purple3 | |
| 3 | * Copyright (C) 2025 Gong Zhile | |
| 4 | * | |
| 5 | * This library is free software; you can redistribute it and/or | |
| 6 | * modify it under the terms of the GNU Lesser General Public | |
| 7 | * License as published by the Free Software Foundation; either | |
| 8 | * version 2 of the License, or (at your option) any later version. | |
| 9 | * | |
| 10 | * This library is distributed in the hope that it will be useful, | |
| 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
| 13 | * Lesser General Public License for more details. | |
| 14 | * | |
| 15 | * You should have received a copy of the GNU Lesser General Public | |
| 16 | * License along with this library; if not, see <https://www.gnu.org/licenses/>. | |
| 17 | */ | |
| 18 | ||
| 19 | #include <glib.h> | |
| 20 | #include <glib-object.h> | |
| 21 | #include <glib/gi18n-lib.h> | |
| 22 | #include <json-glib/json-glib.h> | |
| 23 | ||
| 24 | #include <gio/gio.h> | |
| 25 | #include <libsoup/soup.h> | |
| 26 | #include <libsoup/soup-session.h> | |
| 27 | #include <libsoup/soup-message.h> | |
| 28 | #include <libsoup/soup-types.h> | |
| 29 | #include <libsoup/soup-websocket-connection.h> | |
| 30 | ||
| 31 | #include "purplesatoriconnection.h" | |
| 32 | #include "purplesatoriprotocolcontacts.h" | |
| 33 | #include "satorimessage.h" | |
| 34 | #include "satoritypes.h" | |
| 35 | #include "satoriapi.h" | |
| 36 | ||
| 37 | struct _PurpleSatoriConnection { | |
| 38 | PurpleConnection parent; | |
| 39 | SoupSession *session; | |
| 40 | ||
| 41 | SoupWebsocketConnection *wscon; | |
| 42 | gboolean wsidented; | |
| 43 | }; | |
| 44 | ||
| 45 | G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleSatoriConnection, purple_satori_connection, | |
| 46 | PURPLE_TYPE_CONNECTION, G_TYPE_FLAG_FINAL, {}); | |
| 47 | ||
| 48 | /****************************************************************************** | |
| 49 | * PurpleConnection WS Callbacks | |
| 50 | *****************************************************************************/ | |
| 51 | ||
| 52 | static void | |
| 53 | satori_ws_on_closed(SoupWebsocketConnection *wscon, PurpleSatoriConnection *data) | |
| 54 | { | |
| 55 | PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION(data); | |
| 56 | PurpleAccount *acc = purple_connection_get_account(PURPLE_CONNECTION(con)); | |
| 57 | purple_account_disconnect(acc); | |
| 58 | ||
| 59 | con->wscon = NULL; | |
| 60 | } | |
| 61 | ||
| 62 | static void | |
| 63 | satori_ws_on_message(SoupWebsocketConnection *wscon, gint type, | |
| 64 | GBytes *message, PurpleSatoriConnection *con) | |
| 65 | { | |
| 66 | PurpleAccount *acc = purple_connection_get_account( | |
| 67 | PURPLE_CONNECTION(con)); | |
| 68 | ||
| 69 | if (type != SOUP_WEBSOCKET_DATA_TEXT) { | |
| 70 | purple_debug_warning("satori", "unexpected data recv from ws"); | |
| 71 | return; | |
| 72 | } | |
| 73 | ||
| 74 | gsize sz; | |
| 75 | const gchar *ptr = g_bytes_get_data(message, &sz); | |
| 76 | ||
| 77 | /* g_print("Received text data: %s\n", ptr); */ | |
| 78 | ||
| 79 | JsonParser *parser = json_parser_new(); | |
| 80 | if (!json_parser_load_from_data(parser, ptr, sz, NULL)) { | |
| 81 | purple_debug_warning("satori", "bad json received from ws"); | |
| 82 | g_object_unref(parser); | |
| 83 | return; | |
| 84 | } | |
| 85 | ||
| 86 | JsonObject *root = json_node_get_object(json_parser_get_root(parser)); | |
| 87 | JsonObject *body = json_object_get_object_member(root, "body"); | |
| 88 | SatoriWebsocketOpcode op = json_object_get_int_member(root, "op"); | |
| 89 | ||
| 90 | switch (op) { | |
| 91 | ||
| 92 | case SATORI_WEBSOCKET_OP_READY: | |
| 93 | { | |
| 94 | purple_account_connected(acc); | |
| 95 | ||
| 96 | JsonArray *logins = json_object_get_array_member(body, "logins"); | |
| 97 | JsonObject *user_obj = json_object_get_object_member( | |
| 98 | json_array_get_object_element(logins, 0), "user"); | |
| 99 | SatoriUser user = { 0 }; | |
| 100 | ||
| 101 | if (!user_obj) break; | |
| 102 | satori_user_from_json(user_obj, &user); | |
| 103 | ||
| 104 | PurpleContactInfo *ci = purple_account_get_contact_info(acc); | |
| 105 | purple_contact_info_set_id(ci, user.id); | |
| 106 | purple_contact_info_set_display_name(ci, user.nick | |
| 107 | ? user.nick : user.name); | |
| 108 | ||
| 109 | satori_refresh_buddy_contacts(con, NULL); | |
| 110 | break; | |
| 111 | } | |
| 112 | ||
| 113 | default: /* ignored */ | |
| 114 | break; | |
| 115 | ||
| 116 | } | |
| 117 | ||
| 118 | g_print("op = %d\n", op); | |
| 119 | ||
| 120 | g_object_unref(parser); | |
| 121 | } | |
| 122 | ||
| 123 | static void | |
| 124 | satori_ws_on_connection(SoupSession *session, GAsyncResult *res, gpointer data) | |
| 125 | { | |
| 126 | PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION(data); | |
| 127 | PurpleAccount *acc = purple_connection_get_account(PURPLE_CONNECTION(con)); | |
| 128 | GError *err = NULL; | |
| 129 | ||
| 130 | con->wscon = soup_session_websocket_connect_finish(session, res, &err); | |
| 131 | if (err) { | |
| 132 | purple_account_disconnect_with_error(acc, err); | |
| 133 | return; | |
| 134 | } | |
| 135 | ||
| 136 | g_signal_connect(con->wscon, "message", | |
| 137 | G_CALLBACK(satori_ws_on_message), con); | |
| 138 | g_signal_connect(con->wscon, "closed", | |
| 139 | G_CALLBACK(satori_ws_on_closed), con); | |
| 140 | ||
| 141 | GBytes *frame = satori_message_gen_ident(NULL, 0); | |
| 142 | soup_websocket_connection_send_text(con->wscon, | |
| 143 | g_bytes_get_data(frame, NULL)); | |
| 144 | g_bytes_unref(frame); | |
| 145 | } | |
| 146 | ||
| 147 | /****************************************************************************** | |
| 148 | * PurpleConnection Implementation | |
| 149 | *****************************************************************************/ | |
| 150 | static gboolean | |
| 151 | purple_satori_connection_connect(PurpleConnection *connection, | |
| 152 | G_GNUC_UNUSED GError **error) | |
| 153 | { | |
| 154 | PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION(connection); | |
| 155 | ||
| 156 | if (con->wscon) | |
| 157 | g_object_unref(con->wscon); | |
| 158 | ||
| 159 | SoupMessage *svmsg = satori_message_new("GET", PURPLE_SATORI_WSURL); | |
| 160 | soup_session_websocket_connect_async( | |
| 161 | con->session, svmsg, | |
| 162 | NULL, NULL, 0, NULL, | |
| 163 | (GAsyncReadyCallback) satori_ws_on_connection, | |
| 164 | con); | |
| 165 | ||
| 166 | /* purple_account_connected(account); */ | |
| 167 | /* purple_satori_contacts_load(account); */ | |
| 168 | ||
| 169 | return TRUE; | |
| 170 | } | |
| 171 | ||
| 172 | static gboolean | |
| 173 | purple_satori_connection_disconnect(PurpleConnection *connection, | |
| 174 | G_GNUC_UNUSED GError **error) | |
| 175 | { | |
| 176 | PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION(connection); | |
| 177 | if (!con->wscon) return TRUE; | |
| 178 | ||
| 179 | g_signal_handlers_disconnect_by_data(con->wscon, con); | |
| 180 | soup_websocket_connection_close(con->wscon, | |
| 181 | SOUP_WEBSOCKET_CLOSE_NO_STATUS, NULL); | |
| 182 | con->wscon = NULL; | |
| 183 | return TRUE; | |
| 184 | } | |
| 185 | ||
| 186 | /****************************************************************************** | |
| 187 | * GObject Implementation | |
| 188 | *****************************************************************************/ | |
| 189 | static void | |
| 190 | purple_satori_connection_init(PurpleSatoriConnection *connection) { | |
| 191 | connection->session = soup_session_new(); | |
| 192 | connection->wscon = NULL; | |
| 193 | } | |
| 194 | ||
| 195 | static void | |
| 196 | purple_satori_connection_class_finalize(G_GNUC_UNUSED PurpleSatoriConnectionClass *klass) { | |
| 197 | } | |
| 198 | ||
| 199 | static void | |
| 200 | purple_satori_connection_class_init(PurpleSatoriConnectionClass *klass) { | |
| 201 | PurpleConnectionClass *connection_class = PURPLE_CONNECTION_CLASS(klass); | |
| 202 | ||
| 203 | connection_class->connect = purple_satori_connection_connect; | |
| 204 | connection_class->disconnect = purple_satori_connection_disconnect; | |
| 205 | } | |
| 206 | ||
| 207 | /****************************************************************************** | |
| 208 | * Internal API | |
| 209 | *****************************************************************************/ | |
| 210 | void | |
| 211 | purple_satori_connection_register(GPluginNativePlugin *plugin) { | |
| 212 | purple_satori_connection_register_type(G_TYPE_MODULE(plugin)); | |
| 213 | } | |
| 214 | ||
| 215 | /****************************************************************************** | |
| 216 | * Public API Implementation | |
| 217 | *****************************************************************************/ | |
| 218 | ||
| 219 | void | |
| 220 | purple_satori_connection_send_and_read_async(PurpleSatoriConnection *con, | |
| 221 | SoupMessage *msg, | |
| 222 | int io_priority, | |
| 223 | GCancellable *cancellable, | |
| 224 | GAsyncReadyCallback callback, | |
| 225 | gpointer user_data) | |
| 226 | { | |
| 227 | soup_session_send_and_read_async(con->session, msg, io_priority, | |
| 228 | cancellable, callback, user_data); | |
| 229 | } |