--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/purplesatoriprotocolconversation.c Fri Aug 08 09:46:55 2025 +0800 @@ -0,0 +1,373 @@ +/* + * 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/gi18n-lib.h> + +#include "purplesatoriprotocolconversation.h" + +#include "purplesatoriplugin.h" +#include "purplesatoriprotocol.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); +} + +static gint +purple_satori_protocol_contact_sort(gconstpointer a, gconstpointer b, + G_GNUC_UNUSED gpointer data) +{ + return purple_contact_info_compare(PURPLE_CONTACT_INFO((gpointer)a), + PURPLE_CONTACT_INFO((gpointer)b)); +} + +static char * +purple_satori_protocol_generate_conversation_id(PurpleAccount *account, + PurpleCreateConversationDetails *details) +{ + GChecksum *checksum = NULL; + GListModel *participants = NULL; + GListStore *sorted = NULL; + char *ret = NULL; + const char *id = NULL; + + /* Sort the participants. */ + sorted = g_list_store_new(PURPLE_TYPE_CONTACT); + participants = purple_create_conversation_details_get_participants(details); + for(guint i = 0; i < g_list_model_get_n_items(participants); i++) { + PurpleContactInfo *info = NULL; + + info = g_list_model_get_item(participants, i); + g_list_store_insert_sorted(sorted, info, + purple_satori_protocol_contact_sort, + NULL); + g_clear_object(&info); + } + + /* Build a checksum of the account and the sorted participants. */ + checksum = g_checksum_new(G_CHECKSUM_SHA256); + + id = purple_account_get_id(account); + g_checksum_update(checksum, (guchar *)id, -1); + + for(guint i = 0; i < g_list_model_get_n_items(G_LIST_MODEL(sorted)); i++) { + PurpleContactInfo *info = NULL; + + info = g_list_model_get_item(G_LIST_MODEL(sorted), i); + id = purple_contact_info_get_id(info); + g_checksum_update(checksum, (guchar *)id, -1); + g_clear_object(&info); + } + + ret = g_strdup(g_checksum_get_string(checksum)); + + g_clear_pointer(&checksum, g_checksum_free); + g_clear_object(&sorted); + + return ret; +} + +/****************************************************************************** + * 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 + *****************************************************************************/ +static PurpleCreateConversationDetails * +purple_satori_protocol_get_create_conversation_details(G_GNUC_UNUSED PurpleProtocolConversation *protocol, + G_GNUC_UNUSED PurpleAccount *account) +{ + return purple_create_conversation_details_new(9); +} + +static void +purple_satori_protocol_create_conversation_async(PurpleProtocolConversation *protocol, + PurpleAccount *account, + PurpleCreateConversationDetails *details, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer data) +{ + PurpleConversation *conversation = NULL; + PurpleConversationManager *manager = NULL; + PurpleConversationType type = PURPLE_CONVERSATION_TYPE_UNSET; + GListModel *participants = NULL; + GTask *task = NULL; + char *id = NULL; + guint n_participants = 0; + + task = g_task_new(protocol, cancellable, callback, data); + g_task_set_source_tag(task, + purple_satori_protocol_create_conversation_async); + + participants = purple_create_conversation_details_get_participants(details); + n_participants = g_list_model_get_n_items(participants); + if(n_participants == 0) { + g_task_return_new_error_literal(task, PURPLE_SATORI_DOMAIN, 0, + _("no participants were provided")); + g_clear_object(&task); + + return; + } + + if(n_participants == 1) { + type = PURPLE_CONVERSATION_TYPE_DM; + } else { + type = PURPLE_CONVERSATION_TYPE_GROUP_DM; + } + id = purple_satori_protocol_generate_conversation_id(account, details); + + conversation = g_object_new( + PURPLE_TYPE_CONVERSATION, + "account", account, + "id", id, + "type", type, + "online", TRUE, + NULL); + g_clear_pointer(&id, g_free); + + for(guint i = 0; i < g_list_model_get_n_items(participants); i++) { + PurpleContactInfo *info = NULL; + PurpleConversationMember *member = NULL; + PurpleConversationMembers *members = NULL; + PurpleTags *tags = NULL; + const char *badge_id = NULL; + + info = g_list_model_get_item(participants, i); + members = purple_conversation_get_members(conversation); + member = purple_conversation_members_add_member(members, info, FALSE, + NULL); + + tags = purple_contact_info_get_tags(info); + badge_id = purple_tags_get(tags, "satori-badge"); + if(!purple_strempty(badge_id)) { + PurpleBadge *badge = NULL; + PurpleBadgeManager *manager = NULL; + PurpleBadges *badges = NULL; + + badges = purple_conversation_member_get_badges(member); + + manager = purple_badge_manager_get_default(); + badge = purple_badge_manager_find(manager, badge_id); + if(PURPLE_IS_BADGE(badge)) { + purple_badges_add_badge(badges, badge); + } else { + char *icon_name = NULL; + char *id = NULL; + + id = g_strdup_printf("satori-badge-%s", badge_id); + icon_name = g_strdup_printf("satori-badge-%s", badge_id); + badge = purple_badge_new(id, 0, icon_name, " "); + purple_badge_set_description(badge, badge_id); + g_free(id); + g_free(icon_name); + + purple_badge_manager_add(manager, badge); + purple_badges_add_badge(badges, badge); + g_clear_object(&badge); + } + } + + g_clear_object(&info); + } + g_clear_object(&details); + + manager = purple_conversation_manager_get_default(); + if(!purple_conversation_manager_add(manager, conversation)) { + g_task_return_new_error(task, PURPLE_SATORI_DOMAIN, 0, + _("This conversation already exists.")); + g_clear_object(&task); + + return; + } + + g_task_return_pointer(task, conversation, g_object_unref); + + g_clear_object(&task); +} + +static PurpleConversation * +purple_satori_protocol_create_conversation_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol, + GAsyncResult *result, + GError **error) +{ + GTask *task = G_TASK(result); + + g_return_val_if_fail(g_task_get_source_tag(task) == + purple_satori_protocol_create_conversation_async, + NULL); + + return g_task_propagate_pointer(task, error); +} + +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) +{ + 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); + } + } + + purple_conversation_write_message(conversation, message); + + task = g_task_new(protocol, cancellable, callback, data); + g_task_return_boolean(task, TRUE); + + g_clear_object(&task); +} + +static gboolean +purple_satori_protocol_send_message_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail(G_IS_TASK(result), FALSE); + + return g_task_propagate_boolean(G_TASK(result), error); +} + +void +purple_satori_protocol_conversation_init(PurpleProtocolConversationInterface *iface) { + iface->get_create_conversation_details = + purple_satori_protocol_get_create_conversation_details; + iface->create_conversation_async = + purple_satori_protocol_create_conversation_async; + 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; +}