Sun, 10 Aug 2025 23:03:27 +0800
satoriformat.{c,h}: Add message parsing support
/* * 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 "purplesatoriconnection.h" #include "satoriapi.h" #include <glib.h> #include <pango/pango-attributes.h> #include <string.h> 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("<root>%s</root>", 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); }