# HG changeset patch # User Gong Zhile # Date 1754838207 -28800 # Node ID efafd19ab2feff9e211c67d6eec4b398088d784d # Parent 98bcf06036b89777cf344947be14ae83480b58e3 satoriformat.{c,h}: Add message parsing support diff -r 98bcf06036b8 -r efafd19ab2fe meson.build --- a/meson.build Sat Aug 09 00:19:03 2025 +0800 +++ b/meson.build Sun Aug 10 23:03:27 2025 +0800 @@ -3,6 +3,7 @@ libpurple_dep = dependency('purple-3') libsoup_dep = dependency('libsoup-3.0') libjson_dep = dependency('json-glib-1.0') +libpango_dep = dependency('pango') PURPLE_PLUGINDIR = libpurple_dep.get_pkgconfig_variable('plugindir') PURPLE_SATORI_SRCS = [ @@ -12,6 +13,7 @@ 'purplesatoriprotocolcontacts.c', 'purplesatoriprotocolconversation.c', 'satoriapi.c', + 'satoriformat.c', ] library('libpurplesatori', PURPLE_SATORI_SRCS, @@ -20,7 +22,7 @@ '-DG_LOG_DOMAIN="Purple-Satori"', '-DGETTEXT_PACKAGE="pidgin3"' ], - dependencies : [libpurple_dep, libsoup_dep, libjson_dep], + dependencies : [libpurple_dep, libsoup_dep, libjson_dep, libpango_dep], name_prefix : '', install : true, install_dir : PURPLE_PLUGINDIR) diff -r 98bcf06036b8 -r efafd19ab2fe satoriformat.c --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/satoriformat.c Sun Aug 10 23:03:27 2025 +0800 @@ -0,0 +1,250 @@ +/* + * 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 . + */ + +#include "purplesatoriconnection.h" +#include "satoriapi.h" +#include +#include +#include + +typedef struct { + PangoAttribute *attr; + gint start; +} SatoriHtmlTag; + +typedef struct { + PurpleConversation *conversation; + PangoAttrList *attrs; + GArray *tag_stack; + GString *out; +} SatoriHtmlParseData; + +static SatoriHtmlTag * +satori_html_push(SatoriHtmlParseData *data) +{ + SatoriHtmlTag t = { .attr = NULL, .start = data->out->len }; + g_array_append_val(data->tag_stack, t); + return &g_array_index(data->tag_stack, + SatoriHtmlTag, + data->tag_stack->len - 1); +} + +static void +satori_html_pop(SatoriHtmlParseData *data) +{ + g_assert(data->tag_stack->len); + + SatoriHtmlTag *top = &g_array_index(data->tag_stack, + SatoriHtmlTag, + data->tag_stack->len - 1); + + if (top->attr) { + top->attr->start_index = top->start; + top->attr->end_index = data->out->len; + pango_attr_list_insert(data->attrs, top->attr); + top->attr = NULL; + } + + g_array_remove_index(data->tag_stack, data->tag_stack->len - 1); +} + +static void +satori_format_html_start_element_cb(GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + SatoriHtmlParseData *dptr = user_data; + PurpleSatoriConnection *con = PURPLE_SATORI_CONNECTION( + purple_conversation_get_connection(dptr->conversation)); + + SatoriHtmlTag *tag = satori_html_push(dptr); + + if (g_str_equal(element_name, "quote")) { + g_string_append_printf(dptr->out, "> "); + tag->attr = pango_attr_background_new( + (guint16)(0.8 * 65535), + (guint16)(0.8 * 65535), + (guint16)(0.8 * 65535)); + return; + } else if (g_str_equal(element_name, "at")) { + /* Mention Handling */ + + const gchar *id_value = NULL, *name_value = NULL, *type_value = NULL; + for (int i = 0; attribute_names[i]; i++) { + if (g_str_equal(attribute_names[i], "id")) + id_value = attribute_values[i]; + else if (g_str_equal(attribute_names[i], "name")) + name_value = attribute_values[i]; + else if (g_str_equal(attribute_names[i], "type")) + type_value = attribute_names[i]; + } + + if (!id_value && !name_value && !type_value) + return; + + if (name_value && !id_value) { + g_string_append_printf(dptr->out, "@%s", name_value); + } else { + SatoriUser user = { + .id = id_value, + .name = name_value, + .nick = name_value, + }; + + PurpleConversationMember *member = \ + purple_satori_add_conversation_member_from_user( + con, dptr->conversation, &user); + PurpleContactInfo *info = \ + purple_conversation_member_get_contact_info(member); + + const gchar *display_name = \ + purple_contact_info_get_name_for_display(info); + + g_string_append_printf( + dptr->out, "@%s", + (!display_name || !*display_name) + ? user.id : display_name); + } + + PurpleContactInfo *me = + purple_account_get_contact_info( + purple_conversation_get_account(dptr->conversation)); + + if (dptr->attrs) { + PangoAttribute *bg = NULL; + if (id_value && + g_str_equal(purple_contact_info_get_id(me), id_value)) + bg = pango_attr_background_new( + (guint16)(100.0 / 255 * 65535), + (guint16)(149.0 / 255 * 65535), + (guint16)(237.0 / 255 * 65535)); + else + bg = pango_attr_background_new( + (guint16)(240.0 / 255 * 65535), + (guint16)(248.0 / 255 * 65535), + (guint16)(255.0 / 255 * 65535)); + tag->attr = bg; + } + + return; + } else if (g_str_equal(element_name, "img")) { + /* Image Handling */ + + GString *src_value = NULL; + for (int i = 0; attribute_names[i]; i++) { + if (g_str_equal(attribute_names[i], "src")) + src_value = g_string_new(attribute_values[i]); + } + + if (!src_value) { + g_string_append_printf(dptr->out, "[Image] "); + return; + } + + g_string_replace(src_value, "&", "&", 0); + g_string_append_printf(dptr->out, "[Image: %s ] ", src_value->str); + g_string_free(src_value, TRUE); + + return; + } +} + +static void +satori_format_html_end_element_cb(GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + SatoriHtmlParseData *dptr = user_data; + + if (g_str_equal(element_name, "quote")) { + g_string_append_printf(dptr->out, "\n"); + } + + satori_html_pop(dptr); +} + +static void +satori_format_html_text_cb(GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + SatoriHtmlParseData *dptr = user_data; + g_string_append_len(dptr->out, text, text_len); +} + +void +satori_format_html_to_purple(PurpleConversation *conversation, + const gchar *html, + GString **out_content, + G_GNUC_UNUSED PangoAttrList *attrs) +{ + gchar *wrapped_html = g_strdup_printf("%s", html); + + GMarkupParser parser = { + .start_element = satori_format_html_start_element_cb, + .end_element = satori_format_html_end_element_cb, + .text = satori_format_html_text_cb, + .passthrough = NULL, + .error = NULL, + }; + + *out_content = g_string_new(NULL); + + SatoriHtmlParseData dptr = { + .tag_stack = g_array_new(FALSE, FALSE, sizeof(SatoriHtmlTag)), + .conversation = conversation, + .out = *out_content, + .attrs = attrs, + }; + + GError *error = NULL; + GMarkupParseContext *context = \ + g_markup_parse_context_new(&parser, 0, &dptr, NULL); + + if (!g_markup_parse_context_parse(context, wrapped_html, + strlen(wrapped_html), &error) || + !g_markup_parse_context_end_parse(context, &error)) { + gint bi = dptr.out->len, ei = 0; + g_string_append_printf(*out_content, "[PARSE FAILED: %s]", + error ? error->message : "UNKOWN"); + ei = dptr.out->len; + + if (dptr.attrs) { + PangoAttribute *fg = \ + pango_attr_foreground_new(65535, 0, 0); + + fg->start_index = bi; + fg->end_index = ei; + + pango_attr_list_insert(dptr.attrs, fg); + + } + + if (error) + g_error_free(error); + } + + g_free(wrapped_html); + g_markup_parse_context_free(context); +} diff -r 98bcf06036b8 -r efafd19ab2fe satoriformat.h --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/satoriformat.h Sun Aug 10 23:03:27 2025 +0800 @@ -0,0 +1,31 @@ +/* + * 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 . + */ + +#ifndef SATORI_FORMAT_H +#define SATORI_FORMAT_H + +#include +#include +#include + +void +satori_format_html_to_purple(PurpleConversation *conversation, + const gchar *html, GString **out_content, + PangoAttrList *attrs); + +#endif /* SATORI_FORMAT_H */