Fri, 08 Aug 2025 09:46:55 +0800
Initial Commit
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/.hgignore Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,42 @@ +syntax: glob + +# Meson build directories +build*/ +meson-private/ +meson-logs/ +compile_commands.json +install_manifest.txt + +# Object and binary files +*.o +*.a +*.so +*.so.* +*.dylib +*.dll +*.exe + +# Debug symbols +*.pdb +*.dSYM/ +*.stackdump + +# Generated source files +*.generated.c +*.generated.h + +# Backup / temporary files +*~ +*.swp +*.tmp +*.bak + +# Editor-specific +.vscode/ +.idea/ + +# OS junk +.DS_Store +Thumbs.db + +.cache/ \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/meson.build Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,26 @@ +project('purplesatori', 'c') + +libpurple_dep = dependency('purple-3') +libsoup_dep = dependency('libsoup-3.0') +libjson_dep = dependency('json-glib-1.0') + +PURPLE_PLUGINDIR = libpurple_dep.get_pkgconfig_variable('plugindir') +PURPLE_SATORI_SRCS = [ + 'purplesatori.c', + 'purplesatoriconnection.c', + 'purplesatoriprotocol.c', + 'purplesatoriprotocolcontacts.c', + 'purplesatoriprotocolconversation.c', + 'satoriapi.c', +] + +library('libpurplesatori', PURPLE_SATORI_SRCS, + c_args : [ + '-DG_LOG_USE_STRUCTURED', + '-DG_LOG_DOMAIN="Purple-Satori"', + '-DGETTEXT_PACKAGE="pidgin3"' + ], + dependencies : [libpurple_dep, libsoup_dep, libjson_dep], + name_prefix : '', + install : true, + install_dir : PURPLE_PLUGINDIR)
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatori.c Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,106 @@ +/* + * 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/gi18n-lib.h> + +#include <gplugin.h> +#include <gplugin-native.h> + +#include <purple.h> + +#include "purplesatoriconnection.h" +#include "purplesatoriplugin.h" +#include "purplesatoriprotocol.h" + +/****************************************************************************** + * Globals + *****************************************************************************/ +static PurpleProtocol *satori_protocol = NULL; + +/****************************************************************************** + * GPlugin Implementation + *****************************************************************************/ +static GPluginPluginInfo * +purple_satori_plugin_query(G_GNUC_UNUSED GError **error) { + const gchar *authors[] = { + "Gong Zhile <gongzl@stu.hebust.edu.cn>", + NULL + }; + + return purple_plugin_info_new( + "id", "prpl-satori", + "name", "Satori Protocol Plugin", + "authors", authors, + "version", "0.0.1", + "category", N_("Protocol"), + "summary", N_("Satori protocol plugin for purple3"), + "description", N_("Satori protocol plugin for purple3"), + "website", "https://example.org/", + "abi-version", PURPLE_ABI_VERSION, + NULL + ); +} + +static gboolean +purple_satori_plugin_load(GPluginPlugin *plugin, GError **error) { + PurpleProtocolManager *manager = NULL; + + if(PURPLE_IS_PROTOCOL(satori_protocol)) { + g_set_error_literal(error, PURPLE_SATORI_DOMAIN, 0, + "plugin was not cleaned up properly"); + + return FALSE; + } + + purple_satori_connection_register(GPLUGIN_NATIVE_PLUGIN(plugin)); + purple_satori_protocol_register(GPLUGIN_NATIVE_PLUGIN(plugin)); + + manager = purple_protocol_manager_get_default(); + + satori_protocol = purple_satori_protocol_new(); + if (!purple_protocol_manager_add(manager, satori_protocol, error)) { + g_clear_object(&satori_protocol); + return FALSE; + } + + return TRUE; +} + +static gboolean +purple_satori_plugin_unload(G_GNUC_UNUSED GPluginPlugin *plugin, + G_GNUC_UNUSED gboolean shutdown, + GError **error) +{ + PurpleProtocolManager *manager = purple_protocol_manager_get_default(); + + if (!PURPLE_IS_PROTOCOL(satori_protocol)) { + g_set_error_literal(error, PURPLE_SATORI_DOMAIN, 0, + "plugin was not setup properly"); + return FALSE; + } + + if (!purple_protocol_manager_remove(manager, satori_protocol, error)) { + return FALSE; + } + + g_clear_object(&satori_protocol); + return TRUE; +} + +GPLUGIN_NATIVE_PLUGIN_DECLARE(purple_satori_plugin)
--- /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); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatoriconnection.h Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,50 @@ +/* + * 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/>. + */ + +#ifndef PURPLE_SATORI_CONNECTION_H +#define PURPLE_SATORI_CONNECTION_H + +#include <glib.h> +#include <purple.h> +#include <libsoup/soup.h> + +G_BEGIN_DECLS + +#define PURPLE_SATORI_HOST "192.168.1.154:5600" +#define PURPLE_SATORI_WSURL "ws://" PURPLE_SATORI_HOST "/v1/events" + +#define PURPLE_SATORI_PLATFORM "QQ" +#define PURPLE_SATORI_USER_ID "3751531667" + +#define PURPLE_SATORI_TYPE_CONNECTION (purple_satori_connection_get_type()) +G_DECLARE_FINAL_TYPE(PurpleSatoriConnection, purple_satori_connection, PURPLE_SATORI, + CONNECTION, PurpleConnection) + +G_GNUC_INTERNAL void purple_satori_connection_register(GPluginNativePlugin *plugin); + +void +purple_satori_connection_send_and_read_async(PurpleSatoriConnection *con, + SoupMessage *msg, + int io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +G_END_DECLS + +#endif /* PURPLE_SATORI_CONNECTION_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatoriplugin.h Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,30 @@ +/* + * 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/>. + */ + +#ifndef PURPLE_SATORI_PLUGIN_H +#define PURPLE_SATORI_PLUGIN_H + +#include <glib.h> + +G_BEGIN_DECLS + +#define PURPLE_SATORI_DOMAIN (g_quark_from_static_string("satori-plugin")) + +G_END_DECLS + +#endif /* PURPLE_SATORI_PLUGIN_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatoriprotocol.c Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,98 @@ +/* + * 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/gi18n-lib.h> + +#include "purplesatoriprotocol.h" + +#include "purplesatoriconnection.h" +#include "purplesatoriprotocolcontacts.h" +#include "purplesatoriprotocolconversation.h" + +struct _PurpleSatoriProtocol { + PurpleProtocol parent; +}; + +/****************************************************************************** + * PurpleProtocol Implementation + *****************************************************************************/ +static PurpleConnection * +purple_satori_protocol_create_connection(PurpleProtocol *protocol, + PurpleAccount *account, + const char *password, + G_GNUC_UNUSED GError **error) +{ + g_return_val_if_fail(PURPLE_IS_PROTOCOL(protocol), NULL); + g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL); + + return g_object_new(PURPLE_SATORI_TYPE_CONNECTION, + "protocol", protocol, + "account", account, + "password", password, + NULL); + +} + +/****************************************************************************** + * GObject Implementation + *****************************************************************************/ +G_DEFINE_DYNAMIC_TYPE_EXTENDED( + PurpleSatoriProtocol, + purple_satori_protocol, + PURPLE_TYPE_PROTOCOL, + G_TYPE_FLAG_FINAL, + G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONTACTS, + purple_satori_protocol_contacts_init) + G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONVERSATION, + purple_satori_protocol_conversation_init)) + +static void +purple_satori_protocol_init(G_GNUC_UNUSED PurpleSatoriProtocol *protocol) { +} + +static void +purple_satori_protocol_class_finalize(G_GNUC_UNUSED PurpleSatoriProtocolClass *klass) { +} + +static void +purple_satori_protocol_class_init(PurpleSatoriProtocolClass *klass) { + PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass); + + protocol_class->create_connection = purple_satori_protocol_create_connection; +} + +/****************************************************************************** + * Local Exports + *****************************************************************************/ +void +purple_satori_protocol_register(GPluginNativePlugin *plugin) { + purple_satori_protocol_register_type(G_TYPE_MODULE(plugin)); +} + +PurpleProtocol * +purple_satori_protocol_new(void) { + return g_object_new( + PURPLE_SATORI_TYPE_PROTOCOL, + "id", "prpl-satori", + "name", "Satori", + "description", "Satori Protocol Plugin for Purple 3", + "icon-name", "im-purple-satori", + "icon-resource-path", "/im/pidgin/libpurple/protocols/satori/icons", + "options", OPT_PROTO_NO_PASSWORD, + NULL); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatoriprotocol.h Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,41 @@ +/* + * 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/>. + */ + +#ifndef PURPLE_SATORI_PROTOCOL_H +#define PURPLE_SATORI_PROTOCOL_H + +#include <glib.h> +#include <glib-object.h> + +#include <gplugin-native.h> + +#include <purple.h> + +G_BEGIN_DECLS + +#define PURPLE_SATORI_TYPE_PROTOCOL (purple_satori_protocol_get_type()) +G_DECLARE_FINAL_TYPE(PurpleSatoriProtocol, purple_satori_protocol, PURPLE_SATORI, + PROTOCOL, PurpleProtocol) + +G_GNUC_INTERNAL void purple_satori_protocol_register(GPluginNativePlugin *plugin); + +G_GNUC_INTERNAL PurpleProtocol *purple_satori_protocol_new(void); + +G_END_DECLS + +#endif /* PURPLE_SATORI_PROTOCOL_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatoriprotocolcontacts.c Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,57 @@ +/* + * 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/gi18n-lib.h> + +#include "purplesatoriprotocolcontacts.h" + +/****************************************************************************** + * PurpleProtocolContacts Implementation + *****************************************************************************/ +static char * +purple_satori_protocol_contacts_get_profile_finish(G_GNUC_UNUSED PurpleProtocolContacts *contacts, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail(G_IS_TASK(result), NULL); + + return g_task_propagate_pointer(G_TASK(result), error); +} + +static void +purple_satori_protocol_contacts_get_profile_async(PurpleProtocolContacts *contacts, + PurpleContactInfo *info, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data) +{ + GTask *task = NULL; + const char *profile = NULL; + + task = g_task_new(contacts, cancellable, callback, data); + + profile = g_object_get_data(G_OBJECT(info), "satori-profile"); + g_task_return_pointer(task, g_strdup(profile), g_free); + g_clear_object(&task); +} + +void +purple_satori_protocol_contacts_init(PurpleProtocolContactsInterface *iface) { + iface->get_profile_async = purple_satori_protocol_contacts_get_profile_async; + iface->get_profile_finish = purple_satori_protocol_contacts_get_profile_finish; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatoriprotocolcontacts.h Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,28 @@ +/* + * 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/>. + */ + +#ifndef PURPLE_SATORI_PROTOCOL_CONTACTS_H +#define PURPLE_SATORI_PROTOCOL_CONTACTS_H + +#include <glib.h> + +#include <purple.h> + +G_GNUC_INTERNAL void purple_satori_protocol_contacts_init(PurpleProtocolContactsInterface *iface); + +#endif /* PURPLE_SATORI_PROTOCOL_CONTACTS_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatoriprotocolconversation.c Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,373 @@ +/* + * 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/gi18n-lib.h> + +#include "purplesatoriprotocolconversation.h" + +#include "purplesatoriplugin.h" +#include "purplesatoriprotocol.h" + +typedef struct { + PurpleConversation *conversation; + PurpleMessage *message; +} PurpleSatoriProtocolIMInfo; + +/****************************************************************************** + * Helpers + *****************************************************************************/ +static void +purple_satori_protocol_im_info_free(PurpleSatoriProtocolIMInfo *info) { + g_clear_object(&info->conversation); + g_clear_object(&info->message); + 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 + *****************************************************************************/ +static gboolean +purple_satori_protocol_echo_im_cb(gpointer data) { + PurpleSatoriProtocolIMInfo *info = data; + + purple_conversation_write_message(info->conversation, info->message); + + return G_SOURCE_REMOVE; +} + +/****************************************************************************** + * PurpleProtocolConversation Implementation + *****************************************************************************/ +static PurpleCreateConversationDetails * +purple_satori_protocol_get_create_conversation_details(G_GNUC_UNUSED PurpleProtocolConversation *protocol, + G_GNUC_UNUSED PurpleAccount *account) +{ + return purple_create_conversation_details_new(9); +} + +static void +purple_satori_protocol_create_conversation_async(PurpleProtocolConversation *protocol, + 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; + + task = g_task_new(protocol, cancellable, callback, data); + g_task_set_source_tag(task, + purple_satori_protocol_create_conversation_async); + + participants = purple_create_conversation_details_get_participants(details); + n_participants = g_list_model_get_n_items(participants); + 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; + } + 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; + + 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); + + 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); + } + } + + 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); + + return; + } + + g_task_return_pointer(task, conversation, g_object_unref); + + g_clear_object(&task); +} + +static PurpleConversation * +purple_satori_protocol_create_conversation_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol, + GAsyncResult *result, + GError **error) +{ + GTask *task = G_TASK(result); + + g_return_val_if_fail(g_task_get_source_tag(task) == + purple_satori_protocol_create_conversation_async, + NULL); + + return g_task_propagate_pointer(task, error); +} + +static void +purple_satori_protocol_conversation_leave_conversation_async(PurpleProtocolConversation *protocol, + G_GNUC_UNUSED PurpleConversation *conversation, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data) +{ + GTask *task = NULL; + + task = g_task_new(protocol, cancellable, callback, data); + g_task_set_source_tag(task, + purple_satori_protocol_conversation_leave_conversation_async); + + g_task_return_boolean(task, TRUE); + g_clear_object(&task); +} + +static gboolean +purple_satori_protocol_conversation_leave_conversation_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol, + GAsyncResult *result, + GError **error) +{ + gpointer tag = purple_satori_protocol_conversation_leave_conversation_async; + + g_return_val_if_fail(g_async_result_is_tagged(result, tag), FALSE); + + return g_task_propagate_boolean(G_TASK(result), error); +} + +static void +purple_satori_protocol_send_message_async(G_GNUC_UNUSED PurpleProtocolConversation *protocol, + PurpleConversation *conversation, + PurpleMessage *message, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data) +{ + GTask *task = NULL; + + if(purple_conversation_is_dm(conversation)) { + PurpleAccount *account = NULL; + PurpleContact *contact = NULL; + PurpleContactInfo *contact_info = NULL; + PurpleContactManager *manager = NULL; + PurpleConversationMember *member = NULL; + PurpleConversationMembers *members = NULL; + + account = purple_conversation_get_account(conversation); + members = purple_conversation_get_members(conversation); + + manager = purple_contact_manager_get_default(); + + /* Check if this dm is with echo. */ + contact = purple_contact_manager_find_with_id(manager, account, + "echo"); + contact_info = PURPLE_CONTACT_INFO(contact); + member = purple_conversation_members_find_member(members, contact_info); + if(PURPLE_IS_CONVERSATION_MEMBER(member)) { + PurpleSatoriProtocolIMInfo *info = NULL; + const char *contents = purple_message_get_contents(message); + + info = g_new(PurpleSatoriProtocolIMInfo, 1); + info->conversation = g_object_ref(conversation); + info->message = purple_message_new(member, contents); + purple_message_set_edited(info->message, + purple_message_get_edited(message)); + + g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, + purple_satori_protocol_echo_im_cb, info, + (GDestroyNotify)purple_satori_protocol_im_info_free); + } + + /* Check if this dm is with aegina. */ + contact = purple_contact_manager_find_with_id(manager, account, + "aegina"); + contact_info = PURPLE_CONTACT_INFO(contact); + member = purple_conversation_members_find_member(members, contact_info); + if(PURPLE_IS_CONVERSATION_MEMBER(member)) { + PurpleSatoriProtocolIMInfo *info = g_new(PurpleSatoriProtocolIMInfo, 1); + PurpleConversationMember *author = purple_message_get_author(message); + PurpleContactInfo *author_info = NULL; + const char *contents = NULL; + const char *author_id = NULL; + + author_info = purple_conversation_member_get_contact_info(author); + author_id = purple_contact_info_get_id(author_info); + if(purple_strequal(author_id, "hades")) { + contents = "🫥️"; + } else { + /* TRANSLATORS: This is a reference to the Cap of Invisibility owned by + * various Greek gods, such as Hades, as mentioned. */ + contents = _("Don't tell Hades I have his Cap"); + } + + info->conversation = g_object_ref(conversation); + info->message = purple_message_new(member, contents); + + g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, purple_satori_protocol_echo_im_cb, + info, (GDestroyNotify)purple_satori_protocol_im_info_free); + } + } + + purple_conversation_write_message(conversation, message); + + task = g_task_new(protocol, cancellable, callback, data); + g_task_return_boolean(task, TRUE); + + g_clear_object(&task); +} + +static gboolean +purple_satori_protocol_send_message_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail(G_IS_TASK(result), FALSE); + + return g_task_propagate_boolean(G_TASK(result), error); +} + +void +purple_satori_protocol_conversation_init(PurpleProtocolConversationInterface *iface) { + iface->get_create_conversation_details = + purple_satori_protocol_get_create_conversation_details; + iface->create_conversation_async = + purple_satori_protocol_create_conversation_async; + iface->create_conversation_finish = + purple_satori_protocol_create_conversation_finish; + + iface->leave_conversation_async = + purple_satori_protocol_conversation_leave_conversation_async; + iface->leave_conversation_finish = + purple_satori_protocol_conversation_leave_conversation_finish; + + iface->send_message_async = purple_satori_protocol_send_message_async; + iface->send_message_finish = purple_satori_protocol_send_message_finish; +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatoriprotocolconversation.h Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,28 @@ +/* + * 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/>. + */ + +#ifndef PURPLE_SATORI_PROTOCOL_CONVERSATION_H +#define PURPLE_SATORI_PROTOCOL_CONVERSATION_H + +#include <glib.h> + +#include <purple.h> + +G_GNUC_INTERNAL void purple_satori_protocol_conversation_init(PurpleProtocolConversationInterface *iface); + +#endif /* PURPLE_SATORI_PROTOCOL_CONVERSATION_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/satoriapi.c Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,151 @@ +/* + * 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 "satorimessage.h" +#include "satoriapi.h" + +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; + + 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); + } + } + +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); +}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/satoriapi.h Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,26 @@ +/* + * 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/>. + */ + +#ifndef SATORI_API_H +#define SATORI_API_H + +#include "purplesatoriconnection.h" + +void satori_refresh_buddy_contacts(PurpleSatoriConnection *con, const gchar *next); + +#endif /* SATORI_API_H */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/satorimessage.h Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,113 @@ +/* + * 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/>. + */ + +#ifndef SATORI_MESSAGE_H +#define SATORI_MESSAGE_H + +#include <glib.h> +#include <glib-object.h> +#include <libsoup/soup.h> +#include <libsoup/soup-message.h> +#include <json-glib/json-glib.h> + +#include "satoritypes.h" +#include "purplesatoriconnection.h" + +#define SATORI_ENDPOINT(path) "http://" PURPLE_SATORI_HOST path + +#define JBO(b) json_builder_begin_object(b) +#define JEO(b) json_builder_end_object(b) +#define JBSN(b, name) json_builder_set_member_name(b, name) +#define JBSI(b, val) json_builder_add_int_value(b, val) +#define JBSS(b, val) json_builder_add_string_value(b, val) +#define JBA(b, name, val) (JBSN(b, name), JBSS(b, val)) +#define JBAI(b, name, val) (JBSN(b, name), JBSI(b, val)) + +static inline GBytes * +JB2GBYTES(JsonBuilder *builder) +{ + JsonNode *root = json_builder_get_root(builder); + JsonGenerator *gen = json_generator_new(); + json_generator_set_root(gen, root); + + gchar *json_str = json_generator_to_data(gen, NULL); + GBytes *bytes = g_bytes_new_take(json_str, strlen(json_str)); + + json_node_free(root); + g_object_unref(gen); + + return bytes; +} + +#define JB_BEGIN_OBJ(name) \ + JsonBuilder *name = json_builder_new(); \ + JBO(name); + +#define JB_END_OBJ(lval, name) \ + do { \ + JEO(b); \ + lval = JB2GBYTES(name); \ + g_object_unref(b); \ + } while (0) + +static inline SoupMessage * +satori_message_new(const gchar *method, const gchar *url) +{ + SoupMessage *msg = soup_message_new(method, url); + SoupMessageHeaders *headers = soup_message_get_request_headers(msg); + soup_message_headers_append(headers, "Satori-Platform", + PURPLE_SATORI_PLATFORM); + soup_message_headers_append(headers, "Satori-User-ID", + PURPLE_SATORI_USER_ID); + return msg; +} + +static inline GBytes * +satori_message_gen_ident(const gchar *token, gint sn) +{ + GBytes *msg = NULL; + + JB_BEGIN_OBJ(b); + JBAI(b, "op", SATORI_WEBSOCKET_OP_IDENTIFY); + JBSN(b, "body"); + + JBO(b); + if (token) + JBA(b, "token", token); + if (sn) + JBAI(b, "sn", sn); + JEO(b); + + JB_END_OBJ(msg, b); + 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 */
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/satoritypes.h Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,41 @@ +/* + * 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/>. + */ + +#ifndef SATORI_TYPES_H +#define SATORI_TYPES_H + +#include <glib.h> + +typedef enum { + SATORI_WEBSOCKET_OP_EVENT = 0, + SATORI_WEBSOCKET_OP_PING, + SATORI_WEBSOCKET_OP_PONG, + SATORI_WEBSOCKET_OP_IDENTIFY, + SATORI_WEBSOCKET_OP_READY, + SATORI_WEBSOCKET_OP_META, +} SatoriWebsocketOpcode; + +typedef struct { + const gchar *id; + const gchar *name; + const gchar *nick; + const gchar *avatar; + gboolean is_bot; +} SatoriUser; + +#endif /* SATORI_TYPES_H */