Sun, 10 Aug 2025 23:53:22 +0800
Various improvement, Support configuration from UI
| purplesatoriconnection.c | file | annotate | diff | comparison | revisions | |
| purplesatoriconnection.h | file | annotate | diff | comparison | revisions | |
| purplesatoriprotocol.c | file | annotate | diff | comparison | revisions | |
| purplesatoriprotocolconversation.c | file | annotate | diff | comparison | revisions | |
| satoriapi.c | file | annotate | diff | comparison | revisions | |
| satoriapi.h | file | annotate | diff | comparison | revisions | |
| satorimessage.h | file | annotate | diff | comparison | revisions |
--- a/purplesatoriconnection.c Sun Aug 10 23:03:27 2025 +0800 +++ b/purplesatoriconnection.c Sun Aug 10 23:53:22 2025 +0800 @@ -30,9 +30,11 @@ #include <time.h> #include "purplesatoriconnection.h" +#include "pango/pango-attributes.h" #include "purplesatoriprotocolcontacts.h" #include "satorimessage.h" #include "satoritypes.h" +#include "satoriformat.h" #include "satoriapi.h" struct _PurpleSatoriConnection { @@ -64,14 +66,22 @@ const gchar *id = json_object_get_string_member_with_default( msg_obj, "id", NULL); - const gchar *text = json_object_get_string_member_with_default( + const gchar *html = 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); + PangoAttrList *attrs = pango_attr_list_new(); + GString *text = NULL; + satori_format_html_to_purple(conversation, html, &text, attrs); + + PurpleMessage *message = purple_message_new(mbr, text->str); if (id) purple_message_set_id(message, id); + purple_message_set_attributes(message, attrs); + + pango_attr_list_unref(attrs); + g_free(text); if (created_at) { GDateTime *ts = g_date_time_new_from_unix_local(created_at); @@ -112,8 +122,6 @@ 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"); @@ -144,6 +152,10 @@ purple_contact_info_set_display_name(ci, user.nick ? user.nick : user.name); + PurplePresence *presence = purple_contact_info_get_presence(ci); + purple_presence_set_primitive(presence, + PURPLE_PRESENCE_PRIMITIVE_AVAILABLE); + satori_refresh_buddy_contacts(con, NULL); satori_refresh_conversations(con, NULL); break; @@ -221,20 +233,25 @@ G_GNUC_UNUSED GError **error) { PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION(connection); + PurpleAccount *acc = purple_connection_get_account(connection); if (con->wscon) g_object_unref(con->wscon); - SoupMessage *svmsg = satori_message_new("GET", PURPLE_SATORI_WSURL); + gchar *wsurl = \ + g_strdup_printf("%s://%s%s/events", + purple_account_get_bool(acc, "https", FALSE) ? "wss" : "ws", + purple_account_get_string(acc, "host", "127.0.0.1:5600"), + purple_account_get_string(acc, "path", "/v1")); + + SoupMessage *svmsg = soup_message_new("GET", 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); */ - + g_free(wsurl); return TRUE; }
--- a/purplesatoriconnection.h Sun Aug 10 23:03:27 2025 +0800 +++ b/purplesatoriconnection.h Sun Aug 10 23:53:22 2025 +0800 @@ -22,21 +22,39 @@ #include <glib.h> #include <purple.h> #include <libsoup/soup.h> +#include <libsoup/soup-types.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); +static inline SoupMessage * +SATORI_ENDPOINT(PurpleSatoriConnection *con, const gchar *p) { + PurpleAccount *acc = \ + purple_connection_get_account(PURPLE_CONNECTION(con)); + + gchar *url = \ + g_strdup_printf("%s://%s%s%s", + purple_account_get_bool(acc, "https", FALSE) ? "https" : "http", + purple_account_get_string(acc, "host", "127.0.0.1:5600"), + purple_account_get_string(acc, "path", "/v1"), p); + + SoupMessage *msg = soup_message_new("POST", url); + SoupMessageHeaders *headers = soup_message_get_request_headers(msg); + + soup_message_headers_append(headers, "Satori-Platform", + purple_account_get_string(acc, "platform", "")); + soup_message_headers_append(headers, "Satori-User-ID", + purple_account_get_username(acc)); + + g_free(url); + return msg; +} + void purple_satori_connection_send_and_read_async(PurpleSatoriConnection *con, SoupMessage *msg,
--- a/purplesatoriprotocol.c Sun Aug 10 23:03:27 2025 +0800 +++ b/purplesatoriprotocol.c Sun Aug 10 23:53:22 2025 +0800 @@ -20,6 +20,7 @@ #include "purplesatoriprotocol.h" +#include "glib.h" #include "purplesatoriconnection.h" #include "purplesatoriprotocolcontacts.h" #include "purplesatoriprotocolconversation.h" @@ -48,6 +49,34 @@ } +static GList * +purple_satori_protocol_get_account_options(G_GNUC_UNUSED PurpleProtocol *protocol) +{ + PurpleAccountOption *option = NULL; + GList *options = NULL; + + option = purple_account_option_string_new(_("Satori Platform"), "platform", + "QQ"); + options = g_list_append(options, option); + + option = purple_account_option_bool_new(_("Secure Connection (HTTPS)"), + "https", FALSE); + options = g_list_append(options, option); + + option = purple_account_option_string_new(_("API Host"), + "host", "127.0.0.1:5600"); + options = g_list_append(options, option); + + option = purple_account_option_string_new(_("API Path"), "path", "/v1"), + options = g_list_append(options, option); + + option = purple_account_option_string_new(_("API Token"), "token", NULL); + purple_account_option_string_set_masked(option, TRUE); + options = g_list_append(options, option); + + return options; +} + /****************************************************************************** * GObject Implementation *****************************************************************************/ @@ -73,6 +102,7 @@ purple_satori_protocol_class_init(PurpleSatoriProtocolClass *klass) { PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass); + protocol_class->get_account_options = purple_satori_protocol_get_account_options; protocol_class->create_connection = purple_satori_protocol_create_connection; }
--- a/purplesatoriprotocolconversation.c Sun Aug 10 23:03:27 2025 +0800 +++ b/purplesatoriprotocolconversation.c Sun Aug 10 23:53:22 2025 +0800 @@ -20,39 +20,13 @@ #include "purplesatoriprotocolconversation.h" +#include "glib.h" #include "purplesatoriconnection.h" #include "purplesatoriplugin.h" #include "purplesatoriprotocol.h" #include "satoriapi.h" #include "satoritypes.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); -} - -/****************************************************************************** - * 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 *****************************************************************************/ @@ -60,7 +34,7 @@ purple_satori_protocol_get_create_conversation_details(G_GNUC_UNUSED PurpleProtocolConversation *protocol, G_GNUC_UNUSED PurpleAccount *account) { - return purple_create_conversation_details_new(0); + return purple_create_conversation_details_new(2); } static void @@ -128,113 +102,28 @@ } 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) +purple_satori_protocol_send_message_async(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); - } - } + PurpleSatoriConnection *con = \ + PURPLE_SATORI_CONNECTION(purple_conversation_get_connection(conversation)); purple_conversation_write_message(conversation, message); - task = g_task_new(protocol, cancellable, callback, data); - g_task_return_boolean(task, TRUE); + GTask *task = g_task_new(protocol, cancellable, callback, data); + g_task_set_source_tag(task, + purple_satori_protocol_send_message_async); - g_clear_object(&task); + gchar *content = \ + g_markup_escape_text(purple_message_get_contents(message), -1); + satori_send_message(con, conversation, message, content, task); + g_free(content); + + return; } static gboolean @@ -256,11 +145,6 @@ 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; }
--- a/satoriapi.c Sun Aug 10 23:03:27 2025 +0800 +++ b/satoriapi.c Sun Aug 10 23:53:22 2025 +0800 @@ -25,6 +25,7 @@ #include "purplesatoriconnection.h" #include "purplesatoriplugin.h" +#include "purplesatoriprotocolconversation.h" #include "satorimessage.h" #include "satoritypes.h" #include "satoriapi.h" @@ -153,9 +154,12 @@ found = FALSE; } - purple_contact_info_set_display_name( - PURPLE_CONTACT_INFO(contact), - user->nick ? user->nick : user->name); + if (user->nick) + purple_contact_info_set_display_name( + PURPLE_CONTACT_INFO(contact), user->nick); + else if (user->name) + purple_contact_info_set_display_name( + PURPLE_CONTACT_INFO(contact), user->name); PurplePresence *presence = purple_contact_info_get_presence( PURPLE_CONTACT_INFO(contact)); @@ -252,8 +256,7 @@ JB_END_OBJ(data, b); } - SoupMessage *msg = satori_message_new( - "POST", SATORI_ENDPOINT("/v1/friend.list")); + SoupMessage *msg = SATORI_ENDPOINT(con, "/friend.list"); soup_message_set_request_body_from_bytes(msg, "application/json", data); purple_satori_connection_send_and_read_async( @@ -337,8 +340,7 @@ JB_END_OBJ(data, b); } - SoupMessage *msg = satori_message_new( - "POST", SATORI_ENDPOINT("/v1/channel.list")); + SoupMessage *msg = SATORI_ENDPOINT(con, "/channel.list"); soup_message_set_request_body_from_bytes(msg, "application/json", data); purple_satori_connection_send_and_read_async( @@ -412,8 +414,7 @@ JB_END_OBJ(data, b); } - SoupMessage *msg = satori_message_new( - "POST", SATORI_ENDPOINT("/v1/guild.list")); + SoupMessage *msg = SATORI_ENDPOINT(con, "/guild.list"); soup_message_set_request_body_from_bytes(msg, "application/json", data); purple_satori_connection_send_and_read_async( @@ -425,6 +426,10 @@ g_bytes_unref(data); } +/****************************************************************************** + * DM Creation + *****************************************************************************/ + typedef struct { PurpleSatoriConnection *con; SatoriUser user; @@ -497,8 +502,7 @@ JB_END_OBJ(data, b); } - SoupMessage *msg = satori_message_new( - "POST", SATORI_ENDPOINT("/v1/user.channel.create")); + SoupMessage *msg = SATORI_ENDPOINT(con, "/user.channel.create"); soup_message_set_request_body_from_bytes(msg, "application/json", data); SatoriOnDmChannelData *dptr = g_new0(SatoriOnDmChannelData, 1); @@ -515,3 +519,110 @@ g_object_unref(msg); g_bytes_unref(data); } + +/****************************************************************************** + * Message Routines + *****************************************************************************/ + +typedef struct { + PurpleConversation *conversation; + PurpleMessage *message; + GTask *task; +} SatoriSendMessageData; + +static void +satori_on_message_sent(SoupSession *session, + GAsyncResult *res, + SatoriSendMessageData *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; + } + + /* Initialize ID */ + + JsonArray *root = json_node_get_array(json_parser_get_root(parser)); + JsonObject *msg_obj = json_array_get_object_element(root, 0); + const gchar *id = json_object_get_string_member_with_default( + msg_obj, "id", NULL); + + if (!id) { + g_task_return_new_error_literal(dptr->task, PURPLE_SATORI_DOMAIN, 0, + "message not found"); + goto cleanup; + } + + purple_message_set_id(dptr->message, id); + purple_message_set_delivered(dptr->message, TRUE); + + /* Initialize created_time */ + + time_t created_at = json_object_get_int_member_with_default( + msg_obj, "created_at", 0) / 1000; /* timestamp in mS */ + + if (created_at) { + GDateTime *ts = g_date_time_new_from_unix_local(created_at); + purple_message_set_delivered_at(dptr->message, ts); + g_date_time_unref(ts); + } + + g_task_return_boolean(dptr->task, TRUE); + g_object_unref(parser); +cleanup: + g_clear_object(&dptr->task); + g_free(dptr); +} + +void +satori_send_message(PurpleSatoriConnection *con, + PurpleConversation *conversation, + PurpleMessage *message, + const gchar *content, + GTask *task) +{ + GBytes *data = NULL; + + { + JB_BEGIN_OBJ(b); + JBA(b, "channel_id", purple_conversation_get_id(conversation)); + JBA(b, "content", content); + JB_END_OBJ(data, b); + } + + SoupMessage *msg = SATORI_ENDPOINT(con, "/message.create"); + soup_message_set_request_body_from_bytes(msg, "application/json", data); + + SatoriSendMessageData *dptr = g_new0(SatoriSendMessageData, 1); + dptr->conversation = conversation; + dptr->message = message; + dptr->task = task; + + purple_satori_connection_send_and_read_async( + con, msg, 0, NULL, + (GAsyncReadyCallback) satori_on_message_sent, + dptr); + + g_object_unref(msg); + g_bytes_unref(data); +}
--- a/satoriapi.h Sun Aug 10 23:03:27 2025 +0800 +++ b/satoriapi.h Sun Aug 10 23:53:22 2025 +0800 @@ -30,5 +30,7 @@ void satori_refresh_buddy_contacts(PurpleSatoriConnection *con, const gchar *next); void satori_refresh_conversations(PurpleSatoriConnection *con, const gchar *next); void satori_create_dm_channel(PurpleSatoriConnection *con, SatoriUser *user, GTask *task); +void +satori_send_message(PurpleSatoriConnection *con, PurpleConversation *conversation, PurpleMessage *message, const gchar *content, GTask *task); #endif /* SATORI_API_H */
--- a/satorimessage.h Sun Aug 10 23:03:27 2025 +0800 +++ b/satorimessage.h Sun Aug 10 23:53:22 2025 +0800 @@ -28,8 +28,6 @@ #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) @@ -65,18 +63,6 @@ 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) {