satoriformat.c

Sun, 10 Aug 2025 23:03:27 +0800

author
Gong Zhile <gongzl@stu.hebust.edu.cn>
date
Sun, 10 Aug 2025 23:03:27 +0800
changeset 2
efafd19ab2fe
permissions
-rw-r--r--

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, "&amp;", "&", 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);
}

mercurial