purplesatoriconnection.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
parent 1
98bcf06036b8
child 3
33a7b189a2c6
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 <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 <time.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, {});

/******************************************************************************
 * PurpleConversation Helpers
 *****************************************************************************/

static void
purple_satori_handle_pending_message(PurpleSatoriConnection *con,
				     SatoriUser *user,
				     SatoriChannel *chan,
				     JsonObject *msg_obj)
{
	PurpleConversation *conversation = \
		purple_satori_add_conversation_from_chan(con, chan);
	PurpleConversationMember *mbr = \
		purple_satori_add_conversation_member_from_user(
			con, conversation, user);

	const gchar *id = json_object_get_string_member_with_default(
		msg_obj, "id", NULL);
	const gchar *text = 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);
	if (id) purple_message_set_id(message, id);

	if (created_at) {
		GDateTime *ts = g_date_time_new_from_unix_local(created_at);
		purple_message_set_timestamp(message, ts);
		g_date_time_unref(ts);
	} else purple_message_set_timestamp_now(message);

	purple_conversation_write_message(conversation, message);
	g_clear_object(&message);
}

/******************************************************************************
 * 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);
		satori_refresh_conversations(con, NULL);
		break;
	}

	case SATORI_WEBSOCKET_OP_EVENT:
	{
		const gchar *type = json_object_get_string_member(body, "type");
		if (purple_strequal(type, "message-created")) {
			JsonObject *obj = json_object_get_object_member(
				body, "message");

			JsonObject *usr_obj = json_object_get_object_member(
				json_object_get_object_member(body, "member"),
				"user");

			if (!usr_obj)
				usr_obj = \
					json_object_get_object_member(body, "user");

			if (!usr_obj) break;

			JsonObject *chan_obj = \
				json_object_get_object_member(body, "channel");

			if (!chan_obj) break;

			SatoriChannel chan;
			SatoriUser usr;
			satori_channel_from_json(chan_obj, &chan);
			satori_user_from_json(usr_obj, &usr);

			purple_satori_handle_pending_message(con, &usr, &chan, obj);
		}
		break;
	}

	default:		/* ignored */
		break;

	}

	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);
}

mercurial