pidgin/pidgin

Parents 5ad29b5bf1c7
Children 584895b844d2
Move the Demo and IRCv3 protocols to the new protocols directory

Testing Done:
Ran `meson dist` and `ninja turtles`

Reviewed at https://reviews.imfreedom.org/r/3047/
  • +0 -37
    libpurple/protocols/demo/meson.build
  • +0 -80
    libpurple/protocols/demo/purpledemoconnection.c
  • +0 -36
    libpurple/protocols/demo/purpledemoconnection.h
  • +0 -357
    libpurple/protocols/demo/purpledemocontacts.c
  • +0 -32
    libpurple/protocols/demo/purpledemocontacts.h
  • +0 -113
    libpurple/protocols/demo/purpledemoplugin.c
  • +0 -29
    libpurple/protocols/demo/purpledemoplugin.h
  • +0 -146
    libpurple/protocols/demo/purpledemoprotocol.c
  • +0 -41
    libpurple/protocols/demo/purpledemoprotocol.h
  • +0 -950
    libpurple/protocols/demo/purpledemoprotocolactions.c
  • +0 -28
    libpurple/protocols/demo/purpledemoprotocolactions.h
  • +0 -29
    libpurple/protocols/demo/purpledemoprotocolclient.c
  • +0 -28
    libpurple/protocols/demo/purpledemoprotocolclient.h
  • +0 -57
    libpurple/protocols/demo/purpledemoprotocolcontacts.c
  • +0 -28
    libpurple/protocols/demo/purpledemoprotocolcontacts.h
  • +0 -131
    libpurple/protocols/demo/purpledemoprotocolconversation.c
  • +0 -28
    libpurple/protocols/demo/purpledemoprotocolconversation.h
  • +0 -102
    libpurple/protocols/demo/purpledemoprotocolmedia.c
  • +0 -28
    libpurple/protocols/demo/purpledemoprotocolmedia.h
  • +0 -0
    libpurple/protocols/demo/resources/buddy_icons/Aegina.png
  • +0 -0
    libpurple/protocols/demo/resources/buddy_icons/Echo.png
  • +0 -0
    libpurple/protocols/demo/resources/buddy_icons/Eion.png
  • +0 -0
    libpurple/protocols/demo/resources/buddy_icons/Elliott.png
  • +0 -0
    libpurple/protocols/demo/resources/buddy_icons/Gary.png
  • +0 -0
    libpurple/protocols/demo/resources/buddy_icons/John.png
  • +0 -0
    libpurple/protocols/demo/resources/buddy_icons/Markus.png
  • +0 -0
    libpurple/protocols/demo/resources/buddy_icons/Richard.png
  • +0 -90
    libpurple/protocols/demo/resources/buddy_icons/mkicon.py
  • +0 -109
    libpurple/protocols/demo/resources/contacts.json
  • +0 -0
    libpurple/protocols/demo/resources/icons/16x16/apps/im-purple-demo.png
  • +0 -0
    libpurple/protocols/demo/resources/icons/22x22/apps/im-purple-demo.png
  • +0 -0
    libpurple/protocols/demo/resources/icons/48x48/apps/im-purple-demo.png
  • +0 -78
    libpurple/protocols/demo/resources/icons/scalable/apps/im-purple-demo.svg
  • +0 -18
    libpurple/protocols/demo/resources/purpledemo.gresource.xml
  • +0 -22
    libpurple/protocols/ircv3/README.md
  • +0 -32
    libpurple/protocols/ircv3/ircv3generategir.c
  • +0 -114
    libpurple/protocols/ircv3/meson.build
  • +0 -40
    libpurple/protocols/ircv3/purpleircv3.h.in
  • +0 -685
    libpurple/protocols/ircv3/purpleircv3capabilities.c
  • +0 -172
    libpurple/protocols/ircv3/purpleircv3capabilities.h
  • +0 -964
    libpurple/protocols/ircv3/purpleircv3connection.c
  • +0 -222
    libpurple/protocols/ircv3/purpleircv3connection.h
  • +0 -328
    libpurple/protocols/ircv3/purpleircv3constants.h
  • +0 -128
    libpurple/protocols/ircv3/purpleircv3core.c
  • +0 -39
    libpurple/protocols/ircv3/purpleircv3core.h
  • +0 -160
    libpurple/protocols/ircv3/purpleircv3ctcp.c
  • +0 -68
    libpurple/protocols/ircv3/purpleircv3ctcp.h
  • +0 -118
    libpurple/protocols/ircv3/purpleircv3formatting.c
  • +0 -52
    libpurple/protocols/ircv3/purpleircv3formatting.h
  • +0 -284
    libpurple/protocols/ircv3/purpleircv3message.c
  • +0 -164
    libpurple/protocols/ircv3/purpleircv3message.h
  • +0 -315
    libpurple/protocols/ircv3/purpleircv3messagehandlers.c
  • +0 -67
    libpurple/protocols/ircv3/purpleircv3messagehandlers.h
  • +0 -507
    libpurple/protocols/ircv3/purpleircv3parser.c
  • +0 -126
    libpurple/protocols/ircv3/purpleircv3parser.h
  • +0 -279
    libpurple/protocols/ircv3/purpleircv3protocol.c
  • +0 -71
    libpurple/protocols/ircv3/purpleircv3protocol.h
  • +0 -180
    libpurple/protocols/ircv3/purpleircv3protocolconversation.c
  • +0 -41
    libpurple/protocols/ircv3/purpleircv3protocolconversation.h
  • +0 -563
    libpurple/protocols/ircv3/purpleircv3sasl.c
  • +0 -55
    libpurple/protocols/ircv3/purpleircv3sasl.h
  • +0 -91
    libpurple/protocols/ircv3/purpleircv3source.c
  • +0 -58
    libpurple/protocols/ircv3/purpleircv3source.h
  • +0 -133
    libpurple/protocols/ircv3/purpleircv3version.h
  • +0 -0
    libpurple/protocols/ircv3/resources/icons/16x16/apps/im-ircv3.png
  • +0 -0
    libpurple/protocols/ircv3/resources/icons/22x22/apps/im-ircv3.png
  • +0 -0
    libpurple/protocols/ircv3/resources/icons/48x48/apps/im-ircv3.png
  • +0 -8
    libpurple/protocols/ircv3/resources/ircv3.gresource.xml
  • +0 -15
    libpurple/protocols/ircv3/tests/meson.build
  • +0 -232
    libpurple/protocols/ircv3/tests/test_ircv3_formatting.c
  • +0 -822
    libpurple/protocols/ircv3/tests/test_ircv3_parser.c
  • +0 -158
    libpurple/protocols/ircv3/tests/test_ircv3_source.c
  • +0 -2
    libpurple/protocols/meson.build
  • +3 -3
    po/POTFILES.in
  • +37 -0
    protocols/demo/meson.build
  • +80 -0
    protocols/demo/purpledemoconnection.c
  • +36 -0
    protocols/demo/purpledemoconnection.h
  • +357 -0
    protocols/demo/purpledemocontacts.c
  • +32 -0
    protocols/demo/purpledemocontacts.h
  • +113 -0
    protocols/demo/purpledemoplugin.c
  • +29 -0
    protocols/demo/purpledemoplugin.h
  • +146 -0
    protocols/demo/purpledemoprotocol.c
  • +41 -0
    protocols/demo/purpledemoprotocol.h
  • +950 -0
    protocols/demo/purpledemoprotocolactions.c
  • +28 -0
    protocols/demo/purpledemoprotocolactions.h
  • +29 -0
    protocols/demo/purpledemoprotocolclient.c
  • +28 -0
    protocols/demo/purpledemoprotocolclient.h
  • +57 -0
    protocols/demo/purpledemoprotocolcontacts.c
  • +28 -0
    protocols/demo/purpledemoprotocolcontacts.h
  • +131 -0
    protocols/demo/purpledemoprotocolconversation.c
  • +28 -0
    protocols/demo/purpledemoprotocolconversation.h
  • +102 -0
    protocols/demo/purpledemoprotocolmedia.c
  • +28 -0
    protocols/demo/purpledemoprotocolmedia.h
  • +0 -0
    protocols/demo/resources/buddy_icons/Aegina.png
  • +0 -0
    protocols/demo/resources/buddy_icons/Echo.png
  • +0 -0
    protocols/demo/resources/buddy_icons/Eion.png
  • +0 -0
    protocols/demo/resources/buddy_icons/Elliott.png
  • +0 -0
    protocols/demo/resources/buddy_icons/Gary.png
  • +0 -0
    protocols/demo/resources/buddy_icons/John.png
  • +0 -0
    protocols/demo/resources/buddy_icons/Markus.png
  • +0 -0
    protocols/demo/resources/buddy_icons/Richard.png
  • +90 -0
    protocols/demo/resources/buddy_icons/mkicon.py
  • +109 -0
    protocols/demo/resources/contacts.json
  • +0 -0
    protocols/demo/resources/icons/16x16/apps/im-purple-demo.png
  • +0 -0
    protocols/demo/resources/icons/22x22/apps/im-purple-demo.png
  • +0 -0
    protocols/demo/resources/icons/48x48/apps/im-purple-demo.png
  • +78 -0
    protocols/demo/resources/icons/scalable/apps/im-purple-demo.svg
  • +18 -0
    protocols/demo/resources/purpledemo.gresource.xml
  • +22 -0
    protocols/ircv3/README.md
  • +32 -0
    protocols/ircv3/ircv3generategir.c
  • +114 -0
    protocols/ircv3/meson.build
  • +40 -0
    protocols/ircv3/purpleircv3.h.in
  • +685 -0
    protocols/ircv3/purpleircv3capabilities.c
  • +172 -0
    protocols/ircv3/purpleircv3capabilities.h
  • +964 -0
    protocols/ircv3/purpleircv3connection.c
  • +222 -0
    protocols/ircv3/purpleircv3connection.h
  • +328 -0
    protocols/ircv3/purpleircv3constants.h
  • +128 -0
    protocols/ircv3/purpleircv3core.c
  • +39 -0
    protocols/ircv3/purpleircv3core.h
  • +160 -0
    protocols/ircv3/purpleircv3ctcp.c
  • +68 -0
    protocols/ircv3/purpleircv3ctcp.h
  • +118 -0
    protocols/ircv3/purpleircv3formatting.c
  • +52 -0
    protocols/ircv3/purpleircv3formatting.h
  • +284 -0
    protocols/ircv3/purpleircv3message.c
  • +164 -0
    protocols/ircv3/purpleircv3message.h
  • +315 -0
    protocols/ircv3/purpleircv3messagehandlers.c
  • +67 -0
    protocols/ircv3/purpleircv3messagehandlers.h
  • +507 -0
    protocols/ircv3/purpleircv3parser.c
  • +126 -0
    protocols/ircv3/purpleircv3parser.h
  • +279 -0
    protocols/ircv3/purpleircv3protocol.c
  • +71 -0
    protocols/ircv3/purpleircv3protocol.h
  • +180 -0
    protocols/ircv3/purpleircv3protocolconversation.c
  • +41 -0
    protocols/ircv3/purpleircv3protocolconversation.h
  • +563 -0
    protocols/ircv3/purpleircv3sasl.c
  • +55 -0
    protocols/ircv3/purpleircv3sasl.h
  • +91 -0
    protocols/ircv3/purpleircv3source.c
  • +58 -0
    protocols/ircv3/purpleircv3source.h
  • +133 -0
    protocols/ircv3/purpleircv3version.h
  • +0 -0
    protocols/ircv3/resources/icons/16x16/apps/im-ircv3.png
  • +0 -0
    protocols/ircv3/resources/icons/22x22/apps/im-ircv3.png
  • +0 -0
    protocols/ircv3/resources/icons/48x48/apps/im-ircv3.png
  • +8 -0
    protocols/ircv3/resources/ircv3.gresource.xml
  • +15 -0
    protocols/ircv3/tests/meson.build
  • +232 -0
    protocols/ircv3/tests/test_ircv3_formatting.c
  • +822 -0
    protocols/ircv3/tests/test_ircv3_parser.c
  • +158 -0
    protocols/ircv3/tests/test_ircv3_source.c
  • +2 -0
    protocols/meson.build
  • --- a/libpurple/protocols/demo/meson.build Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,37 +0,0 @@
    -DEMO_SOURCES = [
    - 'purpledemoconnection.c',
    - 'purpledemoconnection.h',
    - 'purpledemocontacts.c',
    - 'purpledemocontacts.h',
    - 'purpledemoplugin.c',
    - 'purpledemoplugin.h',
    - 'purpledemoprotocol.c',
    - 'purpledemoprotocol.h',
    - 'purpledemoprotocolactions.c',
    - 'purpledemoprotocolactions.h',
    - 'purpledemoprotocolclient.c',
    - 'purpledemoprotocolclient.h',
    - 'purpledemoprotocolcontacts.c',
    - 'purpledemoprotocolcontacts.h',
    - 'purpledemoprotocolconversation.c',
    - 'purpledemoprotocolconversation.h',
    - 'purpledemoprotocolmedia.c',
    - 'purpledemoprotocolmedia.h',
    -]
    -
    -if DYNAMIC_DEMO
    - demo_resources = gnome.compile_resources('purpledemoresource',
    - 'resources/purpledemo.gresource.xml',
    - source_dir : 'resources',
    - c_name : 'purple_demo')
    - DEMO_SOURCES += demo_resources
    -
    - shared_library('demo', DEMO_SOURCES,
    - c_args : ['-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Purple-Demo"'],
    - gnu_symbol_visibility : 'hidden',
    - dependencies : [glib, json, libpurple_dep],
    - install : true,
    - install_dir : PURPLE_PLUGINDIR)
    -
    - devenv.append('PURPLE_PLUGIN_PATH', meson.current_build_dir())
    -endif
    --- a/libpurple/protocols/demo/purpledemoconnection.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,80 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * This program is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU General Public License as published by
    - * the Free Software Foundation; either version 2 of the License, or
    - * (at your option) any later version.
    - *
    - * This program 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this program; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib/gi18n-lib.h>
    -
    -#include "purpledemoconnection.h"
    -
    -#include "purpledemocontacts.h"
    -
    -struct _PurpleDemoConnection {
    - PurpleConnection parent;
    -};
    -
    -G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleDemoConnection, purple_demo_connection,
    - PURPLE_TYPE_CONNECTION, G_TYPE_FLAG_FINAL, {})
    -
    -/******************************************************************************
    - * PurpleConnection Implementation
    - *****************************************************************************/
    -static gboolean
    -purple_demo_connection_connect(PurpleConnection *connection,
    - G_GNUC_UNUSED GError **error)
    -{
    - PurpleAccount *account = purple_connection_get_account(connection);
    -
    - purple_connection_set_state(connection, PURPLE_CONNECTION_STATE_CONNECTED);
    -
    - purple_demo_contacts_load(account);
    -
    - return TRUE;
    -}
    -
    -static gboolean
    -purple_demo_connection_disconnect(G_GNUC_UNUSED PurpleConnection *connection,
    - G_GNUC_UNUSED GError **error)
    -{
    - return TRUE;
    -}
    -
    -/******************************************************************************
    - * GObject Implementation
    - *****************************************************************************/
    -static void
    -purple_demo_connection_init(G_GNUC_UNUSED PurpleDemoConnection *connection) {
    -}
    -
    -static void
    -purple_demo_connection_class_finalize(G_GNUC_UNUSED PurpleDemoConnectionClass *klass) {
    -}
    -
    -static void
    -purple_demo_connection_class_init(PurpleDemoConnectionClass *klass) {
    - PurpleConnectionClass *connection_class = PURPLE_CONNECTION_CLASS(klass);
    -
    - connection_class->connect = purple_demo_connection_connect;
    - connection_class->disconnect = purple_demo_connection_disconnect;
    -}
    -
    -/******************************************************************************
    - * Internal API
    - *****************************************************************************/
    -void
    -purple_demo_connection_register(GPluginNativePlugin *plugin) {
    - purple_demo_connection_register_type(G_TYPE_MODULE(plugin));
    -}
    --- a/libpurple/protocols/demo/purpledemoconnection.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,36 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/>.
    - */
    -
    -#ifndef PURPLE_DEMO_CONNECTION_H
    -#define PURPLE_DEMO_CONNECTION_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -G_BEGIN_DECLS
    -
    -#define PURPLE_DEMO_TYPE_CONNECTION (purple_demo_connection_get_type())
    -G_DECLARE_FINAL_TYPE(PurpleDemoConnection, purple_demo_connection, PURPLE_DEMO,
    - CONNECTION, PurpleConnection)
    -
    -G_GNUC_INTERNAL void purple_demo_connection_register(GPluginNativePlugin *plugin);
    -
    -G_BEGIN_DECLS
    -
    -#endif /* PURPLE_DEMO_CONNECTION_H */
    --- a/libpurple/protocols/demo/purpledemocontacts.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,357 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 <json-glib/json-glib.h>
    -
    -#include "purpledemocontacts.h"
    -
    -#include "purpledemoresource.h"
    -
    -/******************************************************************************
    - * Helpers
    - *****************************************************************************/
    -static void
    -purple_demo_contacts_load_contact_icon(PurpleContactInfo *info,
    - const char *name)
    -{
    - PurpleAvatar *avatar = NULL;
    - GError *error = NULL;
    - char *path = NULL;
    -
    - path = g_strdup_printf("/im/pidgin/purple/demo/buddy_icons/%s.png", name);
    - avatar = purple_avatar_new_from_resource(path, &error);
    -
    - if(error != NULL) {
    - g_message("Failed to load find an icon for %s: %s", path,
    - error->message);
    -
    - g_free(path);
    - g_clear_error(&error);
    -
    - return;
    - }
    -
    - g_free(path);
    -
    - if(PURPLE_IS_AVATAR(avatar)) {
    - purple_contact_info_set_avatar(info, avatar);
    -
    - g_clear_object(&avatar);
    - }
    -}
    -
    -static void
    -purple_demo_contacts_load_contact_person(JsonObject *person_object,
    - PurpleContactInfo *info)
    -{
    - PurplePerson *person = NULL;
    - gboolean new_person = FALSE;
    - const char *value = NULL;
    -
    - /* If the person has an id, grab it so we can use it when constructing the
    - * person object.
    - */
    - if(json_object_has_member(person_object, "id")) {
    - value = json_object_get_string_member(person_object, "id");
    - }
    -
    - /* See if the contact has an existing person. */
    - person = purple_contact_info_get_person(info);
    - if(PURPLE_IS_PERSON(person)) {
    - const char *existing_id = NULL;
    -
    - /* If the existing person's id doesn't match the new one, NULL out
    - * person so it'll be recreated with the new id.
    - */
    - existing_id = purple_person_get_id(person);
    - if(!purple_strequal(existing_id, value)) {
    - person = NULL;
    - }
    - }
    -
    - /* If the person didn't exist or it had a different id, create a new person
    - * with the id.
    - */
    - if(!PURPLE_IS_PERSON(person)) {
    - person = g_object_new(PURPLE_TYPE_PERSON, "id", value, NULL);
    - new_person = TRUE;
    - }
    -
    - /* Alias */
    - if(json_object_has_member(person_object, "alias")) {
    - value = json_object_get_string_member(person_object, "alias");
    - if(!purple_strempty(value)) {
    - purple_person_set_alias(person, value);
    - }
    - }
    -
    - /* Create the link between the person and the contact info. */
    - if(new_person) {
    - purple_person_add_contact_info(person, info);
    - purple_contact_info_set_person(info, person);
    -
    - g_clear_object(&person);
    - }
    -}
    -
    -static void
    -purple_demo_contacts_load_contact_presence(JsonObject *presence_object,
    - PurpleContactInfo *info)
    -{
    - PurplePresence *presence = NULL;
    - const gchar *value = NULL;
    -
    - presence = purple_contact_info_get_presence(info);
    -
    - /* Emoji */
    - if(json_object_has_member(presence_object, "emoji")) {
    - value = json_object_get_string_member(presence_object, "emoji");
    - if(!purple_strempty(value)) {
    - purple_presence_set_emoji(presence, value);
    - }
    - }
    -
    - /* Idle Time */
    - if(json_object_has_member(presence_object, "idle")) {
    - GDateTime *now = NULL;
    - GDateTime *idle_since = NULL;
    - gint64 ivalue = 0;
    -
    - ivalue = json_object_get_int_member(presence_object, "idle");
    -
    - now = g_date_time_new_now_local();
    - idle_since = g_date_time_add_minutes(now, -1 * ivalue);
    -
    - purple_presence_set_idle(presence, TRUE, idle_since);
    -
    - g_date_time_unref(idle_since);
    - g_date_time_unref(now);
    - }
    -
    - /* Message */
    - if(json_object_has_member(presence_object, "message")) {
    - value = json_object_get_string_member(presence_object, "message");
    - if(!purple_strempty(value)) {
    - purple_presence_set_message(presence, value);
    - }
    - }
    -
    - /* Mobile */
    - if(json_object_has_member(presence_object, "mobile")) {
    - gboolean bvalue = FALSE;
    - bvalue = json_object_get_boolean_member(presence_object, "mobile");
    - purple_presence_set_mobile(presence, bvalue);
    - }
    -
    - /* Primitive */
    - if(json_object_has_member(presence_object, "primitive")) {
    - PurplePresencePrimitive primitive = PURPLE_PRESENCE_PRIMITIVE_OFFLINE;
    -
    - value = json_object_get_string_member(presence_object, "primitive");
    - if(!purple_strempty(value)) {
    - GEnumClass *enum_class = NULL;
    - GEnumValue *enum_value = NULL;
    -
    - enum_class = g_type_class_ref(PURPLE_TYPE_PRESENCE_PRIMITIVE);
    - enum_value = g_enum_get_value_by_nick(enum_class, value);
    -
    - if(enum_value != NULL) {
    - primitive = enum_value->value;
    - }
    -
    - g_type_class_unref(enum_class);
    - }
    -
    - purple_presence_set_primitive(presence, primitive);
    - }
    -}
    -
    -static void
    -purple_demo_contacts_load_contact(PurpleContactManager *manager,
    - PurpleAccount *account,
    - JsonObject *contact_object)
    -{
    - PurpleContact *contact = NULL;
    - PurpleContactInfo *info = NULL;
    - gboolean new_contact = FALSE;
    - const char *id = NULL;
    - const char *value = NULL;
    -
    - /* If we have an id, grab so we can create the contact with it. */
    - if(json_object_has_member(contact_object, "id")) {
    - id = json_object_get_string_member(contact_object, "id");
    - }
    -
    - /* Look for an existing contact before creating a new one. This stops us
    - * from getting multiples when we trigger connection errors.
    - */
    - if(!purple_strempty(id)) {
    - contact = purple_contact_manager_find_with_id(manager, account, id);
    - }
    -
    - /* If we didn't find an existing contact, create it now with the provided
    - * id.
    - */
    - if(!PURPLE_IS_CONTACT(contact)) {
    - contact = purple_contact_new(account, id);
    - new_contact = TRUE;
    - }
    -
    - info = PURPLE_CONTACT_INFO(contact);
    -
    - /* Alias */
    - if(json_object_has_member(contact_object, "alias")) {
    - value = json_object_get_string_member(contact_object, "alias");
    - if(!purple_strempty(value)) {
    - purple_contact_info_set_alias(info, value);
    - }
    - }
    -
    - /* Color */
    - if(json_object_has_member(contact_object, "color")) {
    - value = json_object_get_string_member(contact_object, "color");
    - if(!purple_strempty(value)) {
    - purple_contact_info_set_color(info, value);
    - }
    - }
    -
    - /* Display Name */
    - if(json_object_has_member(contact_object, "display_name")) {
    - value = json_object_get_string_member(contact_object, "display_name");
    - if(!purple_strempty(value)) {
    - purple_contact_info_set_display_name(info, value);
    - }
    - }
    -
    - /* Username */
    - if(json_object_has_member(contact_object, "username")) {
    - value = json_object_get_string_member(contact_object, "username");
    - if(!purple_strempty(value)) {
    - purple_contact_info_set_username(info, value);
    -
    - purple_demo_contacts_load_contact_icon(info, value);
    - }
    - }
    -
    - /* Load the profile if it exists. */
    - if(json_object_has_member(contact_object, "profile")) {
    - value = json_object_get_string_member(contact_object, "profile");
    - if(!purple_strempty(value)) {
    - g_object_set_data_full(G_OBJECT(info), "demo-profile",
    - g_strdup(value), g_free);
    - }
    - }
    -
    - /* Load the tags. */
    - if(json_object_has_member(contact_object, "tags")) {
    - PurpleTags *tags = purple_contact_info_get_tags(info);
    - JsonArray *array = NULL;
    - GList *elements = NULL;
    -
    - array = json_object_get_array_member(contact_object, "tags");
    - elements = json_array_get_elements(array);
    - while(elements != NULL) {
    - JsonNode *tag_node = elements->data;
    - const char *tag = json_node_get_string(tag_node);
    -
    - purple_tags_add(tags, tag);
    -
    - elements = g_list_delete_link(elements, elements);
    - }
    - }
    -
    - /* Load the person. */
    - if(json_object_has_member(contact_object, "person")) {
    - JsonObject *person_object = NULL;
    -
    - person_object = json_object_get_object_member(contact_object,
    - "person");
    -
    - purple_demo_contacts_load_contact_person(person_object, info);
    - }
    -
    - /* Load the presence. */
    - if(json_object_has_member(contact_object, "presence")) {
    - JsonObject *presence_object = NULL;
    -
    - presence_object = json_object_get_object_member(contact_object,
    - "presence");
    -
    - purple_demo_contacts_load_contact_presence(presence_object, info);
    - }
    -
    - /* Finally add the contact to the contact manager if it's new. */
    - if(new_contact) {
    - purple_contact_manager_add(manager, contact);
    - }
    -
    - g_clear_object(&contact);
    -}
    -
    -/******************************************************************************
    - * Local Exports
    - *****************************************************************************/
    -void
    -purple_demo_contacts_load(PurpleAccount *account) {
    - PurpleContactManager *manager = NULL;
    - GError *error = NULL;
    - GInputStream *istream = NULL;
    - GList *contacts = NULL;
    - JsonArray *contacts_array = NULL;
    - JsonNode *root_node = NULL;
    - JsonParser *parser = NULL;
    -
    - /* get a stream to the contacts.json resource */
    - istream = g_resource_open_stream(purple_demo_get_resource(),
    - "/im/pidgin/purple/demo/contacts.json",
    - G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
    -
    - /* create our parser */
    - parser = json_parser_new();
    -
    - if(!json_parser_load_from_stream(parser, istream, NULL, &error)) {
    - g_critical("%s", error->message);
    - g_clear_error(&error);
    - return;
    - }
    -
    - root_node = json_parser_get_root(parser);
    -
    - manager = purple_contact_manager_get_default();
    -
    - /* Load the contacts! */
    - contacts_array = json_node_get_array(root_node);
    - contacts = json_array_get_elements(contacts_array);
    - while(contacts != NULL) {
    - JsonNode *contact_node = NULL;
    - JsonObject *contact_object = NULL;
    -
    - contact_node = contacts->data;
    - contact_object = json_node_get_object(contact_node);
    -
    - purple_demo_contacts_load_contact(manager, account, contact_object);
    -
    - contacts = g_list_delete_link(contacts, contacts);
    - }
    -
    - /* Clean up everything else... */
    - g_clear_object(&parser);
    -
    - g_input_stream_close(istream, NULL, NULL);
    - g_object_unref(istream);
    -}
    --- a/libpurple/protocols/demo/purpledemocontacts.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,32 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/>.
    - */
    -
    -#ifndef PURPLE_DEMO_CONTACTS_H
    -#define PURPLE_DEMO_CONTACTS_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -G_BEGIN_DECLS
    -
    -G_GNUC_INTERNAL void purple_demo_contacts_load(PurpleAccount *account);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_DEMO_CONTACTS_H */
    --- a/libpurple/protocols/demo/purpledemoplugin.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,113 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/gi18n-lib.h>
    -
    -#include <gplugin.h>
    -#include <gplugin-native.h>
    -
    -#include <purple.h>
    -
    -#include "purpledemoplugin.h"
    -
    -#include "purpledemoconnection.h"
    -#include "purpledemoprotocol.h"
    -
    -/******************************************************************************
    - * Globals
    - *****************************************************************************/
    -static PurpleProtocol *demo_protocol = NULL;
    -
    -/******************************************************************************
    - * GPlugin Implementation
    - *****************************************************************************/
    -static GPluginPluginInfo *
    -purple_demo_plugin_query(G_GNUC_UNUSED GError **error) {
    - const gchar *authors[] = {
    - "Pidgin Developers <devel@pidgin.im>",
    - NULL
    - };
    -
    - return purple_plugin_info_new(
    - "id", "prpl-demo",
    - "name", "Demo Protocol Plugin",
    - "authors", authors,
    - "version", PURPLE_VERSION,
    - "category", N_("Protocol"),
    - "summary", N_("A protocol plugin used for demos."),
    - "description", N_("A protocol plugin that helps to demonstrate "
    - "features of libpurple and clients."),
    - "website", PURPLE_WEBSITE,
    - "abi-version", PURPLE_ABI_VERSION,
    - "flags", PURPLE_PLUGIN_INFO_FLAGS_INTERNAL |
    - PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD,
    - NULL
    - );
    -}
    -
    -static gboolean
    -purple_demo_plugin_load(GPluginPlugin *plugin, GError **error) {
    - PurpleProtocolManager *manager = NULL;
    -
    - if(PURPLE_IS_PROTOCOL(demo_protocol)) {
    - g_set_error_literal(error, PURPLE_DEMO_DOMAIN, 0,
    - "plugin was not cleaned up properly");
    -
    - return FALSE;
    - }
    -
    - purple_demo_connection_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    - purple_demo_protocol_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    -
    - manager = purple_protocol_manager_get_default();
    -
    - demo_protocol = purple_demo_protocol_new();
    - if(!purple_protocol_manager_register(manager, demo_protocol, error)) {
    - g_clear_object(&demo_protocol);
    -
    - return FALSE;
    - }
    -
    - return TRUE;
    -}
    -
    -static gboolean
    -purple_demo_plugin_unload(G_GNUC_UNUSED GPluginPlugin *plugin,
    - G_GNUC_UNUSED gboolean shutdown,
    - GError **error)
    -{
    - PurpleProtocolManager *manager = purple_protocol_manager_get_default();
    -
    - if(!PURPLE_IS_PROTOCOL(demo_protocol)) {
    - g_set_error_literal(error, PURPLE_DEMO_DOMAIN, 0,
    - "plugin was not setup properly");
    -
    - return FALSE;
    - }
    -
    - if(!purple_protocol_manager_unregister(manager, demo_protocol, error)) {
    - return FALSE;
    - }
    -
    - g_clear_object(&demo_protocol);
    -
    - return TRUE;
    -}
    -
    -GPLUGIN_NATIVE_PLUGIN_DECLARE(purple_demo_plugin)
    --- a/libpurple/protocols/demo/purpledemoplugin.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,29 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/>.
    - */
    -#ifndef PURPLE_DEMO_PLUGIN_H
    -#define PURPLE_DEMO_PLUGIN_H
    -
    -#include <glib.h>
    -
    -G_BEGIN_DECLS
    -
    -#define PURPLE_DEMO_DOMAIN (g_quark_from_static_string("demo-plugin"))
    -
    -G_BEGIN_DECLS
    -
    -#endif /* PURPLE_DEMO_PLUGIN_H */
    --- a/libpurple/protocols/demo/purpledemoprotocol.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,146 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 "purpledemoprotocol.h"
    -
    -#include "purpledemoconnection.h"
    -#include "purpledemoprotocolactions.h"
    -#include "purpledemoprotocolclient.h"
    -#include "purpledemoprotocolcontacts.h"
    -#include "purpledemoprotocolconversation.h"
    -#include "purpledemoprotocolmedia.h"
    -
    -struct _PurpleDemoProtocol {
    - PurpleProtocol parent;
    -};
    -
    -/******************************************************************************
    - * PurpleProtocol Implementation
    - *****************************************************************************/
    -static PurpleConnection *
    -purple_demo_protocol_create_connection(PurpleProtocol *protocol,
    - PurpleAccount *account,
    - const char *password,
    - G_GNUC_UNUSED GError **error)
    -{
    - g_return_val_if_fail(PURPLE_IS_PROTOCOL(protocol), NULL);
    - g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
    -
    - return g_object_new(
    - PURPLE_DEMO_TYPE_CONNECTION,
    - "protocol", protocol,
    - "account", account,
    - "password", password,
    - NULL);
    -
    -}
    -
    -static GList *
    -purple_demo_protocol_status_types(G_GNUC_UNUSED PurpleProtocol *protocol,
    - G_GNUC_UNUSED PurpleAccount *account)
    -{
    - PurpleStatusType *type = NULL;
    - GList *status_types = NULL;
    -
    - type = purple_status_type_new_with_attrs(
    - PURPLE_STATUS_AVAILABLE, "available", NULL,
    - TRUE, TRUE, FALSE,
    - "message", _("Message"), purple_value_new(G_TYPE_STRING),
    - NULL);
    - status_types = g_list_append(status_types, type);
    -
    - type = purple_status_type_new_with_attrs(
    - PURPLE_STATUS_AWAY, "away", NULL,
    - TRUE, TRUE, FALSE,
    - "message", _("Message"), purple_value_new(G_TYPE_STRING),
    - NULL);
    - status_types = g_list_append(status_types, type);
    -
    - type = purple_status_type_new_with_attrs(
    - PURPLE_STATUS_EXTENDED_AWAY, "extended_away", NULL,
    - TRUE, TRUE, FALSE,
    - "message", _("Message"), purple_value_new(G_TYPE_STRING),
    - NULL);
    - status_types = g_list_append(status_types, type);
    -
    - type = purple_status_type_new_full(
    - PURPLE_STATUS_OFFLINE, NULL, NULL,
    - TRUE, TRUE, FALSE);
    - status_types = g_list_append(status_types, type);
    -
    - return status_types;
    -}
    -
    -/******************************************************************************
    - * GObject Implementation
    - *****************************************************************************/
    -G_DEFINE_DYNAMIC_TYPE_EXTENDED(
    - PurpleDemoProtocol,
    - purple_demo_protocol,
    - PURPLE_TYPE_PROTOCOL,
    - G_TYPE_FLAG_FINAL,
    - G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_ACTIONS,
    - purple_demo_protocol_actions_init)
    - G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CLIENT,
    - purple_demo_protocol_client_init)
    - G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONTACTS,
    - purple_demo_protocol_contacts_init)
    - G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONVERSATION,
    - purple_demo_protocol_conversation_init)
    - G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_MEDIA,
    - purple_demo_protocol_media_init))
    -
    -static void
    -purple_demo_protocol_init(G_GNUC_UNUSED PurpleDemoProtocol *protocol) {
    -}
    -
    -static void
    -purple_demo_protocol_class_finalize(G_GNUC_UNUSED PurpleDemoProtocolClass *klass) {
    -}
    -
    -static void
    -purple_demo_protocol_class_init(PurpleDemoProtocolClass *klass) {
    - PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass);
    -
    - protocol_class->status_types = purple_demo_protocol_status_types;
    - protocol_class->create_connection = purple_demo_protocol_create_connection;
    -}
    -
    -/******************************************************************************
    - * Local Exports
    - *****************************************************************************/
    -void
    -purple_demo_protocol_register(GPluginNativePlugin *plugin) {
    - purple_demo_protocol_register_type(G_TYPE_MODULE(plugin));
    -}
    -
    -PurpleProtocol *
    -purple_demo_protocol_new(void) {
    - return g_object_new(
    - PURPLE_DEMO_TYPE_PROTOCOL,
    - "id", "prpl-demo",
    - "name", "Demo",
    - "description", "A protocol plugin with static data to be used in "
    - "screen shots.",
    - "icon-name", "im-purple-demo",
    - "icon-resource-path", "/im/pidgin/purple/demo/icons",
    - "options", OPT_PROTO_NO_PASSWORD,
    - NULL);
    -}
    --- a/libpurple/protocols/demo/purpledemoprotocol.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,41 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/>.
    - */
    -
    -#ifndef PURPLE_DEMO_PROTOCOL_H
    -#define PURPLE_DEMO_PROTOCOL_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <gplugin-native.h>
    -
    -#include <purple.h>
    -
    -G_BEGIN_DECLS
    -
    -#define PURPLE_DEMO_TYPE_PROTOCOL (purple_demo_protocol_get_type())
    -G_DECLARE_FINAL_TYPE(PurpleDemoProtocol, purple_demo_protocol, PURPLE_DEMO,
    - PROTOCOL, PurpleProtocol)
    -
    -G_GNUC_INTERNAL void purple_demo_protocol_register(GPluginNativePlugin *plugin);
    -
    -G_GNUC_INTERNAL PurpleProtocol *purple_demo_protocol_new(void);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_DEMO_PROTOCOL_H */
    --- a/libpurple/protocols/demo/purpledemoprotocolactions.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,950 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 "purpledemoprotocol.h"
    -#include "purpledemoprotocolactions.h"
    -#include "purpledemoresource.h"
    -
    -/******************************************************************************
    - * Connection failure action implementations
    - *****************************************************************************/
    -#define REAPER_BUDDY_NAME ("Gary")
    -#define DEFAULT_REAP_TIME (5) /* seconds */
    -#define FATAL_TICK_STR N_("Reaping connection in %d second...")
    -#define FATAL_TICK_PLURAL_STR N_("Reaping connection in %d seconds...")
    -#define FATAL_DISCONNECT_STR N_("%s reaped the connection")
    -#define TEMPORARY_TICK_STR N_("Pruning connection in %d second...")
    -#define TEMPORARY_TICK_PLURAL_STR N_("Pruning connection in %d seconds...")
    -#define TEMPORARY_DISCONNECT_STR N_("%s pruned the connection")
    -
    -static gboolean
    -purple_demo_protocol_failure_tick(gpointer data,
    - PurpleConnectionError error_code,
    - const gchar *tick_str,
    - const gchar *tick_plural_str,
    - const gchar *disconnect_str)
    -{
    - PurpleConnection *connection = PURPLE_CONNECTION(data);
    - PurpleAccount *account = purple_connection_get_account(connection);
    - gchar *message = NULL;
    - gint timeout = 0;
    -
    - timeout = GPOINTER_TO_INT(g_object_steal_data(G_OBJECT(connection),
    - "reaping-time"));
    - timeout--;
    - if(timeout > 0) {
    - PurpleContact *contact = NULL;
    - PurpleContactManager *manager = NULL;
    -
    - g_object_set_data(G_OBJECT(connection), "reaping-time",
    - GINT_TO_POINTER(timeout));
    -
    - manager = purple_contact_manager_get_default();
    - contact = purple_contact_manager_find_with_username(manager, account,
    - REAPER_BUDDY_NAME);
    -
    - if(PURPLE_IS_CONTACT(contact)) {
    - PurpleContactInfo *info = PURPLE_CONTACT_INFO(contact);
    - PurplePresence *presence = purple_contact_info_get_presence(info);
    - const char *format = ngettext(tick_str, tick_plural_str, timeout);
    -
    - message = g_strdup_printf(format, timeout);
    - purple_presence_set_message(presence, message);
    - g_free(message);
    - }
    -
    - return G_SOURCE_CONTINUE;
    - }
    -
    - message = g_strdup_printf(_(disconnect_str), REAPER_BUDDY_NAME);
    - purple_connection_error(connection, error_code, message);
    - g_free(message);
    -
    - return G_SOURCE_REMOVE;
    -}
    -
    -static gboolean
    -purple_demo_protocol_fatal_failure_cb(gpointer data) {
    - return purple_demo_protocol_failure_tick(data,
    - PURPLE_CONNECTION_ERROR_CUSTOM_FATAL,
    - FATAL_TICK_STR,
    - FATAL_TICK_PLURAL_STR,
    - FATAL_DISCONNECT_STR);
    -}
    -
    -static gboolean
    -purple_demo_protocol_temporary_failure_cb(gpointer data) {
    - return purple_demo_protocol_failure_tick(data,
    - PURPLE_CONNECTION_ERROR_CUSTOM_TEMPORARY,
    - TEMPORARY_TICK_STR,
    - TEMPORARY_TICK_PLURAL_STR,
    - TEMPORARY_DISCONNECT_STR);
    -}
    -
    -static void
    -purple_demo_protocol_failure_action_activate(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - const gchar *tick_str,
    - const gchar *tick_plural_str,
    - GSourceFunc cb)
    -{
    - PurpleAccountManager *account_manager = NULL;
    - PurpleAccount *account = NULL;
    - PurpleConnection *connection = NULL;
    - PurpleContact *contact = NULL;
    - PurpleContactManager *contact_manager = NULL;
    - const char *account_id = NULL;
    -
    - if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    - g_critical("Demo failure action parameter is of incorrect type %s",
    - g_variant_get_type_string(parameter));
    - return;
    - }
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - account_manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(account_manager, account_id);
    - connection = purple_account_get_connection(account);
    -
    - /* Do nothing if disconnected, or already in process of reaping. */
    - if(!PURPLE_IS_CONNECTION(connection)) {
    - g_clear_object(&account);
    -
    - return;
    - }
    -
    - if(g_object_get_data(G_OBJECT(connection), "reaping-time")) {
    - g_clear_object(&account);
    -
    - return;
    - }
    -
    - /* Find the reaper. */
    - contact_manager = purple_contact_manager_get_default();
    - contact = purple_contact_manager_find_with_username(contact_manager,
    - account,
    - REAPER_BUDDY_NAME);
    -
    - if(PURPLE_IS_CONTACT(contact)) {
    - PurpleContactInfo *info = PURPLE_CONTACT_INFO(contact);
    - PurplePresence *presence = purple_contact_info_get_presence(info);
    - const char *format = NULL;
    - char *message = NULL;
    -
    - format = ngettext(tick_str, tick_plural_str, DEFAULT_REAP_TIME);
    - message = g_strdup_printf(format, DEFAULT_REAP_TIME);
    -
    - purple_presence_set_idle(presence, FALSE, NULL);
    - purple_presence_set_message(presence, message);
    - g_free(message);
    - }
    -
    - g_object_set_data(G_OBJECT(connection), "reaping-time",
    - GINT_TO_POINTER(DEFAULT_REAP_TIME));
    - g_timeout_add_seconds(1, cb, connection);
    -
    - g_clear_object(&account);
    -}
    -
    -static void
    -purple_demo_protocol_temporary_failure_action_activate(GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - purple_demo_protocol_failure_action_activate(action, parameter,
    - TEMPORARY_TICK_STR,
    - TEMPORARY_TICK_PLURAL_STR,
    - purple_demo_protocol_temporary_failure_cb);
    -}
    -
    -static void
    -purple_demo_protocol_fatal_failure_action_activate(GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - purple_demo_protocol_failure_action_activate(action, parameter,
    - FATAL_TICK_STR,
    - FATAL_TICK_PLURAL_STR,
    - purple_demo_protocol_fatal_failure_cb);
    -}
    -
    -/******************************************************************************
    - * Request API action implementations
    - *****************************************************************************/
    -
    -static void
    -purple_demo_protocol_request_input_ok_cb(G_GNUC_UNUSED gpointer data,
    - const char *value)
    -{
    - g_message(_("Successfully requested input from UI: %s"), value);
    -}
    -
    -static void
    -purple_demo_protocol_request_input_cancel_cb(G_GNUC_UNUSED gpointer data,
    - G_GNUC_UNUSED const char *value)
    -{
    - g_message(_("UI cancelled input request"));
    -}
    -
    -static void
    -purple_demo_protocol_request_input_activate(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - PurpleConnection *connection = NULL;
    - const gchar *account_id = NULL;
    - PurpleAccountManager *manager = NULL;
    - PurpleAccount *account = NULL;
    - static int form = 0;
    - gboolean multiline = FALSE, masked = FALSE;
    - char *secondary = NULL;
    -
    - if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    - g_critical("Demo failure action parameter is of incorrect type %s",
    - g_variant_get_type_string(parameter));
    - return;
    - }
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(manager, account_id);
    - connection = purple_account_get_connection(account);
    - g_clear_object(&account);
    -
    - /* Alternate through all four combinations of {masked, multiline}. */
    - masked = form % 2 == 1;
    - multiline = (form / 2) % 2 == 1;
    - form++;
    - secondary = g_strdup_printf(_("The input will be %s %s."),
    - masked ? "masked" : "unmasked",
    - multiline ? "multiple lines" : "single line");
    -
    - purple_request_input(connection, _("Request Input Demo"),
    - _("Please input some text…"), secondary, _("default"),
    - multiline, masked, NULL,
    - _("OK"),
    - G_CALLBACK(purple_demo_protocol_request_input_ok_cb),
    - _("Cancel"),
    - G_CALLBACK(purple_demo_protocol_request_input_cancel_cb),
    - purple_request_cpar_from_connection(connection), NULL);
    -
    - g_free(secondary);
    -}
    -
    -static void
    -purple_demo_protocol_request_choice_ok_cb(G_GNUC_UNUSED gpointer data,
    - gpointer value)
    -{
    - const char *text = value;
    -
    - g_message(_("Successfully requested a choice from UI: %s"), text);
    -}
    -
    -static void
    -purple_demo_protocol_request_choice_cancel_cb(G_GNUC_UNUSED gpointer data,
    - G_GNUC_UNUSED gpointer value)
    -{
    - g_message(_("UI cancelled choice request"));
    -}
    -
    -static void
    -purple_demo_protocol_request_choice_activate(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - PurpleConnection *connection = NULL;
    - const gchar *account_id = NULL;
    - PurpleAccountManager *manager = NULL;
    - PurpleAccount *account = NULL;
    -
    - if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    - g_critical("Demo failure action parameter is of incorrect type %s",
    - g_variant_get_type_string(parameter));
    - return;
    - }
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(manager, account_id);
    - connection = purple_account_get_connection(account);
    - g_clear_object(&account);
    -
    - purple_request_choice(connection, _("Request Choice Demo"),
    - _("Please pick an option…"), NULL, _("foo"),
    - _("OK"),
    - G_CALLBACK(purple_demo_protocol_request_choice_ok_cb),
    - _("Cancel"),
    - G_CALLBACK(purple_demo_protocol_request_choice_cancel_cb),
    - purple_request_cpar_from_connection(connection),
    - NULL, _("foo"), "foo", _("bar"), "bar",
    - _("baz"), "baz", NULL);
    -}
    -
    -static void
    -purple_demo_protocol_request_action_cb(G_GNUC_UNUSED gpointer data, int action)
    -{
    - g_message(_("Successfully requested an action from the UI: %d"), action);
    -}
    -
    -static void
    -purple_demo_protocol_request_action_activate(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - PurpleConnection *connection = NULL;
    - const gchar *account_id = NULL;
    - PurpleAccountManager *manager = NULL;
    - PurpleAccount *account = NULL;
    -
    - if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    - g_critical("Demo failure action parameter is of incorrect type %s",
    - g_variant_get_type_string(parameter));
    - return;
    - }
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(manager, account_id);
    - connection = purple_account_get_connection(account);
    - g_clear_object(&account);
    -
    - purple_request_action(connection, _("Request Action Demo"),
    - _("Please choose an action…"), NULL, 1,
    - purple_request_cpar_from_connection(connection),
    - NULL, 3,
    - _("foo"), purple_demo_protocol_request_action_cb,
    - _("bar"), purple_demo_protocol_request_action_cb,
    - _("baz"), purple_demo_protocol_request_action_cb);
    -}
    -
    -typedef struct {
    - gint id;
    - gpointer ui_handle;
    -} PurpleDemoProtocolWaitData;
    -
    -static gboolean
    -purple_demo_protocol_request_wait_pulse_cb(gpointer data) {
    - PurpleDemoProtocolWaitData *wait = data;
    -
    - purple_request_wait_pulse(wait->ui_handle);
    -
    - return G_SOURCE_CONTINUE;
    -}
    -
    -static void
    -purple_demo_protocol_request_wait_cancel_cb(G_GNUC_UNUSED gpointer data) {
    - g_message(_("UI cancelled wait request"));
    -}
    -
    -static void
    -purple_demo_protocol_request_wait_close_cb(gpointer data) {
    - PurpleDemoProtocolWaitData *wait = data;
    -
    - g_source_remove(wait->id);
    - g_free(wait);
    -}
    -
    -static void
    -purple_demo_protocol_request_wait_activate(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - PurpleConnection *connection = NULL;
    - const gchar *account_id = NULL;
    - PurpleAccountManager *manager = NULL;
    - PurpleAccount *account = NULL;
    - PurpleDemoProtocolWaitData *wait = NULL;
    -
    - if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    - g_critical("Demo failure action parameter is of incorrect type %s",
    - g_variant_get_type_string(parameter));
    - return;
    - }
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(manager, account_id);
    - connection = purple_account_get_connection(account);
    - g_clear_object(&account);
    -
    - wait = g_new0(PurpleDemoProtocolWaitData, 1);
    -
    - wait->ui_handle = purple_request_wait(connection, _("Request Wait Demo"),
    - _("Please wait…"), NULL, TRUE,
    - purple_demo_protocol_request_wait_cancel_cb,
    - purple_request_cpar_from_connection(connection),
    - wait);
    -
    - wait->id = g_timeout_add(250, purple_demo_protocol_request_wait_pulse_cb,
    - wait);
    -
    - purple_request_add_close_notify(wait->ui_handle,
    - purple_demo_protocol_request_wait_close_cb,
    - wait);
    -}
    -
    -static void
    -purple_demo_protocol_request_fields_ok_cb(G_GNUC_UNUSED gpointer data,
    - PurpleRequestPage *page)
    -{
    - PurpleAccount *account = NULL;
    - PurpleRequestFieldList *field = NULL;
    - GList *list = NULL;
    - const char *tmp = NULL;
    - GString *info = NULL;
    -
    - info = g_string_new(_("Basic group:\n"));
    -
    - g_string_append_printf(info, _("\tString: %s\n"),
    - purple_request_page_get_string(page, "string"));
    - g_string_append_printf(info, _("\tMultiline string: %s\n"),
    - purple_request_page_get_string(page,
    - "multiline-string"));
    - g_string_append_printf(info, _("\tMasked string: %s\n"),
    - purple_request_page_get_string(page,
    - "masked-string"));
    - g_string_append_printf(info, _("\tAlphanumeric string: %s\n"),
    - purple_request_page_get_string(page,
    - "alphanumeric"));
    - g_string_append_printf(info, _("\tEmail string: %s\n"),
    - purple_request_page_get_string(page, "email"));
    - g_string_append_printf(info, _("\tInteger: %d\n"),
    - purple_request_page_get_integer(page, "int"));
    - g_string_append_printf(info, _("\tBoolean: %s\n"),
    - purple_request_page_get_bool(page, "bool") ?
    - _("TRUE") : _("FALSE"));
    -
    - g_string_append(info, _("Multiple-choice group:\n"));
    -
    - tmp = (const char *)purple_request_page_get_choice(page, "choice");
    - g_string_append_printf(info, _("\tChoice: %s\n"), tmp);
    -
    - field = PURPLE_REQUEST_FIELD_LIST(purple_request_page_get_field(page,
    - "list"));
    - list = purple_request_field_list_get_selected(field);
    - if(list != NULL) {
    - tmp = (const char *)list->data;
    - } else {
    - tmp = _("(unset)");
    - }
    - g_string_append_printf(info, _("\tList: %s\n"), tmp);
    -
    - field = PURPLE_REQUEST_FIELD_LIST(purple_request_page_get_field(page,
    - "multilist"));
    - list = purple_request_field_list_get_selected(field);
    - g_string_append(info, _("\tMulti-list: ["));
    - while(list != NULL) {
    - tmp = (const char *)list->data;
    - g_string_append_printf(info, "%s%s", tmp,
    - list->next != NULL ? ", " : "");
    - list = list->next;
    - }
    - g_string_append(info, _("]\n"));
    -
    - g_string_append(info, _("Special group:\n"));
    -
    - account = purple_request_page_get_account(page, "account");
    - if(PURPLE_IS_ACCOUNT(account)) {
    - tmp = purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(account));
    - } else {
    - tmp = _("(unset)");
    - }
    - g_string_append_printf(info, _("\tAccount: %s\n"), tmp);
    -
    - g_message(_("Successfully requested fields:\n%s"), info->str);
    -
    - g_string_free(info, TRUE);
    -}
    -
    -static void
    -purple_demo_protocol_request_fields_cancel_cb(G_GNUC_UNUSED gpointer data,
    - G_GNUC_UNUSED PurpleRequestPage *page)
    -{
    - g_message(_("UI cancelled field request"));
    -}
    -
    -static void
    -purple_demo_protocol_request_fields_activate(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - PurpleConnection *connection = NULL;
    - const gchar *account_id = NULL;
    - PurpleAccountManager *manager = NULL;
    - PurpleAccount *account = NULL;
    - PurpleRequestPage *page = NULL;
    - PurpleRequestGroup *group = NULL;
    - PurpleRequestField *boolfield = NULL;
    - PurpleRequestField *field = NULL;
    - PurpleRequestFieldChoice *choice_field = NULL;
    - PurpleRequestFieldList *list_field = NULL;
    - GBytes *icon = NULL;
    - gconstpointer icon_data = NULL;
    - gsize icon_len = 0;
    -
    - if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    - g_critical("Demo failure action parameter is of incorrect type %s",
    - g_variant_get_type_string(parameter));
    - return;
    - }
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(manager, account_id);
    - connection = purple_account_get_connection(account);
    -
    - page = purple_request_page_new();
    -
    - /* This group will contain basic fields. */
    - group = purple_request_group_new(_("Basic"));
    - purple_request_page_add_group(page, group);
    -
    - boolfield = purple_request_field_bool_new("bool", _("Sensitive?"), TRUE);
    - purple_request_field_set_tooltip(boolfield,
    - _("Allow modifying all fields."));
    - purple_request_group_add_field(group, boolfield);
    -
    - field = purple_request_field_label_new("basic-label",
    - _("This group contains basic fields"));
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    -
    - field = purple_request_field_string_new("string", _("A string"),
    - _("default"), FALSE);
    - purple_request_field_set_required(field, TRUE);
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    - field = purple_request_field_string_new("multiline-string",
    - _("A multiline string"),
    - _("default"), TRUE);
    - purple_request_group_add_field(group, field);
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - field = purple_request_field_string_new("masked-string",
    - _("A masked string"), _("default"),
    - FALSE);
    - purple_request_field_string_set_masked(PURPLE_REQUEST_FIELD_STRING(field),
    - TRUE);
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    - field = purple_request_field_string_new("alphanumeric",
    - _("An alphanumeric string"),
    - _("default"), FALSE);
    - purple_request_field_set_validator(field,
    - purple_request_field_alphanumeric_validator,
    - NULL, NULL);
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    - field = purple_request_field_string_new("email", _("An email"),
    - _("me@example.com"), FALSE);
    - purple_request_field_set_validator(field,
    - purple_request_field_email_validator,
    - NULL, NULL);
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    - field = purple_request_field_int_new("int", _("An integer"), 123, -42, 1337);
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    -
    - /* This group will contain fields with multiple options. */
    - group = purple_request_group_new(_("Multiple"));
    - purple_request_page_add_group(page, group);
    -
    - field = purple_request_field_label_new("multiple-label",
    - _("This group contains fields with multiple options"));
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    -
    - field = purple_request_field_choice_new("choice", _("A choice"), "foo");
    - choice_field = PURPLE_REQUEST_FIELD_CHOICE(field);
    - purple_request_field_choice_add(choice_field, _("foo"), "foo");
    - purple_request_field_choice_add(choice_field, _("bar"), "bar");
    - purple_request_field_choice_add(choice_field, _("baz"), "baz");
    - purple_request_field_choice_add(choice_field, _("quux"), "quux");
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    -
    - field = purple_request_field_list_new("list", _("A list"));
    - list_field = PURPLE_REQUEST_FIELD_LIST(field);
    - purple_request_field_list_add_icon(list_field, _("foo"), NULL, "foo");
    - purple_request_field_list_add_icon(list_field, _("bar"), NULL, "bar");
    - purple_request_field_list_add_icon(list_field, _("baz"), NULL, "baz");
    - purple_request_field_list_add_icon(list_field, _("quux"), NULL, "quux");
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    -
    - field = purple_request_field_list_new("multilist", _("A multi-select list"));
    - list_field = PURPLE_REQUEST_FIELD_LIST(field);
    - purple_request_field_list_set_multi_select(list_field, TRUE);
    - purple_request_field_list_add_icon(list_field, _("foo"), NULL, "foo");
    - purple_request_field_list_add_icon(list_field, _("bar"), NULL, "bar");
    - purple_request_field_list_add_icon(list_field, _("baz"), NULL, "baz");
    - purple_request_field_list_add_icon(list_field, _("quux"), NULL, "quux");
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    -
    - /* This group will contain specialized fields. */
    - group = purple_request_group_new(_("Special"));
    - purple_request_page_add_group(page, group);
    -
    - field = purple_request_field_label_new("special-label",
    - _("This group contains specialized fields"));
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    -
    - icon = g_resource_lookup_data(purple_demo_get_resource(),
    - "/im/pidgin/purple/demo/icons/scalable/apps/im-purple-demo.svg",
    - G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
    - icon_data = g_bytes_get_data(icon, &icon_len);
    - field = purple_request_field_image_new("image", _("An image"),
    - icon_data, icon_len);
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    - g_bytes_unref(icon);
    -
    - field = purple_request_field_account_new("account", _("An account"),
    - account);
    - g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    - purple_request_group_add_field(group, field);
    -
    - purple_request_fields(connection, _("Request Fields Demo"),
    - _("Please fill out these fields…"), NULL, page,
    - _("OK"),
    - G_CALLBACK(purple_demo_protocol_request_fields_ok_cb),
    - _("Cancel"),
    - G_CALLBACK(purple_demo_protocol_request_fields_cancel_cb),
    - purple_request_cpar_from_connection(connection),
    - NULL);
    -
    - g_clear_object(&account);
    -}
    -
    -static void
    -purple_demo_protocol_request_path_ok_cb(gpointer data, const char *filename) {
    - const char *type = data;
    -
    - g_message(_("Successfully requested %s from UI: %s"), type, filename);
    -}
    -
    -static void
    -purple_demo_protocol_request_path_cancel_cb(gpointer data) {
    - const char *type = data;
    -
    - g_message(_("UI cancelled %s request"), type);
    -}
    -
    -static void
    -purple_demo_protocol_request_file_activate(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - PurpleConnection *connection = NULL;
    - const gchar *account_id = NULL;
    - PurpleAccountManager *manager = NULL;
    - PurpleAccount *account = NULL;
    -
    - if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    - g_critical("Demo failure action parameter is of incorrect type %s",
    - g_variant_get_type_string(parameter));
    - return;
    - }
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(manager, account_id);
    - connection = purple_account_get_connection(account);
    - g_clear_object(&account);
    -
    - purple_request_file(connection, _("Request File Demo"),
    - "example.txt", FALSE,
    - G_CALLBACK(purple_demo_protocol_request_path_ok_cb),
    - G_CALLBACK(purple_demo_protocol_request_path_cancel_cb),
    - purple_request_cpar_from_connection(connection),
    - "file");
    -}
    -
    -static void
    -purple_demo_protocol_request_folder_activate(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - PurpleConnection *connection = NULL;
    - const gchar *account_id = NULL;
    - PurpleAccountManager *manager = NULL;
    - PurpleAccount *account = NULL;
    -
    - if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    - g_critical("Demo failure action parameter is of incorrect type %s",
    - g_variant_get_type_string(parameter));
    - return;
    - }
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(manager, account_id);
    - connection = purple_account_get_connection(account);
    - g_clear_object(&account);
    -
    - purple_request_folder(connection, _("Request Folder Demo"), NULL,
    - G_CALLBACK(purple_demo_protocol_request_path_ok_cb),
    - G_CALLBACK(purple_demo_protocol_request_path_cancel_cb),
    - purple_request_cpar_from_connection(connection),
    - "folder");
    -}
    -
    -/******************************************************************************
    - * Contact action implementations
    - *****************************************************************************/
    -static const gchar *contacts[] = {"Alice", "Bob", "Carlos", "Erin" };
    -
    -static void
    -purple_demo_protocol_remote_add(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - PurpleAccount *account = NULL;
    - PurpleAccountManager *account_manager = NULL;
    - PurpleAddContactRequest *request = NULL;
    - PurpleNotification *notification = NULL;
    - PurpleNotificationManager *notification_manager = NULL;
    - const gchar *account_id = NULL;
    - static guint counter = 0;
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - account_manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(account_manager, account_id);
    -
    - request = purple_add_contact_request_new(account, contacts[counter]);
    - notification = purple_notification_new_from_add_contact_request(request);
    -
    - notification_manager = purple_notification_manager_get_default();
    - purple_notification_manager_add(notification_manager, notification);
    -
    - counter++;
    - if(counter >= G_N_ELEMENTS(contacts)) {
    - counter = 0;
    - }
    -
    - g_clear_object(&notification);
    - g_clear_object(&account);
    -}
    -
    -static const char *puns[] = {
    - "Toucan play at that game!",
    - "As long as it's not too much of a birden...",
    - "Sounds like a bit of ostrich...",
    - "People can't stop raven!",
    - "Have you heard about the bird?",
    -};
    -
    -static void
    -purple_demo_protocol_generic_notification(G_GNUC_UNUSED GSimpleAction *action,
    - GVariant *parameter,
    - G_GNUC_UNUSED gpointer data)
    -{
    - PurpleAccount *account = NULL;
    - PurpleAccountManager *account_manager = NULL;
    - PurpleNotification *notification = NULL;
    - PurpleNotificationManager *notification_manager = NULL;
    - const char *account_id = NULL;
    - static guint counter = 0;
    -
    - account_id = g_variant_get_string(parameter, NULL);
    - account_manager = purple_account_manager_get_default();
    - account = purple_account_manager_find_by_id(account_manager, account_id);
    -
    - notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC,
    - account, g_strdup(puns[counter]),
    - g_free);
    -
    - notification_manager = purple_notification_manager_get_default();
    - purple_notification_manager_add(notification_manager, notification);
    -
    - counter++;
    - if(counter >= G_N_ELEMENTS(puns)) {
    - counter = 0;
    - }
    -
    - g_clear_object(&notification);
    - g_clear_object(&account);
    -}
    -
    -/******************************************************************************
    - * PurpleProtocolActions Implementation
    - *****************************************************************************/
    -static const gchar *
    -purple_demo_protocol_get_prefix(G_GNUC_UNUSED PurpleProtocolActions *actions) {
    - return "prpl-demo";
    -}
    -
    -static GActionGroup *
    -purple_demo_protocol_get_action_group(G_GNUC_UNUSED PurpleProtocolActions *actions,
    - G_GNUC_UNUSED PurpleConnection *connection)
    -{
    - GSimpleActionGroup *group = NULL;
    - GActionEntry entries[] = {
    - {
    - .name = "temporary-failure",
    - .activate = purple_demo_protocol_temporary_failure_action_activate,
    - .parameter_type = "s",
    - }, {
    - .name = "fatal-failure",
    - .activate = purple_demo_protocol_fatal_failure_action_activate,
    - .parameter_type = "s",
    - }, {
    - .name = "generic-notification",
    - .activate = purple_demo_protocol_generic_notification,
    - .parameter_type = "s",
    - }, {
    - .name = "remote-add",
    - .activate = purple_demo_protocol_remote_add,
    - .parameter_type = "s",
    - }, {
    - .name = "request-input",
    - .activate = purple_demo_protocol_request_input_activate,
    - .parameter_type = "s",
    - }, {
    - .name = "request-choice",
    - .activate = purple_demo_protocol_request_choice_activate,
    - .parameter_type = "s",
    - }, {
    - .name = "request-action",
    - .activate = purple_demo_protocol_request_action_activate,
    - .parameter_type = "s",
    - }, {
    - .name = "request-wait",
    - .activate = purple_demo_protocol_request_wait_activate,
    - .parameter_type = "s",
    - }, {
    - .name = "request-fields",
    - .activate = purple_demo_protocol_request_fields_activate,
    - .parameter_type = "s",
    - }, {
    - .name = "request-file",
    - .activate = purple_demo_protocol_request_file_activate,
    - .parameter_type = "s",
    - }, {
    - .name = "request-folder",
    - .activate = purple_demo_protocol_request_folder_activate,
    - .parameter_type = "s",
    - }
    - };
    - gsize nentries = G_N_ELEMENTS(entries);
    -
    - group = g_simple_action_group_new();
    - g_action_map_add_action_entries(G_ACTION_MAP(group), entries, nentries,
    - NULL);
    -
    - return G_ACTION_GROUP(group);
    -}
    -
    -static GMenu *
    -purple_demo_protocol_get_menu(G_GNUC_UNUSED PurpleProtocolActions *actions,
    - G_GNUC_UNUSED PurpleConnection *connection)
    -{
    - GMenu *menu = NULL;
    - GMenu *submenu = NULL;
    - GMenuItem *item = NULL;
    -
    - menu = g_menu_new();
    -
    - item = g_menu_item_new(_("Trigger temporary connection failure..."),
    - "prpl-demo.temporary-failure");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(menu, item);
    - g_object_unref(item);
    -
    - item = g_menu_item_new(_("Trigger fatal connection failure..."),
    - "prpl-demo.fatal-failure");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(menu, item);
    - g_object_unref(item);
    -
    - item = g_menu_item_new(_("Trigger a generic notification"),
    - "prpl-demo.generic-notification");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(menu, item);
    - g_object_unref(item);
    -
    - item = g_menu_item_new(_("Trigger a contact adding you"),
    - "prpl-demo.remote-add");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(menu, item);
    - g_object_unref(item);
    -
    - submenu = g_menu_new();
    -
    - item = g_menu_item_new(_("Input"), "prpl-demo.request-input");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(submenu, item);
    - g_object_unref(item);
    -
    - item = g_menu_item_new(_("Choice"), "prpl-demo.request-choice");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(submenu, item);
    - g_object_unref(item);
    -
    - item = g_menu_item_new(_("Action"), "prpl-demo.request-action");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(submenu, item);
    - g_object_unref(item);
    -
    - item = g_menu_item_new(_("Wait"), "prpl-demo.request-wait");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(submenu, item);
    - g_object_unref(item);
    -
    - item = g_menu_item_new(_("Fields"), "prpl-demo.request-fields");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(submenu, item);
    - g_object_unref(item);
    -
    - item = g_menu_item_new(_("File"), "prpl-demo.request-file");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(submenu, item);
    - g_object_unref(item);
    -
    - item = g_menu_item_new(_("Folder"), "prpl-demo.request-folder");
    - g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    - "account");
    - g_menu_append_item(submenu, item);
    - g_object_unref(item);
    -
    - g_menu_append_submenu(menu, _("Trigger requests"), G_MENU_MODEL(submenu));
    - g_object_unref(submenu);
    -
    - return menu;
    -}
    -
    -void
    -purple_demo_protocol_actions_init(PurpleProtocolActionsInterface *iface) {
    - iface->get_prefix = purple_demo_protocol_get_prefix;
    - iface->get_action_group = purple_demo_protocol_get_action_group;
    - iface->get_menu = purple_demo_protocol_get_menu;
    -}
    --- a/libpurple/protocols/demo/purpledemoprotocolactions.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,28 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/>.
    - */
    -
    -#ifndef PURPLE_DEMO_PROTOCOL_ACTIONS_H
    -#define PURPLE_DEMO_PROTOCOL_ACTIONS_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -G_GNUC_INTERNAL void purple_demo_protocol_actions_init(PurpleProtocolActionsInterface *iface);
    -
    -#endif /* PURPLE_DEMO_PROTOCOL_ACTIONS_H */
    --- a/libpurple/protocols/demo/purpledemoprotocolclient.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,29 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 "purpledemoprotocol.h"
    -#include "purpledemoprotocolclient.h"
    -
    -/******************************************************************************
    - * PurpleProtocolClient Implementation
    - *****************************************************************************/
    -void
    -purple_demo_protocol_client_init(G_GNUC_UNUSED PurpleProtocolClientInterface *iface) {
    -}
    --- a/libpurple/protocols/demo/purpledemoprotocolclient.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,28 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/>.
    - */
    -
    -#ifndef PURPLE_DEMO_PROTOCOL_CLIENT_H
    -#define PURPLE_DEMO_PROTOCOL_CLIENT_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -G_GNUC_INTERNAL void purple_demo_protocol_client_init(PurpleProtocolClientInterface *iface);
    -
    -#endif /* PURPLE_DEMO_PROTOCOL_CLIENT_H */
    --- a/libpurple/protocols/demo/purpledemoprotocolcontacts.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,57 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 "purpledemoprotocolcontacts.h"
    -
    -/******************************************************************************
    - * PurpleProtocolContacts Implementation
    - *****************************************************************************/
    -static char *
    -purple_demo_protocol_contacts_get_profile_finish(G_GNUC_UNUSED PurpleProtocolContacts *contacts,
    - GAsyncResult *result,
    - GError **error)
    -{
    - g_return_val_if_fail(G_IS_TASK(result), NULL);
    -
    - return g_task_propagate_pointer(G_TASK(result), error);
    -}
    -
    -static void
    -purple_demo_protocol_contacts_get_profile_async(PurpleProtocolContacts *contacts,
    - PurpleContactInfo *info,
    - GCancellable *cancellable,
    - GAsyncReadyCallback callback,
    - gpointer data)
    -{
    - GTask *task = NULL;
    - const char *profile = NULL;
    -
    - task = g_task_new(contacts, cancellable, callback, data);
    -
    - profile = g_object_get_data(G_OBJECT(info), "demo-profile");
    - g_task_return_pointer(task, g_strdup(profile), g_free);
    - g_clear_object(&task);
    -}
    -
    -void
    -purple_demo_protocol_contacts_init(PurpleProtocolContactsInterface *iface) {
    - iface->get_profile_async = purple_demo_protocol_contacts_get_profile_async;
    - iface->get_profile_finish = purple_demo_protocol_contacts_get_profile_finish;
    -}
    --- a/libpurple/protocols/demo/purpledemoprotocolcontacts.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,28 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/liceng_task_return_pointerses/>.
    - */
    -
    -#ifndef PURPLE_DEMO_PROTOCOL_CONTACTS_H
    -#define PURPLE_DEMO_PROTOCOL_CONTACTS_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -G_GNUC_INTERNAL void purple_demo_protocol_contacts_init(PurpleProtocolContactsInterface *iface);
    -
    -#endif /* PURPLE_DEMO_PROTOCOL_CONTACTS_H */
    --- a/libpurple/protocols/demo/purpledemoprotocolconversation.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,131 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 "purpledemoprotocol.h"
    -#include "purpledemoprotocolconversation.h"
    -
    -/******************************************************************************
    - * PurpleProtocolConversation Implementation
    - *****************************************************************************/
    -typedef struct {
    - PurpleConversation *conversation;
    - PurpleMessage *message;
    -} PurpleDemoProtocolIMInfo;
    -
    -static void
    -purple_demo_protocol_im_info_free(PurpleDemoProtocolIMInfo *info) {
    - g_clear_object(&info->conversation);
    - g_clear_object(&info->message);
    - g_free(info);
    -}
    -
    -static gboolean
    -purple_demo_protocol_echo_im_cb(gpointer data) {
    - PurpleDemoProtocolIMInfo *info = data;
    - PurpleMessage *message = NULL;
    - PurpleMessageFlags flags;
    - const char *who = NULL;
    -
    - /* Turn outgoing message back incoming. */
    - who = purple_conversation_get_name(info->conversation);
    -
    - flags = purple_message_get_flags(info->message);
    - flags &= ~PURPLE_MESSAGE_SEND;
    - flags |= PURPLE_MESSAGE_RECV;
    -
    - message = purple_message_new_incoming(who,
    - purple_message_get_contents(info->message),
    - flags, 0);
    -
    - purple_conversation_write_message(info->conversation, message);
    - g_clear_object(&message);
    -
    - return G_SOURCE_REMOVE;
    -}
    -
    -static void
    -purple_demo_protocol_send_message_async(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
    - PurpleConversation *conversation,
    - PurpleMessage *message,
    - GCancellable *cancellable,
    - GAsyncReadyCallback callback,
    - gpointer data)
    -{
    - GTask *task = NULL;
    - const char *who = NULL;
    -
    - who = purple_conversation_get_name(conversation);
    - if(purple_strempty(who)) {
    - who = purple_message_get_recipient(message);
    - }
    -
    - if(purple_strequal(who, "Echo")) {
    - PurpleDemoProtocolIMInfo *info = g_new(PurpleDemoProtocolIMInfo, 1);
    -
    - info->conversation = g_object_ref(conversation);
    - info->message = g_object_ref(message);
    -
    - g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
    - purple_demo_protocol_echo_im_cb, info,
    - (GDestroyNotify)purple_demo_protocol_im_info_free);
    - } else if(purple_strequal(who, "Aegina")) {
    - PurpleDemoProtocolIMInfo *info = g_new(PurpleDemoProtocolIMInfo, 1);
    - const char *author = purple_message_get_author(message);
    - const char *contents = NULL;
    -
    - if(purple_strequal(author, "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_outgoing(author, who, contents,
    - PURPLE_MESSAGE_SEND);
    -
    - g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, purple_demo_protocol_echo_im_cb,
    - info, (GDestroyNotify)purple_demo_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_demo_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_demo_protocol_conversation_init(PurpleProtocolConversationInterface *iface) {
    - iface->send_message_async = purple_demo_protocol_send_message_async;
    - iface->send_message_finish = purple_demo_protocol_send_message_finish;
    -}
    --- a/libpurple/protocols/demo/purpledemoprotocolconversation.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,28 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/>.
    - */
    -
    -#ifndef PURPLE_DEMO_PROTOCOL_CONVERSATION_H
    -#define PURPLE_DEMO_PROTOCOL_CONVERSATION_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -G_GNUC_INTERNAL void purple_demo_protocol_conversation_init(PurpleProtocolConversationInterface *iface);
    -
    -#endif /* PURPLE_DEMO_PROTOCOL_CONVERSATION_H */
    --- a/libpurple/protocols/demo/purpledemoprotocolmedia.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,102 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 "purpledemoprotocol.h"
    -#include "purpledemoprotocolmedia.h"
    -
    -/******************************************************************************
    - * PurpleProtocolMedia Implementation
    - *****************************************************************************/
    -static PurpleMediaCaps
    -purple_demo_protocol_media_get_caps(G_GNUC_UNUSED PurpleProtocolMedia *media,
    - G_GNUC_UNUSED PurpleAccount *account,
    - const gchar *who)
    -{
    - if(purple_strequal(who, "Echo")) {
    - return PURPLE_MEDIA_CAPS_AUDIO | PURPLE_MEDIA_CAPS_VIDEO |
    - PURPLE_MEDIA_CAPS_AUDIO_VIDEO;
    - }
    -
    - return PURPLE_MEDIA_CAPS_NONE;
    -}
    -
    -static gboolean
    -purple_demo_protocol_media_initiate_session(G_GNUC_UNUSED PurpleProtocolMedia *media,
    - PurpleAccount *account,
    - const gchar *who,
    - PurpleMediaSessionType type)
    -{
    - PurpleConnection *connection = NULL;
    - gchar *session_name = NULL;
    - gchar *message = NULL;
    - GDateTime *timestamp = NULL;
    -
    - connection = purple_account_get_connection(account);
    -
    - session_name = g_flags_to_string(PURPLE_MEDIA_TYPE_SESSION_TYPE, type);
    - message = g_strdup_printf(_("Initiated demo %s session with %s"),
    - session_name, who);
    - timestamp = g_date_time_new_now_utc();
    -
    - purple_serv_got_im(connection, "Echo",
    - message, PURPLE_MESSAGE_RECV,
    - g_date_time_to_unix(timestamp));
    -
    - g_date_time_unref(timestamp);
    - g_free(message);
    - g_free(session_name);
    -
    - /* TODO: When libpurple gets a backend, we can implement more of this. */
    - return FALSE;
    -}
    -
    -static gboolean
    -purple_demo_protocol_media_send_dtmf(G_GNUC_UNUSED PurpleProtocolMedia *protocol_media,
    - PurpleMedia *media, gchar dtmf,
    - guint8 volume, guint8 duration)
    -{
    - PurpleAccount *account = NULL;
    - PurpleConnection *connection = NULL;
    - gchar *message = NULL;
    - GDateTime *timestamp = NULL;
    -
    - account = purple_media_get_account(media);
    - connection = purple_account_get_connection(account);
    -
    - message = g_strdup_printf(_("Received DTMF %c at volume %d for %d seconds"),
    - dtmf, volume, duration);
    - timestamp = g_date_time_new_now_utc();
    -
    - purple_serv_got_im(connection, "Echo",
    - message, PURPLE_MESSAGE_RECV,
    - g_date_time_to_unix(timestamp));
    -
    - g_date_time_unref(timestamp);
    - g_free(message);
    -
    - return TRUE;
    -}
    -
    -void
    -purple_demo_protocol_media_init(PurpleProtocolMediaInterface *iface) {
    - iface->get_caps = purple_demo_protocol_media_get_caps;
    - iface->initiate_session = purple_demo_protocol_media_initiate_session;
    - iface->send_dtmf = purple_demo_protocol_media_send_dtmf;
    -}
    --- a/libpurple/protocols/demo/purpledemoprotocolmedia.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,28 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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/>.
    - */
    -
    -#ifndef PURPLE_DEMO_PROTOCOL_MEDIA_H
    -#define PURPLE_DEMO_PROTOCOL_MEDIA_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -G_GNUC_INTERNAL void purple_demo_protocol_media_init(PurpleProtocolMediaInterface *iface);
    -
    -#endif /* PURPLE_DEMO_PROTOCOL_MEDIA_H */
    Binary file libpurple/protocols/demo/resources/buddy_icons/Aegina.png has changed
    Binary file libpurple/protocols/demo/resources/buddy_icons/Echo.png has changed
    Binary file libpurple/protocols/demo/resources/buddy_icons/Eion.png has changed
    Binary file libpurple/protocols/demo/resources/buddy_icons/Elliott.png has changed
    Binary file libpurple/protocols/demo/resources/buddy_icons/Gary.png has changed
    Binary file libpurple/protocols/demo/resources/buddy_icons/John.png has changed
    Binary file libpurple/protocols/demo/resources/buddy_icons/Markus.png has changed
    Binary file libpurple/protocols/demo/resources/buddy_icons/Richard.png has changed
    --- a/libpurple/protocols/demo/resources/buddy_icons/mkicon.py Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,90 +0,0 @@
    -#!/usr/bin/env python3
    -#
    -# Purple - Internet Messaging Library
    -# Copyright (C) Pidgin Developers <devel@pidgin.im>
    -#
    -# Purple is the legal property of its developers, whose names are too numerous
    -# to list here. Please refer to the COPYRIGHT file distributed with this
    -# source distribution.
    -#
    -# This program is free software; you can redistribute it and/or modify
    -# it under the terms of the GNU General Public License as published by
    -# the Free Software Foundation; either version 2 of the License, or
    -# (at your option) any later version.
    -#
    -# This program 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 General Public License for more details.
    -#
    -# You should have received a copy of the GNU General Public License along with
    -# this program; if not, see <https://www.gnu.org/licenses/>.
    -
    -import argparse
    -import colorsys
    -import hashlib
    -from pathlib import Path
    -
    -from PIL import Image, ImageDraw, ImageFont
    -
    -parser = argparse.ArgumentParser(
    - description='Generate buddy icons from user names.')
    -parser.add_argument('name', nargs='+', help='The name(s) to use.')
    -parser.add_argument('-f', '--font',
    - default='/usr/share/fonts/urw-base35/D050000L.otf',
    - help='Path to (TrueType or OpenType) font to use.')
    -parser.add_argument('-s', '--size', default=96, type=int,
    - help='Size of buddy icons to produce.')
    -parser.add_argument('-o', '--output', default='.',
    - help='Directory in which to place files.')
    -args = parser.parse_args()
    -
    -
    -def calculate_colours_for_text(text):
    - """
    - Calculate the foreground and background colours from text.
    -
    - This is based on pidgin_color_calculate_for_text in Pidgin C code.
    - """
    - # Hash the string and get the first 2 bytes of the digest.
    - checksum = hashlib.sha1()
    - checksum.update(text.encode('utf-8'))
    - digest = checksum.digest()
    -
    - # Calculate the hue based on the digest, scaled to 0-1.
    - hue = (digest[0] << 8 | digest[1]) / 65535
    - # Get the RGB values for the hue at full saturation and value.
    - foreground = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
    -
    - # Calculate the hue based on the end of the digest, scaled to 0-1.
    - hue = (digest[-1] << 8 | digest[-2]) / 65535
    - # Get the RGB values for the hue at full saturation and low value.
    - background = colorsys.hsv_to_rgb(hue, 1.0, 0.2)
    -
    - # Finally calculate the foreground summing 20% of the inverted background
    - # with 80% of the foreground.
    - foreground = (
    - (0.2 * (1 - bc)) + (0.8 * fc) for bc, fc in zip(background, foreground)
    - )
    -
    - # Pillow requires colours in 0-255.
    - return (tuple(int(c * 255) for c in foreground),
    - tuple(int(c * 255) for c in background))
    -
    -
    -output_dir = Path(args.output)
    -font = ImageFont.truetype(args.font, size=int(args.size * 96/72))
    -
    -for name in args.name:
    - fore, back = calculate_colours_for_text(name)
    -
    - # Generate an image using the first letter of the name to choose a glyph
    - # from the specified font. The default font generates some star-like or
    - # flower-like symbols.
    - img = Image.new('RGBA', (args.size, args.size), color=back)
    - draw = ImageDraw.Draw(img)
    - letter = name[0].upper()
    - draw.text((args.size // 2, args.size - 1), letter,
    - font=font, anchor='mb', fill=fore)
    -
    - img.save(output_dir / f'{name}.png', 'PNG')
    --- a/libpurple/protocols/demo/resources/contacts.json Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,109 +0,0 @@
    -[{
    - "id": "grim",
    - "person": {
    - "alias": "Grim",
    - "id": "gary"
    - },
    - "presence": {
    - "primitive": "available",
    - "message": "💤 Sleeping... 😴",
    - "idle": 1337
    - },
    - "username": "Gary",
    - "tags": [
    - "group:Family"
    - ],
    - "profile": "Pidgin 3 will be ready when it is ready! 🗿"
    -}, {
    - "id": "john",
    - "alias": "Rekkanoryo",
    - "person": {
    - "id": "john"
    - },
    - "presence": {
    - "primitive": "away"
    - },
    - "tags": [
    - "group:Friends"
    - ],
    - "username": "John",
    - "profile": "Idiot Windows sysadmin who used to develop for Pidgin and libpurple in the bright days before Windows 8"
    -}, {
    - "id": "elliott",
    - "person": {
    - "id": "elliott"
    - },
    - "presence": {
    - "primitive": "away",
    - "message": "Coding..."
    - },
    - "tags": [
    - "group:Work"
    - ],
    - "username": "Elliott"
    -}, {
    - "id": "richard",
    - "person": {
    - "id": "richard"
    - },
    - "presence": {
    - "primitive": "available"
    - },
    - "tags": [
    - "group:Work"
    - ],
    - "username": "Richard"
    -}, {
    - "id": "eion",
    - "person": {
    - "id": "eion"
    - },
    - "presence": {
    - "primitive": "available",
    - "message": "Writing crazy patches!"
    - },
    - "tags": [
    - "group:School"
    - ],
    - "username": "Eion"
    -}, {
    - "id": "markus",
    - "person": {
    - "id": "markus"
    - },
    - "presence": {
    - "primitive": "do-not-disturb",
    - "message": "Running all the things in valgrind..."
    - },
    - "tags": [
    - "group:School"
    - ],
    - "username": "Markus"
    -}, {
    - "id": "echo",
    - "person": {
    - "id": "echo"
    - },
    - "presence": {
    - "primitive": "available",
    - "message": "Cursed to speak the last words spoken to me"
    - },
    - "tags": [
    - "group:Nymphs"
    - ],
    - "username": "Echo"
    -}, {
    - "id": "aegina",
    - "person": {
    - "id": "aegina"
    - },
    - "presence": {
    - "primitive": "offline",
    - "emoji": "🫥️",
    - "message": "Stole the Cap of Invisibility from Hades"
    - },
    - "tags": [
    - "group:Nymphs"
    - ],
    - "username": "Aegina"
    -}]
    Binary file libpurple/protocols/demo/resources/icons/16x16/apps/im-purple-demo.png has changed
    Binary file libpurple/protocols/demo/resources/icons/22x22/apps/im-purple-demo.png has changed
    Binary file libpurple/protocols/demo/resources/icons/48x48/apps/im-purple-demo.png has changed
    --- a/libpurple/protocols/demo/resources/icons/scalable/apps/im-purple-demo.svg Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,78 +0,0 @@
    -<?xml version="1.0" encoding="UTF-8" standalone="no"?>
    -<!-- Created with Inkscape (http://www.inkscape.org/) -->
    -
    -<svg
    - width="37.851284mm"
    - height="39.057049mm"
    - viewBox="0 0 37.851284 39.057049"
    - version="1.1"
    - id="svg31537"
    - inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
    - sodipodi:docname="im-purple-demo.svg"
    - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
    - xmlns="http://www.w3.org/2000/svg"
    - xmlns:svg="http://www.w3.org/2000/svg"
    - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    - xmlns:cc="http://creativecommons.org/ns#"
    - xmlns:dc="http://purl.org/dc/elements/1.1/">
    - <defs
    - id="defs31531" />
    - <sodipodi:namedview
    - id="base"
    - pagecolor="#ffffff"
    - bordercolor="#666666"
    - borderopacity="1.0"
    - inkscape:pageopacity="0.0"
    - inkscape:pageshadow="2"
    - inkscape:zoom="2.8"
    - inkscape:cx="25.178571"
    - inkscape:cy="33.571429"
    - inkscape:document-units="mm"
    - inkscape:current-layer="layer1"
    - showgrid="false"
    - fit-margin-top="0"
    - fit-margin-left="0"
    - fit-margin-right="0"
    - fit-margin-bottom="0"
    - inkscape:window-width="1916"
    - inkscape:window-height="1017"
    - inkscape:window-x="1080"
    - inkscape:window-y="493"
    - inkscape:window-maximized="1"
    - inkscape:pagecheckerboard="0" />
    - <metadata
    - id="metadata31534">
    - <rdf:RDF>
    - <cc:Work
    - rdf:about="">
    - <dc:format>image/svg+xml</dc:format>
    - <dc:type
    - rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
    - </cc:Work>
    - </rdf:RDF>
    - </metadata>
    - <g
    - inkscape:label="Layer 1"
    - inkscape:groupmode="layer"
    - id="layer1"
    - transform="translate(75.835419,-56.26127)">
    - <g
    - id="g1812">
    - <path
    - style="isolation:isolate;fill:#4b2854;fill-opacity:1;stroke:#fdfdfd;stroke-width:11.7132;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
    - inkscape:connector-curvature="0"
    - id="path2938"
    - d="m 137.45776,361.78346 q 9.2531,10.42501 11.91562,19.37813 a 19.687501,19.687501 0 0 0 -9.08437,2.24061 17.090626,17.090626 0 0 0 -6.84376,6.38437 19.312501,19.312501 0 0 0 -2.74688,9.76876 c -0.15944,5.625 1.64063,10.15313 5.44688,13.54687 a 20.831251,20.831251 0 0 0 13.70626,5.33438 19.687501,19.687501 0 0 0 11.61563,-3.02811 20.690626,20.690626 0 0 0 7.36874,-8.22189 23.690626,23.690626 0 0 0 2.65312,-10.14374 39.196877,39.196877 0 0 0 -6.79688,-22.98749 87.590631,87.590631 0 0 0 -18.20626,-19.98751 z"
    - class="cls-9"
    - transform="matrix(0.37919911,0,0,0.38886781,-123.17216,-79.14694)" />
    - <path
    - style="isolation:isolate;fill:#5c3566;stroke:#fdfdfd;stroke-width:11.7132;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
    - inkscape:connector-curvature="0"
    - id="path2940"
    - d="m 184.75465,435.07723 q 9.25309,-10.42501 11.90622,-19.37814 a 19.687501,19.687501 0 0 1 -9.08437,-2.24064 17.062501,17.062501 0 0 1 -6.83437,-6.38437 19.312501,19.312501 0 0 1 -2.75626,-9.75936 q -0.23457,-8.4375 5.45626,-13.55627 a 20.812501,20.812501 0 0 1 13.70626,-5.33438 19.687501,19.687501 0 0 1 11.6156,3.0375 20.709376,20.709376 0 0 1 7.36877,8.20315 23.765626,23.765626 0 0 1 2.65312,10.15313 39.281254,39.281254 0 0 1 -6.79688,22.98749 87.590631,87.590631 0 0 1 -18.20626,19.98751 z"
    - class="cls-9"
    - transform="matrix(0.37919911,0,0,0.38886781,-123.17216,-79.14694)" />
    - </g>
    - </g>
    -</svg>
    --- a/libpurple/protocols/demo/resources/purpledemo.gresource.xml Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,18 +0,0 @@
    -<?xml version="1.0" encoding="UTF-8"?>
    -<gresources>
    - <gresource prefix="/im/pidgin/purple/demo">
    - <file compressed="true" preprocess="json-stripblanks">contacts.json</file>
    - <file>icons/16x16/apps/im-purple-demo.png</file>
    - <file>icons/22x22/apps/im-purple-demo.png</file>
    - <file>icons/48x48/apps/im-purple-demo.png</file>
    - <file>icons/scalable/apps/im-purple-demo.svg</file>
    - <file>buddy_icons/Aegina.png</file>
    - <file>buddy_icons/Echo.png</file>
    - <file>buddy_icons/Eion.png</file>
    - <file>buddy_icons/Elliott.png</file>
    - <file>buddy_icons/Gary.png</file>
    - <file>buddy_icons/John.png</file>
    - <file>buddy_icons/Markus.png</file>
    - <file>buddy_icons/Richard.png</file>
    - </gresource>
    -</gresources>
    --- a/libpurple/protocols/ircv3/README.md Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,22 +0,0 @@
    -# IRCv3
    -
    -This is a brand new from-scratch protocol plugin which is the first protocol
    -plugin to be 100% code reviewed. It uses regex to tokenize messages.
    -
    -We are intending for it to be subclass-able so other networks like Twitch.tv can
    -be supported but we're not quite there yet.
    -
    -We also are intending to support subclassing in other languages but we've run
    -into some issues with dynamic GObject types and GObject introspection that have
    -slowed us down.
    -
    -## Capability Support
    -
    -This is a list of capabilities that we currently support. We'll do our best to
    -keep this list up to date, but if you notice we've missed something please let
    -us know!
    -
    -* cap-notify
    -* sasl (right now just PLAIN works)
    -* message-tags
    -* msgid
    --- a/libpurple/protocols/ircv3/ircv3generategir.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,32 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib.h>
    -
    -#include <gplugin-introspection.h>
    -
    -int
    -main(int argc, char *argv[]) {
    - return gplugin_introspection_introspect_plugin(&argc, &argv,
    - PLUGIN_FILENAME);
    -}
    -
    --- a/libpurple/protocols/ircv3/meson.build Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,114 +0,0 @@
    -IRCV3_SOURCES = [
    - 'purpleircv3capabilities.c',
    - 'purpleircv3connection.c',
    - 'purpleircv3core.c',
    - 'purpleircv3ctcp.c',
    - 'purpleircv3formatting.c',
    - 'purpleircv3message.c',
    - 'purpleircv3messagehandlers.c',
    - 'purpleircv3parser.c',
    - 'purpleircv3protocol.c',
    - 'purpleircv3protocolconversation.c',
    - 'purpleircv3sasl.c',
    - 'purpleircv3source.c',
    -]
    -
    -IRCV3_HEADERS = [
    - 'purpleircv3capabilities.h',
    - 'purpleircv3connection.h',
    - 'purpleircv3constants.h',
    - 'purpleircv3core.h',
    - 'purpleircv3ctcp.h',
    - 'purpleircv3formatting.h',
    - 'purpleircv3message.h',
    - 'purpleircv3messagehandlers.h',
    - 'purpleircv3parser.h',
    - 'purpleircv3protocol.h',
    - 'purpleircv3protocolconversation.h',
    - 'purpleircv3sasl.h',
    - 'purpleircv3source.h',
    -]
    -
    -if not DYNAMIC_IRCV3
    - subdir_done()
    -endif
    -
    -ircv3_filebase = f'purple-@purple_major_version@-ircv3'
    -
    -ircv3_includes = include_directories('.')
    -ircv3_include_base = purple_include_base / 'protocols/ircv3'
    -
    -
    -ircv3_resources = gnome.compile_resources('ircv3resource',
    - 'resources/ircv3.gresource.xml',
    - source_dir : 'resources',
    - c_name : 'purple_ircv3')
    -IRCV3_SOURCES += ircv3_resources
    -
    -ircv3_h_includes = []
    -foreach header : IRCV3_HEADERS
    - ircv3_h_includes += f'#include <@header@>'
    -endforeach
    -ircv3_h_conf = configuration_data()
    -ircv3_h_conf.set('IRCV3_H_INCLUDES', '\n'.join(ircv3_h_includes))
    -
    -ircv3_h = configure_file(input : 'purpleircv3.h.in',
    - output : 'purpleircv3.h',
    - configuration : ircv3_h_conf,
    - install : true,
    - install_dir : get_option('includedir') / ircv3_include_base)
    -
    -install_headers(IRCV3_HEADERS,
    - subdir : ircv3_include_base)
    -
    -ircv3_prpl = shared_library('ircv3', IRCV3_SOURCES + IRCV3_HEADERS + [ircv3_h],
    - c_args : ['-DPURPLE_IRCV3_COMPILATION', '-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Purple-IRCv3"'],
    - gnu_symbol_visibility : 'hidden',
    - dependencies : [birb_dep, libpurple_dep, glib, gio, hasl],
    - install : true,
    - install_dir : PURPLE_PLUGINDIR)
    -
    -ircv3_dep = declare_dependency(
    - sources : [IRCV3_SOURCES, IRCV3_HEADERS],
    - include_directories : ircv3_includes,
    - dependencies : [birb_dep, libpurple_dep, glib, gio, hasl])
    -
    -pkgconfig.generate(
    - # we purposely don't put the library here because you should not be
    - # linking to the plugin, everything will be resolved during runtime.
    - name : 'ircv3',
    - description : 'a purple3 protocol plugin for IRCv3',
    - version : meson.project_version(),
    - subdirs : [ircv3_include_base],
    - filebase : ircv3_filebase,
    - libraries : [gio, glib, hasl, libpurple])
    -
    -meson.override_dependency(ircv3_filebase, ircv3_dep)
    -
    -devenv.append('PURPLE_PLUGIN_PATH', meson.current_build_dir())
    -
    -if get_option('introspection')
    - GPLUGIN_INTROSPECTION = dependency('gplugin-introspection')
    -
    - plugin_filename = ircv3_prpl.full_path()
    -
    - ircv3_introspection_stub = executable('ircv3generategir',
    - sources : 'ircv3generategir.c',
    - dependencies : [ircv3_dep, libpurple_dep, glib, gio, hasl, GPLUGIN_INTROSPECTION],
    - c_args : ['-DPURPLE_IRCV3_COMPILATION', f'-DPLUGIN_FILENAME="@plugin_filename@"'],
    - install : false)
    -
    - gnome.generate_gir(
    - ircv3_introspection_stub,
    - sources : [IRCV3_SOURCES, IRCV3_HEADERS],
    - includes : ['Birb-1.0', 'GLib-2.0', 'GObject-2.0', 'GPlugin-1.0', libpurple_gir[0]],
    - namespace : 'PurpleIRCv3',
    - symbol_prefix : 'purple_ircv3',
    - nsversion : '1.0',
    - install : true,
    - dependencies: [birb_dep, gplugin_dep],
    - export_packages : ['ircv3'],
    - extra_args : ['-DPURPLE_IRCV3_COMPILATION', '--verbose'])
    -endif
    -
    -subdir('tests')
    --- a/libpurple/protocols/ircv3/purpleircv3.h.in Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,40 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#ifndef PURPLE_IRCV3_H
    -#define PURPLE_IRCV3_H
    -
    -#ifndef __GI_SCANNER__ /* hide this bit from g-ir-scanner */
    -# ifdef PURPLE_IRCV3_COMPILATION
    -# error "ircv3 source files should not be including purpleircv3.h"
    -# endif /* PURPLE_IRCV3_COMPILATION */
    -#endif /* __GI_SCANNER__ */
    -
    -#ifndef PURPLE_IRCV3_GLOBAL_HEADER_INSIDE
    -# define PURPLE_IRCV3_GLOBAL_HEADER_INSIDE
    -#endif /* PURPLE_IRCV3_GLOBAL_HEADER_INSIDE */
    -
    -@IRCV3_H_INCLUDES@
    -
    -#undef PURPLE_IRCV3_GLOBAL_HEADER_INSIDE
    -
    -#endif /* PURPLE_IRCV3_H */
    --- a/libpurple/protocols/ircv3/purpleircv3capabilities.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,685 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include "purpleircv3capabilities.h"
    -
    -#include "purpleircv3connection.h"
    -#include "purpleircv3core.h"
    -#include "purpleircv3sasl.h"
    -
    -enum {
    - PROP_0,
    - PROP_CONNECTION,
    - N_PROPERTIES,
    -};
    -static GParamSpec *properties[N_PROPERTIES] = {NULL, };
    -
    -/* Windows is does something weird with signal handling that includes defining
    - * SIG_ACK. We don't care about that here, so we undef it if we find it.
    - * See https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-action-constants?view=msvc-170
    - */
    -#ifdef SIG_ACK
    -# undef SIG_ACK
    -#endif /* SIG_ACK */
    -
    -enum {
    - SIG_READY,
    - SIG_ACK,
    - SIG_NAK,
    - SIG_DONE,
    - SIG_NEW,
    - SIG_DEL,
    - N_SIGNALS,
    -};
    -static guint signals[N_SIGNALS] = {0, };
    -
    -struct _PurpleIRCv3Capabilities {
    - GObject parent;
    -
    - PurpleIRCv3Connection *connection;
    -
    - GHashTable *caps;
    - GPtrArray *requests;
    -
    - gatomicrefcount wait_counters;
    -};
    -
    -/******************************************************************************
    - * Helpers
    - *****************************************************************************/
    -static void
    -purple_ircv3_capabilities_set_connection(PurpleIRCv3Capabilities *capabilities,
    - PurpleIRCv3Connection *connection)
    -{
    - g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
    -
    - if(g_set_object(&capabilities->connection, connection)) {
    - g_object_notify_by_pspec(G_OBJECT(capabilities),
    - properties[PROP_CONNECTION]);
    - }
    -}
    -
    -static void
    -purple_ircv3_capabilities_finish(PurpleIRCv3Capabilities *capabilities) {
    - purple_ircv3_connection_writef(capabilities->connection,
    - "CAP END");
    -
    - g_signal_emit(capabilities, signals[SIG_DONE], 0);
    -}
    -
    -static void
    -purple_ircv3_capabilities_add(PurpleIRCv3Capabilities *capabilities,
    - const char *capability)
    -{
    - char *equals = g_strstr_len(capability, -1, "=");
    -
    - if(equals != NULL) {
    - char *key = g_strndup(capability, equals - capability);
    - char *value = g_strdup(equals + 1);
    -
    - g_hash_table_insert(capabilities->caps, key, value);
    - } else {
    - g_hash_table_insert(capabilities->caps, g_strdup(capability), NULL);
    - }
    -}
    -
    -/******************************************************************************
    - * Callbacks
    - *****************************************************************************/
    -static void
    -ircv3_capabilities_message_tags_ack_cb(PurpleIRCv3Capabilities *capabilities,
    - G_GNUC_UNUSED const char *capability,
    - G_GNUC_UNUSED gpointer data)
    -{
    - /* We have message tags so add the stuff we support that depends on it. */
    - purple_ircv3_capabilities_lookup_and_request(capabilities, "msgid");
    -}
    -
    -/******************************************************************************
    - * PurpleIRCv3Capabilities Implementation
    - *****************************************************************************/
    -static void
    -purple_ircv3_capabilities_default_ready_cb(PurpleIRCv3Capabilities *capabilities)
    -{
    - PurpleAccount *account = NULL;
    - PurpleConnection *purple_connection = NULL;
    -
    - purple_connection = PURPLE_CONNECTION(capabilities->connection);
    - account = purple_connection_get_account(purple_connection);
    -
    - /* Don't request the sasl capability unless the user has selected the
    - * require-password option.
    - */
    - if(purple_account_get_require_password(account)) {
    - gboolean found = FALSE;
    -
    - purple_ircv3_capabilities_lookup(capabilities, "sasl", &found);
    -
    - if(found) {
    - purple_ircv3_sasl_request(capabilities);
    - }
    - }
    -
    - /* cap-notify is implied when we use CAP LS 302, so this is really just to
    - * make sure it's requested.
    - */
    - purple_ircv3_capabilities_lookup_and_request(capabilities, "cap-notify");
    -
    - /* message-tags is used for a lot of stuff so we need to tell everyone we
    - * do in fact support it.
    - */
    - if(purple_ircv3_capabilities_lookup_and_request(capabilities,
    - "message-tags"))
    - {
    - g_signal_connect(capabilities, "ack::message-tags",
    - G_CALLBACK(ircv3_capabilities_message_tags_ack_cb),
    - NULL);
    - }
    -
    - /* The server-time capability just tells the server to send a tag to
    - * messages, so we just need to request it and then handle the tag when
    - * we're processing messages if it exists.
    - */
    - purple_ircv3_capabilities_lookup_and_request(capabilities,
    - PURPLE_IRCV3_CAPABILITY_SERVER_TIME);
    -}
    -
    -/******************************************************************************
    - * GObject Implementation
    - *****************************************************************************/
    -G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleIRCv3Capabilities,
    - purple_ircv3_capabilities, G_TYPE_OBJECT,
    - G_TYPE_FLAG_FINAL, {})
    -
    -static void
    -purple_ircv3_capabilities_get_property(GObject *obj, guint param_id,
    - GValue *value, GParamSpec *pspec)
    -{
    - PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
    -
    - switch(param_id) {
    - case PROP_CONNECTION:
    - g_value_set_object(value,
    - purple_ircv3_capabilities_get_connection(capabilities));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -purple_ircv3_capabilities_set_property(GObject *obj, guint param_id,
    - const GValue *value, GParamSpec *pspec)
    -{
    - PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
    -
    - switch(param_id) {
    - case PROP_CONNECTION:
    - purple_ircv3_capabilities_set_connection(capabilities,
    - g_value_get_object(value));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -purple_ircv3_capabilities_dispose(GObject *obj) {
    - PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
    -
    - g_clear_object(&capabilities->connection);
    -
    - G_OBJECT_CLASS(purple_ircv3_capabilities_parent_class)->dispose(obj);
    -}
    -
    -static void
    -purple_ircv3_capabilities_finalize(GObject *obj) {
    - PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
    -
    - g_hash_table_destroy(capabilities->caps);
    - g_ptr_array_free(capabilities->requests, TRUE);
    -
    - G_OBJECT_CLASS(purple_ircv3_capabilities_parent_class)->finalize(obj);
    -}
    -
    -static void
    -purple_ircv3_capabilities_init(PurpleIRCv3Capabilities *capabilities) {
    - capabilities->caps = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
    - g_free);
    - capabilities->requests = g_ptr_array_new_full(0, g_free);
    -
    - g_atomic_ref_count_init(&capabilities->wait_counters);
    -}
    -
    -static void
    -purple_ircv3_capabilities_class_finalize(G_GNUC_UNUSED PurpleIRCv3CapabilitiesClass *klass) {
    -}
    -
    -static void
    -purple_ircv3_capabilities_class_init(PurpleIRCv3CapabilitiesClass *klass) {
    - GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    -
    - obj_class->dispose = purple_ircv3_capabilities_dispose;
    - obj_class->finalize = purple_ircv3_capabilities_finalize;
    - obj_class->get_property = purple_ircv3_capabilities_get_property;
    - obj_class->set_property = purple_ircv3_capabilities_set_property;
    -
    - /**
    - * PurpleIRCv3Capabilities:connection:
    - *
    - * The PurpleIRCv3Connection object that this capabilities was created
    - * with.
    - *
    - * Since: 3.0
    - */
    - properties[PROP_CONNECTION] = g_param_spec_object(
    - "connection", "connection",
    - "The connection this capabilities was created for.",
    - PURPLE_IRCV3_TYPE_CONNECTION,
    - G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
    -
    - g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    -
    - /**
    - * PurpleIRCv3Capabilities::ready:
    - * @capabilities: The instance.
    - *
    - * Emitted when @capabilities has finished receiving the list of
    - * capabilities from the server at startup.
    - *
    - * For dynamically added capabilities see the `added` and `removed`
    - * signals.
    - *
    - * Since: 3.0
    - */
    - signals[SIG_READY] = g_signal_new_class_handler(
    - "ready",
    - G_OBJECT_CLASS_TYPE(klass),
    - G_SIGNAL_RUN_LAST,
    - G_CALLBACK(purple_ircv3_capabilities_default_ready_cb),
    - NULL,
    - NULL,
    - NULL,
    - G_TYPE_NONE,
    - 0);
    -
    - /**
    - * PurpleIRCv3Capabilities::ack:
    - * @capabilities: The instance.
    - * @capability: The capability string.
    - *
    - * Emitted when the server has acknowledged a `CAP REQ` call from
    - * purple_ircv3_capabilities_request.
    - *
    - * The value of @capability will be the same as the one that was requested.
    - *
    - * Since: 3.0
    - */
    - signals[SIG_ACK] = g_signal_new_class_handler(
    - "ack",
    - G_OBJECT_CLASS_TYPE(klass),
    - G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
    - NULL,
    - NULL,
    - NULL,
    - NULL,
    - G_TYPE_NONE,
    - 1,
    - G_TYPE_STRING);
    -
    - /**
    - * PurpleIRCv3Capabilities::nak:
    - * @capabilities: The instance.
    - * @capability: The capability string.
    - *
    - * Emitted when the server has nacked a `CAP REQ` call from
    - * purple_ircv3_capabilities_request.
    - *
    - * The value of @capability will be the same as the one that was requested.
    - *
    - * Since: 3.0
    - */
    - signals[SIG_NAK] = g_signal_new_class_handler(
    - "nak",
    - G_OBJECT_CLASS_TYPE(klass),
    - G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
    - NULL,
    - NULL,
    - NULL,
    - NULL,
    - G_TYPE_NONE,
    - 1,
    - G_TYPE_STRING);
    -
    - /**
    - * PurpleIRCv3Capabilities::done:
    - * @capabilities: The instance.
    - *
    - * Emitted when all of the requested capabilities have been either ack'd or
    - * nak'd by the server.
    - *
    - * Since: 3.0
    - */
    - signals[SIG_DONE] = g_signal_new_class_handler(
    - "done",
    - G_OBJECT_CLASS_TYPE(klass),
    - G_SIGNAL_RUN_LAST,
    - NULL,
    - NULL,
    - NULL,
    - NULL,
    - G_TYPE_NONE,
    - 0);
    -
    - /**
    - * PurpleIRCv3Capabilities::new:
    - * @capabilities: The instance.
    - * @added: The newly added capabilities.
    - *
    - * Emitted when the server sends the `CAP NEW` command. @added is a
    - * [type@GLib.Strv] of the new capabilities the server added.
    - *
    - * There are two approaches to how you can use this signal. You can check
    - * each item in @added for the values you need and parsing their values, or
    - * you can call #purple_ircv3_capabilities_lookup to see if the
    - * capabilities you're interested in have been added.
    - *
    - * Since: 3.0
    - */
    - signals[SIG_NEW] = g_signal_new_class_handler(
    - "new",
    - G_OBJECT_CLASS_TYPE(klass),
    - G_SIGNAL_RUN_LAST,
    - NULL,
    - NULL,
    - NULL,
    - NULL,
    - G_TYPE_NONE,
    - 1,
    - G_TYPE_STRV);
    -
    - /**
    - * PurpleIRCv3Capabilities::del:
    - * @capabilities: The instance.
    - * @removed: The capabilities that were removed.
    - *
    - * Emitted when the server sends the `CAP DEL` command. @removed is a
    - * [type@GLib.Strv] of the capabilities that the server removed.
    - *
    - * There are two approaches to how you can use this signal. You can check
    - * each item in @removed for the values you care about, or you can call
    - * #purple_ircv3_capabilities_lookup to see if the capabilities you're
    - * interested in have been removed.
    - *
    - * Since: 3.0
    - */
    - signals[SIG_DEL] = g_signal_new_class_handler(
    - "del",
    - G_OBJECT_CLASS_TYPE(klass),
    - G_SIGNAL_RUN_LAST,
    - NULL,
    - NULL,
    - NULL,
    - NULL,
    - G_TYPE_NONE,
    - 1,
    - G_TYPE_STRV);
    -}
    -
    -/******************************************************************************
    - * Command handlers
    - *****************************************************************************/
    -static gboolean
    -purple_ircv3_capabilities_handle_list(PurpleIRCv3Capabilities *capabilities,
    - guint n_params,
    - GStrv params,
    - G_GNUC_UNUSED GError **error)
    -{
    - gboolean done = TRUE;
    - gchar **parts = NULL;
    -
    - /* Check if we have more messages coming. */
    - if(n_params > 1 && purple_strequal(params[0], "*")) {
    - parts = g_strsplit(params[1], " ", -1);
    - done = FALSE;
    - } else {
    - parts = g_strsplit(params[0], " ", -1);
    - }
    -
    - /* Add each capability to our hash table, splitting the keys and values. */
    - for(int i = 0; parts[i] != NULL; i++) {
    - purple_ircv3_capabilities_add(capabilities, parts[i]);
    - }
    -
    - g_strfreev(parts);
    -
    - if(done) {
    - g_signal_emit(capabilities, signals[SIG_READY], 0, signals[SIG_READY]);
    -
    - /* If no capabilities were requested after we emitted the ready signal
    - * we're done with capability negotiation.
    - */
    - if(capabilities->requests->len == 0) {
    - purple_ircv3_capabilities_remove_wait(capabilities);
    - }
    - }
    -
    - return TRUE;
    -}
    -
    -static gboolean
    -purple_ircv3_capabilities_handle_ack_nak(PurpleIRCv3Capabilities *capabilities,
    - GStrv params,
    - guint sig,
    - const char *method,
    - GError **error)
    -{
    - char *caps = g_strjoinv(" ", params);
    - guint index = 0;
    - gboolean found = FALSE;
    - gboolean ret = TRUE;
    -
    - g_signal_emit(capabilities, sig, g_quark_from_string(caps), caps);
    -
    - found = g_ptr_array_find_with_equal_func(capabilities->requests, caps,
    - g_str_equal, &index);
    - if(found) {
    - g_ptr_array_remove_index(capabilities->requests, index);
    - } else {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "received CAP %s for unknown capability %s", method, caps);
    - ret = FALSE;
    - }
    - g_free(caps);
    -
    - if(capabilities->requests->len == 0) {
    - purple_ircv3_capabilities_remove_wait(capabilities);
    - }
    -
    - return ret;
    -}
    -
    -static gboolean
    -purple_ircv3_capabilities_handle_new(PurpleIRCv3Capabilities *capabilities,
    - guint n_params,
    - GStrv params,
    - G_GNUC_UNUSED GError **error)
    -{
    - for(guint i = 0; i < n_params; i++) {
    - purple_ircv3_capabilities_add(capabilities, params[i]);
    - }
    -
    - g_signal_emit(capabilities, signals[SIG_NEW], 0, params);
    -
    - return TRUE;
    -}
    -
    -static gboolean
    -purple_ircv3_capabilities_handle_del(PurpleIRCv3Capabilities *capabilities,
    - guint n_params,
    - GStrv params,
    - G_GNUC_UNUSED GError **error)
    -{
    - for(guint i = 0; i < n_params; i++) {
    - g_hash_table_remove(capabilities->caps, params[i]);
    - }
    -
    - g_signal_emit(capabilities, signals[SIG_DEL], 0, params);
    -
    - return TRUE;
    -}
    -
    -/******************************************************************************
    - * Internal API
    - *****************************************************************************/
    -void
    -purple_ircv3_capabilities_register(GPluginNativePlugin *plugin) {
    - purple_ircv3_capabilities_register_type(G_TYPE_MODULE(plugin));
    -}
    -
    -PurpleIRCv3Capabilities *
    -purple_ircv3_capabilities_new(PurpleIRCv3Connection *connection) {
    - return g_object_new(
    - PURPLE_IRCV3_TYPE_CAPABILITIES,
    - "connection", connection,
    - NULL);
    -}
    -
    -void
    -purple_ircv3_capabilities_start(PurpleIRCv3Capabilities *capabilities) {
    - purple_ircv3_connection_writef(capabilities->connection, "CAP LS %s",
    - PURPLE_IRCV3_CAPABILITY_CAP_LS_VERSION);
    -}
    -
    -gboolean
    -purple_ircv3_capabilities_message_handler(PurpleIRCv3Message *message,
    - GError **error,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *connection = data;
    - PurpleIRCv3Capabilities *capabilities = NULL;
    - GStrv params = NULL;
    - GStrv subparams = NULL;
    - const char *subcommand = NULL;
    - guint n_params = 0;
    - guint n_subparams = 0;
    -
    - params = purple_ircv3_message_get_params(message);
    - if(params != NULL) {
    - n_params = g_strv_length(params);
    - }
    -
    - if(n_params < 2) {
    - return FALSE;
    - }
    -
    - capabilities = purple_ircv3_connection_get_capabilities(connection);
    -
    - /* Initialize some variables to make it easier to call our sub command
    - * handlers.
    - *
    - * params[0] is the nick or * if it hasn't been negotiated yet, we don't
    - * have a need for this, so we ignore it.
    - *
    - * params[1] is the CAP subcommand sent from the server. We use it here
    - * purely for dispatching to our subcommand handlers.
    - *
    - * params[2] and higher are the parameters to the subcommand. To make the
    - * code a bit easier all around, we subtract 2 from n_params to remove
    - * references to the nick and subcommand name. Like wise, we add 2 to the
    - * params GStrv which will now point to the second item in the array again
    - * ignoring the nick and subcommand.
    - */
    - subcommand = params[1];
    - n_subparams = n_params - 2;
    - subparams = params + 2;
    -
    - /* Dispatch the subcommand. */
    - if(purple_strequal(subcommand, "LS") ||
    - purple_strequal(subcommand, "LIST"))
    - {
    - return purple_ircv3_capabilities_handle_list(capabilities, n_subparams,
    - subparams, error);
    - } else if(purple_strequal(subcommand, "ACK")) {
    - return purple_ircv3_capabilities_handle_ack_nak(capabilities,
    - subparams,
    - signals[SIG_ACK],
    - "ACK",
    - error);
    - } else if(purple_strequal(subcommand, "NAK")) {
    - return purple_ircv3_capabilities_handle_ack_nak(capabilities,
    - subparams,
    - signals[SIG_NAK],
    - "NAK",
    - error);
    - } else if(purple_strequal(subcommand, "NEW")) {
    - return purple_ircv3_capabilities_handle_new(capabilities, n_subparams,
    - subparams, error);
    - } else if(purple_strequal(subcommand, "DEL")) {
    - return purple_ircv3_capabilities_handle_del(capabilities, n_subparams,
    - subparams, error);
    - }
    -
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "No handler for CAP subcommand %s", subcommand);
    -
    - return FALSE;
    -}
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -PurpleIRCv3Connection *
    -purple_ircv3_capabilities_get_connection(PurpleIRCv3Capabilities *capabilities)
    -{
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), NULL);
    -
    - return capabilities->connection;
    -}
    -
    -void
    -purple_ircv3_capabilities_request(PurpleIRCv3Capabilities *capabilities,
    - const char *capability)
    -{
    - g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
    - g_return_if_fail(capability != NULL);
    -
    - g_ptr_array_add(capabilities->requests, g_strdup(capability));
    -
    - purple_ircv3_connection_writef(capabilities->connection, "CAP REQ :%s",
    - capability);
    -}
    -
    -const char *
    -purple_ircv3_capabilities_lookup(PurpleIRCv3Capabilities *capabilities,
    - const char *name, gboolean *found)
    -{
    - gpointer value = NULL;
    - gboolean real_found = FALSE;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), NULL);
    - g_return_val_if_fail(name != NULL, NULL);
    -
    - real_found = g_hash_table_lookup_extended(capabilities->caps, name, NULL,
    - &value);
    -
    - if(found != NULL) {
    - *found = real_found;
    - }
    -
    - return value;
    -}
    -
    -gboolean
    -purple_ircv3_capabilities_lookup_and_request(PurpleIRCv3Capabilities *capabilities,
    - const char *name)
    -{
    - gboolean found = FALSE;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), FALSE);
    - g_return_val_if_fail(name != NULL, FALSE);
    -
    - purple_ircv3_capabilities_lookup(capabilities, name, &found);
    - if(found) {
    - purple_ircv3_capabilities_request(capabilities, name);
    - }
    -
    - return found;
    -}
    -
    -void
    -purple_ircv3_capabilities_add_wait(PurpleIRCv3Capabilities *capabilities) {
    - g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
    -
    - g_atomic_ref_count_inc(&capabilities->wait_counters);
    -}
    -
    -void
    -purple_ircv3_capabilities_remove_wait(PurpleIRCv3Capabilities *capabilities) {
    - g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
    -
    - if(g_atomic_ref_count_dec(&capabilities->wait_counters)) {
    - purple_ircv3_capabilities_finish(capabilities);
    - }
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3capabilities.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,172 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_CAPABILITIES_H
    -#define PURPLE_IRCV3_CAPABILITIES_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <gplugin.h>
    -#include <gplugin-native.h>
    -
    -#include <purple.h>
    -
    -#include "purpleircv3version.h"
    -
    -G_BEGIN_DECLS
    -
    -/* https://ircv3.net/specs/extensions/capability-negotiation */
    -#define PURPLE_IRCV3_CAPABILITY_CAP_LS_VERSION "302"
    -
    -/* https://ircv3.net/specs/extensions/sasl-3.2 */
    -#define PURPLE_IRCV3_CAPABILITY_SASL "sasl"
    -
    -/* https://ircv3.net/specs/extensions/server-time */
    -#define PURPLE_IRCV3_CAPABILITY_SERVER_TIME "server-time"
    -
    -#define PURPLE_IRCV3_TYPE_CAPABILITIES (purple_ircv3_capabilities_get_type())
    -
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -G_DECLARE_FINAL_TYPE(PurpleIRCv3Capabilities, purple_ircv3_capabilities,
    - PURPLE_IRCV3, CAPABILITIES, GObject)
    -
    -#include "purpleircv3connection.h"
    -#include "purpleircv3message.h"
    -
    -/**
    - * purple_ircv3_capabilities_register: (skip)
    - * @plugin: The [class@GPlugin.NativePlugin] instance.
    - *
    - * Dynamically registers the PurpleIRCv3Capabilities type.
    - *
    - * Since: 3.0
    - */
    -G_GNUC_INTERNAL void purple_ircv3_capabilities_register(GPluginNativePlugin *plugin);
    -
    -G_GNUC_INTERNAL PurpleIRCv3Capabilities *purple_ircv3_capabilities_new(PurpleIRCv3Connection *connection);
    -
    -G_GNUC_INTERNAL void purple_ircv3_capabilities_start(PurpleIRCv3Capabilities *capabilities);
    -
    -G_GNUC_INTERNAL gboolean purple_ircv3_capabilities_message_handler(PurpleIRCv3Message *message, GError **error, gpointer data);
    -
    -/**
    - * purple_ircv3_capabilities_get_connection:
    - * @capabilities: The instance.
    - *
    - * Gets the PurpleIRCv3Connection object that @capabilities was created with.
    - *
    - * Returns: (transfer none): The connection instance.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -PurpleIRCv3Connection *purple_ircv3_capabilities_get_connection(PurpleIRCv3Capabilities *capabilities);
    -
    -/**
    - * purple_ircv3_capabilities_request:
    - * @capabilities: The instance.
    - * @capability: The capabilities to request.
    - *
    - * This method will send `CAP REQ @capability` to the server. Listen to the
    - * `::ack` and `::nak` signals which will contain the contents of @capability
    - * that was passed in here.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_capabilities_request(PurpleIRCv3Capabilities *capabilities, const char *capability);
    -
    -/**
    - * purple_ircv3_capabilities_lookup:
    - * @capabilities: The instance.
    - * @name: The name of the capability to look for.
    - * @found: (out) (nullable): A return address for a boolean on whether the
    - * capability was advertised or not.
    - *
    - * Gets the value that the @name capability provided if it was advertised. To
    - * determine if the capability was advertised use the @found parameter.
    - *
    - * Returns: The value of the capability named @name.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -const char *purple_ircv3_capabilities_lookup(PurpleIRCv3Capabilities *capabilities, const char *name, gboolean *found);
    -
    -/**
    - * purple_ircv3_capabilities_lookup_and_request:
    - * @capabilities: The instance.
    - * @name: The name of the capability to look for.
    - *
    - * A helper function to call [method@PurpleIRCv3.Capabilities.Lookup] and if
    - * found, call [method@PurpleIRCv3.Capabilities.Request].
    - *
    - * This method ignores the advertised value, so to get that you'll need to call
    - * [method@PurpleIRCv3.Capabilities.Lookup] yourself.
    - *
    - * Also if you need to do something when the server ACK's or NAK's your
    - * request, you're probably better off just using the methods yourself.
    - *
    - * Returns: %TRUE if @name was found and requested, %FALSE otherwise.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -gboolean purple_ircv3_capabilities_lookup_and_request(PurpleIRCv3Capabilities *capabilities, const char *name);
    -
    -/**
    - * purple_ircv3_capabilties_add_wait:
    - * @capabilities: The instance.
    - *
    - * Adds a wait counter to @capabilities. This counter is used to delay the
    - * call of `CAP END` until all capability negotiation has completed. This is
    - * necessary for SASL and may be necessary for other capabilities as well.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_capabilities_add_wait(PurpleIRCv3Capabilities *capabilities);
    -
    -/**
    - * purple_ircv3_capabilties_remove_wait:
    - * @capabilities: The instance.
    - *
    - * Removes a wait counter from @capabilities. Only when this counter reaches 0,
    - * will `CAP END` be called and registration completed.
    - *
    - * This is necessary for SASL and may be necessary for other capabilities as
    - * well.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_capabilities_remove_wait(PurpleIRCv3Capabilities *capabilities);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_CAPABILITIES_H */
    --- a/libpurple/protocols/ircv3/purpleircv3connection.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,964 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib/gi18n-lib.h>
    -
    -#include <birb.h>
    -
    -#include "purpleircv3connection.h"
    -
    -#include "purpleircv3constants.h"
    -#include "purpleircv3core.h"
    -#include "purpleircv3ctcp.h"
    -#include "purpleircv3formatting.h"
    -#include "purpleircv3parser.h"
    -
    -enum {
    - PROP_0,
    - PROP_CAPABILITIES,
    - PROP_REGISTERED,
    - N_PROPERTIES,
    -};
    -static GParamSpec *properties[N_PROPERTIES] = {NULL, };
    -
    -enum {
    - SIG_REGISTRATION_COMPLETE,
    - SIG_CTCP_REQUEST,
    - SIG_CTCP_RESPONSE,
    - N_SIGNALS,
    -};
    -static guint signals[N_SIGNALS] = {0, };
    -
    -typedef struct {
    - GSocketConnection *connection;
    -
    - gchar *server_name;
    - gboolean registered;
    -
    - GDataInputStream *input;
    - GOutputStream *output;
    -
    - PurpleIRCv3Parser *parser;
    -
    - PurpleIRCv3Capabilities *capabilities;
    -
    - PurpleConversation *status_conversation;
    -} PurpleIRCv3ConnectionPrivate;
    -
    -G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleIRCv3Connection,
    - purple_ircv3_connection,
    - PURPLE_TYPE_CONNECTION,
    - 0,
    - G_ADD_PRIVATE_DYNAMIC(PurpleIRCv3Connection))
    -
    -/******************************************************************************
    - * Helpers
    - *****************************************************************************/
    -static void
    -purple_ircv3_connection_send_pass_command(PurpleIRCv3Connection *connection) {
    - PurpleAccount *account = NULL;
    - const char *password = NULL;
    -
    - account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    -
    - password = purple_account_get_string(account, "server-password", "");
    - if(password != NULL && *password != '\0') {
    - purple_ircv3_connection_writef(connection, "PASS %s", password);
    - }
    -}
    -
    -static void
    -purple_ircv3_connection_send_user_command(PurpleIRCv3Connection *connection) {
    - PurpleAccount *account = NULL;
    - const char *identname = NULL;
    - const char *nickname = NULL;
    - const char *realname = NULL;
    -
    - nickname =
    - purple_connection_get_display_name(PURPLE_CONNECTION(connection));
    -
    - account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    -
    - /* The stored value could be an empty string, so pass a default of empty
    - * string and then if it was empty, set our correct fallback.
    - */
    - identname = purple_account_get_string(account, "ident", "");
    - if(identname == NULL || *identname == '\0') {
    - identname = nickname;
    - }
    -
    - realname = purple_account_get_string(account, "real-name", "");
    - if(realname == NULL || *realname == '\0') {
    - realname = nickname;
    - }
    -
    - purple_ircv3_connection_writef(connection, "USER %s 0 * :%s", identname,
    - realname);
    -}
    -
    -static void
    -purple_ircv3_connection_send_nick_command(PurpleIRCv3Connection *connection) {
    - const char *nickname = NULL;
    -
    - nickname =
    - purple_connection_get_display_name(PURPLE_CONNECTION(connection));
    -
    - purple_ircv3_connection_writef(connection, "NICK %s", nickname);
    -}
    -
    -static void
    -purple_ircv3_connection_rejoin_channels(PurpleIRCv3Connection *connection) {
    - PurpleAccount *account = NULL;
    - PurpleConversationManager *manager = NULL;
    - GList *conversations = NULL;
    -
    - account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    - manager = purple_conversation_manager_get_default();
    -
    - conversations = purple_conversation_manager_get_all(manager);
    - while(conversations != NULL) {
    - PurpleConversation *conversation = conversations->data;
    - PurpleAccount *conv_account = NULL;
    -
    - conv_account = purple_conversation_get_account(conversation);
    - if(conv_account == account) {
    - const char *id = purple_conversation_get_id(conversation);
    -
    - purple_ircv3_connection_writef(connection, "%s %s",
    - PURPLE_IRCV3_MSG_JOIN, id);
    - }
    -
    - conversations = g_list_delete_link(conversations, conversations);
    - }
    -}
    -
    -/******************************************************************************
    - * Callbacks
    - *****************************************************************************/
    -static void
    -purple_ircv3_connection_read_cb(GObject *source, GAsyncResult *result,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *connection = data;
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    - GCancellable *cancellable = NULL;
    - GDataInputStream *istream = G_DATA_INPUT_STREAM(source);
    - GError *error = NULL;
    - gchar *line = NULL;
    - gsize length;
    - gboolean parsed = FALSE;
    -
    - line = g_data_input_stream_read_line_finish(istream, result, &length,
    - &error);
    - if(line == NULL || error != NULL) {
    - if(PURPLE_IS_CONNECTION(connection)) {
    - if(error == NULL) {
    - g_set_error_literal(&error, PURPLE_CONNECTION_ERROR,
    - PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
    - _("Server closed the connection"));
    - } else {
    - g_prefix_error(&error, "%s", _("Lost connection with server: "));
    - }
    -
    - purple_connection_take_error(PURPLE_CONNECTION(connection), error);
    - }
    -
    - /* In the off chance that line was returned, make sure we free it. */
    - g_free(line);
    -
    - return;
    - }
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - parsed = purple_ircv3_parser_parse(priv->parser, line, &error,
    - connection);
    - if(!parsed) {
    - g_warning("failed to handle '%s': %s", line,
    - error != NULL ? error->message : "unknown error");
    - }
    - g_clear_error(&error);
    -
    - g_free(line);
    -
    - /* Call read_line_async again to continue reading lines. */
    - cancellable = purple_connection_get_cancellable(PURPLE_CONNECTION(connection));
    - g_data_input_stream_read_line_async(priv->input,
    - G_PRIORITY_DEFAULT,
    - cancellable,
    - purple_ircv3_connection_read_cb,
    - connection);
    -}
    -
    -static void
    -purple_ircv3_connection_write_cb(GObject *source, GAsyncResult *result,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *connection = data;
    - BirbQueuedOutputStream *stream = BIRB_QUEUED_OUTPUT_STREAM(source);
    - GError *error = NULL;
    - gboolean success = FALSE;
    -
    - success = birb_queued_output_stream_push_bytes_finish(stream, result,
    - &error);
    -
    - if(!success) {
    - birb_queued_output_stream_clear_queue(stream);
    -
    - g_prefix_error(&error, "%s", _("Lost connection with server: "));
    -
    - purple_connection_take_error(PURPLE_CONNECTION(connection), error);
    -
    - return;
    - }
    -}
    -
    -static void
    -purple_ircv3_connection_connected_cb(GObject *source, GAsyncResult *result,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *connection = data;
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    - GCancellable *cancellable = NULL;
    - GError *error = NULL;
    - GInputStream *istream = NULL;
    - GOutputStream *ostream = NULL;
    - GSocketClient *client = G_SOCKET_CLIENT(source);
    - GSocketConnection *conn = NULL;
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - /* Finish the async method. */
    - conn = g_socket_client_connect_to_host_finish(client, result, &error);
    - if(conn == NULL || error != NULL) {
    - g_prefix_error(&error, "%s", _("Unable to connect: "));
    -
    - purple_connection_take_error(PURPLE_CONNECTION(connection), error);
    -
    - return;
    - }
    -
    - g_message("Successfully connected to %s", priv->server_name);
    -
    - /* Save our connection and setup our input and outputs. */
    - priv->connection = conn;
    -
    - /* Create our parser. */
    - priv->parser = purple_ircv3_parser_new();
    - purple_ircv3_parser_add_default_handlers(priv->parser);
    -
    - ostream = g_io_stream_get_output_stream(G_IO_STREAM(conn));
    - priv->output = birb_queued_output_stream_new(ostream);
    -
    - istream = g_io_stream_get_input_stream(G_IO_STREAM(conn));
    - priv->input = g_data_input_stream_new(istream);
    - g_data_input_stream_set_newline_type(G_DATA_INPUT_STREAM(priv->input),
    - G_DATA_STREAM_NEWLINE_TYPE_CR_LF);
    -
    - cancellable = purple_connection_get_cancellable(PURPLE_CONNECTION(connection));
    -
    - /* Add our read callback. */
    - g_data_input_stream_read_line_async(priv->input,
    - G_PRIORITY_DEFAULT,
    - cancellable,
    - purple_ircv3_connection_read_cb,
    - connection);
    -
    - /* Send our registration commands. */
    - purple_ircv3_capabilities_start(priv->capabilities);
    - purple_ircv3_connection_send_pass_command(connection);
    - purple_ircv3_connection_send_user_command(connection);
    - purple_ircv3_connection_send_nick_command(connection);
    -}
    -
    -static void
    -purple_ircv3_connection_caps_done_cb(G_GNUC_UNUSED PurpleIRCv3Capabilities *caps,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *connection = data;
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - priv->registered = TRUE;
    -
    - g_signal_emit(connection, signals[SIG_REGISTRATION_COMPLETE], 0);
    -
    - /* Add our supported CTCP commands. */
    - purple_ircv3_ctcp_add_default_handlers(connection);
    -
    - /* Now that registration is complete, rejoin any channels that the
    - * conversation manager has for us.
    - */
    - purple_ircv3_connection_rejoin_channels(connection);
    -}
    -
    -/******************************************************************************
    - * PurpleConnection Implementation
    - *****************************************************************************/
    -static gboolean
    -purple_ircv3_connection_connect(PurpleConnection *purple_connection,
    - GError **error)
    -{
    - PurpleIRCv3Connection *connection = NULL;
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    - PurpleAccount *account = NULL;
    - GCancellable *cancellable = NULL;
    - GSocketClient *client = NULL;
    - gint default_port = PURPLE_IRCV3_DEFAULT_TLS_PORT;
    - gint port = 0;
    - gboolean use_tls = TRUE;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(purple_connection), FALSE);
    -
    - connection = PURPLE_IRCV3_CONNECTION(purple_connection);
    - priv = purple_ircv3_connection_get_instance_private(connection);
    - account = purple_connection_get_account(purple_connection);
    -
    - client = purple_gio_socket_client_new(account, error);
    - if(!G_IS_SOCKET_CLIENT(client)) {
    - if(error != NULL && *error != NULL) {
    - purple_connection_take_error(purple_connection, *error);
    - }
    -
    - return FALSE;
    - }
    -
    - /* Turn on TLS if requested. */
    - use_tls = purple_account_get_bool(account, "use-tls", TRUE);
    - g_socket_client_set_tls(client, use_tls);
    -
    - /* If TLS is not being used, set the default port to the plain port. */
    - if(!use_tls) {
    - default_port = PURPLE_IRCV3_DEFAULT_PLAIN_PORT;
    - }
    - port = purple_account_get_int(account, "port", default_port);
    -
    - cancellable = purple_connection_get_cancellable(purple_connection);
    -
    - /* Finally start the async connection. */
    - g_socket_client_connect_to_host_async(client, priv->server_name,
    - port, cancellable,
    - purple_ircv3_connection_connected_cb,
    - connection);
    -
    - g_clear_object(&client);
    -
    - return TRUE;
    -}
    -
    -static gboolean
    -purple_ircv3_connection_disconnect(PurpleConnection *purple_connection,
    - GError **error)
    -{
    - PurpleIRCv3Connection *connection = NULL;
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    - PurpleConnectionClass *parent_class = NULL;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(purple_connection), FALSE);
    -
    - /* Chain up to our parent's disconnect to do initial clean up. */
    - parent_class = PURPLE_CONNECTION_CLASS(purple_ircv3_connection_parent_class);
    - parent_class->disconnect(purple_connection, error);
    -
    - connection = PURPLE_IRCV3_CONNECTION(purple_connection);
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - /* TODO: send QUIT command. */
    -
    - if(G_IS_SOCKET_CONNECTION(priv->connection)) {
    - GInputStream *istream = G_INPUT_STREAM(priv->input);
    - GOutputStream *ostream = G_OUTPUT_STREAM(priv->output);
    -
    - purple_gio_graceful_close(G_IO_STREAM(priv->connection),
    - istream, ostream);
    - }
    -
    - g_clear_object(&priv->input);
    - g_clear_object(&priv->output);
    - g_clear_object(&priv->connection);
    -
    - return TRUE;
    -}
    -
    -static void
    -purple_ircv3_connection_registration_complete_cb(PurpleIRCv3Connection *connection) {
    - /* Don't set our connection state to connected until we've completed
    - * registration as connected implies that we can start chatting or join
    - * rooms and other "online" activities.
    - */
    - purple_connection_set_state(PURPLE_CONNECTION(connection),
    - PURPLE_CONNECTION_STATE_CONNECTED);
    -}
    -
    -/******************************************************************************
    - * Default Handlers
    - *****************************************************************************/
    -static gboolean
    -purple_ircv3_connection_ctcp_request_default_handler(G_GNUC_UNUSED PurpleIRCv3Connection *connection,
    - G_GNUC_UNUSED PurpleConversation *conversation,
    - PurpleMessage *message,
    - const char *command,
    - const char *params)
    -{
    - char *contents = NULL;
    -
    - if(!purple_strempty(params)) {
    - contents = g_strdup_printf(_("requested CTCP %s: %s"), command,
    - params);
    - } else {
    - contents = g_strdup_printf(_("requested CTCP %s"),
    - command);
    - }
    -
    - purple_message_set_contents(message, contents);
    -
    - g_clear_pointer(&contents, g_free);
    -
    - return FALSE;
    -}
    -
    -static gboolean
    -purple_ircv3_connection_ctcp_response_default_handler(G_GNUC_UNUSED PurpleIRCv3Connection *connection,
    - G_GNUC_UNUSED PurpleConversation *conversation,
    - PurpleMessage *message,
    - const char *command,
    - const char *params)
    -{
    - char *contents = NULL;
    -
    - if(!purple_strempty(params)) {
    - contents = g_strdup_printf(_("CTCP %s response: %s"), command,
    - params);
    - } else {
    - contents = g_strdup_printf(_("CTCP %s response was empty"),
    - command);
    - }
    -
    - purple_message_set_contents(message, contents);
    -
    - g_clear_pointer(&contents, g_free);
    -
    - return FALSE;
    -}
    -
    -/******************************************************************************
    - * GObject Implementation
    - *****************************************************************************/
    -static void
    -purple_ircv3_connection_get_property(GObject *obj, guint param_id,
    - GValue *value, GParamSpec *pspec)
    -{
    - PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
    -
    - switch(param_id) {
    - case PROP_CAPABILITIES:
    - g_value_set_object(value,
    - purple_ircv3_connection_get_capabilities(connection));
    - break;
    - case PROP_REGISTERED:
    - g_value_set_boolean(value,
    - purple_ircv3_connection_get_registered(connection));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -purple_ircv3_connection_dispose(GObject *obj) {
    - PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - g_clear_object(&priv->input);
    - g_clear_object(&priv->output);
    - g_clear_object(&priv->connection);
    -
    - g_clear_object(&priv->capabilities);
    - g_clear_object(&priv->parser);
    - g_clear_object(&priv->status_conversation);
    -
    - G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->dispose(obj);
    -}
    -
    -static void
    -purple_ircv3_connection_finalize(GObject *obj) {
    - PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - g_clear_pointer(&priv->server_name, g_free);
    -
    - G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->finalize(obj);
    -}
    -
    -static void
    -purple_ircv3_connection_constructed(GObject *obj) {
    - PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    - PurpleAccount *account = NULL;
    - PurpleContactInfo *info = NULL;
    - PurpleConversationManager *conversation_manager = NULL;
    - char **userparts = NULL;
    - const char *username = NULL;
    - char *title = NULL;
    -
    - G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->constructed(obj);
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    - account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    - info = PURPLE_CONTACT_INFO(account);
    -
    - title = g_strdup_printf(_("status for %s"),
    - purple_contact_info_get_username(info));
    -
    - /* Split the username into nick and server and store the values. */
    - username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
    - userparts = g_strsplit(username, "@", 2);
    - purple_connection_set_display_name(PURPLE_CONNECTION(connection),
    - userparts[0]);
    - priv->server_name = g_strdup(userparts[1]);
    -
    - /* Free the userparts vector. */
    - g_strfreev(userparts);
    -
    - /* Check if we have an existing status conversation. */
    - conversation_manager = purple_conversation_manager_get_default();
    - priv->status_conversation = purple_conversation_manager_find_with_id(conversation_manager,
    - account,
    - priv->server_name);
    -
    - if(!PURPLE_IS_CONVERSATION(priv->status_conversation)) {
    - /* Create our status conversation. */
    - priv->status_conversation = g_object_new(
    - PURPLE_TYPE_CONVERSATION,
    - "account", account,
    - "id", priv->server_name,
    - "name", priv->server_name,
    - "title", title,
    - NULL);
    -
    - purple_conversation_manager_register(conversation_manager,
    - priv->status_conversation);
    - } else {
    - /* The conversation existed, so add a reference to it. */
    - g_object_ref(priv->status_conversation);
    - }
    -
    - g_clear_pointer(&title, g_free);
    -
    - /* Finally create our objects. */
    - priv->capabilities = purple_ircv3_capabilities_new(connection);
    - g_signal_connect_object(priv->capabilities, "done",
    - G_CALLBACK(purple_ircv3_connection_caps_done_cb),
    - connection, 0);
    -}
    -
    -static void
    -purple_ircv3_connection_init(G_GNUC_UNUSED PurpleIRCv3Connection *connection) {
    -}
    -
    -static void
    -purple_ircv3_connection_class_finalize(G_GNUC_UNUSED PurpleIRCv3ConnectionClass *klass) {
    -}
    -
    -static void
    -purple_ircv3_connection_class_init(PurpleIRCv3ConnectionClass *klass) {
    - GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    - PurpleConnectionClass *connection_class = PURPLE_CONNECTION_CLASS(klass);
    -
    - obj_class->get_property = purple_ircv3_connection_get_property;
    - obj_class->constructed = purple_ircv3_connection_constructed;
    - obj_class->dispose = purple_ircv3_connection_dispose;
    - obj_class->finalize = purple_ircv3_connection_finalize;
    -
    - connection_class->connect = purple_ircv3_connection_connect;
    - connection_class->disconnect = purple_ircv3_connection_disconnect;
    -
    - /**
    - * PurpleIRCv3Connection:capabilities:
    - *
    - * The capabilities that the server supports.
    - *
    - * This is created during registration of the connection and is useful for
    - * troubleshooting or just reporting them to end users.
    - *
    - * Since: 3.0
    - */
    - properties[PROP_CAPABILITIES] = g_param_spec_object(
    - "capabilities", "capabilities",
    - "The capabilities that the server supports",
    - PURPLE_IRCV3_TYPE_CAPABILITIES,
    - G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    -
    - /**
    - * PurpleIRCv3Connection:registered:
    - *
    - * Whether or not the connection has finished the registration portion of
    - * the connection.
    - *
    - * Since: 3.0
    - */
    - properties[PROP_REGISTERED] = g_param_spec_boolean(
    - "registered", "registered",
    - "Whether or not the connection has finished registration.",
    - FALSE,
    - G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    -
    - g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    -
    - /* Signals */
    -
    - /**
    - * PurpleIRCv3Connection::registration-complete:
    - * @connection: The instance.
    - *
    - * This signal is emitted after the registration process has been
    - * completed. Plugins can use this to perform additional actions before
    - * any channels are auto joined or similar.
    - *
    - * Since: 3.0
    - */
    - signals[SIG_REGISTRATION_COMPLETE] = g_signal_new_class_handler(
    - "registration-complete",
    - G_OBJECT_CLASS_TYPE(klass),
    - G_SIGNAL_RUN_LAST,
    - G_CALLBACK(purple_ircv3_connection_registration_complete_cb),
    - NULL,
    - NULL,
    - NULL,
    - G_TYPE_NONE,
    - 0);
    -
    - /**
    - * PurpleIRCv3Connection::ctcp-request:
    - * @connection: The instance.
    - * @conversation: The conversation.
    - * @message: The message.
    - * @command: The CTCP command.
    - * @params: (nullable): The CTCP parameters.
    - *
    - * This signal is emitted after a CTCP request has been received.
    - *
    - * Signal handlers should return TRUE if they fully handled the request and
    - * do not want it echoed out to the user.
    - *
    - * Handlers may also modify the message. For example, the CTCP ACTION
    - * command has its message contents replaced with just the CTCP parameters
    - * and sets the [property@Message:action] to %TRUE and then returns %TRUE.
    - *
    - * Since: 3.0
    - */
    - signals[SIG_CTCP_REQUEST] = g_signal_new_class_handler(
    - "ctcp-request",
    - G_OBJECT_CLASS_TYPE(klass),
    - G_SIGNAL_RUN_LAST,
    - G_CALLBACK(purple_ircv3_connection_ctcp_request_default_handler),
    - g_signal_accumulator_true_handled,
    - NULL,
    - NULL,
    - G_TYPE_BOOLEAN,
    - 4,
    - PURPLE_TYPE_CONVERSATION,
    - PURPLE_TYPE_MESSAGE,
    - G_TYPE_STRING,
    - G_TYPE_STRING);
    -
    - /**
    - * PurpleIRCv3Connection::ctcp-response:
    - * @connection: The instance.
    - * @conversation: The conversation.
    - * @message: The message.
    - * @command: The CTCP command.
    - * @params: (nullable): The CTCP parameters.
    - *
    - * This signal is emitted after a CTCP response has been received.
    - *
    - * Signal handlers should return TRUE if they fully handled the response
    - * and do not want it echoed out to the user.
    - *
    - * Handlers may modify @conversation or @message to depending on their
    - * needs.
    - *
    - * Since: 3.0
    - */
    - signals[SIG_CTCP_RESPONSE] = g_signal_new_class_handler(
    - "ctcp-response",
    - G_OBJECT_CLASS_TYPE(klass),
    - G_SIGNAL_RUN_LAST,
    - G_CALLBACK(purple_ircv3_connection_ctcp_response_default_handler),
    - g_signal_accumulator_true_handled,
    - NULL,
    - NULL,
    - G_TYPE_BOOLEAN,
    - 4,
    - PURPLE_TYPE_CONVERSATION,
    - PURPLE_TYPE_MESSAGE,
    - G_TYPE_STRING,
    - G_TYPE_STRING);
    -}
    -
    -/******************************************************************************
    - * Internal API
    - *****************************************************************************/
    -void
    -purple_ircv3_connection_register(GPluginNativePlugin *plugin) {
    - GObjectClass *hack = NULL;
    -
    - purple_ircv3_connection_register_type(G_TYPE_MODULE(plugin));
    -
    - /* Without this hack we get some warnings about no reference on this type
    - * when generating the gir file. However, there are no changes to the
    - * generated gir file with or without this hack, so this is purely to get
    - * rid of the warnings.
    - *
    - * - GK 2023-08-21
    - */
    - hack = g_type_class_ref(purple_ircv3_connection_get_type());
    - g_type_class_unref(hack);
    -}
    -
    -gboolean
    -purple_ircv3_connection_emit_ctcp_request(PurpleIRCv3Connection *connection,
    - PurpleConversation *conversation,
    - PurpleMessage *message,
    - const char *command,
    - const char *parameters)
    -{
    - gboolean ret = FALSE;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    - g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
    - g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
    - g_return_val_if_fail(!purple_strempty(command), FALSE);
    -
    - g_signal_emit(connection, signals[SIG_CTCP_REQUEST], 0, conversation,
    - message, command, parameters, &ret);
    -
    - return ret;
    -}
    -
    -gboolean
    -purple_ircv3_connection_emit_ctcp_response(PurpleIRCv3Connection *connection,
    - PurpleConversation *conversation,
    - PurpleMessage *message,
    - const char *command,
    - const char *parameters)
    -{
    - gboolean ret = FALSE;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    - g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
    - g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
    - g_return_val_if_fail(!purple_strempty(command), FALSE);
    -
    - g_signal_emit(connection, signals[SIG_CTCP_RESPONSE], 0, conversation,
    - message, command, parameters, &ret);
    -
    - return ret;
    -}
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -void
    -purple_ircv3_connection_writef(PurpleIRCv3Connection *connection,
    - const char *format, ...)
    -{
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    - GBytes *bytes = NULL;
    - GCancellable *cancellable = NULL;
    - GString *msg = NULL;
    - va_list vargs;
    -
    - g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
    - g_return_if_fail(format != NULL);
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - /* Create our string and append our format to it. */
    - msg = g_string_new("");
    -
    - va_start(vargs, format);
    - g_string_vprintf(msg, format, vargs);
    - va_end(vargs);
    -
    - /* Next add the trailing carriage return line feed. */
    - g_string_append(msg, "\r\n");
    -
    - /* Finally turn the string into bytes and send it! */
    - bytes = g_string_free_to_bytes(msg);
    -
    - cancellable = purple_connection_get_cancellable(PURPLE_CONNECTION(connection));
    - birb_queued_output_stream_push_bytes_async(BIRB_QUEUED_OUTPUT_STREAM(priv->output),
    - bytes,
    - G_PRIORITY_DEFAULT,
    - cancellable,
    - purple_ircv3_connection_write_cb,
    - connection);
    -
    - g_bytes_unref(bytes);
    -}
    -
    -PurpleIRCv3Capabilities *
    -purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection) {
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - return priv->capabilities;
    -}
    -
    -gboolean
    -purple_ircv3_connection_get_registered(PurpleIRCv3Connection *connection) {
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - return priv->registered;
    -}
    -
    -void
    -purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection,
    - PurpleIRCv3Message *v3_message)
    -{
    - PurpleIRCv3ConnectionPrivate *priv = NULL;
    - PurpleMessage *message = NULL;
    - GString *str = NULL;
    - GStrv params = NULL;
    - char *stripped = NULL;
    - const char *command = NULL;
    -
    - g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
    - g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(v3_message));
    -
    - priv = purple_ircv3_connection_get_instance_private(connection);
    -
    - command = purple_ircv3_message_get_command(v3_message);
    -
    - str = g_string_new(command);
    -
    - params = purple_ircv3_message_get_params(v3_message);
    - if(params != NULL && params[0] != NULL) {
    - char *joined = g_strjoinv(" ", params);
    -
    - g_string_append_printf(str, " %s", joined);
    -
    - g_free(joined);
    - }
    -
    - stripped = purple_ircv3_formatting_strip(str->str);
    - g_string_free(str, TRUE);
    -
    - message = g_object_new(
    - PURPLE_TYPE_MESSAGE,
    - "author", purple_ircv3_message_get_source(v3_message),
    - "contents", stripped,
    - NULL);
    - g_free(stripped);
    -
    - purple_conversation_write_message(priv->status_conversation, message);
    -
    - g_clear_object(&message);
    -}
    -
    -gboolean
    -purple_ircv3_connection_is_channel(PurpleIRCv3Connection *connection,
    - const char *id)
    -{
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    - g_return_val_if_fail(id != NULL, FALSE);
    -
    - return (id[0] == '#');
    -}
    -
    -PurpleConversation *
    -purple_ircv3_connection_find_or_create_conversation(PurpleIRCv3Connection *connection,
    - const char *id)
    -{
    - PurpleAccount *account = NULL;
    - PurpleConversation *conversation = NULL;
    - PurpleConversationManager *manager = NULL;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);
    - g_return_val_if_fail(id != NULL, NULL);
    -
    - account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    - manager = purple_conversation_manager_get_default();
    - conversation = purple_conversation_manager_find_with_id(manager, account,
    - id);
    -
    - if(!PURPLE_IS_CONVERSATION(conversation)) {
    - PurpleConversationType type = PurpleConversationTypeDM;
    -
    - if(purple_ircv3_connection_is_channel(connection, id)) {
    - type = PurpleConversationTypeChannel;
    - }
    -
    - conversation = g_object_new(
    - PURPLE_TYPE_CONVERSATION,
    - "account", account,
    - "id", id,
    - "name", id,
    - "type", type,
    - NULL);
    -
    - purple_conversation_manager_register(manager, conversation);
    -
    - /* The manager creates its own reference on our new conversation, so we
    - * borrow it like we do above if it already exists.
    - */
    - g_object_unref(conversation);
    - }
    -
    - return conversation;
    -}
    -
    -PurpleContact *
    -purple_ircv3_connection_find_or_create_contact(PurpleIRCv3Connection *connection,
    - const char *nick)
    -{
    - PurpleAccount *account = NULL;
    - PurpleContact *contact = NULL;
    - PurpleContactManager *manager = NULL;
    -
    - account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    - manager = purple_contact_manager_get_default();
    - contact = purple_contact_manager_find_with_id(manager, account, nick);
    -
    - if(!PURPLE_IS_CONTACT(contact)) {
    - contact = purple_contact_new(account, nick);
    - purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact), nick);
    -
    - purple_contact_manager_add(manager, contact);
    - }
    -
    - return contact;
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3connection.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,222 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_CONNECTION_H
    -#define PURPLE_IRCV3_CONNECTION_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <gplugin.h>
    -#include <gplugin-native.h>
    -
    -#include <purple.h>
    -
    -#include "purpleircv3version.h"
    -
    -G_BEGIN_DECLS
    -
    -#define PURPLE_IRCV3_TYPE_CONNECTION (purple_ircv3_connection_get_type())
    -
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -G_DECLARE_DERIVABLE_TYPE(PurpleIRCv3Connection, purple_ircv3_connection,
    - PURPLE_IRCV3, CONNECTION, PurpleConnection)
    -
    -#include "purpleircv3capabilities.h"
    -#include "purpleircv3message.h"
    -
    -struct _PurpleIRCv3ConnectionClass {
    - /*< private >*/
    - PurpleConnectionClass parent;
    -
    - /*< private >*/
    - gpointer reserved[8];
    -};
    -
    -/**
    - * purple_ircv3_connection_register: (skip)
    - * @plugin: The GTypeModule
    - *
    - * Registers the dynamic type using @plugin.
    - *
    - * Since: 3.0
    - */
    -G_GNUC_INTERNAL void purple_ircv3_connection_register(GPluginNativePlugin *plugin);
    -
    -/**
    - * purple_ircv3_connection_emit_ctcp_request:
    - * @connection: The instance.
    - * @conversation: The conversation.
    - * @message: The message.
    - * @command: The CTCP command.
    - * @parameters: (nullable): The CTCP parameters.
    - *
    - * Emits the [signal@Connection:ctcp-request] signal with the given @command
    - * and @parameters which originated from @message in @conversation.
    - *
    - * The message may be modified by a signal handler. For example, the default
    - * handler will output an internationalized string that describes what command
    - * was requested.
    - *
    - * Returns: %TRUE if the request was handled and the message should not be
    - * echoed, otherwise %FALSE.
    - *
    - * Since: 3.0
    - */
    -G_GNUC_INTERNAL gboolean purple_ircv3_connection_emit_ctcp_request(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message, const char *command, const char *parameters);
    -
    -/**
    - * purple_ircv3_connection_emit_ctcp_response:
    - * @connection: The instance.
    - * @conversation: The conversation.
    - * @message: The message.
    - * @command: The CTCP command.
    - * @parameters: (nullable): The CTCP parameters.
    - *
    - * Emits the [signal@Connection:ctcp-response] signal with the given @command
    - * and @parameters which originated from @message in @conversation.
    - *
    - * The message may be modified by a signal handler. For example, the default
    - * handler will output an internationalized string that describes what the
    - * response was.
    - *
    - * Returns: %TRUE if the request was handled and the message should not be
    - * echoed, otherwise %FALSE.
    - *
    - * Since: 3.0
    - */
    -G_GNUC_INTERNAL gboolean purple_ircv3_connection_emit_ctcp_response(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message, const char *command, const char *parameters);
    -
    -/**
    - * purple_ircv3_connection_writef:
    - * @connection: The instance.
    - * @format: The format string.
    - * @...: The arguments for @format.
    - *
    - * Similar to C `printf()` but writes the format string out to @connection.
    - *
    - * This will add the proper line termination, so you do not need to worry about
    - * that.
    - */
    -void purple_ircv3_connection_writef(PurpleIRCv3Connection *connection, const char *format, ...) G_GNUC_PRINTF(2, 3);
    -
    -/**
    - * purple_ircv3_connection_get_capabilities:
    - * @connection: The instance.
    - *
    - * Gets the list of capabilities that the server supplied during registration.
    - *
    - * Returns: (transfer none): The list of capabilities that the server supports.
    - */
    -PurpleIRCv3Capabilities *purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection);
    -
    -/**
    - * purple_ircv3_connection_get_registered:
    - * @connection: The instance.
    - *
    - * Gets whether or not the connection has finished the registration process.
    - *
    - * Returns: %TRUE if registration has been completed otherwise %FALSE.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -gboolean purple_ircv3_connection_get_registered(PurpleIRCv3Connection *connection);
    -
    -/**
    - * purple_ircv3_connection_add_status_message:
    - * @connection: The instance.
    - * @message: The message.
    - *
    - * Adds a message to the status conversation/window for @connection.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection, PurpleIRCv3Message *message);
    -
    -/**
    - * purple_ircv3_connection_is_channel:
    - * @connection: The instance.
    - * @id: The id to check.
    - *
    - * Checks if @id is a channel.
    - *
    - * Right now this just checks if @id starts with a `#` but in the future this
    - * will be updated to check all channel prefixes that the connection supports.
    - *
    - * Returns: %TRUE if @id is a channel otherwise %FALSE.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -gboolean purple_ircv3_connection_is_channel(PurpleIRCv3Connection *connection, const char *id);
    -
    -/**
    - * purple_ircv3_connection_find_or_create_conversation:
    - * @connection: The instance.
    - * @id: The id of the conversation.
    - *
    - * Looks for an existing conversation belonging to @connection and returns it
    - * if found. If not found a new conversation will be created.
    - *
    - * This will only ever return %NULL if @connection is invalid or @id is %NULL.
    - *
    - * Note that ownership of the conversation remains with the default
    - * [class@Purple.ConversationManager].
    - *
    - * Returns: (transfer none) (nullable): The conversation.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -PurpleConversation *purple_ircv3_connection_find_or_create_conversation(PurpleIRCv3Connection *connection, const char *id);
    -
    -/**
    - * purple_ircv3_connection_find_or_create_contact:
    - * @connection: The instance.
    - * @nick: The nickname of the user.
    - *
    - * Looks for an existing contact belonging to @connection and returns it if
    - * found. If not a new contact will be created.
    - *
    - * This will only ever return %NULL if @connection is invalid or @nick is
    - * %NULL.
    - *
    - * Note that the ownership of the contact remains with the default
    - * [class@Purple.ContactManager].
    - *
    - * Returns: (transfer none) (nullable): The contact.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -PurpleContact *purple_ircv3_connection_find_or_create_contact(PurpleIRCv3Connection *connection, const char *nick);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_CONNECTION_H */
    --- a/libpurple/protocols/ircv3/purpleircv3constants.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,328 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_CONSTANTS_H
    -#define PURPLE_IRCV3_CONSTANTS_H
    -
    -/**
    - * PURPLE_IRCV3_CTCP_ACTION:
    - *
    - * A constant representing the CTCP ACTION command.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_CTCP_ACTION ("ACTION") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_CTCP_DELIMITER:
    - *
    - * The delimiter used for CTCP messages.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_CTCP_DELIMITER (0x1) PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_CTCP_VERSION:
    - *
    - * A constant representing the CTCP VERSION command.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_CTCP_VERSION ("VERSION") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_ERR_NICKLOCKED:
    - *
    - * A constant for the IRC %NICKLOCKED error.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_ERR_NICKLOCKED ("902") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_ERR_SASLABORTED:
    - *
    - * A constant for the IRC %SASLABORTED error.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_ERR_SASLABORTED ("906") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_ERR_SASLALREADY:
    - *
    - * A constant for the IRC %SASLALREADY error.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_ERR_SASLALREADY ("907") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_ERR_SASLFAIL:
    - *
    - * A constant for the IRC %SASLFAIL error.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_ERR_SASLFAIL ("904") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_ERR_SASLTOOLONG:
    - *
    - * A constant for the IRC %SASLTOOLONG error.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_ERR_SASLTOOLONG ("905") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_MSG_AUTHENTICATE:
    - *
    - * A constant for the IRC %AUTHENTICATE message.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_MSG_AUTHENTICATE ("AUTHENTICATE") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_MSG_CAP:
    - *
    - * A constant for the IRC %CAP message.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_MSG_CAP ("CAP") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_MSG_JOIN:
    - *
    - * A constant for the IRC %JOIN message.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_MSG_JOIN ("JOIN") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_MSG_NOTICE:
    - *
    - * A constant for the IRC %NOTICE message.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_MSG_NOTICE ("NOTICE") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_MSG_PING:
    - *
    - * A constant for the IRC %PING message.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_MSG_PING ("PING") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_MSG_PRIVMSG:
    - *
    - * A constant for the IRC %PRIVMSG message.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_MSG_PRIVMSG ("PRIVMSG") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_MSG_TOPIC:
    - *
    - * A constant for the IRC %TOPIC message.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_MSG_TOPIC ("TOPIC") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_CREATED:
    - *
    - * A constant for the IRC %CREATED reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_CREATED ("003") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_ENDOFMOTD:
    - *
    - * A constant for the IRC %ENDOFMOTD reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_ENDOFMOTD ("376") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_LOGGEDIN:
    - *
    - * A constant for the IRC %LOGGEDIN reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_LOGGEDIN ("900") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_LOGGEDOUT:
    - *
    - * A constant for the IRC %LOGGEDOUT reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_LOGGEDOUT ("901") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_LUSERCHANNELS:
    - *
    - * A constant for the IRC %LUSERCHANNELS reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_LUSERCHANNELS ("254") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_LUSERCLIENT:
    - *
    - * A constant for the IRC %LUSERCLIENT reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_LUSERCLIENT ("251") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_LUSERME:
    - *
    - * A constant for the IRC %LUSERME reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_LUSERME ("255") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_LUSEROP:
    - *
    - * A constant for the IRC %LUSEROP reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_LUSEROP ("252") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_LUSERUNKNOWN:
    - *
    - * A constant for the IRC %LUSERUNKNOWN reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_LUSERUNKNOWN ("253") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_MYINFO:
    - *
    - * A constant for the IRC %MYINFO reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_MYINFO ("004") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_MOTD:
    - *
    - * A constant for the IRC %MOTD reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_MOTD ("372") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_MOTDSTART:
    - *
    - * A constant for the IRC %MOTDSTART reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_MOTDSTART ("375") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_NOTOPIC:
    - *
    - * A constant for the IRC %NOTOPIC reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_NOTOPIC ("331") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_SASLMECHS:
    - *
    - * A constant for the IRC %SASLMECHS reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_SASLMECHS ("908") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_SASLSUCCESS:
    - *
    - * A constant for the IRC %SASLSUCCESS reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_SASLSUCCESS ("903") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_TOPIC:
    - *
    - * A constant for the IRC %TOPIC reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_TOPIC ("332") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_WELCOME:
    - *
    - * A constant for the IRC %WELCOME reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_WELCOME ("001") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -/**
    - * PURPLE_IRCV3_RPL_YOURHOST:
    - *
    - * A constant for the IRC %YOURHOST reply.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_RPL_YOURHOST ("002") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -
    -#endif /* PURPLE_IRCV3_CONSTANTS_H */
    --- a/libpurple/protocols/ircv3/purpleircv3core.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,128 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib.h>
    -#include <glib/gi18n-lib.h>
    -
    -#include <gplugin.h>
    -#include <gplugin-native.h>
    -
    -#include <purple.h>
    -
    -#include "purpleircv3core.h"
    -
    -#include "purpleircv3capabilities.h"
    -#include "purpleircv3connection.h"
    -#include "purpleircv3protocol.h"
    -
    -/******************************************************************************
    - * Globals
    - *****************************************************************************/
    -static PurpleProtocol *ircv3_protocol = NULL;
    -
    -/******************************************************************************
    - * GPlugin Exports
    - *****************************************************************************/
    -static GPluginPluginInfo *
    -purple_ircv3_query(G_GNUC_UNUSED GError **error) {
    - PurplePluginInfoFlags flags = PURPLE_PLUGIN_INFO_FLAGS_INTERNAL |
    - PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD;
    - const gchar * const authors[] = {
    - "Pidgin Developers <devel@pidgin.im>",
    - NULL
    - };
    -
    - return purple_plugin_info_new(
    - "id", "prpl-ircv3",
    - "name", "IRCv3 Protocol",
    - "authors", authors,
    - "version", DISPLAY_VERSION,
    - "category", N_("Protocol"),
    - "summary", N_("IRCv3 Protocol Plugin"),
    - "description", N_("Modern IRC Support"),
    - "website", PURPLE_WEBSITE,
    - "abi-version", PURPLE_ABI_VERSION,
    - "flags", flags,
    - "bind-global", TRUE,
    - NULL);
    -}
    -
    -static gboolean
    -purple_ircv3_load(GPluginPlugin *plugin, GError **error) {
    - PurpleProtocolManager *manager = NULL;
    -
    - if(PURPLE_IS_PROTOCOL(ircv3_protocol)) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "plugin was not cleaned up properly");
    -
    - return FALSE;
    - }
    -
    - purple_ircv3_capabilities_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    - purple_ircv3_connection_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    - purple_ircv3_protocol_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    -
    - ircv3_protocol = purple_ircv3_protocol_new();
    -
    - manager = purple_protocol_manager_get_default();
    - /* Manager can be NULL when we're generating the GIR stuff. */
    - if(PURPLE_IS_PROTOCOL_MANAGER(manager)) {
    - if(!purple_protocol_manager_register(manager, ircv3_protocol, error)) {
    - g_clear_object(&ircv3_protocol);
    -
    - return FALSE;
    - }
    - }
    -
    - return TRUE;
    -}
    -
    -static gboolean
    -purple_ircv3_unload(G_GNUC_UNUSED GPluginPlugin *plugin,
    - G_GNUC_UNUSED gboolean shutdown,
    - GError **error)
    -{
    - PurpleProtocolManager *manager = NULL;
    -
    - if(!PURPLE_IS_PROTOCOL(ircv3_protocol)) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "plugin was not setup properly");
    -
    - return FALSE;
    - }
    -
    - manager = purple_protocol_manager_get_default();
    - /* Manager can be NULL when we're generating the GIR stuff. */
    - if(PURPLE_IS_PROTOCOL_MANAGER(manager)) {
    - if(!purple_protocol_manager_unregister(manager, ircv3_protocol,
    - error))
    - {
    - return FALSE;
    - }
    - }
    -
    - g_clear_object(&ircv3_protocol);
    -
    - return TRUE;
    -}
    -
    -GPLUGIN_NATIVE_PLUGIN_DECLARE(purple_ircv3)
    --- a/libpurple/protocols/ircv3/purpleircv3core.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,39 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_CORE_H
    -#define PURPLE_IRCV3_CORE_H
    -
    -#include <glib.h>
    -
    -#define PURPLE_IRCV3_DEFAULT_SERVER "irc.libera.chat"
    -#define PURPLE_IRCV3_DEFAULT_PLAIN_PORT 6667
    -#define PURPLE_IRCV3_DEFAULT_TLS_PORT 6697
    -
    -#define PURPLE_IRCV3_DOMAIN (g_quark_from_static_string("ircv3-plugin"))
    -
    -#endif /* PURPLE_IRCV3_CORE_H */
    --- a/libpurple/protocols/ircv3/purpleircv3ctcp.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,160 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib/gi18n-lib.h>
    -
    -#include "purpleircv3ctcp.h"
    -
    -#include "purpleircv3constants.h"
    -#include "purpleircv3source.h"
    -
    -/******************************************************************************
    - * Helpers
    - *****************************************************************************/
    -static void
    -purple_ircv3_ctcp_respond(PurpleIRCv3Connection *connection,
    - PurpleMessage *message,
    - const char *response)
    -{
    - const char *author = NULL;
    - char *nick = NULL;
    -
    - author = purple_message_get_author(message);
    - purple_ircv3_source_parse(author, &nick, NULL, NULL);
    - if(!purple_strempty(nick)) {
    - PurpleContact *contact = NULL;
    -
    - contact = purple_ircv3_connection_find_or_create_contact(connection,
    - nick);
    -
    - if(PURPLE_IS_CONTACT(contact)) {
    - const char *id = NULL;
    -
    - id = purple_contact_info_get_id(PURPLE_CONTACT_INFO(contact));
    -
    - purple_ircv3_connection_writef(connection, "%s %s :%s",
    - PURPLE_IRCV3_MSG_NOTICE,
    - id,
    - response);
    - }
    - }
    -
    - g_clear_pointer(&nick, g_free);
    -}
    -
    -/******************************************************************************
    - * Callbacks
    - *****************************************************************************/
    -static gboolean
    -purple_ircv3_ctcp_handler(PurpleIRCv3Connection *connection,
    - PurpleConversation *conversation,
    - PurpleMessage *message,
    - const char *command,
    - const char *params,
    - G_GNUC_UNUSED gpointer data)
    -{
    - if(purple_strequal(command, PURPLE_IRCV3_CTCP_ACTION)) {
    - purple_message_set_contents(message, params);
    - purple_message_set_action(message, TRUE);
    -
    - purple_conversation_write_message(conversation, message);
    -
    - return TRUE;
    - } else if(purple_strequal(command, PURPLE_IRCV3_CTCP_VERSION)) {
    - purple_ircv3_ctcp_respond(connection, message, "Purple3 IRCv3");
    - }
    -
    - return FALSE;
    -}
    -
    -/******************************************************************************
    - * Internal API
    - *****************************************************************************/
    -gboolean
    -purple_ircv3_ctcp_handle(PurpleIRCv3Connection *connection,
    - PurpleConversation *conversation,
    - PurpleMessage *message)
    -{
    - PurpleMessageFlags flags;
    - char *command = NULL;
    - char *params = NULL;
    - char *ptr = NULL;
    - const char *contents = NULL;
    - gboolean ret = FALSE;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    - g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
    - g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
    -
    - contents = purple_message_get_contents(message);
    - if(contents == NULL || contents[0] != PURPLE_IRCV3_CTCP_DELIMITER) {
    - return FALSE;
    - }
    -
    - /* Move past the delimiter. */
    - contents += 1;
    -
    - /* Find the delimiter for the command. */
    - ptr = g_strstr_len(contents, -1, " ");
    -
    - /* If we don't have a space, then we have no parameters. */
    - if(ptr == NULL) {
    - size_t len = strlen(contents);
    -
    - command = g_strdup(contents);
    - if(command[len - 1] == PURPLE_IRCV3_CTCP_DELIMITER) {
    - command[len - 1] = '\0';
    - }
    - } else {
    - size_t len = 0;
    -
    - command = g_strndup(contents, ptr - contents);
    -
    - params = g_strdup(ptr + 1);
    - len = strlen(params);
    - if(params[len - 1] == PURPLE_IRCV3_CTCP_DELIMITER) {
    - params[len - 1] = '\0';
    - }
    - }
    -
    - flags = purple_message_get_flags(message);
    - if(flags & PURPLE_MESSAGE_NOTIFY) {
    - ret = purple_ircv3_connection_emit_ctcp_response(connection,
    - conversation, message,
    - command, params);
    - } else {
    - ret = purple_ircv3_connection_emit_ctcp_request(connection, conversation,
    - message, command,
    - params);
    - }
    -
    - g_clear_pointer(&command, g_free);
    - g_clear_pointer(&params, g_free);
    -
    - return ret;
    -}
    -
    -void
    -purple_ircv3_ctcp_add_default_handlers(PurpleIRCv3Connection *connection) {
    - g_signal_connect(connection, "ctcp-request",
    - G_CALLBACK(purple_ircv3_ctcp_handler), NULL);
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3ctcp.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,68 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_CTCP_H
    -#define PURPLE_IRCV3_CTCP_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <purple.h>
    -
    -#include "purpleircv3connection.h"
    -
    -G_BEGIN_DECLS
    -
    -/**
    - * purple_ircv3_ctcp_handle: (skip)
    - * @connection: The connection.
    - * @conversation: The conversation.
    - * @message: The message.
    - *
    - * Check if @message is a CTCP message and handles it accordingly.
    - *
    - * Returns: %TRUE if the message was a CTCP message and %FALSE otherwise.
    - *
    - * Since: 3.0
    - */
    -G_GNUC_INTERNAL
    -gboolean purple_ircv3_ctcp_handle(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message);
    -
    -/**
    - * purple_ircv3_ctcp_add_default_handlers: (skip)
    - * @connection: The connection.
    - *
    - * Adds handlers for the CTCP commands that we support directly.
    - *
    - * Since: 3.0
    - */
    -G_GNUC_INTERNAL
    -void purple_ircv3_ctcp_add_default_handlers(PurpleIRCv3Connection *connection);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_CTCP_H */
    --- a/libpurple/protocols/ircv3/purpleircv3formatting.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,118 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include "purpleircv3formatting.h"
    -
    -#define IRCV3_FORMAT_BOLD (0x02)
    -#define IRCV3_FORMAT_COLOR (0x03)
    -#define IRCV3_FORMAT_HEX_COLOR (0x04)
    -#define IRCV3_FORMAT_ITALIC (0x1d)
    -#define IRCV3_FORMAT_MONOSPACE (0x11)
    -#define IRCV3_FORMAT_RESET (0x0f)
    -#define IRCV3_FORMAT_REVERSE (0x16)
    -#define IRCV3_FORMAT_STRIKETHROUGH (0x1e)
    -#define IRCV3_FORMAT_UNDERLINE (0x1f)
    -
    -/******************************************************************************
    - * Helpers
    - *****************************************************************************/
    -static inline gboolean
    -purple_ircv3_formatting_is_hex_color(const char *text) {
    - for(int i = 0; i < 6; i++) {
    - if(text[i] == '\0') {
    - return FALSE;
    - }
    -
    - if(!g_ascii_isxdigit(text[i])) {
    - return FALSE;
    - }
    - }
    -
    - return TRUE;
    -}
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -char *
    -purple_ircv3_formatting_strip(const char *text) {
    - GString *str = NULL;
    -
    - /* We don't use purple_strempty here because if we're passed an empty
    - * string, we should return a newly allocated empty string.
    - */
    - if(text == NULL) {
    - return NULL;
    - }
    -
    - str = g_string_new("");
    -
    - for(int i = 0; text[i] != '\0'; i++) {
    - switch(text[i]) {
    - case IRCV3_FORMAT_BOLD:
    - case IRCV3_FORMAT_ITALIC:
    - case IRCV3_FORMAT_MONOSPACE:
    - case IRCV3_FORMAT_RESET:
    - case IRCV3_FORMAT_REVERSE:
    - case IRCV3_FORMAT_STRIKETHROUGH:
    - case IRCV3_FORMAT_UNDERLINE:
    - continue;
    - break;
    - case IRCV3_FORMAT_COLOR:
    - if(g_ascii_isdigit(text[i + 1])) {
    - i += 1;
    -
    - if(g_ascii_isdigit(text[i + 1])) {
    - i += 1;
    - }
    -
    - if(text[i + 1] == ',' && g_ascii_isdigit(text[i + 2])) {
    - i += 2;
    -
    - if(g_ascii_isdigit(text[i + 1])) {
    - i += 1;
    - }
    - }
    - }
    -
    - break;
    - case IRCV3_FORMAT_HEX_COLOR:
    - if(purple_ircv3_formatting_is_hex_color(&text[i + 1])) {
    - i += 6;
    - }
    -
    - if(text[i + 1] == ',' &&
    - purple_ircv3_formatting_is_hex_color(&text[i + 2]))
    - {
    - i += 7;
    - }
    -
    - break;
    - default:
    - g_string_append_c(str, text[i]);
    - break;
    - }
    -
    - }
    -
    - return g_string_free(str, FALSE);
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3formatting.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,52 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_FORMATTING_H
    -#define PURPLE_IRCV3_FORMATTING_H
    -
    -#include <glib.h>
    -
    -#include "purpleircv3version.h"
    -
    -G_BEGIN_DECLS
    -
    -/**
    - * purple_ircv3_formatting_strip:
    - * @text: (nullable): The text to strip.
    - *
    - * Removes all formatting from @text and returns the result.
    - *
    - * Returns: (transfer full) (nullable): The result of stripping @text.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -char *purple_ircv3_formatting_strip(const char *text);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_FORMATTING_H */
    --- a/libpurple/protocols/ircv3/purpleircv3message.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,284 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib/gi18n-lib.h>
    -
    -#include "purpleircv3message.h"
    -
    -enum {
    - PROP_0,
    - PROP_COMMAND,
    - PROP_SOURCE,
    - PROP_PARAMS,
    - PROP_TAGS,
    - N_PROPERTIES,
    -};
    -static GParamSpec *properties[N_PROPERTIES] = {NULL, };
    -
    -struct _PurpleIRCv3Message {
    - GObject parent;
    -
    - char *command;
    - GStrv params;
    - char *source;
    - GHashTable *tags;
    -};
    -
    -/******************************************************************************
    - * GObject Implementation
    - *****************************************************************************/
    -G_DEFINE_FINAL_TYPE(PurpleIRCv3Message, purple_ircv3_message, G_TYPE_OBJECT)
    -
    -static void
    -purple_ircv3_message_finalize(GObject *obj) {
    - PurpleIRCv3Message *message = PURPLE_IRCV3_MESSAGE(obj);
    -
    - g_clear_pointer(&message->command, g_free);
    - g_clear_pointer(&message->params, g_strfreev);
    - g_clear_pointer(&message->source, g_free);
    - g_clear_pointer(&message->tags, g_hash_table_unref);
    -
    - G_OBJECT_CLASS(purple_ircv3_message_parent_class)->finalize(obj);
    -}
    -
    -static void
    -purple_ircv3_message_get_property(GObject *obj, guint param_id, GValue *value,
    - GParamSpec *pspec)
    -{
    - PurpleIRCv3Message *message = PURPLE_IRCV3_MESSAGE(obj);
    -
    - switch(param_id) {
    - case PROP_COMMAND:
    - g_value_set_string(value, purple_ircv3_message_get_command(message));
    - break;
    - case PROP_PARAMS:
    - g_value_set_boxed(value, purple_ircv3_message_get_params(message));
    - break;
    - case PROP_SOURCE:
    - g_value_set_string(value, purple_ircv3_message_get_source(message));
    - break;
    - case PROP_TAGS:
    - g_value_set_boxed(value, purple_ircv3_message_get_tags(message));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -purple_ircv3_message_set_property(GObject *obj, guint param_id,
    - const GValue *value, GParamSpec *pspec)
    -{
    - PurpleIRCv3Message *message = PURPLE_IRCV3_MESSAGE(obj);
    -
    - switch(param_id) {
    - case PROP_COMMAND:
    - purple_ircv3_message_set_command(message, g_value_get_string(value));
    - break;
    - case PROP_PARAMS:
    - purple_ircv3_message_set_params(message, g_value_get_boxed(value));
    - break;
    - case PROP_SOURCE:
    - purple_ircv3_message_set_source(message, g_value_get_string(value));
    - break;
    - case PROP_TAGS:
    - purple_ircv3_message_set_tags(message, g_value_get_boxed(value));
    - break;
    - default:
    - G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    - break;
    - }
    -}
    -
    -static void
    -purple_ircv3_message_init(G_GNUC_UNUSED PurpleIRCv3Message *message) {
    -}
    -
    -static void
    -purple_ircv3_message_class_init(PurpleIRCv3MessageClass *klass) {
    - GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    -
    - obj_class->finalize = purple_ircv3_message_finalize;
    - obj_class->get_property = purple_ircv3_message_get_property;
    - obj_class->set_property = purple_ircv3_message_set_property;
    -
    - /**
    - * PurpleIRCv3Message:command:
    - *
    - * The command of this message.
    - *
    - * This could be something like JOIN or a server reply numeric like 005.
    - *
    - * Since: 3.0
    - */
    - properties[PROP_COMMAND] = g_param_spec_string(
    - "command", "command",
    - "The command of the message.",
    - NULL,
    - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    -
    - /**
    - * PurpleIRCv3Message:params:
    - *
    - * The parameters of the message.
    - *
    - * When serialized, the last item will be prefixed with a :.
    - *
    - * Since: 3.0
    - */
    - properties[PROP_PARAMS] = g_param_spec_boxed(
    - "params", "params",
    - "The parameters of this message.",
    - G_TYPE_STRV,
    - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    -
    - /**
    - * PurpleIRCv3Message:source:
    - *
    - * The source of the message.
    - *
    - * This could be a nickname, a full nick!ident@server, a server name, or
    - * %NULL.
    - *
    - * Since: 3.0
    - */
    - properties[PROP_SOURCE] = g_param_spec_string(
    - "source", "source",
    - "The source of the message.",
    - NULL,
    - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    -
    - /**
    - * PurpleIRCv3Message:tags:
    - *
    - * The [ircv3 message tags](https://ircv3.net/specs/extensions/message-tags)
    - * for the message.
    - *
    - * Since: 3.0
    - */
    - properties[PROP_TAGS] = g_param_spec_boxed(
    - "tags", "tags",
    - "The ircv3 message tags from the message.",
    - G_TYPE_HASH_TABLE,
    - G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    -
    - g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    -}
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -PurpleIRCv3Message *
    -purple_ircv3_message_new(const char *command) {
    - return g_object_new(
    - PURPLE_IRCV3_TYPE_MESSAGE,
    - "command", command,
    - NULL);
    -}
    -
    -const char *
    -purple_ircv3_message_get_command(PurpleIRCv3Message *message) {
    - g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
    -
    - return message->command;
    -}
    -
    -void
    -purple_ircv3_message_set_command(PurpleIRCv3Message *message,
    - const char *command)
    -{
    - g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
    - g_return_if_fail(!purple_strempty(command));
    -
    - if(!purple_strequal(message->command, command)) {
    - g_free(message->command);
    - message->command = g_strdup(command);
    -
    - g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_COMMAND]);
    - }
    -}
    -
    -const char *
    -purple_ircv3_message_get_source(PurpleIRCv3Message *message) {
    - g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
    -
    - return message->source;
    -}
    -
    -void
    -purple_ircv3_message_set_source(PurpleIRCv3Message *message,
    - const char *source)
    -{
    - g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
    -
    - if(!purple_strequal(message->source, source)) {
    - g_free(message->source);
    - message->source = g_strdup(source);
    -
    - g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_SOURCE]);
    - }
    -}
    -
    -GHashTable *
    -purple_ircv3_message_get_tags(PurpleIRCv3Message *message) {
    - g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
    -
    - return message->tags;
    -}
    -
    -void
    -purple_ircv3_message_set_tags(PurpleIRCv3Message *message, GHashTable *tags) {
    - g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
    -
    - if(message->tags != tags) {
    - g_clear_pointer(&message->tags, g_hash_table_unref);
    -
    - if(tags != NULL) {
    - message->tags = g_hash_table_ref(tags);
    - }
    -
    - g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_TAGS]);
    - }
    -}
    -
    -GStrv
    -purple_ircv3_message_get_params(PurpleIRCv3Message *message) {
    - g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
    -
    - return message->params;
    -}
    -
    -void
    -purple_ircv3_message_set_params(PurpleIRCv3Message *message, GStrv params) {
    - g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
    -
    - if(message->params != params) {
    - g_clear_pointer(&message->params, g_strfreev);
    -
    - if(params != NULL) {
    - message->params = g_strdupv(params);
    - }
    -
    - g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_PARAMS]);
    - }
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3message.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,164 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_MESSAGE_H
    -#define PURPLE_IRCV3_MESSAGE_H
    -
    -#include <glib.h>
    -
    -#include <gplugin.h>
    -#include <gplugin-native.h>
    -
    -#include <purple.h>
    -
    -#include "purpleircv3version.h"
    -
    -G_BEGIN_DECLS
    -
    -#define PURPLE_IRCV3_TYPE_MESSAGE (purple_ircv3_message_get_type())
    -
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -G_DECLARE_FINAL_TYPE(PurpleIRCv3Message, purple_ircv3_message, PURPLE_IRCV3,
    - MESSAGE, GObject)
    -
    -/**
    - * purple_ircv3_message_new:
    - * @command: (not nullable): The command of the message.
    - *
    - * Creates a new message with @command.
    - *
    - * Returns: (transfer full): The new message.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -PurpleIRCv3Message *purple_ircv3_message_new(const char *command);
    -
    -/**
    - * purple_ircv3_message_get_command:
    - * @message: The instance.
    - *
    - * Gets the command of the message.
    - *
    - * Returns: The command of the message.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -const char *purple_ircv3_message_get_command(PurpleIRCv3Message *message);
    -
    -/**
    - * purple_ircv3_message_set_command:
    - * @message: The instance.
    - * @command: The new command.
    - *
    - * Sets the command for @message to @command.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_message_set_command(PurpleIRCv3Message *message, const char *command);
    -
    -/**
    - * purple_ircv3_message_get_params:
    - * @message: The instance.
    - *
    - * Gets the parameters from @message.
    - *
    - * Returns: (transfer none) (nullable): The parameters from @message.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -GStrv purple_ircv3_message_get_params(PurpleIRCv3Message *message);
    -
    -/**
    - * purple_ircv3_message_set_params:
    - * @message: The instance.
    - * @params: (transfer none) (nullable): The new parameters.
    - *
    - * Sets the parameters of @message to @params.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_message_set_params(PurpleIRCv3Message *message, GStrv params);
    -
    -/**
    - * purple_ircv3_message_get_source:
    - * @message: The instance.
    - *
    - * Gets the the source of @message.
    - *
    - * Returns: (nullable): The source of @message.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -const char *purple_ircv3_message_get_source(PurpleIRCv3Message *message);
    -
    -/**
    - * purple_ircv3_message_set_source:
    - * @message: The instance.
    - * @source: (nullable): The new source.
    - *
    - * Sets the source of @message to @source.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_message_set_source(PurpleIRCv3Message *message, const char *source);
    -
    -/**
    - * purple_ircv3_message_get_tags:
    - * @message: The instance.
    - *
    - * Gets the tags from @message.
    - *
    - * Returns: (transfer none) (element-type utf8 utf8) (nullable): The tags from
    - * @message.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -GHashTable *purple_ircv3_message_get_tags(PurpleIRCv3Message *message);
    -
    -/**
    - * purple_ircv3_message_set_tags:
    - * @message: The instance.
    - * @tags: (transfer none) (element-type utf8 utf8) (nullable): The new tags.
    - *
    - * Sets the tags of @message to @tags.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_message_set_tags(PurpleIRCv3Message *message, GHashTable *tags);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_MESSAGE_H */
    --- a/libpurple/protocols/ircv3/purpleircv3messagehandlers.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,315 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib/gi18n-lib.h>
    -
    -#include "purpleircv3messagehandlers.h"
    -
    -#include "purpleircv3connection.h"
    -#include "purpleircv3constants.h"
    -#include "purpleircv3core.h"
    -#include "purpleircv3ctcp.h"
    -#include "purpleircv3formatting.h"
    -#include "purpleircv3source.h"
    -
    -/******************************************************************************
    - * Fallback
    - *****************************************************************************/
    -gboolean
    -purple_ircv3_message_handler_fallback(PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *connection = data;
    - char *new_command = NULL;
    - const char *command = NULL;
    -
    - command = purple_ircv3_message_get_command(message);
    -
    - new_command = g_strdup_printf(_("unknown command '%s'"), command);
    - purple_ircv3_message_set_command(message, new_command);
    - purple_ircv3_connection_add_status_message(connection, message);
    -
    - g_clear_pointer(&new_command, g_free);
    -
    - return TRUE;
    -}
    -
    -/******************************************************************************
    - * Status Messages
    - *****************************************************************************/
    -gboolean
    -purple_ircv3_message_handler_status(PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer data)
    -{
    - purple_ircv3_connection_add_status_message(data, message);
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_message_handler_status_ignore_param0(PurpleIRCv3Message *message,
    - GError **error,
    - gpointer data)
    -{
    - GStrv params = NULL;
    - GStrv new_params = NULL;
    - guint n_params = 0;
    -
    - params = purple_ircv3_message_get_params(message);
    - if(params != NULL) {
    - n_params = g_strv_length(params);
    - }
    -
    - if(n_params <= 1) {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "expected n_params > 1, got %u", n_params);
    -
    - return FALSE;
    - }
    -
    - /* We need to make a copy because otherwise we'd get a use after free in
    - * set_params.
    - */
    - new_params = g_strdupv(params + 1);
    - purple_ircv3_message_set_params(message, new_params);
    - g_clear_pointer(&new_params, g_strfreev);
    -
    - purple_ircv3_connection_add_status_message(data, message);
    -
    - return TRUE;
    -}
    -
    -/******************************************************************************
    - * General Commands
    - *****************************************************************************/
    -gboolean
    -purple_ircv3_message_handler_ping(PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *connection = data;
    - GStrv params = NULL;
    -
    - params = purple_ircv3_message_get_params(message);
    -
    - if(params != NULL && g_strv_length(params) == 1) {
    - purple_ircv3_connection_writef(connection, "PONG %s", params[0]);
    - } else {
    - purple_ircv3_connection_writef(connection, "PONG");
    - }
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_message_handler_privmsg(PurpleIRCv3Message *v3_message,
    - G_GNUC_UNUSED GError **error,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *connection = data;
    - PurpleContact *contact = NULL;
    - PurpleConversation *conversation = NULL;
    - PurpleMessage *message = NULL;
    - PurpleMessageFlags flags = PURPLE_MESSAGE_RECV;
    - GDateTime *dt = NULL;
    - GHashTable *tags = NULL;
    - GStrv params = NULL;
    - gpointer raw_id = NULL;
    - gpointer raw_timestamp = NULL;
    - char *nick = NULL;
    - char *stripped = NULL;
    - const char *command = NULL;
    - const char *id = NULL;
    - const char *source = NULL;
    - const char *target = NULL;
    -
    - command = purple_ircv3_message_get_command(v3_message);
    - params = purple_ircv3_message_get_params(v3_message);
    - source = purple_ircv3_message_get_source(v3_message);
    - tags = purple_ircv3_message_get_tags(v3_message);
    -
    - if(params == NULL) {
    - g_warning("privmsg received with no parameters");
    -
    - return FALSE;
    - }
    -
    - if(g_strv_length(params) != 2) {
    - char *body = g_strjoinv(" ", params);
    - g_warning("unknown privmsg message format: '%s'", body);
    - g_free(body);
    -
    - return FALSE;
    - }
    -
    - purple_ircv3_source_parse(source, &nick, NULL, NULL);
    -
    - /* Find or create the conversation. */
    - target = params[0];
    - if(!purple_ircv3_connection_is_channel(connection, target)) {
    - target = nick;
    - }
    - conversation = purple_ircv3_connection_find_or_create_conversation(connection,
    - target);
    - /* Find or create the contact. */
    - contact = purple_ircv3_connection_find_or_create_contact(connection, nick);
    - if(PURPLE_IS_CONTACT(contact)) {
    - PurpleConversationMember *member = NULL;
    -
    - /* Update the contact's sid as it may have changed. */
    - purple_contact_info_set_sid(PURPLE_CONTACT_INFO(contact), source);
    -
    - /* Make sure the contact is in the conversation. */
    - member = purple_conversation_find_member(conversation,
    - PURPLE_CONTACT_INFO(contact));
    - if(!PURPLE_IS_CONVERSATION_MEMBER(member)) {
    - purple_conversation_add_member(conversation,
    - PURPLE_CONTACT_INFO(contact),
    - FALSE, NULL);
    - }
    - }
    -
    - /* Grab the msgid if one was provided. */
    - if(g_hash_table_lookup_extended(tags, "msgid", NULL, &raw_id)) {
    - if(!purple_strempty(raw_id)) {
    - id = raw_id;
    - }
    - }
    -
    - if(purple_strequal(command, PURPLE_IRCV3_MSG_NOTICE)) {
    - flags |= PURPLE_MESSAGE_NOTIFY;
    - }
    -
    - /* Determine the timestamp of the message. */
    - if(g_hash_table_lookup_extended(tags, "time", NULL, &raw_timestamp)) {
    - const char *timestamp = raw_timestamp;
    -
    - if(!purple_strempty(timestamp)) {
    - GTimeZone *tz = g_time_zone_new_utc();
    -
    - dt = g_date_time_new_from_iso8601(timestamp, tz);
    -
    - g_time_zone_unref(tz);
    - }
    - }
    -
    - /* If the server didn't provide a time, use the current local time. */
    - if(dt == NULL) {
    - dt = g_date_time_new_now_local();
    - }
    -
    - stripped = purple_ircv3_formatting_strip(params[1]);
    - message = g_object_new(
    - PURPLE_TYPE_MESSAGE,
    - "author", source,
    - "contents", stripped,
    - "flags", flags,
    - "id", id,
    - "timestamp", dt,
    - NULL);
    - g_free(stripped);
    -
    - g_date_time_unref(dt);
    -
    - /* Check if this is a CTCP message. */
    - if(!purple_ircv3_ctcp_handle(connection, conversation, message)) {
    - purple_conversation_write_message(conversation, message);
    - }
    -
    - g_clear_pointer(&nick, g_free);
    - g_clear_object(&message);
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_message_handler_topic(PurpleIRCv3Message *message,
    - GError **error,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *connection = data;
    - PurpleConversation *conversation = NULL;
    - GStrv params = NULL;
    - const char *channel = NULL;
    - const char *command = NULL;
    - const char *topic = NULL;
    - guint n_params = 0;
    -
    - command = purple_ircv3_message_get_command(message);
    - params = purple_ircv3_message_get_params(message);
    - n_params = g_strv_length(params);
    -
    - if(purple_strequal(command, PURPLE_IRCV3_MSG_TOPIC)) {
    - if(n_params != 2) {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "received TOPIC with %u parameters, expected 2",
    - n_params);
    -
    - return FALSE;
    - }
    -
    - channel = params[0];
    - topic = params[1];
    - } else if(purple_strequal(command, PURPLE_IRCV3_RPL_NOTOPIC)) {
    - if(n_params != 3) {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "received RPL_NOTOPIC with %u parameters, expected 3",
    - n_params);
    -
    - return FALSE;
    - }
    -
    - channel = params[1];
    - topic = "";
    - } else if(purple_strequal(command, PURPLE_IRCV3_RPL_TOPIC)) {
    - if(n_params != 3) {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "received RPL_TOPIC with %u parameters, expected 3",
    - n_params);
    -
    - return FALSE;
    - }
    -
    - channel = params[1];
    - topic = params[2];
    - } else {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "unexpected command %s",
    - command);
    -
    - return FALSE;
    - }
    -
    - conversation = purple_ircv3_connection_find_or_create_conversation(connection,
    - channel);
    - if(!PURPLE_IS_CONVERSATION(conversation)) {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "failed to find or create channel '%s'", channel);
    -
    - return FALSE;
    - }
    -
    - purple_conversation_set_topic(conversation, topic);
    -
    - return TRUE;
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3messagehandlers.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,67 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_MESSAGE_HANDLERS_H
    -#define PURPLE_IRCV3_MESSAGE_HANDLERS_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -#include "purpleircv3message.h"
    -#include "purpleircv3version.h"
    -
    -G_BEGIN_DECLS
    -
    -/**
    - * PurpleIRCv3MessageHandler:
    - * @message: The [class@Message] to handle.
    - * @error: (nullable): A return address for a [type@GLib.Error].
    - * @data: The user data that was passed to [method@PurpleIRCv3.Parser.parse].
    - *
    - * Defines the type of a function that will be called to handle @message.
    - *
    - * Returns: %TRUE if the command was handled properly, otherwise %FALSE and
    - * @error may be set.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_TYPE_IN_3_0
    -typedef gboolean (*PurpleIRCv3MessageHandler)(PurpleIRCv3Message *message,
    - GError **error,
    - gpointer data);
    -
    -G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_fallback(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_status(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_status_ignore_param0(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_ping(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_privmsg(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_topic(PurpleIRCv3Message *message, GError **error, gpointer data);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_MESSAGE_HANDLERS_H */
    --- a/libpurple/protocols/ircv3/purpleircv3parser.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,507 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include "purpleircv3parser.h"
    -
    -#include "purpleircv3capabilities.h"
    -#include "purpleircv3constants.h"
    -#include "purpleircv3core.h"
    -#include "purpleircv3message.h"
    -#include "purpleircv3messagehandlers.h"
    -#include "purpleircv3sasl.h"
    -
    -struct _PurpleIRCv3Parser {
    - GObject parent;
    -
    - GRegex *regex_message;
    - GRegex *regex_tags;
    -
    - PurpleIRCv3MessageHandler fallback_handler;
    - GHashTable *handlers;
    -};
    -
    -G_DEFINE_FINAL_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, G_TYPE_OBJECT)
    -
    -/******************************************************************************
    - * Helpers
    - *****************************************************************************/
    -static char *
    -purple_ircv3_parser_unescape_tag_value(const char *value) {
    - GString *unescaped = g_string_new("");
    - gboolean escaping = FALSE;
    -
    - /* Walk the string and replace escaped values according to
    - * https://ircv3.net/specs/extensions/message-tags.html#escaping-values
    - */
    - for(int i = 0; value[i] != '\0'; i++) {
    - if(escaping) {
    - /* Set the replacement to the current character which will fall
    - * through for everything, including '\\'
    - */
    - char replacement = value[i];
    -
    - if(value[i] == ':') {
    - replacement = ';';
    - } else if(value[i] == 's') {
    - replacement = ' ';
    - } else if(value[i] == 'r') {
    - replacement = '\r';
    - } else if(value[i] == 'n') {
    - replacement = '\n';
    - }
    -
    - g_string_append_c(unescaped, replacement);
    - escaping = FALSE;
    - } else {
    - if(value[i] == '\\') {
    - escaping = TRUE;
    - } else {
    - g_string_append_c(unescaped, value[i]);
    - }
    - }
    - }
    -
    - return g_string_free(unescaped, FALSE);
    -}
    -
    -static GHashTable *
    -purple_ircv3_parser_parse_tags(PurpleIRCv3Parser *parser,
    - const gchar *tags_string, GError **error)
    -{
    - GError *local_error = NULL;
    - GHashTable *tags = NULL;
    - GMatchInfo *info = NULL;
    - gboolean matches = FALSE;
    -
    - tags = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    -
    - /* tags_string can never be NULL, because g_match_info_fetch_named always
    - * returns a string. So if we were passed an empty string, just return the
    - * empty hash table.
    - */
    - if(*tags_string == '\0') {
    - return tags;
    - }
    -
    - matches = g_regex_match_full(parser->regex_tags, tags_string, -1, 0, 0,
    - &info, &local_error);
    -
    - if(local_error != NULL) {
    - g_propagate_error(error, local_error);
    -
    - g_match_info_unref(info);
    -
    - return tags;
    - }
    -
    - if(!matches) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "failed to parse tags: unknown error");
    -
    - g_match_info_unref(info);
    -
    - return tags;
    - }
    -
    - while(g_match_info_matches(info)) {
    - char *key = NULL;
    - char *value = NULL;
    - char *unescaped = NULL;
    -
    - key = g_match_info_fetch_named(info, "key");
    - value = g_match_info_fetch_named(info, "value");
    -
    - unescaped = purple_ircv3_parser_unescape_tag_value(value);
    - g_free(value);
    -
    - /* the hash table is created with destroy notifies for both key and
    - * value, so there's no need to free the allocated memory right now.
    - */
    - g_hash_table_insert(tags, key, unescaped);
    - g_match_info_next(info, &local_error);
    -
    - if(local_error != NULL) {
    - g_propagate_error(error, local_error);
    -
    - break;
    - }
    - }
    -
    - g_match_info_unref(info);
    -
    - return tags;
    -}
    -
    -static guint
    -purple_ircv3_parser_extract_params(G_GNUC_UNUSED PurpleIRCv3Parser *parser,
    - GStrvBuilder *builder, const gchar *str)
    -{
    - gchar *ptr = NULL;
    - guint count = 0;
    -
    - /* Loop through str finding each space separated string. */
    - while(str != NULL && *str != '\0') {
    - /* Look for a space. */
    - ptr = strchr(str, ' ');
    -
    - /* If we found one, set it to null terminator and add the string to our
    - * builder.
    - */
    - if(ptr != NULL) {
    - *ptr = '\0';
    - g_strv_builder_add(builder, str);
    -
    - /* Move str to the next character as we know there's another
    - * character which might be another null terminator.
    - */
    - str = ptr + 1;
    -
    - /* And don't forget to increment the count... ah ah ah! */
    - count++;
    - } else {
    - /* Add the remaining string. */
    - g_strv_builder_add(builder, str);
    -
    - /* Give the count another one, ah ah ah! */
    - count++;
    -
    - /* Finally break out of the loop. */
    - break;
    - }
    - }
    -
    - return count;
    -}
    -
    -static GStrv
    -purple_ircv3_parser_build_params(PurpleIRCv3Parser *parser,
    - const gchar *middle, const gchar *coda,
    - const gchar *trailing, guint *n_params)
    -{
    - GStrvBuilder *builder = g_strv_builder_new();
    - GStrv result = NULL;
    -
    - purple_ircv3_parser_extract_params(parser, builder, middle);
    -
    - if(*coda != '\0') {
    - g_strv_builder_add(builder, trailing);
    - }
    -
    - result = g_strv_builder_end(builder);
    -
    - g_strv_builder_unref(builder);
    -
    - if(result != NULL && n_params != NULL) {
    - *n_params = g_strv_length(result);
    - }
    -
    - return result;
    -}
    -
    -/******************************************************************************
    - * Handlers
    - *****************************************************************************/
    -static gboolean
    -purple_ircv3_fallback_handler(PurpleIRCv3Message *message,
    - GError **error,
    - G_GNUC_UNUSED gpointer data)
    -{
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "no handler for command %s",
    - purple_ircv3_message_get_command(message));
    -
    - return FALSE;
    -}
    -
    -/******************************************************************************
    - * GObject Implementation
    - *****************************************************************************/
    -static void
    -purple_ircv3_parser_finalize(GObject *obj) {
    - PurpleIRCv3Parser *parser = PURPLE_IRCV3_PARSER(obj);
    -
    - g_clear_pointer(&parser->regex_message, g_regex_unref);
    - g_clear_pointer(&parser->regex_tags, g_regex_unref);
    -
    - g_hash_table_destroy(parser->handlers);
    -
    - G_OBJECT_CLASS(purple_ircv3_parser_parent_class)->finalize(obj);
    -}
    -
    -static void
    -purple_ircv3_parser_init(PurpleIRCv3Parser *parser) {
    - parser->regex_message = g_regex_new("(?:@(?<tags>[^ ]+) )?"
    - "(?::(?<source>[^ ]+) +)?"
    - "(?<command>[^ :]+)"
    - "(?: +(?<middle>(?:[^ :]+(?: +[^ :]+)*)))*"
    - "(?<coda> +:(?<trailing>.*)?)?",
    - 0, 0, NULL);
    - g_assert(parser->regex_message != NULL);
    -
    - parser->regex_tags = g_regex_new("(?<key>(?<client_prefix>\\+?)"
    - "(?:(?<vendor>[A-Za-z0-9-\\.]+)\\/)?"
    - "(?<key_name>[A-Za-z0-9-]+)"
    - ")"
    - "(?:=(?<value>[^\r\n;]*))?(?:;|$)",
    - 0, 0, NULL);
    - g_assert(parser->regex_tags != NULL);
    -
    - parser->fallback_handler = purple_ircv3_fallback_handler;
    - parser->handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
    - NULL);
    -}
    -
    -static void
    -purple_ircv3_parser_class_init(PurpleIRCv3ParserClass *klass) {
    - GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    -
    - obj_class->finalize = purple_ircv3_parser_finalize;
    -}
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -PurpleIRCv3Parser *
    -purple_ircv3_parser_new(void) {
    - return g_object_new(PURPLE_IRCV3_TYPE_PARSER, NULL);
    -}
    -
    -void
    -purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser,
    - PurpleIRCv3MessageHandler handler)
    -{
    - g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
    -
    - parser->fallback_handler = handler;
    -}
    -
    -gboolean
    -purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer,
    - GError **error, gpointer data)
    -{
    - PurpleIRCv3Message *message = NULL;
    - PurpleIRCv3MessageHandler handler = NULL;
    - GError *local_error = NULL;
    - GHashTable *tags = NULL;
    - GMatchInfo *info = NULL;
    - GStrv params = NULL;
    - gchar *coda = NULL;
    - gchar *command = NULL;
    - gchar *middle = NULL;
    - gchar *source = NULL;
    - gchar *tags_string = NULL;
    - gchar *trailing = NULL;
    - gboolean matches = FALSE;
    - gboolean result = FALSE;
    -
    - g_return_val_if_fail(PURPLE_IRCV3_IS_PARSER(parser), FALSE);
    - g_return_val_if_fail(buffer != NULL, FALSE);
    -
    - /* Check if the buffer matches our regex for messages. */
    - matches = g_regex_match(parser->regex_message, buffer, 0, &info);
    - if(!matches) {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "failed to parser buffer '%s'", buffer);
    -
    - g_match_info_unref(info);
    -
    - return FALSE;
    - }
    -
    - /* Extract the command from the buffer, so we can find the handler. */
    - command = g_match_info_fetch_named(info, "command");
    - handler = g_hash_table_lookup(parser->handlers, command);
    - if(handler == NULL) {
    - if(parser->fallback_handler == NULL) {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "no handler found for command %s and no default "
    - "handler set.", command);
    -
    - g_free(command);
    - g_match_info_unref(info);
    -
    - return FALSE;
    - }
    -
    - handler = parser->fallback_handler;
    - }
    -
    - /* If we made it this far, we have our handler, so lets get the rest of the
    - * parameters and call the handler.
    - */
    - message = purple_ircv3_message_new(command);
    -
    - tags_string = g_match_info_fetch_named(info, "tags");
    - tags = purple_ircv3_parser_parse_tags(parser, tags_string, &local_error);
    - g_free(tags_string);
    - if(local_error != NULL) {
    - g_propagate_error(error, local_error);
    -
    - g_free(command);
    - g_clear_object(&message);
    - g_hash_table_destroy(tags);
    - g_match_info_unref(info);
    -
    - return FALSE;
    - }
    - if(tags != NULL) {
    - purple_ircv3_message_set_tags(message, tags);
    - g_hash_table_unref(tags);
    - }
    -
    - source = g_match_info_fetch_named(info, "source");
    - if(!purple_strempty(source)) {
    - purple_ircv3_message_set_source(message, source);
    - }
    - g_free(source);
    -
    - middle = g_match_info_fetch_named(info, "middle");
    - coda = g_match_info_fetch_named(info, "coda");
    - trailing = g_match_info_fetch_named(info, "trailing");
    - params = purple_ircv3_parser_build_params(parser, middle, coda, trailing,
    - NULL);
    - if(params != NULL) {
    - purple_ircv3_message_set_params(message, params);
    - }
    -
    - g_free(command);
    - g_free(middle);
    - g_free(coda);
    - g_free(trailing);
    - g_strfreev(params);
    -
    - /* Call the handler. */
    - result = handler(message, error, data);
    -
    - /* Cleanup the left overs. */
    - g_match_info_unref(info);
    - g_clear_object(&message);
    -
    - return result;
    -}
    -
    -void
    -purple_ircv3_parser_add_handler(PurpleIRCv3Parser *parser,
    - const char *command,
    - PurpleIRCv3MessageHandler handler)
    -{
    - g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
    - g_return_if_fail(command != NULL);
    - g_return_if_fail(handler != NULL);
    -
    - g_hash_table_insert(parser->handlers, g_strdup(command), handler);
    -}
    -
    -void
    -purple_ircv3_parser_add_handlers(PurpleIRCv3Parser *parser,
    - PurpleIRCv3MessageHandler handler,
    - ...)
    -{
    - va_list vargs;
    - const char *command = NULL;
    -
    - g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
    - g_return_if_fail(handler != NULL);
    -
    - va_start(vargs, handler);
    -
    - while((command = va_arg(vargs, const char *)) != NULL) {
    - purple_ircv3_parser_add_handler(parser, command, handler);
    - }
    -
    - va_end(vargs);
    -}
    -
    -void
    -purple_ircv3_parser_add_default_handlers(PurpleIRCv3Parser *parser) {
    - g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
    -
    - purple_ircv3_parser_set_fallback_handler(parser,
    - purple_ircv3_message_handler_fallback);
    -
    - /* Core functionality. */
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_CAP,
    - purple_ircv3_capabilities_message_handler);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_NOTICE,
    - purple_ircv3_message_handler_privmsg);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_PING,
    - purple_ircv3_message_handler_ping);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_PRIVMSG,
    - purple_ircv3_message_handler_privmsg);
    -
    - /* Topic stuff. */
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_TOPIC,
    - purple_ircv3_message_handler_topic);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_NOTOPIC,
    - purple_ircv3_message_handler_topic);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_TOPIC,
    - purple_ircv3_message_handler_topic);
    -
    - /* Post Registration Greetings */
    - purple_ircv3_parser_add_handlers(parser,
    - purple_ircv3_message_handler_status_ignore_param0,
    - PURPLE_IRCV3_RPL_WELCOME,
    - PURPLE_IRCV3_RPL_YOURHOST,
    - PURPLE_IRCV3_RPL_CREATED,
    - PURPLE_IRCV3_RPL_MYINFO,
    - NULL);
    -
    - /* Luser's */
    - purple_ircv3_parser_add_handlers(parser,
    - purple_ircv3_message_handler_status_ignore_param0,
    - PURPLE_IRCV3_RPL_LUSERCLIENT,
    - PURPLE_IRCV3_RPL_LUSEROP,
    - PURPLE_IRCV3_RPL_LUSERUNKNOWN,
    - PURPLE_IRCV3_RPL_LUSERCHANNELS,
    - PURPLE_IRCV3_RPL_LUSERME,
    - NULL);
    -
    - /* MOTD */
    - purple_ircv3_parser_add_handlers(parser,
    - purple_ircv3_message_handler_status_ignore_param0,
    - PURPLE_IRCV3_RPL_MOTD,
    - PURPLE_IRCV3_RPL_MOTDSTART,
    - PURPLE_IRCV3_RPL_ENDOFMOTD,
    - NULL);
    -
    - /* SASL stuff. */
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_LOGGEDIN,
    - purple_ircv3_sasl_logged_in);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_LOGGEDOUT,
    - purple_ircv3_sasl_logged_out);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_NICKLOCKED,
    - purple_ircv3_sasl_nick_locked);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_SASLSUCCESS,
    - purple_ircv3_sasl_success);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLFAIL,
    - purple_ircv3_sasl_failed);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLTOOLONG,
    - purple_ircv3_sasl_message_too_long);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLABORTED,
    - purple_ircv3_sasl_aborted);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLALREADY,
    - purple_ircv3_sasl_already_authed);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_SASLMECHS,
    - purple_ircv3_sasl_mechanisms);
    - purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_AUTHENTICATE,
    - purple_ircv3_sasl_authenticate);
    -
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3parser.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,126 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_PARSER_H
    -#define PURPLE_IRCV3_PARSER_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <purple.h>
    -
    -#include "purpleircv3messagehandlers.h"
    -#include "purpleircv3version.h"
    -
    -G_BEGIN_DECLS
    -
    -#define PURPLE_IRCV3_TYPE_PARSER (purple_ircv3_parser_get_type())
    -
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -G_DECLARE_FINAL_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, PURPLE_IRCV3,
    - PARSER, GObject)
    -
    -/**
    - * purple_ircv3_parser_new:
    - *
    - * Creates a new instance.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -PurpleIRCv3Parser *purple_ircv3_parser_new(void);
    -
    -/**
    - * purple_ircv3_parser_set_fallback_handler: (skip):
    - * @parser: The instance.
    - * @handler: A [func@PurpleIRCv3.MessageHandler].
    - *
    - * Sets @handler to be called for any messages that @parser doesn't know how to
    - * handle.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser, PurpleIRCv3MessageHandler handler);
    -
    -/**
    - * purple_ircv3_parser_parse:
    - * @parser: The instance.
    - * @buffer: The buffer to parse.
    - * @error: Return address for a #GError, or %NULL.
    - * @data: (nullable): Optional data to pass to the handler.
    - *
    - * Parses @buffer with @parser.
    - *
    - * Returns: %TRUE if the buffer was parsed correctly or %FALSE with @error set.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -gboolean purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer, GError **error, gpointer data);
    -
    -/**
    - * purple_ircv3_parser_add_handler:
    - * @parser: The instance.
    - * @command: The command string.
    - * @handler: (scope forever): The handler to call.
    - *
    - * Calls @handler every time @parser finds the command named @command.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_parser_add_handler(PurpleIRCv3Parser *parser, const char *command, PurpleIRCv3MessageHandler handler);
    -
    -/**
    - * purple_ircv3_parser_add_handlers:
    - * @parser: The instance.
    - * @handler: (scope forever): The handler to call when the command is received.
    - * @...: A %NULL terminated list of string command names.
    - *
    - * Like [method@Parser.add_handler] but allows you to add multiple commands at
    - * once that share a handler.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_parser_add_handlers(PurpleIRCv3Parser *parser, PurpleIRCv3MessageHandler handler, ...) G_GNUC_NULL_TERMINATED;
    -
    -/**
    - * purple_ircv3_parser_add_default_handlers:
    - * @parser: The instance.
    - *
    - * Adds all of the default handlers to @parser.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_parser_add_default_handlers(PurpleIRCv3Parser *parser);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_PARSER_H */
    --- a/libpurple/protocols/ircv3/purpleircv3protocol.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,279 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib/gi18n-lib.h>
    -
    -#include "purpleircv3protocol.h"
    -
    -#include "purpleircv3connection.h"
    -#include "purpleircv3core.h"
    -#include "purpleircv3protocolconversation.h"
    -
    -/******************************************************************************
    - * Callbacks
    - *****************************************************************************/
    -static void
    -purple_ircv3_protocol_can_reach_cb(GObject *self, GAsyncResult *result,
    - gpointer data)
    -{
    - GError *error = NULL;
    - GTask *task = data;
    - gboolean can_reach = FALSE;
    -
    - /* task and result share a cancellable, so either can be used here. */
    - if(g_task_return_error_if_cancelled(task)) {
    - g_clear_object(&task);
    -
    - return;
    - }
    -
    - can_reach = g_network_monitor_can_reach_finish(G_NETWORK_MONITOR(self), result,
    - &error);
    -
    - if(error != NULL) {
    - g_task_return_error(task, error);
    - } else if(!can_reach) {
    - g_task_return_new_error(task, PURPLE_IRCV3_DOMAIN, 0,
    - _("Unknown network error."));
    - } else {
    - g_task_return_boolean(task, TRUE);
    - }
    -
    - g_clear_object(&task);
    -}
    -
    -/******************************************************************************
    - * PurpleProtocol Implementation
    - *****************************************************************************/
    -static GList *
    -purple_ircv3_protocol_get_user_splits(G_GNUC_UNUSED PurpleProtocol *protocol) {
    - PurpleAccountUserSplit *split = NULL;
    - GList *splits = NULL;
    -
    - split = purple_account_user_split_new(_("Server"),
    - PURPLE_IRCV3_DEFAULT_SERVER,
    - '@');
    - splits = g_list_append(splits, split);
    -
    - return splits;
    -}
    -
    -static GList *
    -purple_ircv3_protocol_get_account_options(G_GNUC_UNUSED PurpleProtocol *protocol)
    -{
    - PurpleAccountOption *option;
    - GList *options = NULL;
    -
    - option = purple_account_option_int_new(_("Port"), "port",
    - PURPLE_IRCV3_DEFAULT_TLS_PORT);
    - options = g_list_append(options, option);
    -
    - option = purple_account_option_bool_new(_("Use TLS"), "use-tls", TRUE);
    - options = g_list_append(options, option);
    -
    - option = purple_account_option_string_new(_("Server password"),
    - "server-password", "");
    - purple_account_option_string_set_masked(option, TRUE);
    - options = g_list_append(options, option);
    -
    - option = purple_account_option_string_new(_("Ident name"), "ident", "");
    - options = g_list_append(options, option);
    -
    - option = purple_account_option_string_new(_("Real name"), "real-name", "");
    - options = g_list_append(options, option);
    -
    - option = purple_account_option_string_new(_("SASL login name"),
    - "sasl-login-name", "");
    - options = g_list_append(options, option);
    -
    - option = purple_account_option_string_new(_("SASL mechanisms"),
    - "sasl-mechanisms", "");
    - options = g_list_append(options, option);
    -
    - option = purple_account_option_bool_new(_("Allow plaintext SASL auth over "
    - "unencrypted connection"),
    - "plain-sasl-in-clear", FALSE);
    - options = g_list_append(options, option);
    -
    - option = purple_account_option_int_new(_("Seconds between sending "
    - "messages"),
    - "rate-limit-interval", 2);
    - options = g_list_append(options, option);
    -
    - option = purple_account_option_int_new(_("Maximum messages to send at "
    - "once"),
    - "rate-limit-burst", 5);
    - options = g_list_append(options, option);
    -
    - return options;
    -}
    -
    -static PurpleConnection *
    -purple_ircv3_protocol_create_connection(PurpleProtocol *protocol,
    - PurpleAccount *account,
    - const char *password,
    - GError **error)
    -{
    - const char *username = NULL;
    -
    - g_return_val_if_fail(PURPLE_IS_PROTOCOL(protocol), NULL);
    - g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
    -
    - /* Make sure the username (which includes the servername via usersplits),
    - * does not contain any whitespace.
    - */
    - username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
    - if(strpbrk(username, " \t\v\r\n") != NULL) {
    - g_set_error(error,
    - PURPLE_CONNECTION_ERROR,
    - PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
    - _("IRC nick and server may not contain whitespace"));
    -
    - return NULL;
    - }
    -
    - return g_object_new(
    - PURPLE_IRCV3_TYPE_CONNECTION,
    - "protocol", protocol,
    - "account", account,
    - "password", password,
    - NULL);
    -}
    -
    -static GList *
    -purple_ircv3_protocol_status_types(G_GNUC_UNUSED PurpleProtocol *protocol,
    - G_GNUC_UNUSED PurpleAccount *account)
    -{
    - PurpleStatusType *type = NULL;
    - GList *types = NULL;
    -
    - type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, NULL, NULL, TRUE);
    - types = g_list_append(types, type);
    -
    - type = purple_status_type_new_with_attrs(
    - PURPLE_STATUS_AWAY, NULL, NULL, TRUE, TRUE, FALSE,
    - "message", _("Message"), purple_value_new(G_TYPE_STRING),
    - NULL);
    - types = g_list_append(types, type);
    -
    - type = purple_status_type_new(PURPLE_STATUS_OFFLINE, NULL, NULL, TRUE);
    - types = g_list_append(types, type);
    -
    - return types;
    -}
    -
    -static void
    -purple_ircv3_protocol_can_connect_async(PurpleProtocol *protocol,
    - PurpleAccount *account,
    - GCancellable *cancellable,
    - GAsyncReadyCallback callback,
    - gpointer data)
    -{
    - GNetworkMonitor *monitor = NULL;
    - GSocketConnectable *connectable = NULL;
    - GStrv parts = NULL;
    - GTask *task = NULL;
    - const char *username = NULL;
    - gint port = 0;
    -
    - task = g_task_new(protocol, cancellable, callback, data);
    -
    - monitor = g_network_monitor_get_default();
    -
    - username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
    - parts = g_strsplit(username, "@", 2);
    - port = purple_account_get_int(account, "port",
    - PURPLE_IRCV3_DEFAULT_TLS_PORT);
    -
    - connectable = g_network_address_new(parts[1], (guint16)port);
    - g_strfreev(parts);
    -
    - g_network_monitor_can_reach_async(monitor, connectable, cancellable,
    - purple_ircv3_protocol_can_reach_cb,
    - task);
    - g_clear_object(&connectable);
    -}
    -
    -static gboolean
    -purple_ircv3_protocol_can_connect_finish(G_GNUC_UNUSED PurpleProtocol *protocol,
    - GAsyncResult *result,
    - GError **error)
    -{
    - return g_task_propagate_boolean(G_TASK(result), error);
    -}
    -
    -/******************************************************************************
    - * GObject Implementation
    - *****************************************************************************/
    -G_DEFINE_DYNAMIC_TYPE_EXTENDED(
    - PurpleIRCv3Protocol,
    - purple_ircv3_protocol,
    - PURPLE_TYPE_PROTOCOL,
    - 0,
    - G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONVERSATION,
    - purple_ircv3_protocol_conversation_init))
    -
    -static void
    -purple_ircv3_protocol_init(G_GNUC_UNUSED PurpleIRCv3Protocol *protocol) {
    -}
    -
    -static void
    -purple_ircv3_protocol_class_finalize(G_GNUC_UNUSED PurpleIRCv3ProtocolClass *klass) {
    -}
    -
    -static void
    -purple_ircv3_protocol_class_init(PurpleIRCv3ProtocolClass *klass) {
    - PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass);
    -
    - protocol_class->get_user_splits = purple_ircv3_protocol_get_user_splits;
    - protocol_class->get_account_options =
    - purple_ircv3_protocol_get_account_options;
    - protocol_class->create_connection =
    - purple_ircv3_protocol_create_connection;
    - protocol_class->status_types = purple_ircv3_protocol_status_types;
    - protocol_class->can_connect_async =
    - purple_ircv3_protocol_can_connect_async;
    - protocol_class->can_connect_finish =
    - purple_ircv3_protocol_can_connect_finish;
    -}
    -
    -/******************************************************************************
    - * GObject Implementation
    - *****************************************************************************/
    -void
    -purple_ircv3_protocol_register(GPluginNativePlugin *plugin) {
    - purple_ircv3_protocol_register_type(G_TYPE_MODULE(plugin));
    -}
    -
    -PurpleProtocol *
    -purple_ircv3_protocol_new(void) {
    - return g_object_new(
    - PURPLE_IRCV3_TYPE_PROTOCOL,
    - "id", "prpl-ircv3",
    - "name", "IRCv3",
    - "description", _("Version 3 of Internet Relay Chat (IRC)."),
    - "icon-name", "im-ircv3",
    - "icon-resource-path", "/im/pidgin/libpurple/ircv3/icons",
    - "options", OPT_PROTO_CHAT_TOPIC | OPT_PROTO_PASSWORD_OPTIONAL |
    - OPT_PROTO_SLASH_COMMANDS_NATIVE,
    - NULL);
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3protocol.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,71 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_PROTOCOL_H
    -#define PURPLE_IRCV3_PROTOCOL_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include <gplugin.h>
    -#include <gplugin-native.h>
    -
    -#include <purple.h>
    -
    -#include "purpleircv3version.h"
    -
    -G_BEGIN_DECLS
    -
    -#define PURPLE_IRCV3_TYPE_PROTOCOL (purple_ircv3_protocol_get_type())
    -
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -G_DECLARE_DERIVABLE_TYPE(PurpleIRCv3Protocol, purple_ircv3_protocol,
    - PURPLE_IRCV3, PROTOCOL, PurpleProtocol)
    -
    -struct _PurpleIRCv3ProtocolClass {
    - /*< private >*/
    - PurpleProtocolClass parent;
    -
    - /*< private >*/
    - gpointer reserved[4];
    -};
    -
    -/**
    - * purple_ircv3_protocol_register: (skip)
    - * @plugin: The GTypeModule
    - *
    - * Registers the dynamic type using @plugin.
    - *
    - * Since: 3.0
    - */
    -G_GNUC_INTERNAL void purple_ircv3_protocol_register(GPluginNativePlugin *plugin);
    -
    -G_GNUC_INTERNAL PurpleProtocol *purple_ircv3_protocol_new(void);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_PROTOCOL_H */
    --- a/libpurple/protocols/ircv3/purpleircv3protocolconversation.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,180 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib/gi18n-lib.h>
    -
    -#include "purpleircv3protocolconversation.h"
    -
    -#include "purpleircv3connection.h"
    -#include "purpleircv3constants.h"
    -#include "purpleircv3core.h"
    -
    -/******************************************************************************
    - * PurpleProtocolConversation Implementation
    - *****************************************************************************/
    -static void
    -purple_ircv3_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol,
    - PurpleConversation *conversation,
    - PurpleMessage *message,
    - GCancellable *cancellable,
    - GAsyncReadyCallback callback,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *v3_connection = NULL;
    - PurpleAccount *account = NULL;
    - PurpleConnection *connection = NULL;
    - GTask *task = NULL;
    - const char *id = NULL;
    -
    - account = purple_conversation_get_account(conversation);
    - connection = purple_account_get_connection(account);
    - v3_connection = PURPLE_IRCV3_CONNECTION(connection);
    -
    - id = purple_conversation_get_id(conversation);
    - /* TODO: the new message dialog sets the name but not the id and we want to
    - * use the id only, so for now if id is NULL we grab the name.
    - */
    - if(purple_strempty(id)) {
    - id = purple_conversation_get_name(conversation);
    - }
    -
    - purple_ircv3_connection_writef(v3_connection, "PRIVMSG %s :%s", id,
    - purple_message_get_contents(message));
    -
    - task = g_task_new(protocol, cancellable, callback, data);
    - g_task_return_boolean(task, TRUE);
    - g_clear_object(&task);
    -
    - /* This will be made conditional when we add echo-message support.
    - * https://ircv3.net/specs/extensions/echo-message
    - */
    - purple_conversation_write_message(conversation, message);
    -}
    -
    -static gboolean
    -purple_ircv3_protocol_conversation_send_message_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
    - GAsyncResult *result,
    - GError **error)
    -{
    - return g_task_propagate_boolean(G_TASK(result), error);
    -}
    -
    -static PurpleChannelJoinDetails *
    -purple_ircv3_protocol_conversation_get_channel_join_details(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
    - G_GNUC_UNUSED PurpleAccount *account)
    -{
    - return purple_channel_join_details_new(FALSE, TRUE);
    -}
    -
    -static void
    -purple_ircv3_protocol_conversation_join_channel_async(PurpleProtocolConversation *protocol,
    - PurpleAccount *account,
    - PurpleChannelJoinDetails* details,
    - GCancellable* cancellable,
    - GAsyncReadyCallback callback,
    - gpointer data)
    -{
    - PurpleIRCv3Connection *v3_connection = NULL;
    - PurpleConnection *connection = NULL;
    - PurpleConversation *conversation = NULL;
    - PurpleConversationManager *manager = NULL;
    - GString *cmd = NULL;
    - GTask *task = NULL;
    - const char *name = NULL;
    - const char *password = NULL;
    -
    - connection = purple_account_get_connection(account);
    - v3_connection = PURPLE_IRCV3_CONNECTION(connection);
    -
    - task = g_task_new(protocol, cancellable, callback, data);
    -
    - /* Validate that the name isn't empty. */
    - /* TODO: check that name match the ISUPPORT channel prefixes. */
    - name = purple_channel_join_details_get_name(details);
    - if(purple_strempty(name)) {
    - g_task_return_new_error(task, PURPLE_IRCV3_DOMAIN, 0,
    - "channel name is empty");
    - g_clear_object(&task);
    -
    - return;
    - }
    -
    - manager = purple_conversation_manager_get_default();
    - conversation = purple_conversation_manager_find_with_id(manager, account,
    - name);
    -
    - /* If the conversation already exists, just return TRUE. */
    - if(PURPLE_IS_CONVERSATION(conversation)) {
    - g_task_return_boolean(task, TRUE);
    - g_clear_object(&task);
    -
    - return;
    - }
    -
    - /* Build our join string. */
    - cmd = g_string_new(NULL);
    - g_string_append_printf(cmd, "%s %s", PURPLE_IRCV3_MSG_JOIN, name);
    -
    - password = purple_channel_join_details_get_password(details);
    - if(!purple_strempty(password)) {
    - g_string_append_printf(cmd, " %s", password);
    - }
    -
    - conversation = g_object_new(
    - PURPLE_TYPE_CONVERSATION,
    - "account", account,
    - "type", PurpleConversationTypeChannel,
    - "id", name,
    - "name", name,
    - NULL);
    - purple_conversation_manager_register(manager, conversation);
    - g_clear_object(&conversation);
    -
    - purple_ircv3_connection_writef(v3_connection, "%s", cmd->str);
    - g_string_free(cmd, TRUE);
    -
    - g_task_return_boolean(task, TRUE);
    - g_clear_object(&task);
    -}
    -
    -static gboolean
    -purple_ircv3_protocol_conversation_join_channel_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
    - GAsyncResult *result,
    - GError **error)
    -{
    - return g_task_propagate_boolean(G_TASK(result), error);
    -}
    -
    -void
    -purple_ircv3_protocol_conversation_init(PurpleProtocolConversationInterface *iface) {
    - iface->send_message_async =
    - purple_ircv3_protocol_conversation_send_message_async;
    - iface->send_message_finish =
    - purple_ircv3_protocol_conversation_send_message_finish;
    -
    - iface->get_channel_join_details =
    - purple_ircv3_protocol_conversation_get_channel_join_details;
    - iface->join_channel_async =
    - purple_ircv3_protocol_conversation_join_channel_async;
    - iface->join_channel_finish =
    - purple_ircv3_protocol_conversation_join_channel_finish;
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3protocolconversation.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,41 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_PROTOCOL_CONVERSATION_H
    -#define PURPLE_IRCV3_PROTOCOL_CONVERSATION_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -G_BEGIN_DECLS
    -
    -G_GNUC_INTERNAL void purple_ircv3_protocol_conversation_init(PurpleProtocolConversationInterface *iface);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_PROTOCOL_CONVERSATION_H */
    --- a/libpurple/protocols/ircv3/purpleircv3sasl.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,563 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <glib/gi18n-lib.h>
    -
    -#include <hasl.h>
    -
    -#include "purpleircv3sasl.h"
    -
    -#include "purpleircv3capabilities.h"
    -#include "purpleircv3connection.h"
    -#include "purpleircv3constants.h"
    -#include "purpleircv3core.h"
    -
    -#define PURPLE_IRCV3_SASL_DATA_KEY ("sasl-data")
    -
    -typedef struct {
    - PurpleConnection *connection;
    -
    - HaslContext *ctx;
    -
    - GString *server_in_buffer;
    -} PurpleIRCv3SASLData;
    -
    -/******************************************************************************
    - * Helpers
    - *****************************************************************************/
    -static const char *
    -purple_ircv3_sasl_get_username(PurpleConnection *connection) {
    - PurpleAccount *account = NULL;
    - const char *username = NULL;
    -
    - account = purple_connection_get_account(connection);
    -
    - username = purple_account_get_string(account, "sasl-login-name", "");
    - if(username != NULL && username[0] != '\0') {
    - return username;
    - }
    -
    - return purple_connection_get_display_name(connection);
    -}
    -
    -/******************************************************************************
    - * SASL Helpers
    - *****************************************************************************/
    -static void
    -purple_ircv3_sasl_data_free(PurpleIRCv3SASLData *data) {
    - g_clear_object(&data->ctx);
    -
    - g_string_free(data->server_in_buffer, TRUE);
    -
    - g_free(data);
    -}
    -
    -static void
    -purple_ircv3_sasl_data_add(PurpleConnection *connection, HaslContext *ctx) {
    - PurpleIRCv3SASLData *data = NULL;
    -
    - data = g_new0(PurpleIRCv3SASLData, 1);
    - g_object_set_data_full(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY,
    - data, (GDestroyNotify)purple_ircv3_sasl_data_free);
    -
    - /* We don't reference this because the life cycle of this data is tied
    - * directly to the connection and adding a reference to the connection
    - * would keep both alive forever.
    - */
    - data->connection = connection;
    - data->ctx = ctx;
    -
    - /* We truncate the server_in_buffer when we need to so that we can minimize
    - * allocations and simplify the logic involved with it.
    - */
    - data->server_in_buffer = g_string_new("");
    -}
    -
    -static void
    -purple_ircv3_sasl_attempt(PurpleIRCv3Connection *connection) {
    - PurpleIRCv3SASLData *data = NULL;
    - const char *next_mechanism = NULL;
    - const char *current = NULL;
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    -
    - current = hasl_context_get_current_mechanism(data->ctx);
    - if(current != NULL) {
    - g_message("SASL '%s' mechanism failed", current);
    - }
    -
    - next_mechanism = hasl_context_next(data->ctx);
    - if(next_mechanism == NULL) {
    - GError *error = g_error_new(PURPLE_CONNECTION_ERROR,
    - PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
    - _("No valid SASL mechanisms found"));
    -
    - purple_connection_take_error(PURPLE_CONNECTION(connection), error);
    -
    - return;
    - }
    -
    - g_message("trying SASL '%s' mechanism", next_mechanism);
    -
    - purple_ircv3_connection_writef(connection, "%s %s",
    - PURPLE_IRCV3_MSG_AUTHENTICATE,
    - next_mechanism);
    -}
    -
    -static void
    -purple_ircv3_sasl_start(PurpleIRCv3Capabilities *caps) {
    - PurpleIRCv3Connection *connection = NULL;
    - PurpleAccount *account = NULL;
    - PurpleConnection *purple_connection = NULL;
    - HaslContext *ctx = NULL;
    - const char *mechanisms = NULL;
    - gboolean toggle = FALSE;
    -
    - connection = purple_ircv3_capabilities_get_connection(caps);
    - purple_connection = PURPLE_CONNECTION(connection);
    - account = purple_connection_get_account(purple_connection);
    -
    - ctx = hasl_context_new();
    -
    - /* At this point we are ready to start our SASL negotiation, so add a wait
    - * counter to the capabilities and start the negotiations!
    - */
    - purple_ircv3_capabilities_add_wait(caps);
    -
    - /* Determine what mechanisms we're allowing and tell the context. */
    - mechanisms = purple_account_get_string(account, "sasl-mechanisms", "");
    - if(purple_strempty(mechanisms)) {
    - /* If the user didn't specify any mechanisms, grab the mechanisms that
    - * the server advertised.
    - */
    - mechanisms = purple_ircv3_capabilities_lookup(caps, "sasl", NULL);
    - }
    - hasl_context_set_allowed_mechanisms(ctx, mechanisms);
    -
    - /* Add the values we know to the context. */
    - hasl_context_set_username(ctx, purple_ircv3_sasl_get_username(purple_connection));
    - hasl_context_set_password(ctx, purple_connection_get_password(purple_connection));
    -
    - toggle = purple_account_get_bool(account, "use-tls", TRUE);
    - hasl_context_set_tls(ctx, toggle);
    -
    - toggle = purple_account_get_bool(account, "plain-sasl-in-clear", FALSE);
    - hasl_context_set_allow_clear_text(ctx, toggle);
    -
    - /* Create our SASLData object, add it to the connection. */
    - purple_ircv3_sasl_data_add(purple_connection, ctx);
    -
    - /* Make it go! */
    - purple_ircv3_sasl_attempt(connection);
    -}
    -
    -/******************************************************************************
    - * Callbacks
    - *****************************************************************************/
    -static void
    -purple_ircv3_sasl_ack_cb(PurpleIRCv3Capabilities *caps,
    - G_GNUC_UNUSED const char *capability,
    - G_GNUC_UNUSED gpointer data)
    -{
    - purple_ircv3_sasl_start(caps);
    -}
    -
    -/******************************************************************************
    - * Internal API
    - *****************************************************************************/
    -void
    -purple_ircv3_sasl_request(PurpleIRCv3Capabilities *capabilities) {
    - purple_ircv3_capabilities_request(capabilities,
    - PURPLE_IRCV3_CAPABILITY_SASL);
    -
    - g_signal_connect(capabilities, "ack::" PURPLE_IRCV3_CAPABILITY_SASL,
    - G_CALLBACK(purple_ircv3_sasl_ack_cb), NULL);
    -}
    -
    -gboolean
    -purple_ircv3_sasl_logged_in(G_GNUC_UNUSED PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    - PurpleAccount *account = NULL;
    - const char *sasl_name = NULL;
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "RPL_LOGGEDIN received with no SASL data "
    - "present");
    -
    - return FALSE;
    - }
    -
    - /* Check if the SASL login name is not set. If it is not set, set it to the
    - * current nick as it was successful.
    - */
    - account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    - sasl_name = purple_account_get_string(account, "sasl-login-name", "");
    - if(purple_strempty(sasl_name)) {
    - char **userparts = NULL;
    - const char *username = NULL;
    -
    - username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
    - userparts = g_strsplit(username, "@", 2);
    -
    - purple_account_set_string(account, "sasl-login-name", userparts[0]);
    -
    - g_strfreev(userparts);
    - }
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_sasl_logged_out(G_GNUC_UNUSED PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "RPL_LOGGEDOUT received with no SASL data "
    - "present");
    -
    - return FALSE;
    - }
    -
    - /* Not sure how to trigger this or what we should do in this case to be
    - * honest, so just note it for now.
    - * -- GK 2023-01-12
    - */
    - g_warning("Server sent SASL logged out");
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_sasl_nick_locked(PurpleIRCv3Message *v3_message,
    - GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    - GStrv params = NULL;
    - char *message = NULL;
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "ERR_NICKLOCKED received with no SASL data "
    - "present");
    -
    - return FALSE;
    - }
    -
    - params = purple_ircv3_message_get_params(v3_message);
    - message = g_strjoinv(" ", params);
    -
    - g_set_error(error, PURPLE_CONNECTION_ERROR,
    - PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
    - _("Nick name is locked: %s"), message);
    -
    - g_free(message);
    -
    - return FALSE;
    -}
    -
    -
    -gboolean
    -purple_ircv3_sasl_success(G_GNUC_UNUSED PurpleIRCv3Message *message,
    - GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Capabilities *capabilities = NULL;
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    -
    - capabilities = purple_ircv3_connection_get_capabilities(connection);
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "RPL_SASLSUCCESS received with no SASL data "
    - "present");
    -
    - return FALSE;
    - }
    -
    - /* This needs to be after we've checked the SASL data otherwise we might
    - * end up removing a wait that we don't own.
    - */
    - purple_ircv3_capabilities_remove_wait(capabilities);
    -
    - g_message("successfully authenticated with SASL '%s' mechanism.",
    - hasl_context_get_current_mechanism(data->ctx));
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_sasl_failed(G_GNUC_UNUSED PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "ERR_SASLFAIL received with no SASL data present");
    -
    - return FALSE;
    - }
    -
    - purple_ircv3_sasl_attempt(connection);
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_sasl_message_too_long(G_GNUC_UNUSED PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "ERR_SASLTOOLONG received with no SASL data "
    - "present");
    -
    - return FALSE;
    - }
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_sasl_aborted(G_GNUC_UNUSED PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "ERR_SASLABORTED received with no SASL data "
    - "present");
    -
    - return FALSE;
    - }
    -
    - /* This is supposed to get sent, when the client sends `AUTHENTICATE *`,
    - * but we don't do this, so I guess we'll note it for now...?
    - * --GK 2023-01-12
    - */
    - g_warning("The server claims we aborted SASL authentication.");
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_sasl_already_authed(G_GNUC_UNUSED PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "ERR_SASLALREADY received with no SASL data "
    - "present");
    -
    - return FALSE;
    - }
    -
    - /* Similar to aborted above, we don't allow this, so just note that it
    - * happened.
    - * -- GK 2023-01-12
    - */
    - g_warning("Server claims we tried to SASL authenticate again.");
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_sasl_mechanisms(PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    - GStrv params = NULL;
    - guint n_params = 0;
    -
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "RPL_SASLMECHS received with no SASL data "
    - "present");
    -
    - return FALSE;
    - }
    -
    - params = purple_ircv3_message_get_params(message);
    - if(params != NULL) {
    - n_params = g_strv_length(params);
    - }
    -
    - /* We need to find a server that sends this message. The specification says
    - * it _may_ be sent when the client sends AUTHENTICATE with an unknown
    - * mechanism, but ergo doesn't.
    - *
    - * We can't just blindly accept the new list either, as depending on how
    - * the server implements it, we'll need to remove mechanisms we've already
    - * tried in the event the server just dumps the entire list. As we're not
    - * currently tracking which mechanisms we've tried, this will have to be
    - * addressed as well.
    - */
    - if(n_params > 0) {
    - char *message = g_strjoinv(" ", params);
    -
    - g_message("Server sent the following SASL mechanisms: %s", message);
    -
    - g_free(message);
    - } else {
    - g_message("Server sent an empty list of SASL mechanisms");
    - }
    -
    - return TRUE;
    -}
    -
    -gboolean
    -purple_ircv3_sasl_authenticate(PurpleIRCv3Message *message,
    - GError **error,
    - gpointer user_data)
    -{
    - PurpleIRCv3Connection *connection = user_data;
    - PurpleIRCv3SASLData *data = NULL;
    - GStrv params = NULL;
    - char *payload = NULL;
    - gboolean done = FALSE;
    - guint n_params = 0;
    -
    - params = purple_ircv3_message_get_params(message);
    - if(params != NULL) {
    - n_params = g_strv_length(params);
    - }
    -
    - if(n_params != 1) {
    - g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    - "ignoring AUTHENTICATE with %d parameters", n_params);
    -
    - return FALSE;
    - }
    -
    - payload = params[0];
    - data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    - if(data == NULL) {
    - g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    - "AUTHENTICATE received with no SASL data present");
    -
    - return FALSE;
    - }
    -
    - /* If the server sent us a payload, combine the chunks. */
    - if(payload[0] != '+') {
    - g_string_append(data->server_in_buffer, payload);
    -
    - if(strlen(payload) < 400) {
    - done = TRUE;
    - }
    - } else {
    - /* The server sent a + which is an empty message or the final message
    - * ended on a 400 byte barrier. */
    - done = TRUE;
    - }
    -
    - if(done) {
    - HaslMechanismResult res = 0;
    - GError *local_error = NULL;
    - guint8 *server_in = NULL;
    - guint8 *client_out = NULL;
    - gsize server_in_length = 0;
    - size_t client_out_length = 0;
    -
    - /* If we have a buffer, base64 decode it, and then truncate it. */
    - if(data->server_in_buffer->len > 0) {
    - server_in = g_base64_decode(data->server_in_buffer->str,
    - &server_in_length);
    - g_string_truncate(data->server_in_buffer, 0);
    - }
    -
    - /* Try to move to the next step of the sasl client. */
    - res = hasl_context_step(data->ctx, server_in, server_in_length,
    - &client_out, &client_out_length, &local_error);
    -
    - /* We should be done with server_in, so free it.*/
    - g_clear_pointer(&server_in, g_free);
    -
    - if(res == HASL_MECHANISM_RESULT_ERROR) {
    - g_propagate_error(error, local_error);
    -
    - return FALSE;
    - }
    -
    - if(local_error != NULL) {
    - g_warning("hasl_context_step returned an error without an error "
    - "status: %s", local_error->message);
    -
    - g_clear_error(&local_error);
    - }
    -
    - /* If we got an output for the client, write it out. */
    - if(client_out_length > 0) {
    - char *encoded = NULL;
    -
    - encoded = g_base64_encode(client_out, client_out_length);
    - g_clear_pointer(&client_out, g_free);
    -
    - purple_ircv3_connection_writef(connection, "%s %s",
    - PURPLE_IRCV3_MSG_AUTHENTICATE,
    - encoded);
    - g_free(encoded);
    - } else {
    - purple_ircv3_connection_writef(connection, "%s +",
    - PURPLE_IRCV3_MSG_AUTHENTICATE);
    - }
    - }
    -
    - return TRUE;
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3sasl.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,55 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_SASL_H
    -#define PURPLE_IRCV3_SASL_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -#include "purpleircv3capabilities.h"
    -#include "purpleircv3message.h"
    -
    -G_BEGIN_DECLS
    -
    -G_GNUC_INTERNAL void purple_ircv3_sasl_request(PurpleIRCv3Capabilities *capabilities);
    -
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_logged_in(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_logged_out(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_nick_locked(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_success(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_failed(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_message_too_long(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_aborted(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_already_authed(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_mechanisms(PurpleIRCv3Message *message, GError **error, gpointer data);
    -G_GNUC_INTERNAL gboolean purple_ircv3_sasl_authenticate(PurpleIRCv3Message *message, GError **error, gpointer data);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_SASL_H */
    --- a/libpurple/protocols/ircv3/purpleircv3source.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,91 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#include <purple.h>
    -
    -#include "purpleircv3source.h"
    -
    -/******************************************************************************
    - * Public API
    - *****************************************************************************/
    -void
    -purple_ircv3_source_parse(const char *source, char **nick, char **user,
    - char **host)
    -{
    - GRegex *regex = NULL;
    - GMatchInfo *info = NULL;
    - gboolean matched = FALSE;
    -
    - g_return_if_fail(!purple_strempty(source));
    - g_return_if_fail(nick != NULL || user != NULL || host != NULL);
    -
    - /* If we find any \r \n or spaces in our source, it's invalid so we just
    - * bail immediately.
    - */
    - matched = g_regex_match_simple("[\r\n ]", source, G_REGEX_DEFAULT,
    - G_REGEX_MATCH_DEFAULT);
    - if(matched) {
    - return;
    - }
    -
    - regex = g_regex_new("^(?P<nick>[^ \r\n!]+)(?:!(?P<user>[^@]+)(?:@(?P<host>[^!@]+))?)?$",
    - G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT, NULL);
    -
    - matched = g_regex_match(regex, source, G_REGEX_MATCH_DEFAULT, &info);
    - if(!matched) {
    - if(nick != NULL) {
    - *nick = g_strdup(source);
    - }
    -
    - g_clear_pointer(&info, g_match_info_unref);
    - g_clear_pointer(&regex, g_regex_unref);
    -
    - return;
    - }
    -
    - if(nick != NULL) {
    - *nick = g_match_info_fetch_named(info, "nick");
    - }
    -
    - if(user != NULL) {
    - char *tmp = g_match_info_fetch_named(info, "user");
    -
    - if(!purple_strempty(tmp)) {
    - *user = tmp;
    - } else {
    - g_free(tmp);
    - }
    - }
    -
    - if(host != NULL) {
    - char *tmp = g_match_info_fetch_named(info, "host");
    -
    - if(!purple_strempty(tmp)) {
    - *host = tmp;
    - } else {
    - g_free(tmp);
    - }
    - }
    -
    - g_clear_pointer(&info, g_match_info_unref);
    - g_clear_pointer(&regex, g_regex_unref);
    -}
    --- a/libpurple/protocols/ircv3/purpleircv3source.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,58 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_SOURCE_H
    -#define PURPLE_IRCV3_SOURCE_H
    -
    -#include <glib.h>
    -#include <glib-object.h>
    -
    -#include "purpleircv3version.h"
    -
    -G_BEGIN_DECLS
    -
    -/**
    - * purple_ircv3_source_parse:
    - * @source: The source to parse.
    - * @nick: (out) (transfer full): A return address for the nick.
    - * @user: (out) (transfer full): A return address for the user.
    - * @host: (out) (transfer full): A return address for the host.
    - *
    - * Parses a `source` string like `pidgy!~u@53unc8n42i868.irc` into its nick,
    - * user, and host parts per https://modern.ircdocs.horse/#source.
    - *
    - * If the user or host aren't present in @source, but a return address is
    - * provided for them, that pointer will be set to %NULL.
    - *
    - * Since: 3.0
    - */
    -PURPLE_IRCV3_AVAILABLE_IN_ALL
    -void purple_ircv3_source_parse(const char *source, char **nick, char **user, char **host);
    -
    -G_END_DECLS
    -
    -#endif /* PURPLE_IRCV3_SOURCE_H */
    --- a/libpurple/protocols/ircv3/purpleircv3version.h Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,133 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * Purple is the legal property of its developers, whose names are too numerous
    - * to list here. Please refer to the COPYRIGHT file distributed with this
    - * source distribution.
    - *
    - * This library is free software; you can redistribute it and/or modify
    - * it under the terms of the GNU 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 General Public License for more details.
    - *
    - * You should have received a copy of the GNU General Public License
    - * along with this library; if not, see <https://www.gnu.org/licenses/>.
    - */
    -
    -#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    - !defined(PURPLE_IRCV3_COMPILATION)
    -# error "only <libpurple/protocols/ircv3.h> may be included directly"
    -#endif
    -
    -#ifndef PURPLE_IRCV3_VERSION_H
    -#define PURPLE_IRCV3_VERSION_H
    -
    -#include <glib.h>
    -
    -#include <purple.h>
    -
    -#if (defined(_WIN32) || defined(__CYGWIN__)) && \
    - !defined(PURPLE_IRCV3_STATIC_COMPILATION)
    -#define _PURPLE_IRCV3_EXPORT __declspec(dllexport)
    -#define _PURPLE_IRCV3_IMPORT __declspec(dllimport)
    -#elif __GNUC__ >= 4
    -#define _PURPLE_IRCV3_EXPORT __attribute__((visibility("default")))
    -#define _PURPLE_IRCV3_IMPORT
    -#else
    -#define _PURPLE_IRCV3_EXPORT
    -#define _PURPLE_IRCV3_IMPORT
    -#endif
    -#ifdef PURPLE_IRCV3_COMPILATION
    -#define _PURPLE_IRCV3_API _PURPLE_IRCV3_EXPORT
    -#else
    -#define _PURPLE_IRCV3_API _PURPLE_IRCV3_IMPORT
    -#endif
    -
    -#define _PURPLE_IRCV3_EXTERN _PURPLE_IRCV3_API extern
    -
    -#ifdef PURPLE_IRCV3_DISABLE_DEPRECATION_WARNINGS
    -#define PURPLE_IRCV3_DEPRECATED _PURPLE_IRCV3_EXTERN
    -#define PURPLE_IRCV3_DEPRECATED_FOR(f) _PURPLE_IRCV3_EXTERN
    -#define PURPLE_IRCV3_UNAVAILABLE(maj, min) _PURPLE_IRCV3_EXTERN
    -#define PURPLE_IRCV3_UNAVAILABLE_STATIC_INLINE(maj, min)
    -#define PURPLE_IRCV3_UNAVAILABLE_TYPE(maj, min)
    -#else
    -#define PURPLE_IRCV3_DEPRECATED G_DEPRECATED _PURPLE_IRCV3_EXTERN
    -#define PURPLE_IRCV3_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) _PURPLE_IRCV3_EXTERN
    -#define PURPLE_IRCV3_UNAVAILABLE(maj, min) G_UNAVAILABLE(maj, min) _PURPLE_IRCV3_EXTERN
    -#define PURPLE_IRCV3_UNAVAILABLE_STATIC_INLINE(maj, min) G_UNAVAILABLE(maj, min)
    -#define PURPLE_IRCV3_UNAVAILABLE_TYPE(maj, min) G_UNAVAILABLE(maj, min)
    -#endif
    -
    -/**
    - * PURPLE_IRCV3_VERSION_CUR_STABLE:
    - *
    - * A macro that evaluates to the current stable version of the IRCv3 protocol
    - * plugin, in a format that can be used by the C pre-processor.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_VERSION_CUR_STABLE \
    - (G_ENCODE_VERSION(PURPLE_MAJOR_VERSION, PURPLE_MINOR_VERSION))
    -
    -/* If the package sets PURPLE_IRCV3_VERSION_MIN_REQUIRED to some future
    - * PURPLE_IRCV3_VERSION_X_Y value that we don't know about, it will compare as 0 in
    - * preprocessor tests.
    - */
    -#ifndef PURPLE_IRCV3_VERSION_MIN_REQUIRED
    -#define PURPLE_IRCV3_VERSION_MIN_REQUIRED (PURPLE_IRCV3_VERSION_CUR_STABLE)
    -#elif PURPLE_IRCV3_VERSION_MIN_REQUIRED == 0
    -#undef PURPLE_IRCV3_VERSION_MIN_REQUIRED
    -#define PURPLE_IRCV3_VERSION_MIN_REQUIRED (PURPLE_IRCV3_VERSION_CUR_STABLE + 1)
    -#endif /* PURPLE_IRCV3_VERSION_MIN_REQUIRED */
    -
    -#if !defined(PURPLE_IRCV3_VERSION_MAX_ALLOWED) || (PURPLE_IRCV3_VERSION_MAX_ALLOWED == 0)
    -#undef PURPLE_IRCV3_VERSION_MAX_ALLOWED
    -#define PURPLE_IRCV3_VERSION_MAX_ALLOWED (PURPLE_IRCV3_VERSION_CUR_STABLE)
    -#endif /* PURPLE_IRCV3_VERSION_MAX_ALLOWED */
    -
    -/* sanity checks */
    -#if PURPLE_IRCV3_VERSION_MIN_REQUIRED > PURPLE_IRCV3_VERSION_CUR_STABLE
    -#error "PURPLE_IRCV3_VERSION_MIN_REQUIRED must be <= PURPLE_IRCV3_VERSION_CUR_STABLE"
    -#endif
    -#if PURPLE_IRCV3_VERSION_MAX_ALLOWED < PURPLE_IRCV3_VERSION_MIN_REQUIRED
    -#error "PURPLE_IRCV3_VERSION_MAX_ALLOWED must be >= PURPLE_IRCV3_VERSION_MIN_REQUIRED"
    -#endif
    -#if PURPLE_IRCV3_VERSION_MIN_REQUIRED < G_ENCODE_VERSION(3, 0)
    -#error "PURPLE_IRCV3_VERSION_MIN_REQUIRED must be >= PURPLE_IRCV3_VERSION_3_0"
    -#endif
    -
    -#define PURPLE_IRCV3_VAR _PURPLE_IRCV3_EXTERN
    -#define PURPLE_IRCV3_AVAILABLE_IN_ALL _PURPLE_IRCV3_EXTERN
    -
    -/**
    - * PURPLE_IRCV3_VERSION_3_0:
    - *
    - * A macro that evaluates to the 3.0 version of the IRCv3 protocol plugin, in a
    - * format that can be used by the C pre-processor.
    - *
    - * Since: 3.0
    - */
    -#define PURPLE_IRCV3_VERSION_3_0 (G_ENCODE_VERSION(3, 0))
    -
    -#if PURPLE_IRCV3_VERSION_MAX_ALLOWED < PURPLE_IRCV3_VERSION_3_0
    -#define PURPLE_IRCV3_AVAILABLE_IN_3_0 PURPLE_IRCV3_UNAVAILABLE(3, 0)
    -#define PURPLE_IRCV3_AVAILABLE_STATIC_INLINE_IN_3_0 PURPLE_IRCV3_UNAVAILABLE_STATIC_INLINE(3, 0)
    -#define PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0 PURPLE_IRCV3_UNAVAILABLE_MACRO(3, 0)
    -#define PURPLE_IRCV3_AVAILABLE_ENUMERATOR_IN_3_0 PURPLE_IRCV3_UNAVAILABLE_ENUMERATOR(3, 0)
    -#define PURPLE_IRCV3_AVAILABLE_TYPE_IN_3_0 PURPLE_IRCV3_UNAVAILABLE_TYPE(3, 0)
    -#else
    -#define PURPLE_IRCV3_AVAILABLE_IN_3_0 _PURPLE_IRCV3_EXTERN
    -#define PURPLE_IRCV3_AVAILABLE_STATIC_INLINE_IN_3_0
    -#define PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    -#define PURPLE_IRCV3_AVAILABLE_ENUMERATOR_IN_3_0
    -#define PURPLE_IRCV3_AVAILABLE_TYPE_IN_3_0
    -#endif
    -
    -#endif /* PURPLE_IRCV3_VERSION_H */
    Binary file libpurple/protocols/ircv3/resources/icons/16x16/apps/im-ircv3.png has changed
    Binary file libpurple/protocols/ircv3/resources/icons/22x22/apps/im-ircv3.png has changed
    Binary file libpurple/protocols/ircv3/resources/icons/48x48/apps/im-ircv3.png has changed
    --- a/libpurple/protocols/ircv3/resources/ircv3.gresource.xml Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,8 +0,0 @@
    -<?xml version="1.0" encoding="UTF-8"?>
    -<gresources>
    - <gresource prefix="/im/pidgin/libpurple/ircv3">
    - <file>icons/16x16/apps/im-ircv3.png</file>
    - <file>icons/22x22/apps/im-ircv3.png</file>
    - <file>icons/48x48/apps/im-ircv3.png</file>
    - </gresource>
    -</gresources>
    --- a/libpurple/protocols/ircv3/tests/meson.build Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,15 +0,0 @@
    -TESTS = [
    - 'formatting',
    - 'parser',
    - 'source',
    -]
    -
    -foreach prog : TESTS
    - e = executable(
    - f'test_ircv3_@prog@', f'test_ircv3_@prog@.c',
    - dependencies : [birb_dep, libpurple_dep, glib, hasl],
    - objects : ircv3_prpl.extract_all_objects(),
    - c_args : ['-DPURPLE_IRCV3_COMPILATION'])
    -
    - test(f'ircv3_@prog@', e)
    -endforeach
    --- a/libpurple/protocols/ircv3/tests/test_ircv3_formatting.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,232 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 <purple.h>
    -
    -#include "../purpleircv3formatting.h"
    -
    -/******************************************************************************
    - * Strip Tests
    - *****************************************************************************/
    -static void
    -test_ircv3_formatting_strip(const char *input, const char *expected) {
    - char *actual = NULL;
    -
    - actual = purple_ircv3_formatting_strip(input);
    - g_assert_cmpstr(actual, ==, expected);
    - g_clear_pointer(&actual, g_free);
    -}
    -
    -static void
    -test_ircv3_formatting_strip_null(void) {
    - test_ircv3_formatting_strip(NULL, NULL);
    -}
    -
    -static void
    -test_ircv3_formatting_strip_empty(void) {
    - test_ircv3_formatting_strip("", "");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_color_comma(void) {
    - test_ircv3_formatting_strip("\003,", ",");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_color_foreground_comma(void) {
    - test_ircv3_formatting_strip("\0033,", ",");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_color_full(void) {
    - test_ircv3_formatting_strip("\0033,9wee", "wee");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_color_foreground_3_digit(void) {
    - test_ircv3_formatting_strip("\003314", "4");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_color_background_3_digit(void) {
    - test_ircv3_formatting_strip("\0031,234", "4");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_hex_color(void) {
    - test_ircv3_formatting_strip("\004FF00FFwoo!", "woo!");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_hex_color_full(void) {
    - test_ircv3_formatting_strip("\004FF00FF,00FF00woo!", "woo!");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_hex_color_comma(void) {
    - test_ircv3_formatting_strip("\004,", ",");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_hex_color_foreground_comma(void) {
    - test_ircv3_formatting_strip("\004FEFEFE,", ",");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_hex_color_foreground_7_characters(void) {
    - test_ircv3_formatting_strip("\004FEFEFEF", "F");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_hex_color_background_7_characters(void) {
    - test_ircv3_formatting_strip("\004FEFEFE,2222223", "3");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_bold(void) {
    - test_ircv3_formatting_strip("this is \002bold\002!", "this is bold!");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_italic(void) {
    - test_ircv3_formatting_strip("what do you \035mean\035?!",
    - "what do you mean?!");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_monospace(void) {
    - test_ircv3_formatting_strip("\021i++;\021", "i++;");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_reset(void) {
    - test_ircv3_formatting_strip("end of formatting\017", "end of formatting");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_reverse(void) {
    - test_ircv3_formatting_strip("re\026ver\026se", "reverse");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_strikethrough(void) {
    - test_ircv3_formatting_strip("\036I could be wrong\036",
    - "I could be wrong");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_underline(void) {
    - test_ircv3_formatting_strip("You can't handle the \037truth\037!",
    - "You can't handle the truth!");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_spec_example1(void) {
    - test_ircv3_formatting_strip(
    - "I love \0033IRC! \003It is the \0037best protocol ever!",
    - "I love IRC! It is the best protocol ever!");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_spec_example2(void) {
    - test_ircv3_formatting_strip(
    - "This is a \035\00313,9cool \003message",
    - "This is a cool message");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_spec_example3(void) {
    - test_ircv3_formatting_strip(
    - "IRC \002is \0034,12so \003great\017!",
    - "IRC is so great!");
    -}
    -
    -static void
    -test_ircv3_formatting_strip_spec_example4(void) {
    - test_ircv3_formatting_strip(
    - "Rules: Don't spam 5\00313,8,6\003,7,8, and especially not \0029\002\035!",
    - "Rules: Don't spam 5,6,7,8, and especially not 9!");
    -}
    -
    -/******************************************************************************
    - * Main
    - *****************************************************************************/
    -int
    -main(int argc, char *argv[]) {
    - g_test_init(&argc, &argv, NULL);
    -
    - g_test_add_func("/ircv3/formatting/strip/null",
    - test_ircv3_formatting_strip_null);
    - g_test_add_func("/ircv3/formatting/strip/empty",
    - test_ircv3_formatting_strip_empty);
    -
    - g_test_add_func("/ircv3/formatting/strip/color-comma",
    - test_ircv3_formatting_strip_color_comma);
    - g_test_add_func("/ircv3/formatting/strip/color-full",
    - test_ircv3_formatting_strip_color_full);
    - g_test_add_func("/ircv3/formatting/strip/color-foreground-comma",
    - test_ircv3_formatting_strip_color_foreground_comma);
    - g_test_add_func("/ircv3/formatting/strip/color-foreground-3-digit",
    - test_ircv3_formatting_strip_color_foreground_3_digit);
    - g_test_add_func("/ircv3/formatting/strip/color-background-3-digit",
    - test_ircv3_formatting_strip_color_background_3_digit);
    -
    - g_test_add_func("/ircv3/formatting/strip/hex-color",
    - test_ircv3_formatting_strip_hex_color);
    - g_test_add_func("/ircv3/formatting/strip/hex-color-full",
    - test_ircv3_formatting_strip_hex_color_full);
    - g_test_add_func("/ircv3/formatting/strip/hex-color-comma",
    - test_ircv3_formatting_strip_hex_color_comma);
    - g_test_add_func("/ircv3/formatting/strip/hex-color-foreground-comma",
    - test_ircv3_formatting_strip_hex_color_foreground_comma);
    - g_test_add_func("/ircv3/formatting/strip/hex-color-foreground-7-characters",
    - test_ircv3_formatting_strip_hex_color_foreground_7_characters);
    - g_test_add_func("/ircv3/formatting/strip/hex-color-background-7-characters",
    - test_ircv3_formatting_strip_hex_color_background_7_characters);
    -
    - g_test_add_func("/ircv3/formatting/strip/bold",
    - test_ircv3_formatting_strip_bold);
    - g_test_add_func("/ircv3/formatting/strip/italic",
    - test_ircv3_formatting_strip_italic);
    - g_test_add_func("/ircv3/formatting/strip/monospace",
    - test_ircv3_formatting_strip_monospace);
    - g_test_add_func("/ircv3/formatting/strip/reset",
    - test_ircv3_formatting_strip_reset);
    - g_test_add_func("/ircv3/formatting/strip/reverse",
    - test_ircv3_formatting_strip_reverse);
    - g_test_add_func("/ircv3/formatting/strip/strikethrough",
    - test_ircv3_formatting_strip_strikethrough);
    - g_test_add_func("/ircv3/formatting/strip/underline",
    - test_ircv3_formatting_strip_underline);
    -
    - /* These tests are based on the examples here
    - * https://modern.ircdocs.horse/formatting.html#examples
    - */
    - g_test_add_func("/ircv3/formatting/strip/spec-example1",
    - test_ircv3_formatting_strip_spec_example1);
    - g_test_add_func("/ircv3/formatting/strip/spec-example2",
    - test_ircv3_formatting_strip_spec_example2);
    - g_test_add_func("/ircv3/formatting/strip/spec-example3",
    - test_ircv3_formatting_strip_spec_example3);
    - g_test_add_func("/ircv3/formatting/strip/spec-example4",
    - test_ircv3_formatting_strip_spec_example4);
    -
    - return g_test_run();
    -}
    --- a/libpurple/protocols/ircv3/tests/test_ircv3_parser.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,822 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 <purple.h>
    -
    -#include "../purpleircv3constants.h"
    -#include "../purpleircv3message.h"
    -#include "../purpleircv3parser.h"
    -
    -#define TEST_IRCV3_PARSER_DOMAIN (g_quark_from_static_string("test-ircv3-parser"))
    -
    -typedef struct {
    - GHashTable *tags;
    - gchar *source;
    - gchar *command;
    - guint n_params;
    - const gchar * const params[16];
    -} TestPurpleIRCv3ParserData;
    -
    -/******************************************************************************
    - * Handlers
    - *****************************************************************************/
    -static gboolean
    -test_purple_ircv3_test_handler(PurpleIRCv3Message *message,
    - G_GNUC_UNUSED GError **error,
    - gpointer data)
    -{
    - TestPurpleIRCv3ParserData *d = data;
    - GHashTable *tags = NULL;
    - GHashTableIter iter;
    - GStrv params = NULL;
    - const char *command = NULL;
    - const char *source = NULL;
    - guint n_params = 0;
    -
    - command = purple_ircv3_message_get_command(message);
    - params = purple_ircv3_message_get_params(message);
    - source = purple_ircv3_message_get_source(message);
    - tags = purple_ircv3_message_get_tags(message);
    -
    - if(params != NULL) {
    - n_params = g_strv_length(params);
    - }
    -
    - /* Make sure we have an expected tags hash table before checking them. */
    - if(d->tags != NULL) {
    - gpointer expected_key;
    - gpointer expected_value;
    - guint actual_size;
    - guint expected_size;
    -
    - /* Make sure the tag hash tables have the same size. */
    - expected_size = g_hash_table_size(d->tags);
    - actual_size = g_hash_table_size(tags);
    - g_assert_cmpuint(actual_size, ==, expected_size);
    -
    - /* Since the tables have the same size, we can walk through the expected
    - * table and use it to verify the actual table.
    - */
    - g_hash_table_iter_init(&iter, d->tags);
    - while(g_hash_table_iter_next(&iter, &expected_key, &expected_value)) {
    - gpointer actual_value = NULL;
    - gboolean found = FALSE;
    -
    - found = g_hash_table_lookup_extended(tags, expected_key, NULL,
    - &actual_value);
    - g_assert_true(found);
    - g_assert_cmpstr(actual_value, ==, expected_value);
    - }
    - }
    -
    - /* Walk through the params checking against the expected values. */
    - if(d->n_params > 0) {
    - g_assert_cmpuint(n_params, ==, d->n_params);
    -
    - for(guint i = 0; i < d->n_params; i++) {
    - g_assert_cmpstr(params[i], ==, d->params[i]);
    - }
    - }
    -
    - /* Validate all the string parameters. */
    - g_assert_cmpstr(source, ==, d->source);
    - g_assert_cmpstr(command, ==, d->command);
    -
    - /* Cleanup everything the caller allocated. */
    - g_clear_pointer(&d->tags, g_hash_table_destroy);
    -
    - /* Return the return value the caller asked for. */
    - return TRUE;
    -}
    -
    -static gboolean
    -test_purple_ircv3_test_handler_error(G_GNUC_UNUSED PurpleIRCv3Message *message,
    - GError **error,
    - G_GNUC_UNUSED gpointer data)
    -{
    - g_set_error(error, TEST_IRCV3_PARSER_DOMAIN, 0, "test error");
    -
    - return FALSE;
    -}
    -
    -/******************************************************************************
    - * Helpers
    - *****************************************************************************/
    -static void
    -test_purple_ircv3_parser(const gchar *source, TestPurpleIRCv3ParserData *d) {
    - PurpleIRCv3Parser *parser = purple_ircv3_parser_new();
    - GError *error = NULL;
    - gboolean result = FALSE;
    -
    - purple_ircv3_parser_set_fallback_handler(parser,
    - test_purple_ircv3_test_handler);
    -
    - result = purple_ircv3_parser_parse(parser, source, &error, d);
    -
    - g_assert_no_error(error);
    - g_assert_true(result);
    -
    - g_clear_object(&parser);
    -}
    -
    -/******************************************************************************
    - * Tests
    - *****************************************************************************/
    -static void
    -test_purple_ircv3_parser_propagates_errors(void) {
    - PurpleIRCv3Parser *parser = purple_ircv3_parser_new();
    - GError *error = NULL;
    - gboolean result = FALSE;
    -
    - purple_ircv3_parser_set_fallback_handler(parser,
    - test_purple_ircv3_test_handler_error);
    -
    - result = purple_ircv3_parser_parse(parser, "COMMAND", &error, NULL);
    - g_assert_error(error, TEST_IRCV3_PARSER_DOMAIN, 0);
    - g_clear_error(&error);
    -
    - g_assert_false(result);
    -
    - g_clear_object(&parser);
    -}
    -
    -static void
    -test_purple_ircv3_parser_simple(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "foo",
    - .n_params = 3,
    - .params = {"bar", "baz", "asdf"},
    - };
    -
    - test_purple_ircv3_parser("foo bar baz asdf", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_source(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "coolguy",
    - .command = "foo",
    - .n_params = 3,
    - .params = {"bar", "baz", "asdf"},
    - };
    -
    - test_purple_ircv3_parser(":coolguy foo bar baz asdf", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_trailing(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "foo",
    - .n_params = 3,
    - .params = {"bar", "baz", "asdf quux"},
    - };
    -
    - test_purple_ircv3_parser("foo bar baz :asdf quux", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_empty_trailing(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "foo",
    - .n_params = 3,
    - .params = {"bar", "baz", ""},
    - };
    -
    - test_purple_ircv3_parser("foo bar baz :", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_trailing_starting_colon(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "foo",
    - .n_params = 3,
    - .params = {"bar", "baz", ":asdf"},
    - };
    -
    - test_purple_ircv3_parser("foo bar baz ::asdf", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_source_and_trailing(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "coolguy",
    - .command = "foo",
    - .n_params = 3,
    - .params = {"bar", "baz", "asdf quux"},
    - };
    -
    - test_purple_ircv3_parser(":coolguy foo bar baz :asdf quux", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_source_and_trailing_whitespace(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "coolguy",
    - .command = "foo",
    - .n_params = 3,
    - .params = {"bar", "baz", " asdf quux "},
    - };
    -
    - test_purple_ircv3_parser(":coolguy foo bar baz : asdf quux ", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_source_and_trailing_colon(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "coolguy",
    - .command = PURPLE_IRCV3_MSG_PRIVMSG,
    - .n_params = 2,
    - .params = {"bar", "lol :) "},
    - };
    -
    - test_purple_ircv3_parser(":coolguy PRIVMSG bar :lol :) ", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_source_and_empty_trailing(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "coolguy",
    - .command = "foo",
    - .n_params = 3,
    - .params = {"bar", "baz", ""},
    - };
    -
    - test_purple_ircv3_parser(":coolguy foo bar baz :", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_source_and_trailing_only_whitespace(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "coolguy",
    - .command = "foo",
    - .n_params = 3,
    - .params = {"bar", "baz", " "},
    - };
    -
    - test_purple_ircv3_parser(":coolguy foo bar baz : ", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_tags(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "foo",
    - };
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "a", "b");
    - g_hash_table_insert(data.tags, "c", "32");
    - g_hash_table_insert(data.tags, "k", "");
    - g_hash_table_insert(data.tags, "rt", "ql7");
    -
    - test_purple_ircv3_parser("@a=b;c=32;k;rt=ql7 foo", &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_with_escaped_tags(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "foo",
    - };
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "a", "b\\and\nk");
    - g_hash_table_insert(data.tags, "c", "72 45");
    - g_hash_table_insert(data.tags, "d", "gh;764");
    -
    - test_purple_ircv3_parser("@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo",
    - &data);
    -}
    -
    -static void
    -test_purple_ircv3_with_tags_and_source(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "quux",
    - .command = "ab",
    - .n_params = 1,
    - .params = {"cd"},
    - };
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "c", "");
    - g_hash_table_insert(data.tags, "h", "");
    - g_hash_table_insert(data.tags, "a", "b");
    -
    - test_purple_ircv3_parser("@c;h=;a=b :quux ab cd", &data);
    -}
    -
    -static void
    -test_purple_ircv3_last_param_no_colon(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "src",
    - .command = PURPLE_IRCV3_MSG_JOIN,
    - .n_params = 1,
    - .params = {"#chan"},
    - };
    -
    - test_purple_ircv3_parser(":src JOIN #chan", &data);
    -}
    -
    -static void
    -test_purple_ircv3_last_param_with_colon(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "src",
    - .command = PURPLE_IRCV3_MSG_JOIN,
    - .n_params = 1,
    - .params = {"#chan"},
    - };
    -
    - test_purple_ircv3_parser(":src JOIN :#chan", &data);
    -}
    -
    -static void
    -test_purple_ircv3_without_last_param(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "src",
    - .command = "AWAY",
    - };
    -
    - test_purple_ircv3_parser(":src AWAY", &data);
    -}
    -
    -static void
    -test_purple_ircv3_with_last_param(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "src",
    - .command = "AWAY",
    - };
    -
    - test_purple_ircv3_parser(":src AWAY ", &data);
    -}
    -
    -static void
    -test_purple_ircv3_tab_is_not_space(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "cool\tguy",
    - .command = "foo",
    - .n_params = 2,
    - .params = {"bar", "baz"},
    - };
    -
    - test_purple_ircv3_parser(":cool\tguy foo bar baz", &data);
    -}
    -
    -static void
    -test_purple_ircv3_source_control_characters_1(void) {
    - /* Break each string after the hex escape as they are supposed to only be
    - * a single byte, but the c compiler will keep unescaping unless we break
    - * the string.
    - */
    - TestPurpleIRCv3ParserData data = {
    - .source = "coolguy!ag@net\x03" "5w\x03" "ork.admin",
    - .command = PURPLE_IRCV3_MSG_PRIVMSG,
    - .n_params = 2,
    - .params = {"foo", "bar baz"},
    - };
    - const gchar *msg = NULL;
    -
    - msg = ":coolguy!ag@net\x03" "5w\x03" "ork.admin PRIVMSG foo :bar baz";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_source_control_characters_2(void) {
    - /* Break each string after the hex escape as they are supposed to only be
    - * a single byte, but the c compiler will keep unescaping unless we break
    - * the string.
    - */
    - TestPurpleIRCv3ParserData data = {
    - .source = "coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin",
    - .command = PURPLE_IRCV3_MSG_PRIVMSG,
    - .n_params = 2,
    - .params = {"foo", "bar baz"},
    - };
    - const gchar *msg = NULL;
    -
    - msg = ":coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin PRIVMSG foo :bar "
    - "baz";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_everything(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "irc.example.com",
    - .command = "COMMAND",
    - .n_params = 3,
    - .params = {"param1", "param2", "param3 param3"},
    - };
    - const gchar *msg = NULL;
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "tag1", "value1");
    - g_hash_table_insert(data.tags, "tag2", "");
    - g_hash_table_insert(data.tags, "vendor1/tag3", "value2");
    - g_hash_table_insert(data.tags, "vendor2/tag4", "");
    -
    - msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= "
    - ":irc.example.com COMMAND param1 param2 :param3 param3";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_everything_but_tags(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "irc.example.com",
    - .command = "COMMAND",
    - .n_params = 3,
    - .params = {"param1", "param2", "param3 param3"},
    - };
    - const gchar *msg = NULL;
    -
    - msg = ":irc.example.com COMMAND param1 param2 :param3 param3";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_everything_but_source(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "COMMAND",
    - .n_params = 3,
    - .params = {"param1", "param2", "param3 param3"},
    - };
    - const gchar *msg = NULL;
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "tag1", "value1");
    - g_hash_table_insert(data.tags, "tag2", "");
    - g_hash_table_insert(data.tags, "vendor1/tag3", "value2");
    - g_hash_table_insert(data.tags, "vendor2/tag4", "");
    -
    - msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 "
    - "COMMAND param1 param2 :param3 param3";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_command_only(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "COMMAND",
    - };
    -
    - test_purple_ircv3_parser("COMMAND", &data);
    -}
    -
    -static void
    -test_purple_ircv3_slashes_are_fun(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "COMMAND",
    - };
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "foo", "\\\\;\\s \r\n");
    -
    - test_purple_ircv3_parser("@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND", &data);
    -}
    -
    -static void
    -test_purple_ircv3_unreal_broken_1(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "gravel.mozilla.org",
    - .command = "432",
    - .n_params = 2,
    - .params = {"#momo", "Erroneous Nickname: Illegal characters"},
    - };
    - const gchar *msg = NULL;
    -
    - msg = ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal "
    - "characters";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_unreal_broken_2(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "gravel.mozilla.org",
    - .command = "MODE",
    - .n_params = 2,
    - .params = {"#tckk", "+n"},
    - };
    -
    - test_purple_ircv3_parser(":gravel.mozilla.org MODE #tckk +n ", &data);
    -}
    -
    -static void
    -test_purple_ircv3_unreal_broken_3(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "services.esper.net",
    - .command = "MODE",
    - .n_params = 3,
    - .params = {"#foo-bar", "+o", "foobar"},
    - };
    -
    - test_purple_ircv3_parser(":services.esper.net MODE #foo-bar +o foobar ",
    - &data);
    -}
    -
    -static void
    -test_purple_ircv3_tag_escape_char_at_a_time(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "COMMAND",
    - };
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "tag1", "value\\ntest");
    -
    - test_purple_ircv3_parser("@tag1=value\\\\ntest COMMAND", &data);
    -}
    -
    -static void
    -test_purple_ircv3_tag_drop_unnecessary_escapes(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "COMMAND",
    - };
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "tag1", "value1");
    -
    - test_purple_ircv3_parser("@tag1=value\\1 COMMAND", &data);
    -}
    -
    -static void
    -test_purple_ircv3_tag_drop_trailing_slash(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "COMMAND",
    - };
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "tag1", "value1");
    -
    - test_purple_ircv3_parser("@tag1=value1\\ COMMAND", &data);
    -}
    -
    -static void
    -test_purple_ircv3_duplicate_tags(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "COMMAND",
    - };
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "tag1", "5");
    - g_hash_table_insert(data.tags, "tag2", "3");
    - g_hash_table_insert(data.tags, "tag3", "4");
    -
    - test_purple_ircv3_parser("@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND", &data);
    -}
    -
    -static void
    -test_purple_ircv3_vendor_tags_are_namespaced(void) {
    - TestPurpleIRCv3ParserData data = {
    - .command = "COMMAND",
    - };
    - const gchar *msg = NULL;
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "tag1", "5");
    - g_hash_table_insert(data.tags, "tag2", "3");
    - g_hash_table_insert(data.tags, "tag3", "4");
    - g_hash_table_insert(data.tags, "vendor/tag2", "8");
    -
    - msg = "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_special_mode_1(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "SomeOp",
    - .command = "MODE",
    - .n_params = 2,
    - .params = {"#channel", "+i"},
    - };
    -
    - test_purple_ircv3_parser(":SomeOp MODE #channel :+i", &data);
    -}
    -
    -static void
    -test_purple_ircv3_special_mode_2(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "SomeOp",
    - .command = "MODE",
    - .n_params = 4,
    - .params = {"#channel", "+oo", "SomeUser", "AnotherUser"},
    - };
    -
    - test_purple_ircv3_parser(":SomeOp MODE #channel +oo SomeUser :AnotherUser",
    - &data);
    -}
    -
    -/******************************************************************************
    - * Message tags examples
    - *****************************************************************************/
    -static void
    -test_purple_ircv3_parser_message_tags_none(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "nick!ident@host.com",
    - .command = PURPLE_IRCV3_MSG_PRIVMSG,
    - .n_params = 2,
    - .params = {"me", "Hello"},
    - };
    - const char *msg = NULL;
    -
    - msg = ":nick!ident@host.com PRIVMSG me :Hello";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_message_tags_3_tags(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "nick!ident@host.com",
    - .command = PURPLE_IRCV3_MSG_PRIVMSG,
    - .n_params = 2,
    - .params = {"me", "Hello"},
    - };
    - const char *msg = NULL;
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "aaa", "bbb");
    - g_hash_table_insert(data.tags, "ccc", "");
    - g_hash_table_insert(data.tags, "example.com/ddd", "eee");
    -
    - msg = "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me "
    - ":Hello";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_message_tags_client_only(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "url_bot!bot@example.com",
    - .command = PURPLE_IRCV3_MSG_PRIVMSG,
    - .n_params = 2,
    - .params = {"#channel", "Example.com: A News Story"},
    - };
    - const char *msg = NULL;
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "+icon", "https://example.com/favicon.png");
    -
    - msg = "@+icon=https://example.com/favicon.png :url_bot!bot@example.com "
    - "PRIVMSG #channel :Example.com: A News Story";
    -
    - test_purple_ircv3_parser(msg, &data);
    -}
    -
    -static void
    -test_purple_ircv3_parser_message_tags_labeled_response(void) {
    - TestPurpleIRCv3ParserData data = {
    - .source = "nick!user@example.com",
    - .command = "TAGMSG",
    - .n_params = 1,
    - .params = {"#channel"},
    - };
    - const char *msg = NULL;
    -
    - data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    - g_hash_table_insert(data.tags, "label", "123");
    - g_hash_table_insert(data.tags, "msgid", "abc");
    - g_hash_table_insert(data.tags, "+example-client-tag", "example-value");
    -
    - msg = "@label=123;msgid=abc;+example-client-tag=example-value "
    - ":nick!user@example.com TAGMSG #channel";
    -
    - test_purple_ircv3_parser(msg, &data);
    -
    -}
    -/******************************************************************************
    - * Main
    - *****************************************************************************/
    -gint
    -main(gint argc, gchar *argv[]) {
    - g_test_init(&argc, &argv, NULL);
    -
    - /* Make sure an error in the handler is propagated back up to the calling
    - * function.
    - */
    - g_test_add_func("/ircv3/parser/propagates-errors",
    - test_purple_ircv3_parser_propagates_errors);
    -
    - /* These tests are based on the msg-split tests from
    - * https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml
    - */
    - g_test_add_func("/ircv3/parser/simple",
    - test_purple_ircv3_parser_simple);
    - g_test_add_func("/ircv3/parser/with-source",
    - test_purple_ircv3_parser_with_source);
    - g_test_add_func("/ircv3/parser/with-trailing",
    - test_purple_ircv3_parser_with_trailing);
    - g_test_add_func("/ircv3/parser/with-empty-trailing",
    - test_purple_ircv3_parser_with_empty_trailing);
    - g_test_add_func("/ircv3/parser/with-trailing-starting-colon",
    - test_purple_ircv3_parser_with_trailing_starting_colon);
    - g_test_add_func("/ircv3/parser/with-source-and-trailing",
    - test_purple_ircv3_parser_with_source_and_trailing);
    - g_test_add_func("/ircv3/parser/with-source-and-trailing-whitespace",
    - test_purple_ircv3_parser_with_source_and_trailing_whitespace);
    - g_test_add_func("/ircv3/parser/with-source-and-trailing-colon",
    - test_purple_ircv3_parser_with_source_and_trailing_colon);
    - g_test_add_func("/ircv3/parser/with-source-and-empty-trailing",
    - test_purple_ircv3_parser_with_source_and_empty_trailing);
    - g_test_add_func("/ircv3/parser/with-source-and-trailing-only-whitespace",
    - test_purple_ircv3_parser_with_source_and_trailing_only_whitespace);
    -
    - g_test_add_func("/ircv3/parser/with-tags",
    - test_purple_ircv3_parser_with_tags);
    - g_test_add_func("/ircv3/parser/with-escaped-tags",
    - test_purple_ircv3_parser_with_escaped_tags);
    - g_test_add_func("/ircv3/parser/with-tags-and-source",
    - test_purple_ircv3_with_tags_and_source);
    -
    - g_test_add_func("/ircv3/parser/last-param-no-colon",
    - test_purple_ircv3_last_param_no_colon);
    - g_test_add_func("/ircv3/parser/last-param-with-colon",
    - test_purple_ircv3_last_param_with_colon);
    -
    - g_test_add_func("/ircv3/parser/without-last-param",
    - test_purple_ircv3_without_last_param);
    - g_test_add_func("/ircv3/parser/with-last-parsm",
    - test_purple_ircv3_with_last_param);
    -
    - g_test_add_func("/ircv3/parser/tab-is-not-space",
    - test_purple_ircv3_tab_is_not_space);
    -
    - g_test_add_func("/ircv3/parser/source_control_characters_1",
    - test_purple_ircv3_source_control_characters_1);
    - g_test_add_func("/ircv3/parser/source_control_characters_2",
    - test_purple_ircv3_source_control_characters_2);
    -
    - g_test_add_func("/ircv3/parser/everything",
    - test_purple_ircv3_everything);
    - g_test_add_func("/ircv3/parser/everything-but-tags",
    - test_purple_ircv3_everything_but_tags);
    - g_test_add_func("/ircv3/parser/everything-but-source",
    - test_purple_ircv3_everything_but_source);
    -
    - g_test_add_func("/ircv3/parser/command-only",
    - test_purple_ircv3_command_only);
    -
    - g_test_add_func("/ircv3/parser/slashes-are-fun",
    - test_purple_ircv3_slashes_are_fun);
    -
    - g_test_add_func("/ircv3/parser/unreal-broken-1",
    - test_purple_ircv3_unreal_broken_1);
    - g_test_add_func("/ircv3/parser/unreal-broken-2",
    - test_purple_ircv3_unreal_broken_2);
    - g_test_add_func("/ircv3/parser/unreal-broken-3",
    - test_purple_ircv3_unreal_broken_3);
    -
    - g_test_add_func("/ircv3/parser/tag-escape-char-at-a-time",
    - test_purple_ircv3_tag_escape_char_at_a_time);
    - g_test_add_func("/ircv3/parser/tag-drop-unnecessary-escapes",
    - test_purple_ircv3_tag_drop_unnecessary_escapes);
    - g_test_add_func("/ircv3/parser/tag-drop-trailing-slash",
    - test_purple_ircv3_tag_drop_trailing_slash);
    -
    - g_test_add_func("/ircv3/parser/duplicate-tags",
    - test_purple_ircv3_duplicate_tags);
    - g_test_add_func("/ircv3/parser/vendor-tags-are-namespaced",
    - test_purple_ircv3_vendor_tags_are_namespaced);
    -
    - g_test_add_func("/ircv3/parser/special-mode-1",
    - test_purple_ircv3_special_mode_1);
    - g_test_add_func("/ircv3/parser/special-mode-2",
    - test_purple_ircv3_special_mode_2);
    -
    - /* These tests are the examples from the message-tags specification,
    - * https://ircv3.net/specs/extensions/message-tags.html.
    - */
    - g_test_add_func("/ircv3/parser/message-tags/none",
    - test_purple_ircv3_parser_message_tags_none);
    - g_test_add_func("/ircv3/parser/message-tags/3-tags",
    - test_purple_ircv3_parser_message_tags_3_tags);
    - g_test_add_func("/ircv3/parser/message-tags/client-only",
    - test_purple_ircv3_parser_message_tags_client_only);
    - g_test_add_func("/ircv3/parser/message-tags/labeled-message",
    - test_purple_ircv3_parser_message_tags_labeled_response);
    -
    - return g_test_run();
    -}
    --- a/libpurple/protocols/ircv3/tests/test_ircv3_source.c Thu Mar 21 22:20:50 2024 -0500
    +++ /dev/null Thu Jan 01 00:00:00 1970 +0000
    @@ -1,158 +0,0 @@
    -/*
    - * Purple - Internet Messaging Library
    - * Copyright (C) Pidgin Developers <devel@pidgin.im>
    - *
    - * 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 <purple.h>
    -
    -#include "../purpleircv3source.h"
    -
    -/******************************************************************************
    - * Tests
    - *****************************************************************************/
    -static void
    -test_ircv3_source_parse_return_address_required(void) {
    - if(g_test_subprocess()) {
    - purple_ircv3_source_parse("pidgy", NULL, NULL, NULL);
    - }
    -
    - g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_DEFAULT);
    - g_test_trap_assert_stderr("*nick != NULL || user != NULL || host != NULL' failed*");
    -}
    -
    -static void
    -test_ircv3_source_parse_nick(void) {
    - char *nick = NULL;
    - char *user = NULL;
    - char *host = NULL;
    -
    - /* Once to make sure that user and host are null. */
    - purple_ircv3_source_parse("pidgy", &nick, &user, &host);
    - g_assert_cmpstr(nick, ==, "pidgy");
    - g_clear_pointer(&nick, g_free);
    -
    - g_assert_null(user);
    - g_assert_null(host);
    -
    - /* Again to make sure it works without user and host. */
    - purple_ircv3_source_parse("pidgy", &nick, NULL, NULL);
    - g_assert_cmpstr(nick, ==, "pidgy");
    - g_clear_pointer(&nick, g_free);
    -}
    -
    -static void
    -test_ircv3_source_parse_user(void) {
    - char *nick = NULL;
    - char *user = NULL;
    - char *host = NULL;
    -
    - /* Once to make sure host is null. */
    - purple_ircv3_source_parse("pidgy!~u", &nick, &user, &host);
    - g_assert_cmpstr(nick, ==, "pidgy");
    - g_clear_pointer(&nick, g_free);
    -
    - g_assert_cmpstr(user, ==, "~u");
    - g_clear_pointer(&user, g_free);
    -
    - g_assert_null(host);
    -
    - /* Again to make sure nick and host aren't required. */
    - purple_ircv3_source_parse("pidgy!~u", NULL, &user, NULL);
    - g_assert_cmpstr(user, ==, "~u");
    - g_clear_pointer(&user, g_free);
    -}
    -
    -static void
    -test_ircv3_source_parse_host(void) {
    - char *nick = NULL;
    - char *user = NULL;
    - char *host = NULL;
    -
    - /* Once to make sure everything works. */
    - purple_ircv3_source_parse("pidgy!~u@53unc8n42i868.irc", &nick, &user,
    - &host);
    - g_assert_cmpstr(nick, ==, "pidgy");
    - g_clear_pointer(&nick, g_free);
    -
    - g_assert_cmpstr(user, ==, "~u");
    - g_clear_pointer(&user, g_free);
    -
    - g_assert_cmpstr(host, ==, "53unc8n42i868.irc");
    - g_clear_pointer(&host, g_free);
    -
    - /* Again to make sure nick and host aren't required. */
    - purple_ircv3_source_parse("pidgy!~u@53unc8n42i868.irc", NULL, NULL, &host);
    - g_assert_cmpstr(host, ==, "53unc8n42i868.irc");
    - g_clear_pointer(&host, g_free);
    -}
    -
    -static void
    -test_ircv3_source_parse_baddies(void) {
    - char *nick = NULL;
    - char *user = NULL;
    - char *server = NULL;
    -
    - purple_ircv3_source_parse("\n", &nick, NULL, NULL);
    - g_assert_null(nick);
    -
    - purple_ircv3_source_parse("a\nb", &nick, NULL, NULL);
    - g_assert_null(nick);
    -
    - purple_ircv3_source_parse("\r", &nick, NULL, NULL);
    - g_assert_null(nick);
    -
    - purple_ircv3_source_parse("c\rd", &nick, NULL, NULL);
    - g_assert_null(nick);
    -
    - purple_ircv3_source_parse(" ", &nick, NULL, NULL);
    - g_assert_null(nick);
    -
    - purple_ircv3_source_parse("e f", &nick, NULL, NULL);
    - g_assert_null(nick);
    -
    - purple_ircv3_source_parse("nick@foo!user@server", &nick, &user, &server);
    - g_assert_cmpstr(nick, ==, "nick@foo");
    - g_clear_pointer(&nick, g_free);
    - g_assert_cmpstr(user, ==, "user");
    - g_clear_pointer(&user, g_free);
    - g_assert_cmpstr(server, ==, "server");
    - g_clear_pointer(&server, g_free);
    -
    - purple_ircv3_source_parse("nick!user@server!foo", &nick, &user, &server);
    - g_assert_cmpstr(nick, ==, "nick!user@server!foo");
    - g_assert_null(user);
    - g_assert_null(server);
    -}
    -
    -/******************************************************************************
    - * Main
    - *****************************************************************************/
    -int
    -main(int argc, char *argv[]) {
    - g_test_init(&argc, &argv, NULL);
    -
    - g_test_add_func("/ircv3/source/parse/return-address-required",
    - test_ircv3_source_parse_return_address_required);
    - g_test_add_func("/ircv3/source/parse/nick", test_ircv3_source_parse_nick);
    - g_test_add_func("/ircv3/source/parse/user", test_ircv3_source_parse_user);
    - g_test_add_func("/ircv3/source/parse/host", test_ircv3_source_parse_host);
    - g_test_add_func("/ircv3/source/parse/baddies",
    - test_ircv3_source_parse_baddies);
    -
    - return g_test_run();
    -}
    \ No newline at end of file
    --- a/libpurple/protocols/meson.build Thu Mar 21 22:20:50 2024 -0500
    +++ b/libpurple/protocols/meson.build Mon Mar 25 21:43:28 2024 -0500
    @@ -1,5 +1,3 @@
    subdir('bonjour')
    -subdir('demo')
    subdir('gg')
    -subdir('ircv3')
    subdir('jabber')
    --- a/po/POTFILES.in Thu Mar 21 22:20:50 2024 -0500
    +++ b/po/POTFILES.in Mon Mar 25 21:43:28 2024 -0500
    @@ -72,9 +72,6 @@
    libpurple/protocols/bonjour/parser.c
    libpurple/protocols/bonjour/xmpp.c
    libpurple/protocols.c
    -libpurple/protocols/demo/purpledemocontacts.c
    -libpurple/protocols/demo/purpledemoplugin.c
    -libpurple/protocols/demo/purpledemoprotocol.c
    libpurple/protocols/gg/avatar.c
    libpurple/protocols/gg/blist.c
    libpurple/protocols/gg/chat.c
    @@ -316,6 +313,9 @@
    pidgin/win32/winpidgin.c
    protocols/bonjour/purplebonjourcore.c
    protocols/bonjour/purplebonjourprotocol.c
    +protocols/demo/purpledemocontacts.c
    +protocols/demo/purpledemoplugin.c
    +protocols/demo/purpledemoprotocol.c
    protocols/xmpp/purplexmppconnection.c
    protocols/xmpp/purplexmppcore.c
    protocols/xmpp/purplexmppprotocol.c
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/meson.build Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,37 @@
    +DEMO_SOURCES = [
    + 'purpledemoconnection.c',
    + 'purpledemoconnection.h',
    + 'purpledemocontacts.c',
    + 'purpledemocontacts.h',
    + 'purpledemoplugin.c',
    + 'purpledemoplugin.h',
    + 'purpledemoprotocol.c',
    + 'purpledemoprotocol.h',
    + 'purpledemoprotocolactions.c',
    + 'purpledemoprotocolactions.h',
    + 'purpledemoprotocolclient.c',
    + 'purpledemoprotocolclient.h',
    + 'purpledemoprotocolcontacts.c',
    + 'purpledemoprotocolcontacts.h',
    + 'purpledemoprotocolconversation.c',
    + 'purpledemoprotocolconversation.h',
    + 'purpledemoprotocolmedia.c',
    + 'purpledemoprotocolmedia.h',
    +]
    +
    +if DYNAMIC_DEMO
    + demo_resources = gnome.compile_resources('purpledemoresource',
    + 'resources/purpledemo.gresource.xml',
    + source_dir : 'resources',
    + c_name : 'purple_demo')
    + DEMO_SOURCES += demo_resources
    +
    + shared_library('demo', DEMO_SOURCES,
    + c_args : ['-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Purple-Demo"'],
    + gnu_symbol_visibility : 'hidden',
    + dependencies : [glib, json, libpurple_dep],
    + install : true,
    + install_dir : PURPLE_PLUGINDIR)
    +
    + devenv.append('PURPLE_PLUGIN_PATH', meson.current_build_dir())
    +endif
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoconnection.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,80 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * This program is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU General Public License as published by
    + * the Free Software Foundation; either version 2 of the License, or
    + * (at your option) any later version.
    + *
    + * This program 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this program; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib/gi18n-lib.h>
    +
    +#include "purpledemoconnection.h"
    +
    +#include "purpledemocontacts.h"
    +
    +struct _PurpleDemoConnection {
    + PurpleConnection parent;
    +};
    +
    +G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleDemoConnection, purple_demo_connection,
    + PURPLE_TYPE_CONNECTION, G_TYPE_FLAG_FINAL, {})
    +
    +/******************************************************************************
    + * PurpleConnection Implementation
    + *****************************************************************************/
    +static gboolean
    +purple_demo_connection_connect(PurpleConnection *connection,
    + G_GNUC_UNUSED GError **error)
    +{
    + PurpleAccount *account = purple_connection_get_account(connection);
    +
    + purple_connection_set_state(connection, PURPLE_CONNECTION_STATE_CONNECTED);
    +
    + purple_demo_contacts_load(account);
    +
    + return TRUE;
    +}
    +
    +static gboolean
    +purple_demo_connection_disconnect(G_GNUC_UNUSED PurpleConnection *connection,
    + G_GNUC_UNUSED GError **error)
    +{
    + return TRUE;
    +}
    +
    +/******************************************************************************
    + * GObject Implementation
    + *****************************************************************************/
    +static void
    +purple_demo_connection_init(G_GNUC_UNUSED PurpleDemoConnection *connection) {
    +}
    +
    +static void
    +purple_demo_connection_class_finalize(G_GNUC_UNUSED PurpleDemoConnectionClass *klass) {
    +}
    +
    +static void
    +purple_demo_connection_class_init(PurpleDemoConnectionClass *klass) {
    + PurpleConnectionClass *connection_class = PURPLE_CONNECTION_CLASS(klass);
    +
    + connection_class->connect = purple_demo_connection_connect;
    + connection_class->disconnect = purple_demo_connection_disconnect;
    +}
    +
    +/******************************************************************************
    + * Internal API
    + *****************************************************************************/
    +void
    +purple_demo_connection_register(GPluginNativePlugin *plugin) {
    + purple_demo_connection_register_type(G_TYPE_MODULE(plugin));
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoconnection.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,36 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/>.
    + */
    +
    +#ifndef PURPLE_DEMO_CONNECTION_H
    +#define PURPLE_DEMO_CONNECTION_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +G_BEGIN_DECLS
    +
    +#define PURPLE_DEMO_TYPE_CONNECTION (purple_demo_connection_get_type())
    +G_DECLARE_FINAL_TYPE(PurpleDemoConnection, purple_demo_connection, PURPLE_DEMO,
    + CONNECTION, PurpleConnection)
    +
    +G_GNUC_INTERNAL void purple_demo_connection_register(GPluginNativePlugin *plugin);
    +
    +G_BEGIN_DECLS
    +
    +#endif /* PURPLE_DEMO_CONNECTION_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemocontacts.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,357 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 <json-glib/json-glib.h>
    +
    +#include "purpledemocontacts.h"
    +
    +#include "purpledemoresource.h"
    +
    +/******************************************************************************
    + * Helpers
    + *****************************************************************************/
    +static void
    +purple_demo_contacts_load_contact_icon(PurpleContactInfo *info,
    + const char *name)
    +{
    + PurpleAvatar *avatar = NULL;
    + GError *error = NULL;
    + char *path = NULL;
    +
    + path = g_strdup_printf("/im/pidgin/purple/demo/buddy_icons/%s.png", name);
    + avatar = purple_avatar_new_from_resource(path, &error);
    +
    + if(error != NULL) {
    + g_message("Failed to load find an icon for %s: %s", path,
    + error->message);
    +
    + g_free(path);
    + g_clear_error(&error);
    +
    + return;
    + }
    +
    + g_free(path);
    +
    + if(PURPLE_IS_AVATAR(avatar)) {
    + purple_contact_info_set_avatar(info, avatar);
    +
    + g_clear_object(&avatar);
    + }
    +}
    +
    +static void
    +purple_demo_contacts_load_contact_person(JsonObject *person_object,
    + PurpleContactInfo *info)
    +{
    + PurplePerson *person = NULL;
    + gboolean new_person = FALSE;
    + const char *value = NULL;
    +
    + /* If the person has an id, grab it so we can use it when constructing the
    + * person object.
    + */
    + if(json_object_has_member(person_object, "id")) {
    + value = json_object_get_string_member(person_object, "id");
    + }
    +
    + /* See if the contact has an existing person. */
    + person = purple_contact_info_get_person(info);
    + if(PURPLE_IS_PERSON(person)) {
    + const char *existing_id = NULL;
    +
    + /* If the existing person's id doesn't match the new one, NULL out
    + * person so it'll be recreated with the new id.
    + */
    + existing_id = purple_person_get_id(person);
    + if(!purple_strequal(existing_id, value)) {
    + person = NULL;
    + }
    + }
    +
    + /* If the person didn't exist or it had a different id, create a new person
    + * with the id.
    + */
    + if(!PURPLE_IS_PERSON(person)) {
    + person = g_object_new(PURPLE_TYPE_PERSON, "id", value, NULL);
    + new_person = TRUE;
    + }
    +
    + /* Alias */
    + if(json_object_has_member(person_object, "alias")) {
    + value = json_object_get_string_member(person_object, "alias");
    + if(!purple_strempty(value)) {
    + purple_person_set_alias(person, value);
    + }
    + }
    +
    + /* Create the link between the person and the contact info. */
    + if(new_person) {
    + purple_person_add_contact_info(person, info);
    + purple_contact_info_set_person(info, person);
    +
    + g_clear_object(&person);
    + }
    +}
    +
    +static void
    +purple_demo_contacts_load_contact_presence(JsonObject *presence_object,
    + PurpleContactInfo *info)
    +{
    + PurplePresence *presence = NULL;
    + const gchar *value = NULL;
    +
    + presence = purple_contact_info_get_presence(info);
    +
    + /* Emoji */
    + if(json_object_has_member(presence_object, "emoji")) {
    + value = json_object_get_string_member(presence_object, "emoji");
    + if(!purple_strempty(value)) {
    + purple_presence_set_emoji(presence, value);
    + }
    + }
    +
    + /* Idle Time */
    + if(json_object_has_member(presence_object, "idle")) {
    + GDateTime *now = NULL;
    + GDateTime *idle_since = NULL;
    + gint64 ivalue = 0;
    +
    + ivalue = json_object_get_int_member(presence_object, "idle");
    +
    + now = g_date_time_new_now_local();
    + idle_since = g_date_time_add_minutes(now, -1 * ivalue);
    +
    + purple_presence_set_idle(presence, TRUE, idle_since);
    +
    + g_date_time_unref(idle_since);
    + g_date_time_unref(now);
    + }
    +
    + /* Message */
    + if(json_object_has_member(presence_object, "message")) {
    + value = json_object_get_string_member(presence_object, "message");
    + if(!purple_strempty(value)) {
    + purple_presence_set_message(presence, value);
    + }
    + }
    +
    + /* Mobile */
    + if(json_object_has_member(presence_object, "mobile")) {
    + gboolean bvalue = FALSE;
    + bvalue = json_object_get_boolean_member(presence_object, "mobile");
    + purple_presence_set_mobile(presence, bvalue);
    + }
    +
    + /* Primitive */
    + if(json_object_has_member(presence_object, "primitive")) {
    + PurplePresencePrimitive primitive = PURPLE_PRESENCE_PRIMITIVE_OFFLINE;
    +
    + value = json_object_get_string_member(presence_object, "primitive");
    + if(!purple_strempty(value)) {
    + GEnumClass *enum_class = NULL;
    + GEnumValue *enum_value = NULL;
    +
    + enum_class = g_type_class_ref(PURPLE_TYPE_PRESENCE_PRIMITIVE);
    + enum_value = g_enum_get_value_by_nick(enum_class, value);
    +
    + if(enum_value != NULL) {
    + primitive = enum_value->value;
    + }
    +
    + g_type_class_unref(enum_class);
    + }
    +
    + purple_presence_set_primitive(presence, primitive);
    + }
    +}
    +
    +static void
    +purple_demo_contacts_load_contact(PurpleContactManager *manager,
    + PurpleAccount *account,
    + JsonObject *contact_object)
    +{
    + PurpleContact *contact = NULL;
    + PurpleContactInfo *info = NULL;
    + gboolean new_contact = FALSE;
    + const char *id = NULL;
    + const char *value = NULL;
    +
    + /* If we have an id, grab so we can create the contact with it. */
    + if(json_object_has_member(contact_object, "id")) {
    + id = json_object_get_string_member(contact_object, "id");
    + }
    +
    + /* Look for an existing contact before creating a new one. This stops us
    + * from getting multiples when we trigger connection errors.
    + */
    + if(!purple_strempty(id)) {
    + contact = purple_contact_manager_find_with_id(manager, account, id);
    + }
    +
    + /* If we didn't find an existing contact, create it now with the provided
    + * id.
    + */
    + if(!PURPLE_IS_CONTACT(contact)) {
    + contact = purple_contact_new(account, id);
    + new_contact = TRUE;
    + }
    +
    + info = PURPLE_CONTACT_INFO(contact);
    +
    + /* Alias */
    + if(json_object_has_member(contact_object, "alias")) {
    + value = json_object_get_string_member(contact_object, "alias");
    + if(!purple_strempty(value)) {
    + purple_contact_info_set_alias(info, value);
    + }
    + }
    +
    + /* Color */
    + if(json_object_has_member(contact_object, "color")) {
    + value = json_object_get_string_member(contact_object, "color");
    + if(!purple_strempty(value)) {
    + purple_contact_info_set_color(info, value);
    + }
    + }
    +
    + /* Display Name */
    + if(json_object_has_member(contact_object, "display_name")) {
    + value = json_object_get_string_member(contact_object, "display_name");
    + if(!purple_strempty(value)) {
    + purple_contact_info_set_display_name(info, value);
    + }
    + }
    +
    + /* Username */
    + if(json_object_has_member(contact_object, "username")) {
    + value = json_object_get_string_member(contact_object, "username");
    + if(!purple_strempty(value)) {
    + purple_contact_info_set_username(info, value);
    +
    + purple_demo_contacts_load_contact_icon(info, value);
    + }
    + }
    +
    + /* Load the profile if it exists. */
    + if(json_object_has_member(contact_object, "profile")) {
    + value = json_object_get_string_member(contact_object, "profile");
    + if(!purple_strempty(value)) {
    + g_object_set_data_full(G_OBJECT(info), "demo-profile",
    + g_strdup(value), g_free);
    + }
    + }
    +
    + /* Load the tags. */
    + if(json_object_has_member(contact_object, "tags")) {
    + PurpleTags *tags = purple_contact_info_get_tags(info);
    + JsonArray *array = NULL;
    + GList *elements = NULL;
    +
    + array = json_object_get_array_member(contact_object, "tags");
    + elements = json_array_get_elements(array);
    + while(elements != NULL) {
    + JsonNode *tag_node = elements->data;
    + const char *tag = json_node_get_string(tag_node);
    +
    + purple_tags_add(tags, tag);
    +
    + elements = g_list_delete_link(elements, elements);
    + }
    + }
    +
    + /* Load the person. */
    + if(json_object_has_member(contact_object, "person")) {
    + JsonObject *person_object = NULL;
    +
    + person_object = json_object_get_object_member(contact_object,
    + "person");
    +
    + purple_demo_contacts_load_contact_person(person_object, info);
    + }
    +
    + /* Load the presence. */
    + if(json_object_has_member(contact_object, "presence")) {
    + JsonObject *presence_object = NULL;
    +
    + presence_object = json_object_get_object_member(contact_object,
    + "presence");
    +
    + purple_demo_contacts_load_contact_presence(presence_object, info);
    + }
    +
    + /* Finally add the contact to the contact manager if it's new. */
    + if(new_contact) {
    + purple_contact_manager_add(manager, contact);
    + }
    +
    + g_clear_object(&contact);
    +}
    +
    +/******************************************************************************
    + * Local Exports
    + *****************************************************************************/
    +void
    +purple_demo_contacts_load(PurpleAccount *account) {
    + PurpleContactManager *manager = NULL;
    + GError *error = NULL;
    + GInputStream *istream = NULL;
    + GList *contacts = NULL;
    + JsonArray *contacts_array = NULL;
    + JsonNode *root_node = NULL;
    + JsonParser *parser = NULL;
    +
    + /* get a stream to the contacts.json resource */
    + istream = g_resource_open_stream(purple_demo_get_resource(),
    + "/im/pidgin/purple/demo/contacts.json",
    + G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
    +
    + /* create our parser */
    + parser = json_parser_new();
    +
    + if(!json_parser_load_from_stream(parser, istream, NULL, &error)) {
    + g_critical("%s", error->message);
    + g_clear_error(&error);
    + return;
    + }
    +
    + root_node = json_parser_get_root(parser);
    +
    + manager = purple_contact_manager_get_default();
    +
    + /* Load the contacts! */
    + contacts_array = json_node_get_array(root_node);
    + contacts = json_array_get_elements(contacts_array);
    + while(contacts != NULL) {
    + JsonNode *contact_node = NULL;
    + JsonObject *contact_object = NULL;
    +
    + contact_node = contacts->data;
    + contact_object = json_node_get_object(contact_node);
    +
    + purple_demo_contacts_load_contact(manager, account, contact_object);
    +
    + contacts = g_list_delete_link(contacts, contacts);
    + }
    +
    + /* Clean up everything else... */
    + g_clear_object(&parser);
    +
    + g_input_stream_close(istream, NULL, NULL);
    + g_object_unref(istream);
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemocontacts.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,32 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/>.
    + */
    +
    +#ifndef PURPLE_DEMO_CONTACTS_H
    +#define PURPLE_DEMO_CONTACTS_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +G_BEGIN_DECLS
    +
    +G_GNUC_INTERNAL void purple_demo_contacts_load(PurpleAccount *account);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_DEMO_CONTACTS_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoplugin.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,113 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/gi18n-lib.h>
    +
    +#include <gplugin.h>
    +#include <gplugin-native.h>
    +
    +#include <purple.h>
    +
    +#include "purpledemoplugin.h"
    +
    +#include "purpledemoconnection.h"
    +#include "purpledemoprotocol.h"
    +
    +/******************************************************************************
    + * Globals
    + *****************************************************************************/
    +static PurpleProtocol *demo_protocol = NULL;
    +
    +/******************************************************************************
    + * GPlugin Implementation
    + *****************************************************************************/
    +static GPluginPluginInfo *
    +purple_demo_plugin_query(G_GNUC_UNUSED GError **error) {
    + const gchar *authors[] = {
    + "Pidgin Developers <devel@pidgin.im>",
    + NULL
    + };
    +
    + return purple_plugin_info_new(
    + "id", "prpl-demo",
    + "name", "Demo Protocol Plugin",
    + "authors", authors,
    + "version", PURPLE_VERSION,
    + "category", N_("Protocol"),
    + "summary", N_("A protocol plugin used for demos."),
    + "description", N_("A protocol plugin that helps to demonstrate "
    + "features of libpurple and clients."),
    + "website", PURPLE_WEBSITE,
    + "abi-version", PURPLE_ABI_VERSION,
    + "flags", PURPLE_PLUGIN_INFO_FLAGS_INTERNAL |
    + PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD,
    + NULL
    + );
    +}
    +
    +static gboolean
    +purple_demo_plugin_load(GPluginPlugin *plugin, GError **error) {
    + PurpleProtocolManager *manager = NULL;
    +
    + if(PURPLE_IS_PROTOCOL(demo_protocol)) {
    + g_set_error_literal(error, PURPLE_DEMO_DOMAIN, 0,
    + "plugin was not cleaned up properly");
    +
    + return FALSE;
    + }
    +
    + purple_demo_connection_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    + purple_demo_protocol_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    +
    + manager = purple_protocol_manager_get_default();
    +
    + demo_protocol = purple_demo_protocol_new();
    + if(!purple_protocol_manager_register(manager, demo_protocol, error)) {
    + g_clear_object(&demo_protocol);
    +
    + return FALSE;
    + }
    +
    + return TRUE;
    +}
    +
    +static gboolean
    +purple_demo_plugin_unload(G_GNUC_UNUSED GPluginPlugin *plugin,
    + G_GNUC_UNUSED gboolean shutdown,
    + GError **error)
    +{
    + PurpleProtocolManager *manager = purple_protocol_manager_get_default();
    +
    + if(!PURPLE_IS_PROTOCOL(demo_protocol)) {
    + g_set_error_literal(error, PURPLE_DEMO_DOMAIN, 0,
    + "plugin was not setup properly");
    +
    + return FALSE;
    + }
    +
    + if(!purple_protocol_manager_unregister(manager, demo_protocol, error)) {
    + return FALSE;
    + }
    +
    + g_clear_object(&demo_protocol);
    +
    + return TRUE;
    +}
    +
    +GPLUGIN_NATIVE_PLUGIN_DECLARE(purple_demo_plugin)
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoplugin.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,29 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/>.
    + */
    +#ifndef PURPLE_DEMO_PLUGIN_H
    +#define PURPLE_DEMO_PLUGIN_H
    +
    +#include <glib.h>
    +
    +G_BEGIN_DECLS
    +
    +#define PURPLE_DEMO_DOMAIN (g_quark_from_static_string("demo-plugin"))
    +
    +G_BEGIN_DECLS
    +
    +#endif /* PURPLE_DEMO_PLUGIN_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocol.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,146 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 "purpledemoprotocol.h"
    +
    +#include "purpledemoconnection.h"
    +#include "purpledemoprotocolactions.h"
    +#include "purpledemoprotocolclient.h"
    +#include "purpledemoprotocolcontacts.h"
    +#include "purpledemoprotocolconversation.h"
    +#include "purpledemoprotocolmedia.h"
    +
    +struct _PurpleDemoProtocol {
    + PurpleProtocol parent;
    +};
    +
    +/******************************************************************************
    + * PurpleProtocol Implementation
    + *****************************************************************************/
    +static PurpleConnection *
    +purple_demo_protocol_create_connection(PurpleProtocol *protocol,
    + PurpleAccount *account,
    + const char *password,
    + G_GNUC_UNUSED GError **error)
    +{
    + g_return_val_if_fail(PURPLE_IS_PROTOCOL(protocol), NULL);
    + g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
    +
    + return g_object_new(
    + PURPLE_DEMO_TYPE_CONNECTION,
    + "protocol", protocol,
    + "account", account,
    + "password", password,
    + NULL);
    +
    +}
    +
    +static GList *
    +purple_demo_protocol_status_types(G_GNUC_UNUSED PurpleProtocol *protocol,
    + G_GNUC_UNUSED PurpleAccount *account)
    +{
    + PurpleStatusType *type = NULL;
    + GList *status_types = NULL;
    +
    + type = purple_status_type_new_with_attrs(
    + PURPLE_STATUS_AVAILABLE, "available", NULL,
    + TRUE, TRUE, FALSE,
    + "message", _("Message"), purple_value_new(G_TYPE_STRING),
    + NULL);
    + status_types = g_list_append(status_types, type);
    +
    + type = purple_status_type_new_with_attrs(
    + PURPLE_STATUS_AWAY, "away", NULL,
    + TRUE, TRUE, FALSE,
    + "message", _("Message"), purple_value_new(G_TYPE_STRING),
    + NULL);
    + status_types = g_list_append(status_types, type);
    +
    + type = purple_status_type_new_with_attrs(
    + PURPLE_STATUS_EXTENDED_AWAY, "extended_away", NULL,
    + TRUE, TRUE, FALSE,
    + "message", _("Message"), purple_value_new(G_TYPE_STRING),
    + NULL);
    + status_types = g_list_append(status_types, type);
    +
    + type = purple_status_type_new_full(
    + PURPLE_STATUS_OFFLINE, NULL, NULL,
    + TRUE, TRUE, FALSE);
    + status_types = g_list_append(status_types, type);
    +
    + return status_types;
    +}
    +
    +/******************************************************************************
    + * GObject Implementation
    + *****************************************************************************/
    +G_DEFINE_DYNAMIC_TYPE_EXTENDED(
    + PurpleDemoProtocol,
    + purple_demo_protocol,
    + PURPLE_TYPE_PROTOCOL,
    + G_TYPE_FLAG_FINAL,
    + G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_ACTIONS,
    + purple_demo_protocol_actions_init)
    + G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CLIENT,
    + purple_demo_protocol_client_init)
    + G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONTACTS,
    + purple_demo_protocol_contacts_init)
    + G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONVERSATION,
    + purple_demo_protocol_conversation_init)
    + G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_MEDIA,
    + purple_demo_protocol_media_init))
    +
    +static void
    +purple_demo_protocol_init(G_GNUC_UNUSED PurpleDemoProtocol *protocol) {
    +}
    +
    +static void
    +purple_demo_protocol_class_finalize(G_GNUC_UNUSED PurpleDemoProtocolClass *klass) {
    +}
    +
    +static void
    +purple_demo_protocol_class_init(PurpleDemoProtocolClass *klass) {
    + PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass);
    +
    + protocol_class->status_types = purple_demo_protocol_status_types;
    + protocol_class->create_connection = purple_demo_protocol_create_connection;
    +}
    +
    +/******************************************************************************
    + * Local Exports
    + *****************************************************************************/
    +void
    +purple_demo_protocol_register(GPluginNativePlugin *plugin) {
    + purple_demo_protocol_register_type(G_TYPE_MODULE(plugin));
    +}
    +
    +PurpleProtocol *
    +purple_demo_protocol_new(void) {
    + return g_object_new(
    + PURPLE_DEMO_TYPE_PROTOCOL,
    + "id", "prpl-demo",
    + "name", "Demo",
    + "description", "A protocol plugin with static data to be used in "
    + "screen shots.",
    + "icon-name", "im-purple-demo",
    + "icon-resource-path", "/im/pidgin/purple/demo/icons",
    + "options", OPT_PROTO_NO_PASSWORD,
    + NULL);
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocol.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,41 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/>.
    + */
    +
    +#ifndef PURPLE_DEMO_PROTOCOL_H
    +#define PURPLE_DEMO_PROTOCOL_H
    +
    +#include <glib.h>
    +#include <glib-object.h>
    +
    +#include <gplugin-native.h>
    +
    +#include <purple.h>
    +
    +G_BEGIN_DECLS
    +
    +#define PURPLE_DEMO_TYPE_PROTOCOL (purple_demo_protocol_get_type())
    +G_DECLARE_FINAL_TYPE(PurpleDemoProtocol, purple_demo_protocol, PURPLE_DEMO,
    + PROTOCOL, PurpleProtocol)
    +
    +G_GNUC_INTERNAL void purple_demo_protocol_register(GPluginNativePlugin *plugin);
    +
    +G_GNUC_INTERNAL PurpleProtocol *purple_demo_protocol_new(void);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_DEMO_PROTOCOL_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolactions.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,950 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 "purpledemoprotocol.h"
    +#include "purpledemoprotocolactions.h"
    +#include "purpledemoresource.h"
    +
    +/******************************************************************************
    + * Connection failure action implementations
    + *****************************************************************************/
    +#define REAPER_BUDDY_NAME ("Gary")
    +#define DEFAULT_REAP_TIME (5) /* seconds */
    +#define FATAL_TICK_STR N_("Reaping connection in %d second...")
    +#define FATAL_TICK_PLURAL_STR N_("Reaping connection in %d seconds...")
    +#define FATAL_DISCONNECT_STR N_("%s reaped the connection")
    +#define TEMPORARY_TICK_STR N_("Pruning connection in %d second...")
    +#define TEMPORARY_TICK_PLURAL_STR N_("Pruning connection in %d seconds...")
    +#define TEMPORARY_DISCONNECT_STR N_("%s pruned the connection")
    +
    +static gboolean
    +purple_demo_protocol_failure_tick(gpointer data,
    + PurpleConnectionError error_code,
    + const gchar *tick_str,
    + const gchar *tick_plural_str,
    + const gchar *disconnect_str)
    +{
    + PurpleConnection *connection = PURPLE_CONNECTION(data);
    + PurpleAccount *account = purple_connection_get_account(connection);
    + gchar *message = NULL;
    + gint timeout = 0;
    +
    + timeout = GPOINTER_TO_INT(g_object_steal_data(G_OBJECT(connection),
    + "reaping-time"));
    + timeout--;
    + if(timeout > 0) {
    + PurpleContact *contact = NULL;
    + PurpleContactManager *manager = NULL;
    +
    + g_object_set_data(G_OBJECT(connection), "reaping-time",
    + GINT_TO_POINTER(timeout));
    +
    + manager = purple_contact_manager_get_default();
    + contact = purple_contact_manager_find_with_username(manager, account,
    + REAPER_BUDDY_NAME);
    +
    + if(PURPLE_IS_CONTACT(contact)) {
    + PurpleContactInfo *info = PURPLE_CONTACT_INFO(contact);
    + PurplePresence *presence = purple_contact_info_get_presence(info);
    + const char *format = ngettext(tick_str, tick_plural_str, timeout);
    +
    + message = g_strdup_printf(format, timeout);
    + purple_presence_set_message(presence, message);
    + g_free(message);
    + }
    +
    + return G_SOURCE_CONTINUE;
    + }
    +
    + message = g_strdup_printf(_(disconnect_str), REAPER_BUDDY_NAME);
    + purple_connection_error(connection, error_code, message);
    + g_free(message);
    +
    + return G_SOURCE_REMOVE;
    +}
    +
    +static gboolean
    +purple_demo_protocol_fatal_failure_cb(gpointer data) {
    + return purple_demo_protocol_failure_tick(data,
    + PURPLE_CONNECTION_ERROR_CUSTOM_FATAL,
    + FATAL_TICK_STR,
    + FATAL_TICK_PLURAL_STR,
    + FATAL_DISCONNECT_STR);
    +}
    +
    +static gboolean
    +purple_demo_protocol_temporary_failure_cb(gpointer data) {
    + return purple_demo_protocol_failure_tick(data,
    + PURPLE_CONNECTION_ERROR_CUSTOM_TEMPORARY,
    + TEMPORARY_TICK_STR,
    + TEMPORARY_TICK_PLURAL_STR,
    + TEMPORARY_DISCONNECT_STR);
    +}
    +
    +static void
    +purple_demo_protocol_failure_action_activate(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + const gchar *tick_str,
    + const gchar *tick_plural_str,
    + GSourceFunc cb)
    +{
    + PurpleAccountManager *account_manager = NULL;
    + PurpleAccount *account = NULL;
    + PurpleConnection *connection = NULL;
    + PurpleContact *contact = NULL;
    + PurpleContactManager *contact_manager = NULL;
    + const char *account_id = NULL;
    +
    + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    + g_critical("Demo failure action parameter is of incorrect type %s",
    + g_variant_get_type_string(parameter));
    + return;
    + }
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + account_manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(account_manager, account_id);
    + connection = purple_account_get_connection(account);
    +
    + /* Do nothing if disconnected, or already in process of reaping. */
    + if(!PURPLE_IS_CONNECTION(connection)) {
    + g_clear_object(&account);
    +
    + return;
    + }
    +
    + if(g_object_get_data(G_OBJECT(connection), "reaping-time")) {
    + g_clear_object(&account);
    +
    + return;
    + }
    +
    + /* Find the reaper. */
    + contact_manager = purple_contact_manager_get_default();
    + contact = purple_contact_manager_find_with_username(contact_manager,
    + account,
    + REAPER_BUDDY_NAME);
    +
    + if(PURPLE_IS_CONTACT(contact)) {
    + PurpleContactInfo *info = PURPLE_CONTACT_INFO(contact);
    + PurplePresence *presence = purple_contact_info_get_presence(info);
    + const char *format = NULL;
    + char *message = NULL;
    +
    + format = ngettext(tick_str, tick_plural_str, DEFAULT_REAP_TIME);
    + message = g_strdup_printf(format, DEFAULT_REAP_TIME);
    +
    + purple_presence_set_idle(presence, FALSE, NULL);
    + purple_presence_set_message(presence, message);
    + g_free(message);
    + }
    +
    + g_object_set_data(G_OBJECT(connection), "reaping-time",
    + GINT_TO_POINTER(DEFAULT_REAP_TIME));
    + g_timeout_add_seconds(1, cb, connection);
    +
    + g_clear_object(&account);
    +}
    +
    +static void
    +purple_demo_protocol_temporary_failure_action_activate(GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + purple_demo_protocol_failure_action_activate(action, parameter,
    + TEMPORARY_TICK_STR,
    + TEMPORARY_TICK_PLURAL_STR,
    + purple_demo_protocol_temporary_failure_cb);
    +}
    +
    +static void
    +purple_demo_protocol_fatal_failure_action_activate(GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + purple_demo_protocol_failure_action_activate(action, parameter,
    + FATAL_TICK_STR,
    + FATAL_TICK_PLURAL_STR,
    + purple_demo_protocol_fatal_failure_cb);
    +}
    +
    +/******************************************************************************
    + * Request API action implementations
    + *****************************************************************************/
    +
    +static void
    +purple_demo_protocol_request_input_ok_cb(G_GNUC_UNUSED gpointer data,
    + const char *value)
    +{
    + g_message(_("Successfully requested input from UI: %s"), value);
    +}
    +
    +static void
    +purple_demo_protocol_request_input_cancel_cb(G_GNUC_UNUSED gpointer data,
    + G_GNUC_UNUSED const char *value)
    +{
    + g_message(_("UI cancelled input request"));
    +}
    +
    +static void
    +purple_demo_protocol_request_input_activate(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + PurpleConnection *connection = NULL;
    + const gchar *account_id = NULL;
    + PurpleAccountManager *manager = NULL;
    + PurpleAccount *account = NULL;
    + static int form = 0;
    + gboolean multiline = FALSE, masked = FALSE;
    + char *secondary = NULL;
    +
    + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    + g_critical("Demo failure action parameter is of incorrect type %s",
    + g_variant_get_type_string(parameter));
    + return;
    + }
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(manager, account_id);
    + connection = purple_account_get_connection(account);
    + g_clear_object(&account);
    +
    + /* Alternate through all four combinations of {masked, multiline}. */
    + masked = form % 2 == 1;
    + multiline = (form / 2) % 2 == 1;
    + form++;
    + secondary = g_strdup_printf(_("The input will be %s %s."),
    + masked ? "masked" : "unmasked",
    + multiline ? "multiple lines" : "single line");
    +
    + purple_request_input(connection, _("Request Input Demo"),
    + _("Please input some text…"), secondary, _("default"),
    + multiline, masked, NULL,
    + _("OK"),
    + G_CALLBACK(purple_demo_protocol_request_input_ok_cb),
    + _("Cancel"),
    + G_CALLBACK(purple_demo_protocol_request_input_cancel_cb),
    + purple_request_cpar_from_connection(connection), NULL);
    +
    + g_free(secondary);
    +}
    +
    +static void
    +purple_demo_protocol_request_choice_ok_cb(G_GNUC_UNUSED gpointer data,
    + gpointer value)
    +{
    + const char *text = value;
    +
    + g_message(_("Successfully requested a choice from UI: %s"), text);
    +}
    +
    +static void
    +purple_demo_protocol_request_choice_cancel_cb(G_GNUC_UNUSED gpointer data,
    + G_GNUC_UNUSED gpointer value)
    +{
    + g_message(_("UI cancelled choice request"));
    +}
    +
    +static void
    +purple_demo_protocol_request_choice_activate(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + PurpleConnection *connection = NULL;
    + const gchar *account_id = NULL;
    + PurpleAccountManager *manager = NULL;
    + PurpleAccount *account = NULL;
    +
    + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    + g_critical("Demo failure action parameter is of incorrect type %s",
    + g_variant_get_type_string(parameter));
    + return;
    + }
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(manager, account_id);
    + connection = purple_account_get_connection(account);
    + g_clear_object(&account);
    +
    + purple_request_choice(connection, _("Request Choice Demo"),
    + _("Please pick an option…"), NULL, _("foo"),
    + _("OK"),
    + G_CALLBACK(purple_demo_protocol_request_choice_ok_cb),
    + _("Cancel"),
    + G_CALLBACK(purple_demo_protocol_request_choice_cancel_cb),
    + purple_request_cpar_from_connection(connection),
    + NULL, _("foo"), "foo", _("bar"), "bar",
    + _("baz"), "baz", NULL);
    +}
    +
    +static void
    +purple_demo_protocol_request_action_cb(G_GNUC_UNUSED gpointer data, int action)
    +{
    + g_message(_("Successfully requested an action from the UI: %d"), action);
    +}
    +
    +static void
    +purple_demo_protocol_request_action_activate(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + PurpleConnection *connection = NULL;
    + const gchar *account_id = NULL;
    + PurpleAccountManager *manager = NULL;
    + PurpleAccount *account = NULL;
    +
    + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    + g_critical("Demo failure action parameter is of incorrect type %s",
    + g_variant_get_type_string(parameter));
    + return;
    + }
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(manager, account_id);
    + connection = purple_account_get_connection(account);
    + g_clear_object(&account);
    +
    + purple_request_action(connection, _("Request Action Demo"),
    + _("Please choose an action…"), NULL, 1,
    + purple_request_cpar_from_connection(connection),
    + NULL, 3,
    + _("foo"), purple_demo_protocol_request_action_cb,
    + _("bar"), purple_demo_protocol_request_action_cb,
    + _("baz"), purple_demo_protocol_request_action_cb);
    +}
    +
    +typedef struct {
    + gint id;
    + gpointer ui_handle;
    +} PurpleDemoProtocolWaitData;
    +
    +static gboolean
    +purple_demo_protocol_request_wait_pulse_cb(gpointer data) {
    + PurpleDemoProtocolWaitData *wait = data;
    +
    + purple_request_wait_pulse(wait->ui_handle);
    +
    + return G_SOURCE_CONTINUE;
    +}
    +
    +static void
    +purple_demo_protocol_request_wait_cancel_cb(G_GNUC_UNUSED gpointer data) {
    + g_message(_("UI cancelled wait request"));
    +}
    +
    +static void
    +purple_demo_protocol_request_wait_close_cb(gpointer data) {
    + PurpleDemoProtocolWaitData *wait = data;
    +
    + g_source_remove(wait->id);
    + g_free(wait);
    +}
    +
    +static void
    +purple_demo_protocol_request_wait_activate(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + PurpleConnection *connection = NULL;
    + const gchar *account_id = NULL;
    + PurpleAccountManager *manager = NULL;
    + PurpleAccount *account = NULL;
    + PurpleDemoProtocolWaitData *wait = NULL;
    +
    + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    + g_critical("Demo failure action parameter is of incorrect type %s",
    + g_variant_get_type_string(parameter));
    + return;
    + }
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(manager, account_id);
    + connection = purple_account_get_connection(account);
    + g_clear_object(&account);
    +
    + wait = g_new0(PurpleDemoProtocolWaitData, 1);
    +
    + wait->ui_handle = purple_request_wait(connection, _("Request Wait Demo"),
    + _("Please wait…"), NULL, TRUE,
    + purple_demo_protocol_request_wait_cancel_cb,
    + purple_request_cpar_from_connection(connection),
    + wait);
    +
    + wait->id = g_timeout_add(250, purple_demo_protocol_request_wait_pulse_cb,
    + wait);
    +
    + purple_request_add_close_notify(wait->ui_handle,
    + purple_demo_protocol_request_wait_close_cb,
    + wait);
    +}
    +
    +static void
    +purple_demo_protocol_request_fields_ok_cb(G_GNUC_UNUSED gpointer data,
    + PurpleRequestPage *page)
    +{
    + PurpleAccount *account = NULL;
    + PurpleRequestFieldList *field = NULL;
    + GList *list = NULL;
    + const char *tmp = NULL;
    + GString *info = NULL;
    +
    + info = g_string_new(_("Basic group:\n"));
    +
    + g_string_append_printf(info, _("\tString: %s\n"),
    + purple_request_page_get_string(page, "string"));
    + g_string_append_printf(info, _("\tMultiline string: %s\n"),
    + purple_request_page_get_string(page,
    + "multiline-string"));
    + g_string_append_printf(info, _("\tMasked string: %s\n"),
    + purple_request_page_get_string(page,
    + "masked-string"));
    + g_string_append_printf(info, _("\tAlphanumeric string: %s\n"),
    + purple_request_page_get_string(page,
    + "alphanumeric"));
    + g_string_append_printf(info, _("\tEmail string: %s\n"),
    + purple_request_page_get_string(page, "email"));
    + g_string_append_printf(info, _("\tInteger: %d\n"),
    + purple_request_page_get_integer(page, "int"));
    + g_string_append_printf(info, _("\tBoolean: %s\n"),
    + purple_request_page_get_bool(page, "bool") ?
    + _("TRUE") : _("FALSE"));
    +
    + g_string_append(info, _("Multiple-choice group:\n"));
    +
    + tmp = (const char *)purple_request_page_get_choice(page, "choice");
    + g_string_append_printf(info, _("\tChoice: %s\n"), tmp);
    +
    + field = PURPLE_REQUEST_FIELD_LIST(purple_request_page_get_field(page,
    + "list"));
    + list = purple_request_field_list_get_selected(field);
    + if(list != NULL) {
    + tmp = (const char *)list->data;
    + } else {
    + tmp = _("(unset)");
    + }
    + g_string_append_printf(info, _("\tList: %s\n"), tmp);
    +
    + field = PURPLE_REQUEST_FIELD_LIST(purple_request_page_get_field(page,
    + "multilist"));
    + list = purple_request_field_list_get_selected(field);
    + g_string_append(info, _("\tMulti-list: ["));
    + while(list != NULL) {
    + tmp = (const char *)list->data;
    + g_string_append_printf(info, "%s%s", tmp,
    + list->next != NULL ? ", " : "");
    + list = list->next;
    + }
    + g_string_append(info, _("]\n"));
    +
    + g_string_append(info, _("Special group:\n"));
    +
    + account = purple_request_page_get_account(page, "account");
    + if(PURPLE_IS_ACCOUNT(account)) {
    + tmp = purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(account));
    + } else {
    + tmp = _("(unset)");
    + }
    + g_string_append_printf(info, _("\tAccount: %s\n"), tmp);
    +
    + g_message(_("Successfully requested fields:\n%s"), info->str);
    +
    + g_string_free(info, TRUE);
    +}
    +
    +static void
    +purple_demo_protocol_request_fields_cancel_cb(G_GNUC_UNUSED gpointer data,
    + G_GNUC_UNUSED PurpleRequestPage *page)
    +{
    + g_message(_("UI cancelled field request"));
    +}
    +
    +static void
    +purple_demo_protocol_request_fields_activate(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + PurpleConnection *connection = NULL;
    + const gchar *account_id = NULL;
    + PurpleAccountManager *manager = NULL;
    + PurpleAccount *account = NULL;
    + PurpleRequestPage *page = NULL;
    + PurpleRequestGroup *group = NULL;
    + PurpleRequestField *boolfield = NULL;
    + PurpleRequestField *field = NULL;
    + PurpleRequestFieldChoice *choice_field = NULL;
    + PurpleRequestFieldList *list_field = NULL;
    + GBytes *icon = NULL;
    + gconstpointer icon_data = NULL;
    + gsize icon_len = 0;
    +
    + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    + g_critical("Demo failure action parameter is of incorrect type %s",
    + g_variant_get_type_string(parameter));
    + return;
    + }
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(manager, account_id);
    + connection = purple_account_get_connection(account);
    +
    + page = purple_request_page_new();
    +
    + /* This group will contain basic fields. */
    + group = purple_request_group_new(_("Basic"));
    + purple_request_page_add_group(page, group);
    +
    + boolfield = purple_request_field_bool_new("bool", _("Sensitive?"), TRUE);
    + purple_request_field_set_tooltip(boolfield,
    + _("Allow modifying all fields."));
    + purple_request_group_add_field(group, boolfield);
    +
    + field = purple_request_field_label_new("basic-label",
    + _("This group contains basic fields"));
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    +
    + field = purple_request_field_string_new("string", _("A string"),
    + _("default"), FALSE);
    + purple_request_field_set_required(field, TRUE);
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    + field = purple_request_field_string_new("multiline-string",
    + _("A multiline string"),
    + _("default"), TRUE);
    + purple_request_group_add_field(group, field);
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + field = purple_request_field_string_new("masked-string",
    + _("A masked string"), _("default"),
    + FALSE);
    + purple_request_field_string_set_masked(PURPLE_REQUEST_FIELD_STRING(field),
    + TRUE);
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    + field = purple_request_field_string_new("alphanumeric",
    + _("An alphanumeric string"),
    + _("default"), FALSE);
    + purple_request_field_set_validator(field,
    + purple_request_field_alphanumeric_validator,
    + NULL, NULL);
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    + field = purple_request_field_string_new("email", _("An email"),
    + _("me@example.com"), FALSE);
    + purple_request_field_set_validator(field,
    + purple_request_field_email_validator,
    + NULL, NULL);
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    + field = purple_request_field_int_new("int", _("An integer"), 123, -42, 1337);
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    +
    + /* This group will contain fields with multiple options. */
    + group = purple_request_group_new(_("Multiple"));
    + purple_request_page_add_group(page, group);
    +
    + field = purple_request_field_label_new("multiple-label",
    + _("This group contains fields with multiple options"));
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    +
    + field = purple_request_field_choice_new("choice", _("A choice"), "foo");
    + choice_field = PURPLE_REQUEST_FIELD_CHOICE(field);
    + purple_request_field_choice_add(choice_field, _("foo"), "foo");
    + purple_request_field_choice_add(choice_field, _("bar"), "bar");
    + purple_request_field_choice_add(choice_field, _("baz"), "baz");
    + purple_request_field_choice_add(choice_field, _("quux"), "quux");
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    +
    + field = purple_request_field_list_new("list", _("A list"));
    + list_field = PURPLE_REQUEST_FIELD_LIST(field);
    + purple_request_field_list_add_icon(list_field, _("foo"), NULL, "foo");
    + purple_request_field_list_add_icon(list_field, _("bar"), NULL, "bar");
    + purple_request_field_list_add_icon(list_field, _("baz"), NULL, "baz");
    + purple_request_field_list_add_icon(list_field, _("quux"), NULL, "quux");
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    +
    + field = purple_request_field_list_new("multilist", _("A multi-select list"));
    + list_field = PURPLE_REQUEST_FIELD_LIST(field);
    + purple_request_field_list_set_multi_select(list_field, TRUE);
    + purple_request_field_list_add_icon(list_field, _("foo"), NULL, "foo");
    + purple_request_field_list_add_icon(list_field, _("bar"), NULL, "bar");
    + purple_request_field_list_add_icon(list_field, _("baz"), NULL, "baz");
    + purple_request_field_list_add_icon(list_field, _("quux"), NULL, "quux");
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    +
    + /* This group will contain specialized fields. */
    + group = purple_request_group_new(_("Special"));
    + purple_request_page_add_group(page, group);
    +
    + field = purple_request_field_label_new("special-label",
    + _("This group contains specialized fields"));
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    +
    + icon = g_resource_lookup_data(purple_demo_get_resource(),
    + "/im/pidgin/purple/demo/icons/scalable/apps/im-purple-demo.svg",
    + G_RESOURCE_LOOKUP_FLAGS_NONE, NULL);
    + icon_data = g_bytes_get_data(icon, &icon_len);
    + field = purple_request_field_image_new("image", _("An image"),
    + icon_data, icon_len);
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    + g_bytes_unref(icon);
    +
    + field = purple_request_field_account_new("account", _("An account"),
    + account);
    + g_object_bind_property(boolfield, "value", field, "sensitive", 0);
    + purple_request_group_add_field(group, field);
    +
    + purple_request_fields(connection, _("Request Fields Demo"),
    + _("Please fill out these fields…"), NULL, page,
    + _("OK"),
    + G_CALLBACK(purple_demo_protocol_request_fields_ok_cb),
    + _("Cancel"),
    + G_CALLBACK(purple_demo_protocol_request_fields_cancel_cb),
    + purple_request_cpar_from_connection(connection),
    + NULL);
    +
    + g_clear_object(&account);
    +}
    +
    +static void
    +purple_demo_protocol_request_path_ok_cb(gpointer data, const char *filename) {
    + const char *type = data;
    +
    + g_message(_("Successfully requested %s from UI: %s"), type, filename);
    +}
    +
    +static void
    +purple_demo_protocol_request_path_cancel_cb(gpointer data) {
    + const char *type = data;
    +
    + g_message(_("UI cancelled %s request"), type);
    +}
    +
    +static void
    +purple_demo_protocol_request_file_activate(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + PurpleConnection *connection = NULL;
    + const gchar *account_id = NULL;
    + PurpleAccountManager *manager = NULL;
    + PurpleAccount *account = NULL;
    +
    + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    + g_critical("Demo failure action parameter is of incorrect type %s",
    + g_variant_get_type_string(parameter));
    + return;
    + }
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(manager, account_id);
    + connection = purple_account_get_connection(account);
    + g_clear_object(&account);
    +
    + purple_request_file(connection, _("Request File Demo"),
    + "example.txt", FALSE,
    + G_CALLBACK(purple_demo_protocol_request_path_ok_cb),
    + G_CALLBACK(purple_demo_protocol_request_path_cancel_cb),
    + purple_request_cpar_from_connection(connection),
    + "file");
    +}
    +
    +static void
    +purple_demo_protocol_request_folder_activate(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + PurpleConnection *connection = NULL;
    + const gchar *account_id = NULL;
    + PurpleAccountManager *manager = NULL;
    + PurpleAccount *account = NULL;
    +
    + if(!g_variant_is_of_type(parameter, G_VARIANT_TYPE_STRING)) {
    + g_critical("Demo failure action parameter is of incorrect type %s",
    + g_variant_get_type_string(parameter));
    + return;
    + }
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(manager, account_id);
    + connection = purple_account_get_connection(account);
    + g_clear_object(&account);
    +
    + purple_request_folder(connection, _("Request Folder Demo"), NULL,
    + G_CALLBACK(purple_demo_protocol_request_path_ok_cb),
    + G_CALLBACK(purple_demo_protocol_request_path_cancel_cb),
    + purple_request_cpar_from_connection(connection),
    + "folder");
    +}
    +
    +/******************************************************************************
    + * Contact action implementations
    + *****************************************************************************/
    +static const gchar *contacts[] = {"Alice", "Bob", "Carlos", "Erin" };
    +
    +static void
    +purple_demo_protocol_remote_add(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + PurpleAccount *account = NULL;
    + PurpleAccountManager *account_manager = NULL;
    + PurpleAddContactRequest *request = NULL;
    + PurpleNotification *notification = NULL;
    + PurpleNotificationManager *notification_manager = NULL;
    + const gchar *account_id = NULL;
    + static guint counter = 0;
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + account_manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(account_manager, account_id);
    +
    + request = purple_add_contact_request_new(account, contacts[counter]);
    + notification = purple_notification_new_from_add_contact_request(request);
    +
    + notification_manager = purple_notification_manager_get_default();
    + purple_notification_manager_add(notification_manager, notification);
    +
    + counter++;
    + if(counter >= G_N_ELEMENTS(contacts)) {
    + counter = 0;
    + }
    +
    + g_clear_object(&notification);
    + g_clear_object(&account);
    +}
    +
    +static const char *puns[] = {
    + "Toucan play at that game!",
    + "As long as it's not too much of a birden...",
    + "Sounds like a bit of ostrich...",
    + "People can't stop raven!",
    + "Have you heard about the bird?",
    +};
    +
    +static void
    +purple_demo_protocol_generic_notification(G_GNUC_UNUSED GSimpleAction *action,
    + GVariant *parameter,
    + G_GNUC_UNUSED gpointer data)
    +{
    + PurpleAccount *account = NULL;
    + PurpleAccountManager *account_manager = NULL;
    + PurpleNotification *notification = NULL;
    + PurpleNotificationManager *notification_manager = NULL;
    + const char *account_id = NULL;
    + static guint counter = 0;
    +
    + account_id = g_variant_get_string(parameter, NULL);
    + account_manager = purple_account_manager_get_default();
    + account = purple_account_manager_find_by_id(account_manager, account_id);
    +
    + notification = purple_notification_new(PURPLE_NOTIFICATION_TYPE_GENERIC,
    + account, g_strdup(puns[counter]),
    + g_free);
    +
    + notification_manager = purple_notification_manager_get_default();
    + purple_notification_manager_add(notification_manager, notification);
    +
    + counter++;
    + if(counter >= G_N_ELEMENTS(puns)) {
    + counter = 0;
    + }
    +
    + g_clear_object(&notification);
    + g_clear_object(&account);
    +}
    +
    +/******************************************************************************
    + * PurpleProtocolActions Implementation
    + *****************************************************************************/
    +static const gchar *
    +purple_demo_protocol_get_prefix(G_GNUC_UNUSED PurpleProtocolActions *actions) {
    + return "prpl-demo";
    +}
    +
    +static GActionGroup *
    +purple_demo_protocol_get_action_group(G_GNUC_UNUSED PurpleProtocolActions *actions,
    + G_GNUC_UNUSED PurpleConnection *connection)
    +{
    + GSimpleActionGroup *group = NULL;
    + GActionEntry entries[] = {
    + {
    + .name = "temporary-failure",
    + .activate = purple_demo_protocol_temporary_failure_action_activate,
    + .parameter_type = "s",
    + }, {
    + .name = "fatal-failure",
    + .activate = purple_demo_protocol_fatal_failure_action_activate,
    + .parameter_type = "s",
    + }, {
    + .name = "generic-notification",
    + .activate = purple_demo_protocol_generic_notification,
    + .parameter_type = "s",
    + }, {
    + .name = "remote-add",
    + .activate = purple_demo_protocol_remote_add,
    + .parameter_type = "s",
    + }, {
    + .name = "request-input",
    + .activate = purple_demo_protocol_request_input_activate,
    + .parameter_type = "s",
    + }, {
    + .name = "request-choice",
    + .activate = purple_demo_protocol_request_choice_activate,
    + .parameter_type = "s",
    + }, {
    + .name = "request-action",
    + .activate = purple_demo_protocol_request_action_activate,
    + .parameter_type = "s",
    + }, {
    + .name = "request-wait",
    + .activate = purple_demo_protocol_request_wait_activate,
    + .parameter_type = "s",
    + }, {
    + .name = "request-fields",
    + .activate = purple_demo_protocol_request_fields_activate,
    + .parameter_type = "s",
    + }, {
    + .name = "request-file",
    + .activate = purple_demo_protocol_request_file_activate,
    + .parameter_type = "s",
    + }, {
    + .name = "request-folder",
    + .activate = purple_demo_protocol_request_folder_activate,
    + .parameter_type = "s",
    + }
    + };
    + gsize nentries = G_N_ELEMENTS(entries);
    +
    + group = g_simple_action_group_new();
    + g_action_map_add_action_entries(G_ACTION_MAP(group), entries, nentries,
    + NULL);
    +
    + return G_ACTION_GROUP(group);
    +}
    +
    +static GMenu *
    +purple_demo_protocol_get_menu(G_GNUC_UNUSED PurpleProtocolActions *actions,
    + G_GNUC_UNUSED PurpleConnection *connection)
    +{
    + GMenu *menu = NULL;
    + GMenu *submenu = NULL;
    + GMenuItem *item = NULL;
    +
    + menu = g_menu_new();
    +
    + item = g_menu_item_new(_("Trigger temporary connection failure..."),
    + "prpl-demo.temporary-failure");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(menu, item);
    + g_object_unref(item);
    +
    + item = g_menu_item_new(_("Trigger fatal connection failure..."),
    + "prpl-demo.fatal-failure");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(menu, item);
    + g_object_unref(item);
    +
    + item = g_menu_item_new(_("Trigger a generic notification"),
    + "prpl-demo.generic-notification");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(menu, item);
    + g_object_unref(item);
    +
    + item = g_menu_item_new(_("Trigger a contact adding you"),
    + "prpl-demo.remote-add");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(menu, item);
    + g_object_unref(item);
    +
    + submenu = g_menu_new();
    +
    + item = g_menu_item_new(_("Input"), "prpl-demo.request-input");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(submenu, item);
    + g_object_unref(item);
    +
    + item = g_menu_item_new(_("Choice"), "prpl-demo.request-choice");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(submenu, item);
    + g_object_unref(item);
    +
    + item = g_menu_item_new(_("Action"), "prpl-demo.request-action");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(submenu, item);
    + g_object_unref(item);
    +
    + item = g_menu_item_new(_("Wait"), "prpl-demo.request-wait");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(submenu, item);
    + g_object_unref(item);
    +
    + item = g_menu_item_new(_("Fields"), "prpl-demo.request-fields");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(submenu, item);
    + g_object_unref(item);
    +
    + item = g_menu_item_new(_("File"), "prpl-demo.request-file");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(submenu, item);
    + g_object_unref(item);
    +
    + item = g_menu_item_new(_("Folder"), "prpl-demo.request-folder");
    + g_menu_item_set_attribute(item, PURPLE_MENU_ATTRIBUTE_DYNAMIC_TARGET, "s",
    + "account");
    + g_menu_append_item(submenu, item);
    + g_object_unref(item);
    +
    + g_menu_append_submenu(menu, _("Trigger requests"), G_MENU_MODEL(submenu));
    + g_object_unref(submenu);
    +
    + return menu;
    +}
    +
    +void
    +purple_demo_protocol_actions_init(PurpleProtocolActionsInterface *iface) {
    + iface->get_prefix = purple_demo_protocol_get_prefix;
    + iface->get_action_group = purple_demo_protocol_get_action_group;
    + iface->get_menu = purple_demo_protocol_get_menu;
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolactions.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,28 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/>.
    + */
    +
    +#ifndef PURPLE_DEMO_PROTOCOL_ACTIONS_H
    +#define PURPLE_DEMO_PROTOCOL_ACTIONS_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +G_GNUC_INTERNAL void purple_demo_protocol_actions_init(PurpleProtocolActionsInterface *iface);
    +
    +#endif /* PURPLE_DEMO_PROTOCOL_ACTIONS_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolclient.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,29 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 "purpledemoprotocol.h"
    +#include "purpledemoprotocolclient.h"
    +
    +/******************************************************************************
    + * PurpleProtocolClient Implementation
    + *****************************************************************************/
    +void
    +purple_demo_protocol_client_init(G_GNUC_UNUSED PurpleProtocolClientInterface *iface) {
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolclient.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,28 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/>.
    + */
    +
    +#ifndef PURPLE_DEMO_PROTOCOL_CLIENT_H
    +#define PURPLE_DEMO_PROTOCOL_CLIENT_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +G_GNUC_INTERNAL void purple_demo_protocol_client_init(PurpleProtocolClientInterface *iface);
    +
    +#endif /* PURPLE_DEMO_PROTOCOL_CLIENT_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolcontacts.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,57 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 "purpledemoprotocolcontacts.h"
    +
    +/******************************************************************************
    + * PurpleProtocolContacts Implementation
    + *****************************************************************************/
    +static char *
    +purple_demo_protocol_contacts_get_profile_finish(G_GNUC_UNUSED PurpleProtocolContacts *contacts,
    + GAsyncResult *result,
    + GError **error)
    +{
    + g_return_val_if_fail(G_IS_TASK(result), NULL);
    +
    + return g_task_propagate_pointer(G_TASK(result), error);
    +}
    +
    +static void
    +purple_demo_protocol_contacts_get_profile_async(PurpleProtocolContacts *contacts,
    + PurpleContactInfo *info,
    + GCancellable *cancellable,
    + GAsyncReadyCallback callback,
    + gpointer data)
    +{
    + GTask *task = NULL;
    + const char *profile = NULL;
    +
    + task = g_task_new(contacts, cancellable, callback, data);
    +
    + profile = g_object_get_data(G_OBJECT(info), "demo-profile");
    + g_task_return_pointer(task, g_strdup(profile), g_free);
    + g_clear_object(&task);
    +}
    +
    +void
    +purple_demo_protocol_contacts_init(PurpleProtocolContactsInterface *iface) {
    + iface->get_profile_async = purple_demo_protocol_contacts_get_profile_async;
    + iface->get_profile_finish = purple_demo_protocol_contacts_get_profile_finish;
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolcontacts.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,28 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/liceng_task_return_pointerses/>.
    + */
    +
    +#ifndef PURPLE_DEMO_PROTOCOL_CONTACTS_H
    +#define PURPLE_DEMO_PROTOCOL_CONTACTS_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +G_GNUC_INTERNAL void purple_demo_protocol_contacts_init(PurpleProtocolContactsInterface *iface);
    +
    +#endif /* PURPLE_DEMO_PROTOCOL_CONTACTS_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolconversation.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,131 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 "purpledemoprotocol.h"
    +#include "purpledemoprotocolconversation.h"
    +
    +/******************************************************************************
    + * PurpleProtocolConversation Implementation
    + *****************************************************************************/
    +typedef struct {
    + PurpleConversation *conversation;
    + PurpleMessage *message;
    +} PurpleDemoProtocolIMInfo;
    +
    +static void
    +purple_demo_protocol_im_info_free(PurpleDemoProtocolIMInfo *info) {
    + g_clear_object(&info->conversation);
    + g_clear_object(&info->message);
    + g_free(info);
    +}
    +
    +static gboolean
    +purple_demo_protocol_echo_im_cb(gpointer data) {
    + PurpleDemoProtocolIMInfo *info = data;
    + PurpleMessage *message = NULL;
    + PurpleMessageFlags flags;
    + const char *who = NULL;
    +
    + /* Turn outgoing message back incoming. */
    + who = purple_conversation_get_name(info->conversation);
    +
    + flags = purple_message_get_flags(info->message);
    + flags &= ~PURPLE_MESSAGE_SEND;
    + flags |= PURPLE_MESSAGE_RECV;
    +
    + message = purple_message_new_incoming(who,
    + purple_message_get_contents(info->message),
    + flags, 0);
    +
    + purple_conversation_write_message(info->conversation, message);
    + g_clear_object(&message);
    +
    + return G_SOURCE_REMOVE;
    +}
    +
    +static void
    +purple_demo_protocol_send_message_async(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
    + PurpleConversation *conversation,
    + PurpleMessage *message,
    + GCancellable *cancellable,
    + GAsyncReadyCallback callback,
    + gpointer data)
    +{
    + GTask *task = NULL;
    + const char *who = NULL;
    +
    + who = purple_conversation_get_name(conversation);
    + if(purple_strempty(who)) {
    + who = purple_message_get_recipient(message);
    + }
    +
    + if(purple_strequal(who, "Echo")) {
    + PurpleDemoProtocolIMInfo *info = g_new(PurpleDemoProtocolIMInfo, 1);
    +
    + info->conversation = g_object_ref(conversation);
    + info->message = g_object_ref(message);
    +
    + g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
    + purple_demo_protocol_echo_im_cb, info,
    + (GDestroyNotify)purple_demo_protocol_im_info_free);
    + } else if(purple_strequal(who, "Aegina")) {
    + PurpleDemoProtocolIMInfo *info = g_new(PurpleDemoProtocolIMInfo, 1);
    + const char *author = purple_message_get_author(message);
    + const char *contents = NULL;
    +
    + if(purple_strequal(author, "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_outgoing(author, who, contents,
    + PURPLE_MESSAGE_SEND);
    +
    + g_idle_add_full(G_PRIORITY_DEFAULT_IDLE, purple_demo_protocol_echo_im_cb,
    + info, (GDestroyNotify)purple_demo_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_demo_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_demo_protocol_conversation_init(PurpleProtocolConversationInterface *iface) {
    + iface->send_message_async = purple_demo_protocol_send_message_async;
    + iface->send_message_finish = purple_demo_protocol_send_message_finish;
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolconversation.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,28 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/>.
    + */
    +
    +#ifndef PURPLE_DEMO_PROTOCOL_CONVERSATION_H
    +#define PURPLE_DEMO_PROTOCOL_CONVERSATION_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +G_GNUC_INTERNAL void purple_demo_protocol_conversation_init(PurpleProtocolConversationInterface *iface);
    +
    +#endif /* PURPLE_DEMO_PROTOCOL_CONVERSATION_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolmedia.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,102 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 "purpledemoprotocol.h"
    +#include "purpledemoprotocolmedia.h"
    +
    +/******************************************************************************
    + * PurpleProtocolMedia Implementation
    + *****************************************************************************/
    +static PurpleMediaCaps
    +purple_demo_protocol_media_get_caps(G_GNUC_UNUSED PurpleProtocolMedia *media,
    + G_GNUC_UNUSED PurpleAccount *account,
    + const gchar *who)
    +{
    + if(purple_strequal(who, "Echo")) {
    + return PURPLE_MEDIA_CAPS_AUDIO | PURPLE_MEDIA_CAPS_VIDEO |
    + PURPLE_MEDIA_CAPS_AUDIO_VIDEO;
    + }
    +
    + return PURPLE_MEDIA_CAPS_NONE;
    +}
    +
    +static gboolean
    +purple_demo_protocol_media_initiate_session(G_GNUC_UNUSED PurpleProtocolMedia *media,
    + PurpleAccount *account,
    + const gchar *who,
    + PurpleMediaSessionType type)
    +{
    + PurpleConnection *connection = NULL;
    + gchar *session_name = NULL;
    + gchar *message = NULL;
    + GDateTime *timestamp = NULL;
    +
    + connection = purple_account_get_connection(account);
    +
    + session_name = g_flags_to_string(PURPLE_MEDIA_TYPE_SESSION_TYPE, type);
    + message = g_strdup_printf(_("Initiated demo %s session with %s"),
    + session_name, who);
    + timestamp = g_date_time_new_now_utc();
    +
    + purple_serv_got_im(connection, "Echo",
    + message, PURPLE_MESSAGE_RECV,
    + g_date_time_to_unix(timestamp));
    +
    + g_date_time_unref(timestamp);
    + g_free(message);
    + g_free(session_name);
    +
    + /* TODO: When libpurple gets a backend, we can implement more of this. */
    + return FALSE;
    +}
    +
    +static gboolean
    +purple_demo_protocol_media_send_dtmf(G_GNUC_UNUSED PurpleProtocolMedia *protocol_media,
    + PurpleMedia *media, gchar dtmf,
    + guint8 volume, guint8 duration)
    +{
    + PurpleAccount *account = NULL;
    + PurpleConnection *connection = NULL;
    + gchar *message = NULL;
    + GDateTime *timestamp = NULL;
    +
    + account = purple_media_get_account(media);
    + connection = purple_account_get_connection(account);
    +
    + message = g_strdup_printf(_("Received DTMF %c at volume %d for %d seconds"),
    + dtmf, volume, duration);
    + timestamp = g_date_time_new_now_utc();
    +
    + purple_serv_got_im(connection, "Echo",
    + message, PURPLE_MESSAGE_RECV,
    + g_date_time_to_unix(timestamp));
    +
    + g_date_time_unref(timestamp);
    + g_free(message);
    +
    + return TRUE;
    +}
    +
    +void
    +purple_demo_protocol_media_init(PurpleProtocolMediaInterface *iface) {
    + iface->get_caps = purple_demo_protocol_media_get_caps;
    + iface->initiate_session = purple_demo_protocol_media_initiate_session;
    + iface->send_dtmf = purple_demo_protocol_media_send_dtmf;
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/purpledemoprotocolmedia.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,28 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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/>.
    + */
    +
    +#ifndef PURPLE_DEMO_PROTOCOL_MEDIA_H
    +#define PURPLE_DEMO_PROTOCOL_MEDIA_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +G_GNUC_INTERNAL void purple_demo_protocol_media_init(PurpleProtocolMediaInterface *iface);
    +
    +#endif /* PURPLE_DEMO_PROTOCOL_MEDIA_H */
    Binary file protocols/demo/resources/buddy_icons/Aegina.png has changed
    Binary file protocols/demo/resources/buddy_icons/Echo.png has changed
    Binary file protocols/demo/resources/buddy_icons/Eion.png has changed
    Binary file protocols/demo/resources/buddy_icons/Elliott.png has changed
    Binary file protocols/demo/resources/buddy_icons/Gary.png has changed
    Binary file protocols/demo/resources/buddy_icons/John.png has changed
    Binary file protocols/demo/resources/buddy_icons/Markus.png has changed
    Binary file protocols/demo/resources/buddy_icons/Richard.png has changed
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/resources/buddy_icons/mkicon.py Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,90 @@
    +#!/usr/bin/env python3
    +#
    +# Purple - Internet Messaging Library
    +# Copyright (C) Pidgin Developers <devel@pidgin.im>
    +#
    +# Purple is the legal property of its developers, whose names are too numerous
    +# to list here. Please refer to the COPYRIGHT file distributed with this
    +# source distribution.
    +#
    +# This program is free software; you can redistribute it and/or modify
    +# it under the terms of the GNU General Public License as published by
    +# the Free Software Foundation; either version 2 of the License, or
    +# (at your option) any later version.
    +#
    +# This program 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 General Public License for more details.
    +#
    +# You should have received a copy of the GNU General Public License along with
    +# this program; if not, see <https://www.gnu.org/licenses/>.
    +
    +import argparse
    +import colorsys
    +import hashlib
    +from pathlib import Path
    +
    +from PIL import Image, ImageDraw, ImageFont
    +
    +parser = argparse.ArgumentParser(
    + description='Generate buddy icons from user names.')
    +parser.add_argument('name', nargs='+', help='The name(s) to use.')
    +parser.add_argument('-f', '--font',
    + default='/usr/share/fonts/urw-base35/D050000L.otf',
    + help='Path to (TrueType or OpenType) font to use.')
    +parser.add_argument('-s', '--size', default=96, type=int,
    + help='Size of buddy icons to produce.')
    +parser.add_argument('-o', '--output', default='.',
    + help='Directory in which to place files.')
    +args = parser.parse_args()
    +
    +
    +def calculate_colours_for_text(text):
    + """
    + Calculate the foreground and background colours from text.
    +
    + This is based on pidgin_color_calculate_for_text in Pidgin C code.
    + """
    + # Hash the string and get the first 2 bytes of the digest.
    + checksum = hashlib.sha1()
    + checksum.update(text.encode('utf-8'))
    + digest = checksum.digest()
    +
    + # Calculate the hue based on the digest, scaled to 0-1.
    + hue = (digest[0] << 8 | digest[1]) / 65535
    + # Get the RGB values for the hue at full saturation and value.
    + foreground = colorsys.hsv_to_rgb(hue, 1.0, 1.0)
    +
    + # Calculate the hue based on the end of the digest, scaled to 0-1.
    + hue = (digest[-1] << 8 | digest[-2]) / 65535
    + # Get the RGB values for the hue at full saturation and low value.
    + background = colorsys.hsv_to_rgb(hue, 1.0, 0.2)
    +
    + # Finally calculate the foreground summing 20% of the inverted background
    + # with 80% of the foreground.
    + foreground = (
    + (0.2 * (1 - bc)) + (0.8 * fc) for bc, fc in zip(background, foreground)
    + )
    +
    + # Pillow requires colours in 0-255.
    + return (tuple(int(c * 255) for c in foreground),
    + tuple(int(c * 255) for c in background))
    +
    +
    +output_dir = Path(args.output)
    +font = ImageFont.truetype(args.font, size=int(args.size * 96/72))
    +
    +for name in args.name:
    + fore, back = calculate_colours_for_text(name)
    +
    + # Generate an image using the first letter of the name to choose a glyph
    + # from the specified font. The default font generates some star-like or
    + # flower-like symbols.
    + img = Image.new('RGBA', (args.size, args.size), color=back)
    + draw = ImageDraw.Draw(img)
    + letter = name[0].upper()
    + draw.text((args.size // 2, args.size - 1), letter,
    + font=font, anchor='mb', fill=fore)
    +
    + img.save(output_dir / f'{name}.png', 'PNG')
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/resources/contacts.json Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,109 @@
    +[{
    + "id": "grim",
    + "person": {
    + "alias": "Grim",
    + "id": "gary"
    + },
    + "presence": {
    + "primitive": "available",
    + "message": "💤 Sleeping... 😴",
    + "idle": 1337
    + },
    + "username": "Gary",
    + "tags": [
    + "group:Family"
    + ],
    + "profile": "Pidgin 3 will be ready when it is ready! 🗿"
    +}, {
    + "id": "john",
    + "alias": "Rekkanoryo",
    + "person": {
    + "id": "john"
    + },
    + "presence": {
    + "primitive": "away"
    + },
    + "tags": [
    + "group:Friends"
    + ],
    + "username": "John",
    + "profile": "Idiot Windows sysadmin who used to develop for Pidgin and libpurple in the bright days before Windows 8"
    +}, {
    + "id": "elliott",
    + "person": {
    + "id": "elliott"
    + },
    + "presence": {
    + "primitive": "away",
    + "message": "Coding..."
    + },
    + "tags": [
    + "group:Work"
    + ],
    + "username": "Elliott"
    +}, {
    + "id": "richard",
    + "person": {
    + "id": "richard"
    + },
    + "presence": {
    + "primitive": "available"
    + },
    + "tags": [
    + "group:Work"
    + ],
    + "username": "Richard"
    +}, {
    + "id": "eion",
    + "person": {
    + "id": "eion"
    + },
    + "presence": {
    + "primitive": "available",
    + "message": "Writing crazy patches!"
    + },
    + "tags": [
    + "group:School"
    + ],
    + "username": "Eion"
    +}, {
    + "id": "markus",
    + "person": {
    + "id": "markus"
    + },
    + "presence": {
    + "primitive": "do-not-disturb",
    + "message": "Running all the things in valgrind..."
    + },
    + "tags": [
    + "group:School"
    + ],
    + "username": "Markus"
    +}, {
    + "id": "echo",
    + "person": {
    + "id": "echo"
    + },
    + "presence": {
    + "primitive": "available",
    + "message": "Cursed to speak the last words spoken to me"
    + },
    + "tags": [
    + "group:Nymphs"
    + ],
    + "username": "Echo"
    +}, {
    + "id": "aegina",
    + "person": {
    + "id": "aegina"
    + },
    + "presence": {
    + "primitive": "offline",
    + "emoji": "🫥️",
    + "message": "Stole the Cap of Invisibility from Hades"
    + },
    + "tags": [
    + "group:Nymphs"
    + ],
    + "username": "Aegina"
    +}]
    Binary file protocols/demo/resources/icons/16x16/apps/im-purple-demo.png has changed
    Binary file protocols/demo/resources/icons/22x22/apps/im-purple-demo.png has changed
    Binary file protocols/demo/resources/icons/48x48/apps/im-purple-demo.png has changed
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/resources/icons/scalable/apps/im-purple-demo.svg Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,78 @@
    +<?xml version="1.0" encoding="UTF-8" standalone="no"?>
    +<!-- Created with Inkscape (http://www.inkscape.org/) -->
    +
    +<svg
    + width="37.851284mm"
    + height="39.057049mm"
    + viewBox="0 0 37.851284 39.057049"
    + version="1.1"
    + id="svg31537"
    + inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20)"
    + sodipodi:docname="im-purple-demo.svg"
    + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
    + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
    + xmlns="http://www.w3.org/2000/svg"
    + xmlns:svg="http://www.w3.org/2000/svg"
    + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
    + xmlns:cc="http://creativecommons.org/ns#"
    + xmlns:dc="http://purl.org/dc/elements/1.1/">
    + <defs
    + id="defs31531" />
    + <sodipodi:namedview
    + id="base"
    + pagecolor="#ffffff"
    + bordercolor="#666666"
    + borderopacity="1.0"
    + inkscape:pageopacity="0.0"
    + inkscape:pageshadow="2"
    + inkscape:zoom="2.8"
    + inkscape:cx="25.178571"
    + inkscape:cy="33.571429"
    + inkscape:document-units="mm"
    + inkscape:current-layer="layer1"
    + showgrid="false"
    + fit-margin-top="0"
    + fit-margin-left="0"
    + fit-margin-right="0"
    + fit-margin-bottom="0"
    + inkscape:window-width="1916"
    + inkscape:window-height="1017"
    + inkscape:window-x="1080"
    + inkscape:window-y="493"
    + inkscape:window-maximized="1"
    + inkscape:pagecheckerboard="0" />
    + <metadata
    + id="metadata31534">
    + <rdf:RDF>
    + <cc:Work
    + rdf:about="">
    + <dc:format>image/svg+xml</dc:format>
    + <dc:type
    + rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
    + </cc:Work>
    + </rdf:RDF>
    + </metadata>
    + <g
    + inkscape:label="Layer 1"
    + inkscape:groupmode="layer"
    + id="layer1"
    + transform="translate(75.835419,-56.26127)">
    + <g
    + id="g1812">
    + <path
    + style="isolation:isolate;fill:#4b2854;fill-opacity:1;stroke:#fdfdfd;stroke-width:11.7132;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
    + inkscape:connector-curvature="0"
    + id="path2938"
    + d="m 137.45776,361.78346 q 9.2531,10.42501 11.91562,19.37813 a 19.687501,19.687501 0 0 0 -9.08437,2.24061 17.090626,17.090626 0 0 0 -6.84376,6.38437 19.312501,19.312501 0 0 0 -2.74688,9.76876 c -0.15944,5.625 1.64063,10.15313 5.44688,13.54687 a 20.831251,20.831251 0 0 0 13.70626,5.33438 19.687501,19.687501 0 0 0 11.61563,-3.02811 20.690626,20.690626 0 0 0 7.36874,-8.22189 23.690626,23.690626 0 0 0 2.65312,-10.14374 39.196877,39.196877 0 0 0 -6.79688,-22.98749 87.590631,87.590631 0 0 0 -18.20626,-19.98751 z"
    + class="cls-9"
    + transform="matrix(0.37919911,0,0,0.38886781,-123.17216,-79.14694)" />
    + <path
    + style="isolation:isolate;fill:#5c3566;stroke:#fdfdfd;stroke-width:11.7132;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
    + inkscape:connector-curvature="0"
    + id="path2940"
    + d="m 184.75465,435.07723 q 9.25309,-10.42501 11.90622,-19.37814 a 19.687501,19.687501 0 0 1 -9.08437,-2.24064 17.062501,17.062501 0 0 1 -6.83437,-6.38437 19.312501,19.312501 0 0 1 -2.75626,-9.75936 q -0.23457,-8.4375 5.45626,-13.55627 a 20.812501,20.812501 0 0 1 13.70626,-5.33438 19.687501,19.687501 0 0 1 11.6156,3.0375 20.709376,20.709376 0 0 1 7.36877,8.20315 23.765626,23.765626 0 0 1 2.65312,10.15313 39.281254,39.281254 0 0 1 -6.79688,22.98749 87.590631,87.590631 0 0 1 -18.20626,19.98751 z"
    + class="cls-9"
    + transform="matrix(0.37919911,0,0,0.38886781,-123.17216,-79.14694)" />
    + </g>
    + </g>
    +</svg>
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/demo/resources/purpledemo.gresource.xml Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,18 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<gresources>
    + <gresource prefix="/im/pidgin/purple/demo">
    + <file compressed="true" preprocess="json-stripblanks">contacts.json</file>
    + <file>icons/16x16/apps/im-purple-demo.png</file>
    + <file>icons/22x22/apps/im-purple-demo.png</file>
    + <file>icons/48x48/apps/im-purple-demo.png</file>
    + <file>icons/scalable/apps/im-purple-demo.svg</file>
    + <file>buddy_icons/Aegina.png</file>
    + <file>buddy_icons/Echo.png</file>
    + <file>buddy_icons/Eion.png</file>
    + <file>buddy_icons/Elliott.png</file>
    + <file>buddy_icons/Gary.png</file>
    + <file>buddy_icons/John.png</file>
    + <file>buddy_icons/Markus.png</file>
    + <file>buddy_icons/Richard.png</file>
    + </gresource>
    +</gresources>
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/README.md Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,22 @@
    +# IRCv3
    +
    +This is a brand new from-scratch protocol plugin which is the first protocol
    +plugin to be 100% code reviewed. It uses regex to tokenize messages.
    +
    +We are intending for it to be subclass-able so other networks like Twitch.tv can
    +be supported but we're not quite there yet.
    +
    +We also are intending to support subclassing in other languages but we've run
    +into some issues with dynamic GObject types and GObject introspection that have
    +slowed us down.
    +
    +## Capability Support
    +
    +This is a list of capabilities that we currently support. We'll do our best to
    +keep this list up to date, but if you notice we've missed something please let
    +us know!
    +
    +* cap-notify
    +* sasl (right now just PLAIN works)
    +* message-tags
    +* msgid
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/ircv3generategir.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,32 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib.h>
    +
    +#include <gplugin-introspection.h>
    +
    +int
    +main(int argc, char *argv[]) {
    + return gplugin_introspection_introspect_plugin(&argc, &argv,
    + PLUGIN_FILENAME);
    +}
    +
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/meson.build Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,114 @@
    +IRCV3_SOURCES = [
    + 'purpleircv3capabilities.c',
    + 'purpleircv3connection.c',
    + 'purpleircv3core.c',
    + 'purpleircv3ctcp.c',
    + 'purpleircv3formatting.c',
    + 'purpleircv3message.c',
    + 'purpleircv3messagehandlers.c',
    + 'purpleircv3parser.c',
    + 'purpleircv3protocol.c',
    + 'purpleircv3protocolconversation.c',
    + 'purpleircv3sasl.c',
    + 'purpleircv3source.c',
    +]
    +
    +IRCV3_HEADERS = [
    + 'purpleircv3capabilities.h',
    + 'purpleircv3connection.h',
    + 'purpleircv3constants.h',
    + 'purpleircv3core.h',
    + 'purpleircv3ctcp.h',
    + 'purpleircv3formatting.h',
    + 'purpleircv3message.h',
    + 'purpleircv3messagehandlers.h',
    + 'purpleircv3parser.h',
    + 'purpleircv3protocol.h',
    + 'purpleircv3protocolconversation.h',
    + 'purpleircv3sasl.h',
    + 'purpleircv3source.h',
    +]
    +
    +if not DYNAMIC_IRCV3
    + subdir_done()
    +endif
    +
    +ircv3_filebase = f'purple-@purple_major_version@-ircv3'
    +
    +ircv3_includes = include_directories('.')
    +ircv3_include_base = purple_include_base / 'protocols/ircv3'
    +
    +
    +ircv3_resources = gnome.compile_resources('ircv3resource',
    + 'resources/ircv3.gresource.xml',
    + source_dir : 'resources',
    + c_name : 'purple_ircv3')
    +IRCV3_SOURCES += ircv3_resources
    +
    +ircv3_h_includes = []
    +foreach header : IRCV3_HEADERS
    + ircv3_h_includes += f'#include <@header@>'
    +endforeach
    +ircv3_h_conf = configuration_data()
    +ircv3_h_conf.set('IRCV3_H_INCLUDES', '\n'.join(ircv3_h_includes))
    +
    +ircv3_h = configure_file(input : 'purpleircv3.h.in',
    + output : 'purpleircv3.h',
    + configuration : ircv3_h_conf,
    + install : true,
    + install_dir : get_option('includedir') / ircv3_include_base)
    +
    +install_headers(IRCV3_HEADERS,
    + subdir : ircv3_include_base)
    +
    +ircv3_prpl = shared_library('ircv3', IRCV3_SOURCES + IRCV3_HEADERS + [ircv3_h],
    + c_args : ['-DPURPLE_IRCV3_COMPILATION', '-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="Purple-IRCv3"'],
    + gnu_symbol_visibility : 'hidden',
    + dependencies : [birb_dep, libpurple_dep, glib, gio, hasl],
    + install : true,
    + install_dir : PURPLE_PLUGINDIR)
    +
    +ircv3_dep = declare_dependency(
    + sources : [IRCV3_SOURCES, IRCV3_HEADERS],
    + include_directories : ircv3_includes,
    + dependencies : [birb_dep, libpurple_dep, glib, gio, hasl])
    +
    +pkgconfig.generate(
    + # we purposely don't put the library here because you should not be
    + # linking to the plugin, everything will be resolved during runtime.
    + name : 'ircv3',
    + description : 'a purple3 protocol plugin for IRCv3',
    + version : meson.project_version(),
    + subdirs : [ircv3_include_base],
    + filebase : ircv3_filebase,
    + libraries : [gio, glib, hasl, libpurple])
    +
    +meson.override_dependency(ircv3_filebase, ircv3_dep)
    +
    +devenv.append('PURPLE_PLUGIN_PATH', meson.current_build_dir())
    +
    +if get_option('introspection')
    + GPLUGIN_INTROSPECTION = dependency('gplugin-introspection')
    +
    + plugin_filename = ircv3_prpl.full_path()
    +
    + ircv3_introspection_stub = executable('ircv3generategir',
    + sources : 'ircv3generategir.c',
    + dependencies : [ircv3_dep, libpurple_dep, glib, gio, hasl, GPLUGIN_INTROSPECTION],
    + c_args : ['-DPURPLE_IRCV3_COMPILATION', f'-DPLUGIN_FILENAME="@plugin_filename@"'],
    + install : false)
    +
    + gnome.generate_gir(
    + ircv3_introspection_stub,
    + sources : [IRCV3_SOURCES, IRCV3_HEADERS],
    + includes : ['Birb-1.0', 'GLib-2.0', 'GObject-2.0', 'GPlugin-1.0', libpurple_gir[0]],
    + namespace : 'PurpleIRCv3',
    + symbol_prefix : 'purple_ircv3',
    + nsversion : '1.0',
    + install : true,
    + dependencies: [birb_dep, gplugin_dep],
    + export_packages : ['ircv3'],
    + extra_args : ['-DPURPLE_IRCV3_COMPILATION', '--verbose'])
    +endif
    +
    +subdir('tests')
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3.h.in Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,40 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#ifndef PURPLE_IRCV3_H
    +#define PURPLE_IRCV3_H
    +
    +#ifndef __GI_SCANNER__ /* hide this bit from g-ir-scanner */
    +# ifdef PURPLE_IRCV3_COMPILATION
    +# error "ircv3 source files should not be including purpleircv3.h"
    +# endif /* PURPLE_IRCV3_COMPILATION */
    +#endif /* __GI_SCANNER__ */
    +
    +#ifndef PURPLE_IRCV3_GLOBAL_HEADER_INSIDE
    +# define PURPLE_IRCV3_GLOBAL_HEADER_INSIDE
    +#endif /* PURPLE_IRCV3_GLOBAL_HEADER_INSIDE */
    +
    +@IRCV3_H_INCLUDES@
    +
    +#undef PURPLE_IRCV3_GLOBAL_HEADER_INSIDE
    +
    +#endif /* PURPLE_IRCV3_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3capabilities.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,685 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include "purpleircv3capabilities.h"
    +
    +#include "purpleircv3connection.h"
    +#include "purpleircv3core.h"
    +#include "purpleircv3sasl.h"
    +
    +enum {
    + PROP_0,
    + PROP_CONNECTION,
    + N_PROPERTIES,
    +};
    +static GParamSpec *properties[N_PROPERTIES] = {NULL, };
    +
    +/* Windows is does something weird with signal handling that includes defining
    + * SIG_ACK. We don't care about that here, so we undef it if we find it.
    + * See https://learn.microsoft.com/en-us/cpp/c-runtime-library/signal-action-constants?view=msvc-170
    + */
    +#ifdef SIG_ACK
    +# undef SIG_ACK
    +#endif /* SIG_ACK */
    +
    +enum {
    + SIG_READY,
    + SIG_ACK,
    + SIG_NAK,
    + SIG_DONE,
    + SIG_NEW,
    + SIG_DEL,
    + N_SIGNALS,
    +};
    +static guint signals[N_SIGNALS] = {0, };
    +
    +struct _PurpleIRCv3Capabilities {
    + GObject parent;
    +
    + PurpleIRCv3Connection *connection;
    +
    + GHashTable *caps;
    + GPtrArray *requests;
    +
    + gatomicrefcount wait_counters;
    +};
    +
    +/******************************************************************************
    + * Helpers
    + *****************************************************************************/
    +static void
    +purple_ircv3_capabilities_set_connection(PurpleIRCv3Capabilities *capabilities,
    + PurpleIRCv3Connection *connection)
    +{
    + g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
    +
    + if(g_set_object(&capabilities->connection, connection)) {
    + g_object_notify_by_pspec(G_OBJECT(capabilities),
    + properties[PROP_CONNECTION]);
    + }
    +}
    +
    +static void
    +purple_ircv3_capabilities_finish(PurpleIRCv3Capabilities *capabilities) {
    + purple_ircv3_connection_writef(capabilities->connection,
    + "CAP END");
    +
    + g_signal_emit(capabilities, signals[SIG_DONE], 0);
    +}
    +
    +static void
    +purple_ircv3_capabilities_add(PurpleIRCv3Capabilities *capabilities,
    + const char *capability)
    +{
    + char *equals = g_strstr_len(capability, -1, "=");
    +
    + if(equals != NULL) {
    + char *key = g_strndup(capability, equals - capability);
    + char *value = g_strdup(equals + 1);
    +
    + g_hash_table_insert(capabilities->caps, key, value);
    + } else {
    + g_hash_table_insert(capabilities->caps, g_strdup(capability), NULL);
    + }
    +}
    +
    +/******************************************************************************
    + * Callbacks
    + *****************************************************************************/
    +static void
    +ircv3_capabilities_message_tags_ack_cb(PurpleIRCv3Capabilities *capabilities,
    + G_GNUC_UNUSED const char *capability,
    + G_GNUC_UNUSED gpointer data)
    +{
    + /* We have message tags so add the stuff we support that depends on it. */
    + purple_ircv3_capabilities_lookup_and_request(capabilities, "msgid");
    +}
    +
    +/******************************************************************************
    + * PurpleIRCv3Capabilities Implementation
    + *****************************************************************************/
    +static void
    +purple_ircv3_capabilities_default_ready_cb(PurpleIRCv3Capabilities *capabilities)
    +{
    + PurpleAccount *account = NULL;
    + PurpleConnection *purple_connection = NULL;
    +
    + purple_connection = PURPLE_CONNECTION(capabilities->connection);
    + account = purple_connection_get_account(purple_connection);
    +
    + /* Don't request the sasl capability unless the user has selected the
    + * require-password option.
    + */
    + if(purple_account_get_require_password(account)) {
    + gboolean found = FALSE;
    +
    + purple_ircv3_capabilities_lookup(capabilities, "sasl", &found);
    +
    + if(found) {
    + purple_ircv3_sasl_request(capabilities);
    + }
    + }
    +
    + /* cap-notify is implied when we use CAP LS 302, so this is really just to
    + * make sure it's requested.
    + */
    + purple_ircv3_capabilities_lookup_and_request(capabilities, "cap-notify");
    +
    + /* message-tags is used for a lot of stuff so we need to tell everyone we
    + * do in fact support it.
    + */
    + if(purple_ircv3_capabilities_lookup_and_request(capabilities,
    + "message-tags"))
    + {
    + g_signal_connect(capabilities, "ack::message-tags",
    + G_CALLBACK(ircv3_capabilities_message_tags_ack_cb),
    + NULL);
    + }
    +
    + /* The server-time capability just tells the server to send a tag to
    + * messages, so we just need to request it and then handle the tag when
    + * we're processing messages if it exists.
    + */
    + purple_ircv3_capabilities_lookup_and_request(capabilities,
    + PURPLE_IRCV3_CAPABILITY_SERVER_TIME);
    +}
    +
    +/******************************************************************************
    + * GObject Implementation
    + *****************************************************************************/
    +G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleIRCv3Capabilities,
    + purple_ircv3_capabilities, G_TYPE_OBJECT,
    + G_TYPE_FLAG_FINAL, {})
    +
    +static void
    +purple_ircv3_capabilities_get_property(GObject *obj, guint param_id,
    + GValue *value, GParamSpec *pspec)
    +{
    + PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
    +
    + switch(param_id) {
    + case PROP_CONNECTION:
    + g_value_set_object(value,
    + purple_ircv3_capabilities_get_connection(capabilities));
    + break;
    + default:
    + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    + break;
    + }
    +}
    +
    +static void
    +purple_ircv3_capabilities_set_property(GObject *obj, guint param_id,
    + const GValue *value, GParamSpec *pspec)
    +{
    + PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
    +
    + switch(param_id) {
    + case PROP_CONNECTION:
    + purple_ircv3_capabilities_set_connection(capabilities,
    + g_value_get_object(value));
    + break;
    + default:
    + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    + break;
    + }
    +}
    +
    +static void
    +purple_ircv3_capabilities_dispose(GObject *obj) {
    + PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
    +
    + g_clear_object(&capabilities->connection);
    +
    + G_OBJECT_CLASS(purple_ircv3_capabilities_parent_class)->dispose(obj);
    +}
    +
    +static void
    +purple_ircv3_capabilities_finalize(GObject *obj) {
    + PurpleIRCv3Capabilities *capabilities = PURPLE_IRCV3_CAPABILITIES(obj);
    +
    + g_hash_table_destroy(capabilities->caps);
    + g_ptr_array_free(capabilities->requests, TRUE);
    +
    + G_OBJECT_CLASS(purple_ircv3_capabilities_parent_class)->finalize(obj);
    +}
    +
    +static void
    +purple_ircv3_capabilities_init(PurpleIRCv3Capabilities *capabilities) {
    + capabilities->caps = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
    + g_free);
    + capabilities->requests = g_ptr_array_new_full(0, g_free);
    +
    + g_atomic_ref_count_init(&capabilities->wait_counters);
    +}
    +
    +static void
    +purple_ircv3_capabilities_class_finalize(G_GNUC_UNUSED PurpleIRCv3CapabilitiesClass *klass) {
    +}
    +
    +static void
    +purple_ircv3_capabilities_class_init(PurpleIRCv3CapabilitiesClass *klass) {
    + GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    +
    + obj_class->dispose = purple_ircv3_capabilities_dispose;
    + obj_class->finalize = purple_ircv3_capabilities_finalize;
    + obj_class->get_property = purple_ircv3_capabilities_get_property;
    + obj_class->set_property = purple_ircv3_capabilities_set_property;
    +
    + /**
    + * PurpleIRCv3Capabilities:connection:
    + *
    + * The PurpleIRCv3Connection object that this capabilities was created
    + * with.
    + *
    + * Since: 3.0
    + */
    + properties[PROP_CONNECTION] = g_param_spec_object(
    + "connection", "connection",
    + "The connection this capabilities was created for.",
    + PURPLE_IRCV3_TYPE_CONNECTION,
    + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
    +
    + g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    +
    + /**
    + * PurpleIRCv3Capabilities::ready:
    + * @capabilities: The instance.
    + *
    + * Emitted when @capabilities has finished receiving the list of
    + * capabilities from the server at startup.
    + *
    + * For dynamically added capabilities see the `added` and `removed`
    + * signals.
    + *
    + * Since: 3.0
    + */
    + signals[SIG_READY] = g_signal_new_class_handler(
    + "ready",
    + G_OBJECT_CLASS_TYPE(klass),
    + G_SIGNAL_RUN_LAST,
    + G_CALLBACK(purple_ircv3_capabilities_default_ready_cb),
    + NULL,
    + NULL,
    + NULL,
    + G_TYPE_NONE,
    + 0);
    +
    + /**
    + * PurpleIRCv3Capabilities::ack:
    + * @capabilities: The instance.
    + * @capability: The capability string.
    + *
    + * Emitted when the server has acknowledged a `CAP REQ` call from
    + * purple_ircv3_capabilities_request.
    + *
    + * The value of @capability will be the same as the one that was requested.
    + *
    + * Since: 3.0
    + */
    + signals[SIG_ACK] = g_signal_new_class_handler(
    + "ack",
    + G_OBJECT_CLASS_TYPE(klass),
    + G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
    + NULL,
    + NULL,
    + NULL,
    + NULL,
    + G_TYPE_NONE,
    + 1,
    + G_TYPE_STRING);
    +
    + /**
    + * PurpleIRCv3Capabilities::nak:
    + * @capabilities: The instance.
    + * @capability: The capability string.
    + *
    + * Emitted when the server has nacked a `CAP REQ` call from
    + * purple_ircv3_capabilities_request.
    + *
    + * The value of @capability will be the same as the one that was requested.
    + *
    + * Since: 3.0
    + */
    + signals[SIG_NAK] = g_signal_new_class_handler(
    + "nak",
    + G_OBJECT_CLASS_TYPE(klass),
    + G_SIGNAL_DETAILED | G_SIGNAL_RUN_LAST,
    + NULL,
    + NULL,
    + NULL,
    + NULL,
    + G_TYPE_NONE,
    + 1,
    + G_TYPE_STRING);
    +
    + /**
    + * PurpleIRCv3Capabilities::done:
    + * @capabilities: The instance.
    + *
    + * Emitted when all of the requested capabilities have been either ack'd or
    + * nak'd by the server.
    + *
    + * Since: 3.0
    + */
    + signals[SIG_DONE] = g_signal_new_class_handler(
    + "done",
    + G_OBJECT_CLASS_TYPE(klass),
    + G_SIGNAL_RUN_LAST,
    + NULL,
    + NULL,
    + NULL,
    + NULL,
    + G_TYPE_NONE,
    + 0);
    +
    + /**
    + * PurpleIRCv3Capabilities::new:
    + * @capabilities: The instance.
    + * @added: The newly added capabilities.
    + *
    + * Emitted when the server sends the `CAP NEW` command. @added is a
    + * [type@GLib.Strv] of the new capabilities the server added.
    + *
    + * There are two approaches to how you can use this signal. You can check
    + * each item in @added for the values you need and parsing their values, or
    + * you can call #purple_ircv3_capabilities_lookup to see if the
    + * capabilities you're interested in have been added.
    + *
    + * Since: 3.0
    + */
    + signals[SIG_NEW] = g_signal_new_class_handler(
    + "new",
    + G_OBJECT_CLASS_TYPE(klass),
    + G_SIGNAL_RUN_LAST,
    + NULL,
    + NULL,
    + NULL,
    + NULL,
    + G_TYPE_NONE,
    + 1,
    + G_TYPE_STRV);
    +
    + /**
    + * PurpleIRCv3Capabilities::del:
    + * @capabilities: The instance.
    + * @removed: The capabilities that were removed.
    + *
    + * Emitted when the server sends the `CAP DEL` command. @removed is a
    + * [type@GLib.Strv] of the capabilities that the server removed.
    + *
    + * There are two approaches to how you can use this signal. You can check
    + * each item in @removed for the values you care about, or you can call
    + * #purple_ircv3_capabilities_lookup to see if the capabilities you're
    + * interested in have been removed.
    + *
    + * Since: 3.0
    + */
    + signals[SIG_DEL] = g_signal_new_class_handler(
    + "del",
    + G_OBJECT_CLASS_TYPE(klass),
    + G_SIGNAL_RUN_LAST,
    + NULL,
    + NULL,
    + NULL,
    + NULL,
    + G_TYPE_NONE,
    + 1,
    + G_TYPE_STRV);
    +}
    +
    +/******************************************************************************
    + * Command handlers
    + *****************************************************************************/
    +static gboolean
    +purple_ircv3_capabilities_handle_list(PurpleIRCv3Capabilities *capabilities,
    + guint n_params,
    + GStrv params,
    + G_GNUC_UNUSED GError **error)
    +{
    + gboolean done = TRUE;
    + gchar **parts = NULL;
    +
    + /* Check if we have more messages coming. */
    + if(n_params > 1 && purple_strequal(params[0], "*")) {
    + parts = g_strsplit(params[1], " ", -1);
    + done = FALSE;
    + } else {
    + parts = g_strsplit(params[0], " ", -1);
    + }
    +
    + /* Add each capability to our hash table, splitting the keys and values. */
    + for(int i = 0; parts[i] != NULL; i++) {
    + purple_ircv3_capabilities_add(capabilities, parts[i]);
    + }
    +
    + g_strfreev(parts);
    +
    + if(done) {
    + g_signal_emit(capabilities, signals[SIG_READY], 0, signals[SIG_READY]);
    +
    + /* If no capabilities were requested after we emitted the ready signal
    + * we're done with capability negotiation.
    + */
    + if(capabilities->requests->len == 0) {
    + purple_ircv3_capabilities_remove_wait(capabilities);
    + }
    + }
    +
    + return TRUE;
    +}
    +
    +static gboolean
    +purple_ircv3_capabilities_handle_ack_nak(PurpleIRCv3Capabilities *capabilities,
    + GStrv params,
    + guint sig,
    + const char *method,
    + GError **error)
    +{
    + char *caps = g_strjoinv(" ", params);
    + guint index = 0;
    + gboolean found = FALSE;
    + gboolean ret = TRUE;
    +
    + g_signal_emit(capabilities, sig, g_quark_from_string(caps), caps);
    +
    + found = g_ptr_array_find_with_equal_func(capabilities->requests, caps,
    + g_str_equal, &index);
    + if(found) {
    + g_ptr_array_remove_index(capabilities->requests, index);
    + } else {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "received CAP %s for unknown capability %s", method, caps);
    + ret = FALSE;
    + }
    + g_free(caps);
    +
    + if(capabilities->requests->len == 0) {
    + purple_ircv3_capabilities_remove_wait(capabilities);
    + }
    +
    + return ret;
    +}
    +
    +static gboolean
    +purple_ircv3_capabilities_handle_new(PurpleIRCv3Capabilities *capabilities,
    + guint n_params,
    + GStrv params,
    + G_GNUC_UNUSED GError **error)
    +{
    + for(guint i = 0; i < n_params; i++) {
    + purple_ircv3_capabilities_add(capabilities, params[i]);
    + }
    +
    + g_signal_emit(capabilities, signals[SIG_NEW], 0, params);
    +
    + return TRUE;
    +}
    +
    +static gboolean
    +purple_ircv3_capabilities_handle_del(PurpleIRCv3Capabilities *capabilities,
    + guint n_params,
    + GStrv params,
    + G_GNUC_UNUSED GError **error)
    +{
    + for(guint i = 0; i < n_params; i++) {
    + g_hash_table_remove(capabilities->caps, params[i]);
    + }
    +
    + g_signal_emit(capabilities, signals[SIG_DEL], 0, params);
    +
    + return TRUE;
    +}
    +
    +/******************************************************************************
    + * Internal API
    + *****************************************************************************/
    +void
    +purple_ircv3_capabilities_register(GPluginNativePlugin *plugin) {
    + purple_ircv3_capabilities_register_type(G_TYPE_MODULE(plugin));
    +}
    +
    +PurpleIRCv3Capabilities *
    +purple_ircv3_capabilities_new(PurpleIRCv3Connection *connection) {
    + return g_object_new(
    + PURPLE_IRCV3_TYPE_CAPABILITIES,
    + "connection", connection,
    + NULL);
    +}
    +
    +void
    +purple_ircv3_capabilities_start(PurpleIRCv3Capabilities *capabilities) {
    + purple_ircv3_connection_writef(capabilities->connection, "CAP LS %s",
    + PURPLE_IRCV3_CAPABILITY_CAP_LS_VERSION);
    +}
    +
    +gboolean
    +purple_ircv3_capabilities_message_handler(PurpleIRCv3Message *message,
    + GError **error,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *connection = data;
    + PurpleIRCv3Capabilities *capabilities = NULL;
    + GStrv params = NULL;
    + GStrv subparams = NULL;
    + const char *subcommand = NULL;
    + guint n_params = 0;
    + guint n_subparams = 0;
    +
    + params = purple_ircv3_message_get_params(message);
    + if(params != NULL) {
    + n_params = g_strv_length(params);
    + }
    +
    + if(n_params < 2) {
    + return FALSE;
    + }
    +
    + capabilities = purple_ircv3_connection_get_capabilities(connection);
    +
    + /* Initialize some variables to make it easier to call our sub command
    + * handlers.
    + *
    + * params[0] is the nick or * if it hasn't been negotiated yet, we don't
    + * have a need for this, so we ignore it.
    + *
    + * params[1] is the CAP subcommand sent from the server. We use it here
    + * purely for dispatching to our subcommand handlers.
    + *
    + * params[2] and higher are the parameters to the subcommand. To make the
    + * code a bit easier all around, we subtract 2 from n_params to remove
    + * references to the nick and subcommand name. Like wise, we add 2 to the
    + * params GStrv which will now point to the second item in the array again
    + * ignoring the nick and subcommand.
    + */
    + subcommand = params[1];
    + n_subparams = n_params - 2;
    + subparams = params + 2;
    +
    + /* Dispatch the subcommand. */
    + if(purple_strequal(subcommand, "LS") ||
    + purple_strequal(subcommand, "LIST"))
    + {
    + return purple_ircv3_capabilities_handle_list(capabilities, n_subparams,
    + subparams, error);
    + } else if(purple_strequal(subcommand, "ACK")) {
    + return purple_ircv3_capabilities_handle_ack_nak(capabilities,
    + subparams,
    + signals[SIG_ACK],
    + "ACK",
    + error);
    + } else if(purple_strequal(subcommand, "NAK")) {
    + return purple_ircv3_capabilities_handle_ack_nak(capabilities,
    + subparams,
    + signals[SIG_NAK],
    + "NAK",
    + error);
    + } else if(purple_strequal(subcommand, "NEW")) {
    + return purple_ircv3_capabilities_handle_new(capabilities, n_subparams,
    + subparams, error);
    + } else if(purple_strequal(subcommand, "DEL")) {
    + return purple_ircv3_capabilities_handle_del(capabilities, n_subparams,
    + subparams, error);
    + }
    +
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "No handler for CAP subcommand %s", subcommand);
    +
    + return FALSE;
    +}
    +
    +/******************************************************************************
    + * Public API
    + *****************************************************************************/
    +PurpleIRCv3Connection *
    +purple_ircv3_capabilities_get_connection(PurpleIRCv3Capabilities *capabilities)
    +{
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), NULL);
    +
    + return capabilities->connection;
    +}
    +
    +void
    +purple_ircv3_capabilities_request(PurpleIRCv3Capabilities *capabilities,
    + const char *capability)
    +{
    + g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
    + g_return_if_fail(capability != NULL);
    +
    + g_ptr_array_add(capabilities->requests, g_strdup(capability));
    +
    + purple_ircv3_connection_writef(capabilities->connection, "CAP REQ :%s",
    + capability);
    +}
    +
    +const char *
    +purple_ircv3_capabilities_lookup(PurpleIRCv3Capabilities *capabilities,
    + const char *name, gboolean *found)
    +{
    + gpointer value = NULL;
    + gboolean real_found = FALSE;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), NULL);
    + g_return_val_if_fail(name != NULL, NULL);
    +
    + real_found = g_hash_table_lookup_extended(capabilities->caps, name, NULL,
    + &value);
    +
    + if(found != NULL) {
    + *found = real_found;
    + }
    +
    + return value;
    +}
    +
    +gboolean
    +purple_ircv3_capabilities_lookup_and_request(PurpleIRCv3Capabilities *capabilities,
    + const char *name)
    +{
    + gboolean found = FALSE;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities), FALSE);
    + g_return_val_if_fail(name != NULL, FALSE);
    +
    + purple_ircv3_capabilities_lookup(capabilities, name, &found);
    + if(found) {
    + purple_ircv3_capabilities_request(capabilities, name);
    + }
    +
    + return found;
    +}
    +
    +void
    +purple_ircv3_capabilities_add_wait(PurpleIRCv3Capabilities *capabilities) {
    + g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
    +
    + g_atomic_ref_count_inc(&capabilities->wait_counters);
    +}
    +
    +void
    +purple_ircv3_capabilities_remove_wait(PurpleIRCv3Capabilities *capabilities) {
    + g_return_if_fail(PURPLE_IRCV3_IS_CAPABILITIES(capabilities));
    +
    + if(g_atomic_ref_count_dec(&capabilities->wait_counters)) {
    + purple_ircv3_capabilities_finish(capabilities);
    + }
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3capabilities.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,172 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_CAPABILITIES_H
    +#define PURPLE_IRCV3_CAPABILITIES_H
    +
    +#include <glib.h>
    +#include <glib-object.h>
    +
    +#include <gplugin.h>
    +#include <gplugin-native.h>
    +
    +#include <purple.h>
    +
    +#include "purpleircv3version.h"
    +
    +G_BEGIN_DECLS
    +
    +/* https://ircv3.net/specs/extensions/capability-negotiation */
    +#define PURPLE_IRCV3_CAPABILITY_CAP_LS_VERSION "302"
    +
    +/* https://ircv3.net/specs/extensions/sasl-3.2 */
    +#define PURPLE_IRCV3_CAPABILITY_SASL "sasl"
    +
    +/* https://ircv3.net/specs/extensions/server-time */
    +#define PURPLE_IRCV3_CAPABILITY_SERVER_TIME "server-time"
    +
    +#define PURPLE_IRCV3_TYPE_CAPABILITIES (purple_ircv3_capabilities_get_type())
    +
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +G_DECLARE_FINAL_TYPE(PurpleIRCv3Capabilities, purple_ircv3_capabilities,
    + PURPLE_IRCV3, CAPABILITIES, GObject)
    +
    +#include "purpleircv3connection.h"
    +#include "purpleircv3message.h"
    +
    +/**
    + * purple_ircv3_capabilities_register: (skip)
    + * @plugin: The [class@GPlugin.NativePlugin] instance.
    + *
    + * Dynamically registers the PurpleIRCv3Capabilities type.
    + *
    + * Since: 3.0
    + */
    +G_GNUC_INTERNAL void purple_ircv3_capabilities_register(GPluginNativePlugin *plugin);
    +
    +G_GNUC_INTERNAL PurpleIRCv3Capabilities *purple_ircv3_capabilities_new(PurpleIRCv3Connection *connection);
    +
    +G_GNUC_INTERNAL void purple_ircv3_capabilities_start(PurpleIRCv3Capabilities *capabilities);
    +
    +G_GNUC_INTERNAL gboolean purple_ircv3_capabilities_message_handler(PurpleIRCv3Message *message, GError **error, gpointer data);
    +
    +/**
    + * purple_ircv3_capabilities_get_connection:
    + * @capabilities: The instance.
    + *
    + * Gets the PurpleIRCv3Connection object that @capabilities was created with.
    + *
    + * Returns: (transfer none): The connection instance.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +PurpleIRCv3Connection *purple_ircv3_capabilities_get_connection(PurpleIRCv3Capabilities *capabilities);
    +
    +/**
    + * purple_ircv3_capabilities_request:
    + * @capabilities: The instance.
    + * @capability: The capabilities to request.
    + *
    + * This method will send `CAP REQ @capability` to the server. Listen to the
    + * `::ack` and `::nak` signals which will contain the contents of @capability
    + * that was passed in here.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_capabilities_request(PurpleIRCv3Capabilities *capabilities, const char *capability);
    +
    +/**
    + * purple_ircv3_capabilities_lookup:
    + * @capabilities: The instance.
    + * @name: The name of the capability to look for.
    + * @found: (out) (nullable): A return address for a boolean on whether the
    + * capability was advertised or not.
    + *
    + * Gets the value that the @name capability provided if it was advertised. To
    + * determine if the capability was advertised use the @found parameter.
    + *
    + * Returns: The value of the capability named @name.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +const char *purple_ircv3_capabilities_lookup(PurpleIRCv3Capabilities *capabilities, const char *name, gboolean *found);
    +
    +/**
    + * purple_ircv3_capabilities_lookup_and_request:
    + * @capabilities: The instance.
    + * @name: The name of the capability to look for.
    + *
    + * A helper function to call [method@PurpleIRCv3.Capabilities.Lookup] and if
    + * found, call [method@PurpleIRCv3.Capabilities.Request].
    + *
    + * This method ignores the advertised value, so to get that you'll need to call
    + * [method@PurpleIRCv3.Capabilities.Lookup] yourself.
    + *
    + * Also if you need to do something when the server ACK's or NAK's your
    + * request, you're probably better off just using the methods yourself.
    + *
    + * Returns: %TRUE if @name was found and requested, %FALSE otherwise.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +gboolean purple_ircv3_capabilities_lookup_and_request(PurpleIRCv3Capabilities *capabilities, const char *name);
    +
    +/**
    + * purple_ircv3_capabilties_add_wait:
    + * @capabilities: The instance.
    + *
    + * Adds a wait counter to @capabilities. This counter is used to delay the
    + * call of `CAP END` until all capability negotiation has completed. This is
    + * necessary for SASL and may be necessary for other capabilities as well.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_capabilities_add_wait(PurpleIRCv3Capabilities *capabilities);
    +
    +/**
    + * purple_ircv3_capabilties_remove_wait:
    + * @capabilities: The instance.
    + *
    + * Removes a wait counter from @capabilities. Only when this counter reaches 0,
    + * will `CAP END` be called and registration completed.
    + *
    + * This is necessary for SASL and may be necessary for other capabilities as
    + * well.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_capabilities_remove_wait(PurpleIRCv3Capabilities *capabilities);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_CAPABILITIES_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3connection.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,964 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib/gi18n-lib.h>
    +
    +#include <birb.h>
    +
    +#include "purpleircv3connection.h"
    +
    +#include "purpleircv3constants.h"
    +#include "purpleircv3core.h"
    +#include "purpleircv3ctcp.h"
    +#include "purpleircv3formatting.h"
    +#include "purpleircv3parser.h"
    +
    +enum {
    + PROP_0,
    + PROP_CAPABILITIES,
    + PROP_REGISTERED,
    + N_PROPERTIES,
    +};
    +static GParamSpec *properties[N_PROPERTIES] = {NULL, };
    +
    +enum {
    + SIG_REGISTRATION_COMPLETE,
    + SIG_CTCP_REQUEST,
    + SIG_CTCP_RESPONSE,
    + N_SIGNALS,
    +};
    +static guint signals[N_SIGNALS] = {0, };
    +
    +typedef struct {
    + GSocketConnection *connection;
    +
    + gchar *server_name;
    + gboolean registered;
    +
    + GDataInputStream *input;
    + GOutputStream *output;
    +
    + PurpleIRCv3Parser *parser;
    +
    + PurpleIRCv3Capabilities *capabilities;
    +
    + PurpleConversation *status_conversation;
    +} PurpleIRCv3ConnectionPrivate;
    +
    +G_DEFINE_DYNAMIC_TYPE_EXTENDED(PurpleIRCv3Connection,
    + purple_ircv3_connection,
    + PURPLE_TYPE_CONNECTION,
    + 0,
    + G_ADD_PRIVATE_DYNAMIC(PurpleIRCv3Connection))
    +
    +/******************************************************************************
    + * Helpers
    + *****************************************************************************/
    +static void
    +purple_ircv3_connection_send_pass_command(PurpleIRCv3Connection *connection) {
    + PurpleAccount *account = NULL;
    + const char *password = NULL;
    +
    + account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    +
    + password = purple_account_get_string(account, "server-password", "");
    + if(password != NULL && *password != '\0') {
    + purple_ircv3_connection_writef(connection, "PASS %s", password);
    + }
    +}
    +
    +static void
    +purple_ircv3_connection_send_user_command(PurpleIRCv3Connection *connection) {
    + PurpleAccount *account = NULL;
    + const char *identname = NULL;
    + const char *nickname = NULL;
    + const char *realname = NULL;
    +
    + nickname =
    + purple_connection_get_display_name(PURPLE_CONNECTION(connection));
    +
    + account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    +
    + /* The stored value could be an empty string, so pass a default of empty
    + * string and then if it was empty, set our correct fallback.
    + */
    + identname = purple_account_get_string(account, "ident", "");
    + if(identname == NULL || *identname == '\0') {
    + identname = nickname;
    + }
    +
    + realname = purple_account_get_string(account, "real-name", "");
    + if(realname == NULL || *realname == '\0') {
    + realname = nickname;
    + }
    +
    + purple_ircv3_connection_writef(connection, "USER %s 0 * :%s", identname,
    + realname);
    +}
    +
    +static void
    +purple_ircv3_connection_send_nick_command(PurpleIRCv3Connection *connection) {
    + const char *nickname = NULL;
    +
    + nickname =
    + purple_connection_get_display_name(PURPLE_CONNECTION(connection));
    +
    + purple_ircv3_connection_writef(connection, "NICK %s", nickname);
    +}
    +
    +static void
    +purple_ircv3_connection_rejoin_channels(PurpleIRCv3Connection *connection) {
    + PurpleAccount *account = NULL;
    + PurpleConversationManager *manager = NULL;
    + GList *conversations = NULL;
    +
    + account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    + manager = purple_conversation_manager_get_default();
    +
    + conversations = purple_conversation_manager_get_all(manager);
    + while(conversations != NULL) {
    + PurpleConversation *conversation = conversations->data;
    + PurpleAccount *conv_account = NULL;
    +
    + conv_account = purple_conversation_get_account(conversation);
    + if(conv_account == account) {
    + const char *id = purple_conversation_get_id(conversation);
    +
    + purple_ircv3_connection_writef(connection, "%s %s",
    + PURPLE_IRCV3_MSG_JOIN, id);
    + }
    +
    + conversations = g_list_delete_link(conversations, conversations);
    + }
    +}
    +
    +/******************************************************************************
    + * Callbacks
    + *****************************************************************************/
    +static void
    +purple_ircv3_connection_read_cb(GObject *source, GAsyncResult *result,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *connection = data;
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    + GCancellable *cancellable = NULL;
    + GDataInputStream *istream = G_DATA_INPUT_STREAM(source);
    + GError *error = NULL;
    + gchar *line = NULL;
    + gsize length;
    + gboolean parsed = FALSE;
    +
    + line = g_data_input_stream_read_line_finish(istream, result, &length,
    + &error);
    + if(line == NULL || error != NULL) {
    + if(PURPLE_IS_CONNECTION(connection)) {
    + if(error == NULL) {
    + g_set_error_literal(&error, PURPLE_CONNECTION_ERROR,
    + PURPLE_CONNECTION_ERROR_NETWORK_ERROR,
    + _("Server closed the connection"));
    + } else {
    + g_prefix_error(&error, "%s", _("Lost connection with server: "));
    + }
    +
    + purple_connection_take_error(PURPLE_CONNECTION(connection), error);
    + }
    +
    + /* In the off chance that line was returned, make sure we free it. */
    + g_free(line);
    +
    + return;
    + }
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + parsed = purple_ircv3_parser_parse(priv->parser, line, &error,
    + connection);
    + if(!parsed) {
    + g_warning("failed to handle '%s': %s", line,
    + error != NULL ? error->message : "unknown error");
    + }
    + g_clear_error(&error);
    +
    + g_free(line);
    +
    + /* Call read_line_async again to continue reading lines. */
    + cancellable = purple_connection_get_cancellable(PURPLE_CONNECTION(connection));
    + g_data_input_stream_read_line_async(priv->input,
    + G_PRIORITY_DEFAULT,
    + cancellable,
    + purple_ircv3_connection_read_cb,
    + connection);
    +}
    +
    +static void
    +purple_ircv3_connection_write_cb(GObject *source, GAsyncResult *result,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *connection = data;
    + BirbQueuedOutputStream *stream = BIRB_QUEUED_OUTPUT_STREAM(source);
    + GError *error = NULL;
    + gboolean success = FALSE;
    +
    + success = birb_queued_output_stream_push_bytes_finish(stream, result,
    + &error);
    +
    + if(!success) {
    + birb_queued_output_stream_clear_queue(stream);
    +
    + g_prefix_error(&error, "%s", _("Lost connection with server: "));
    +
    + purple_connection_take_error(PURPLE_CONNECTION(connection), error);
    +
    + return;
    + }
    +}
    +
    +static void
    +purple_ircv3_connection_connected_cb(GObject *source, GAsyncResult *result,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *connection = data;
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    + GCancellable *cancellable = NULL;
    + GError *error = NULL;
    + GInputStream *istream = NULL;
    + GOutputStream *ostream = NULL;
    + GSocketClient *client = G_SOCKET_CLIENT(source);
    + GSocketConnection *conn = NULL;
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + /* Finish the async method. */
    + conn = g_socket_client_connect_to_host_finish(client, result, &error);
    + if(conn == NULL || error != NULL) {
    + g_prefix_error(&error, "%s", _("Unable to connect: "));
    +
    + purple_connection_take_error(PURPLE_CONNECTION(connection), error);
    +
    + return;
    + }
    +
    + g_message("Successfully connected to %s", priv->server_name);
    +
    + /* Save our connection and setup our input and outputs. */
    + priv->connection = conn;
    +
    + /* Create our parser. */
    + priv->parser = purple_ircv3_parser_new();
    + purple_ircv3_parser_add_default_handlers(priv->parser);
    +
    + ostream = g_io_stream_get_output_stream(G_IO_STREAM(conn));
    + priv->output = birb_queued_output_stream_new(ostream);
    +
    + istream = g_io_stream_get_input_stream(G_IO_STREAM(conn));
    + priv->input = g_data_input_stream_new(istream);
    + g_data_input_stream_set_newline_type(G_DATA_INPUT_STREAM(priv->input),
    + G_DATA_STREAM_NEWLINE_TYPE_CR_LF);
    +
    + cancellable = purple_connection_get_cancellable(PURPLE_CONNECTION(connection));
    +
    + /* Add our read callback. */
    + g_data_input_stream_read_line_async(priv->input,
    + G_PRIORITY_DEFAULT,
    + cancellable,
    + purple_ircv3_connection_read_cb,
    + connection);
    +
    + /* Send our registration commands. */
    + purple_ircv3_capabilities_start(priv->capabilities);
    + purple_ircv3_connection_send_pass_command(connection);
    + purple_ircv3_connection_send_user_command(connection);
    + purple_ircv3_connection_send_nick_command(connection);
    +}
    +
    +static void
    +purple_ircv3_connection_caps_done_cb(G_GNUC_UNUSED PurpleIRCv3Capabilities *caps,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *connection = data;
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + priv->registered = TRUE;
    +
    + g_signal_emit(connection, signals[SIG_REGISTRATION_COMPLETE], 0);
    +
    + /* Add our supported CTCP commands. */
    + purple_ircv3_ctcp_add_default_handlers(connection);
    +
    + /* Now that registration is complete, rejoin any channels that the
    + * conversation manager has for us.
    + */
    + purple_ircv3_connection_rejoin_channels(connection);
    +}
    +
    +/******************************************************************************
    + * PurpleConnection Implementation
    + *****************************************************************************/
    +static gboolean
    +purple_ircv3_connection_connect(PurpleConnection *purple_connection,
    + GError **error)
    +{
    + PurpleIRCv3Connection *connection = NULL;
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    + PurpleAccount *account = NULL;
    + GCancellable *cancellable = NULL;
    + GSocketClient *client = NULL;
    + gint default_port = PURPLE_IRCV3_DEFAULT_TLS_PORT;
    + gint port = 0;
    + gboolean use_tls = TRUE;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(purple_connection), FALSE);
    +
    + connection = PURPLE_IRCV3_CONNECTION(purple_connection);
    + priv = purple_ircv3_connection_get_instance_private(connection);
    + account = purple_connection_get_account(purple_connection);
    +
    + client = purple_gio_socket_client_new(account, error);
    + if(!G_IS_SOCKET_CLIENT(client)) {
    + if(error != NULL && *error != NULL) {
    + purple_connection_take_error(purple_connection, *error);
    + }
    +
    + return FALSE;
    + }
    +
    + /* Turn on TLS if requested. */
    + use_tls = purple_account_get_bool(account, "use-tls", TRUE);
    + g_socket_client_set_tls(client, use_tls);
    +
    + /* If TLS is not being used, set the default port to the plain port. */
    + if(!use_tls) {
    + default_port = PURPLE_IRCV3_DEFAULT_PLAIN_PORT;
    + }
    + port = purple_account_get_int(account, "port", default_port);
    +
    + cancellable = purple_connection_get_cancellable(purple_connection);
    +
    + /* Finally start the async connection. */
    + g_socket_client_connect_to_host_async(client, priv->server_name,
    + port, cancellable,
    + purple_ircv3_connection_connected_cb,
    + connection);
    +
    + g_clear_object(&client);
    +
    + return TRUE;
    +}
    +
    +static gboolean
    +purple_ircv3_connection_disconnect(PurpleConnection *purple_connection,
    + GError **error)
    +{
    + PurpleIRCv3Connection *connection = NULL;
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    + PurpleConnectionClass *parent_class = NULL;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(purple_connection), FALSE);
    +
    + /* Chain up to our parent's disconnect to do initial clean up. */
    + parent_class = PURPLE_CONNECTION_CLASS(purple_ircv3_connection_parent_class);
    + parent_class->disconnect(purple_connection, error);
    +
    + connection = PURPLE_IRCV3_CONNECTION(purple_connection);
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + /* TODO: send QUIT command. */
    +
    + if(G_IS_SOCKET_CONNECTION(priv->connection)) {
    + GInputStream *istream = G_INPUT_STREAM(priv->input);
    + GOutputStream *ostream = G_OUTPUT_STREAM(priv->output);
    +
    + purple_gio_graceful_close(G_IO_STREAM(priv->connection),
    + istream, ostream);
    + }
    +
    + g_clear_object(&priv->input);
    + g_clear_object(&priv->output);
    + g_clear_object(&priv->connection);
    +
    + return TRUE;
    +}
    +
    +static void
    +purple_ircv3_connection_registration_complete_cb(PurpleIRCv3Connection *connection) {
    + /* Don't set our connection state to connected until we've completed
    + * registration as connected implies that we can start chatting or join
    + * rooms and other "online" activities.
    + */
    + purple_connection_set_state(PURPLE_CONNECTION(connection),
    + PURPLE_CONNECTION_STATE_CONNECTED);
    +}
    +
    +/******************************************************************************
    + * Default Handlers
    + *****************************************************************************/
    +static gboolean
    +purple_ircv3_connection_ctcp_request_default_handler(G_GNUC_UNUSED PurpleIRCv3Connection *connection,
    + G_GNUC_UNUSED PurpleConversation *conversation,
    + PurpleMessage *message,
    + const char *command,
    + const char *params)
    +{
    + char *contents = NULL;
    +
    + if(!purple_strempty(params)) {
    + contents = g_strdup_printf(_("requested CTCP %s: %s"), command,
    + params);
    + } else {
    + contents = g_strdup_printf(_("requested CTCP %s"),
    + command);
    + }
    +
    + purple_message_set_contents(message, contents);
    +
    + g_clear_pointer(&contents, g_free);
    +
    + return FALSE;
    +}
    +
    +static gboolean
    +purple_ircv3_connection_ctcp_response_default_handler(G_GNUC_UNUSED PurpleIRCv3Connection *connection,
    + G_GNUC_UNUSED PurpleConversation *conversation,
    + PurpleMessage *message,
    + const char *command,
    + const char *params)
    +{
    + char *contents = NULL;
    +
    + if(!purple_strempty(params)) {
    + contents = g_strdup_printf(_("CTCP %s response: %s"), command,
    + params);
    + } else {
    + contents = g_strdup_printf(_("CTCP %s response was empty"),
    + command);
    + }
    +
    + purple_message_set_contents(message, contents);
    +
    + g_clear_pointer(&contents, g_free);
    +
    + return FALSE;
    +}
    +
    +/******************************************************************************
    + * GObject Implementation
    + *****************************************************************************/
    +static void
    +purple_ircv3_connection_get_property(GObject *obj, guint param_id,
    + GValue *value, GParamSpec *pspec)
    +{
    + PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
    +
    + switch(param_id) {
    + case PROP_CAPABILITIES:
    + g_value_set_object(value,
    + purple_ircv3_connection_get_capabilities(connection));
    + break;
    + case PROP_REGISTERED:
    + g_value_set_boolean(value,
    + purple_ircv3_connection_get_registered(connection));
    + break;
    + default:
    + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    + break;
    + }
    +}
    +
    +static void
    +purple_ircv3_connection_dispose(GObject *obj) {
    + PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + g_clear_object(&priv->input);
    + g_clear_object(&priv->output);
    + g_clear_object(&priv->connection);
    +
    + g_clear_object(&priv->capabilities);
    + g_clear_object(&priv->parser);
    + g_clear_object(&priv->status_conversation);
    +
    + G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->dispose(obj);
    +}
    +
    +static void
    +purple_ircv3_connection_finalize(GObject *obj) {
    + PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + g_clear_pointer(&priv->server_name, g_free);
    +
    + G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->finalize(obj);
    +}
    +
    +static void
    +purple_ircv3_connection_constructed(GObject *obj) {
    + PurpleIRCv3Connection *connection = PURPLE_IRCV3_CONNECTION(obj);
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    + PurpleAccount *account = NULL;
    + PurpleContactInfo *info = NULL;
    + PurpleConversationManager *conversation_manager = NULL;
    + char **userparts = NULL;
    + const char *username = NULL;
    + char *title = NULL;
    +
    + G_OBJECT_CLASS(purple_ircv3_connection_parent_class)->constructed(obj);
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    + account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    + info = PURPLE_CONTACT_INFO(account);
    +
    + title = g_strdup_printf(_("status for %s"),
    + purple_contact_info_get_username(info));
    +
    + /* Split the username into nick and server and store the values. */
    + username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
    + userparts = g_strsplit(username, "@", 2);
    + purple_connection_set_display_name(PURPLE_CONNECTION(connection),
    + userparts[0]);
    + priv->server_name = g_strdup(userparts[1]);
    +
    + /* Free the userparts vector. */
    + g_strfreev(userparts);
    +
    + /* Check if we have an existing status conversation. */
    + conversation_manager = purple_conversation_manager_get_default();
    + priv->status_conversation = purple_conversation_manager_find_with_id(conversation_manager,
    + account,
    + priv->server_name);
    +
    + if(!PURPLE_IS_CONVERSATION(priv->status_conversation)) {
    + /* Create our status conversation. */
    + priv->status_conversation = g_object_new(
    + PURPLE_TYPE_CONVERSATION,
    + "account", account,
    + "id", priv->server_name,
    + "name", priv->server_name,
    + "title", title,
    + NULL);
    +
    + purple_conversation_manager_register(conversation_manager,
    + priv->status_conversation);
    + } else {
    + /* The conversation existed, so add a reference to it. */
    + g_object_ref(priv->status_conversation);
    + }
    +
    + g_clear_pointer(&title, g_free);
    +
    + /* Finally create our objects. */
    + priv->capabilities = purple_ircv3_capabilities_new(connection);
    + g_signal_connect_object(priv->capabilities, "done",
    + G_CALLBACK(purple_ircv3_connection_caps_done_cb),
    + connection, 0);
    +}
    +
    +static void
    +purple_ircv3_connection_init(G_GNUC_UNUSED PurpleIRCv3Connection *connection) {
    +}
    +
    +static void
    +purple_ircv3_connection_class_finalize(G_GNUC_UNUSED PurpleIRCv3ConnectionClass *klass) {
    +}
    +
    +static void
    +purple_ircv3_connection_class_init(PurpleIRCv3ConnectionClass *klass) {
    + GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    + PurpleConnectionClass *connection_class = PURPLE_CONNECTION_CLASS(klass);
    +
    + obj_class->get_property = purple_ircv3_connection_get_property;
    + obj_class->constructed = purple_ircv3_connection_constructed;
    + obj_class->dispose = purple_ircv3_connection_dispose;
    + obj_class->finalize = purple_ircv3_connection_finalize;
    +
    + connection_class->connect = purple_ircv3_connection_connect;
    + connection_class->disconnect = purple_ircv3_connection_disconnect;
    +
    + /**
    + * PurpleIRCv3Connection:capabilities:
    + *
    + * The capabilities that the server supports.
    + *
    + * This is created during registration of the connection and is useful for
    + * troubleshooting or just reporting them to end users.
    + *
    + * Since: 3.0
    + */
    + properties[PROP_CAPABILITIES] = g_param_spec_object(
    + "capabilities", "capabilities",
    + "The capabilities that the server supports",
    + PURPLE_IRCV3_TYPE_CAPABILITIES,
    + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    +
    + /**
    + * PurpleIRCv3Connection:registered:
    + *
    + * Whether or not the connection has finished the registration portion of
    + * the connection.
    + *
    + * Since: 3.0
    + */
    + properties[PROP_REGISTERED] = g_param_spec_boolean(
    + "registered", "registered",
    + "Whether or not the connection has finished registration.",
    + FALSE,
    + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
    +
    + g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    +
    + /* Signals */
    +
    + /**
    + * PurpleIRCv3Connection::registration-complete:
    + * @connection: The instance.
    + *
    + * This signal is emitted after the registration process has been
    + * completed. Plugins can use this to perform additional actions before
    + * any channels are auto joined or similar.
    + *
    + * Since: 3.0
    + */
    + signals[SIG_REGISTRATION_COMPLETE] = g_signal_new_class_handler(
    + "registration-complete",
    + G_OBJECT_CLASS_TYPE(klass),
    + G_SIGNAL_RUN_LAST,
    + G_CALLBACK(purple_ircv3_connection_registration_complete_cb),
    + NULL,
    + NULL,
    + NULL,
    + G_TYPE_NONE,
    + 0);
    +
    + /**
    + * PurpleIRCv3Connection::ctcp-request:
    + * @connection: The instance.
    + * @conversation: The conversation.
    + * @message: The message.
    + * @command: The CTCP command.
    + * @params: (nullable): The CTCP parameters.
    + *
    + * This signal is emitted after a CTCP request has been received.
    + *
    + * Signal handlers should return TRUE if they fully handled the request and
    + * do not want it echoed out to the user.
    + *
    + * Handlers may also modify the message. For example, the CTCP ACTION
    + * command has its message contents replaced with just the CTCP parameters
    + * and sets the [property@Message:action] to %TRUE and then returns %TRUE.
    + *
    + * Since: 3.0
    + */
    + signals[SIG_CTCP_REQUEST] = g_signal_new_class_handler(
    + "ctcp-request",
    + G_OBJECT_CLASS_TYPE(klass),
    + G_SIGNAL_RUN_LAST,
    + G_CALLBACK(purple_ircv3_connection_ctcp_request_default_handler),
    + g_signal_accumulator_true_handled,
    + NULL,
    + NULL,
    + G_TYPE_BOOLEAN,
    + 4,
    + PURPLE_TYPE_CONVERSATION,
    + PURPLE_TYPE_MESSAGE,
    + G_TYPE_STRING,
    + G_TYPE_STRING);
    +
    + /**
    + * PurpleIRCv3Connection::ctcp-response:
    + * @connection: The instance.
    + * @conversation: The conversation.
    + * @message: The message.
    + * @command: The CTCP command.
    + * @params: (nullable): The CTCP parameters.
    + *
    + * This signal is emitted after a CTCP response has been received.
    + *
    + * Signal handlers should return TRUE if they fully handled the response
    + * and do not want it echoed out to the user.
    + *
    + * Handlers may modify @conversation or @message to depending on their
    + * needs.
    + *
    + * Since: 3.0
    + */
    + signals[SIG_CTCP_RESPONSE] = g_signal_new_class_handler(
    + "ctcp-response",
    + G_OBJECT_CLASS_TYPE(klass),
    + G_SIGNAL_RUN_LAST,
    + G_CALLBACK(purple_ircv3_connection_ctcp_response_default_handler),
    + g_signal_accumulator_true_handled,
    + NULL,
    + NULL,
    + G_TYPE_BOOLEAN,
    + 4,
    + PURPLE_TYPE_CONVERSATION,
    + PURPLE_TYPE_MESSAGE,
    + G_TYPE_STRING,
    + G_TYPE_STRING);
    +}
    +
    +/******************************************************************************
    + * Internal API
    + *****************************************************************************/
    +void
    +purple_ircv3_connection_register(GPluginNativePlugin *plugin) {
    + GObjectClass *hack = NULL;
    +
    + purple_ircv3_connection_register_type(G_TYPE_MODULE(plugin));
    +
    + /* Without this hack we get some warnings about no reference on this type
    + * when generating the gir file. However, there are no changes to the
    + * generated gir file with or without this hack, so this is purely to get
    + * rid of the warnings.
    + *
    + * - GK 2023-08-21
    + */
    + hack = g_type_class_ref(purple_ircv3_connection_get_type());
    + g_type_class_unref(hack);
    +}
    +
    +gboolean
    +purple_ircv3_connection_emit_ctcp_request(PurpleIRCv3Connection *connection,
    + PurpleConversation *conversation,
    + PurpleMessage *message,
    + const char *command,
    + const char *parameters)
    +{
    + gboolean ret = FALSE;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
    + g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
    + g_return_val_if_fail(!purple_strempty(command), FALSE);
    +
    + g_signal_emit(connection, signals[SIG_CTCP_REQUEST], 0, conversation,
    + message, command, parameters, &ret);
    +
    + return ret;
    +}
    +
    +gboolean
    +purple_ircv3_connection_emit_ctcp_response(PurpleIRCv3Connection *connection,
    + PurpleConversation *conversation,
    + PurpleMessage *message,
    + const char *command,
    + const char *parameters)
    +{
    + gboolean ret = FALSE;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
    + g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
    + g_return_val_if_fail(!purple_strempty(command), FALSE);
    +
    + g_signal_emit(connection, signals[SIG_CTCP_RESPONSE], 0, conversation,
    + message, command, parameters, &ret);
    +
    + return ret;
    +}
    +
    +/******************************************************************************
    + * Public API
    + *****************************************************************************/
    +void
    +purple_ircv3_connection_writef(PurpleIRCv3Connection *connection,
    + const char *format, ...)
    +{
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    + GBytes *bytes = NULL;
    + GCancellable *cancellable = NULL;
    + GString *msg = NULL;
    + va_list vargs;
    +
    + g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
    + g_return_if_fail(format != NULL);
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + /* Create our string and append our format to it. */
    + msg = g_string_new("");
    +
    + va_start(vargs, format);
    + g_string_vprintf(msg, format, vargs);
    + va_end(vargs);
    +
    + /* Next add the trailing carriage return line feed. */
    + g_string_append(msg, "\r\n");
    +
    + /* Finally turn the string into bytes and send it! */
    + bytes = g_string_free_to_bytes(msg);
    +
    + cancellable = purple_connection_get_cancellable(PURPLE_CONNECTION(connection));
    + birb_queued_output_stream_push_bytes_async(BIRB_QUEUED_OUTPUT_STREAM(priv->output),
    + bytes,
    + G_PRIORITY_DEFAULT,
    + cancellable,
    + purple_ircv3_connection_write_cb,
    + connection);
    +
    + g_bytes_unref(bytes);
    +}
    +
    +PurpleIRCv3Capabilities *
    +purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection) {
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + return priv->capabilities;
    +}
    +
    +gboolean
    +purple_ircv3_connection_get_registered(PurpleIRCv3Connection *connection) {
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + return priv->registered;
    +}
    +
    +void
    +purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection,
    + PurpleIRCv3Message *v3_message)
    +{
    + PurpleIRCv3ConnectionPrivate *priv = NULL;
    + PurpleMessage *message = NULL;
    + GString *str = NULL;
    + GStrv params = NULL;
    + char *stripped = NULL;
    + const char *command = NULL;
    +
    + g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
    + g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(v3_message));
    +
    + priv = purple_ircv3_connection_get_instance_private(connection);
    +
    + command = purple_ircv3_message_get_command(v3_message);
    +
    + str = g_string_new(command);
    +
    + params = purple_ircv3_message_get_params(v3_message);
    + if(params != NULL && params[0] != NULL) {
    + char *joined = g_strjoinv(" ", params);
    +
    + g_string_append_printf(str, " %s", joined);
    +
    + g_free(joined);
    + }
    +
    + stripped = purple_ircv3_formatting_strip(str->str);
    + g_string_free(str, TRUE);
    +
    + message = g_object_new(
    + PURPLE_TYPE_MESSAGE,
    + "author", purple_ircv3_message_get_source(v3_message),
    + "contents", stripped,
    + NULL);
    + g_free(stripped);
    +
    + purple_conversation_write_message(priv->status_conversation, message);
    +
    + g_clear_object(&message);
    +}
    +
    +gboolean
    +purple_ircv3_connection_is_channel(PurpleIRCv3Connection *connection,
    + const char *id)
    +{
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    + g_return_val_if_fail(id != NULL, FALSE);
    +
    + return (id[0] == '#');
    +}
    +
    +PurpleConversation *
    +purple_ircv3_connection_find_or_create_conversation(PurpleIRCv3Connection *connection,
    + const char *id)
    +{
    + PurpleAccount *account = NULL;
    + PurpleConversation *conversation = NULL;
    + PurpleConversationManager *manager = NULL;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), NULL);
    + g_return_val_if_fail(id != NULL, NULL);
    +
    + account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    + manager = purple_conversation_manager_get_default();
    + conversation = purple_conversation_manager_find_with_id(manager, account,
    + id);
    +
    + if(!PURPLE_IS_CONVERSATION(conversation)) {
    + PurpleConversationType type = PurpleConversationTypeDM;
    +
    + if(purple_ircv3_connection_is_channel(connection, id)) {
    + type = PurpleConversationTypeChannel;
    + }
    +
    + conversation = g_object_new(
    + PURPLE_TYPE_CONVERSATION,
    + "account", account,
    + "id", id,
    + "name", id,
    + "type", type,
    + NULL);
    +
    + purple_conversation_manager_register(manager, conversation);
    +
    + /* The manager creates its own reference on our new conversation, so we
    + * borrow it like we do above if it already exists.
    + */
    + g_object_unref(conversation);
    + }
    +
    + return conversation;
    +}
    +
    +PurpleContact *
    +purple_ircv3_connection_find_or_create_contact(PurpleIRCv3Connection *connection,
    + const char *nick)
    +{
    + PurpleAccount *account = NULL;
    + PurpleContact *contact = NULL;
    + PurpleContactManager *manager = NULL;
    +
    + account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    + manager = purple_contact_manager_get_default();
    + contact = purple_contact_manager_find_with_id(manager, account, nick);
    +
    + if(!PURPLE_IS_CONTACT(contact)) {
    + contact = purple_contact_new(account, nick);
    + purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact), nick);
    +
    + purple_contact_manager_add(manager, contact);
    + }
    +
    + return contact;
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3connection.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,222 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_CONNECTION_H
    +#define PURPLE_IRCV3_CONNECTION_H
    +
    +#include <glib.h>
    +#include <glib-object.h>
    +
    +#include <gplugin.h>
    +#include <gplugin-native.h>
    +
    +#include <purple.h>
    +
    +#include "purpleircv3version.h"
    +
    +G_BEGIN_DECLS
    +
    +#define PURPLE_IRCV3_TYPE_CONNECTION (purple_ircv3_connection_get_type())
    +
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +G_DECLARE_DERIVABLE_TYPE(PurpleIRCv3Connection, purple_ircv3_connection,
    + PURPLE_IRCV3, CONNECTION, PurpleConnection)
    +
    +#include "purpleircv3capabilities.h"
    +#include "purpleircv3message.h"
    +
    +struct _PurpleIRCv3ConnectionClass {
    + /*< private >*/
    + PurpleConnectionClass parent;
    +
    + /*< private >*/
    + gpointer reserved[8];
    +};
    +
    +/**
    + * purple_ircv3_connection_register: (skip)
    + * @plugin: The GTypeModule
    + *
    + * Registers the dynamic type using @plugin.
    + *
    + * Since: 3.0
    + */
    +G_GNUC_INTERNAL void purple_ircv3_connection_register(GPluginNativePlugin *plugin);
    +
    +/**
    + * purple_ircv3_connection_emit_ctcp_request:
    + * @connection: The instance.
    + * @conversation: The conversation.
    + * @message: The message.
    + * @command: The CTCP command.
    + * @parameters: (nullable): The CTCP parameters.
    + *
    + * Emits the [signal@Connection:ctcp-request] signal with the given @command
    + * and @parameters which originated from @message in @conversation.
    + *
    + * The message may be modified by a signal handler. For example, the default
    + * handler will output an internationalized string that describes what command
    + * was requested.
    + *
    + * Returns: %TRUE if the request was handled and the message should not be
    + * echoed, otherwise %FALSE.
    + *
    + * Since: 3.0
    + */
    +G_GNUC_INTERNAL gboolean purple_ircv3_connection_emit_ctcp_request(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message, const char *command, const char *parameters);
    +
    +/**
    + * purple_ircv3_connection_emit_ctcp_response:
    + * @connection: The instance.
    + * @conversation: The conversation.
    + * @message: The message.
    + * @command: The CTCP command.
    + * @parameters: (nullable): The CTCP parameters.
    + *
    + * Emits the [signal@Connection:ctcp-response] signal with the given @command
    + * and @parameters which originated from @message in @conversation.
    + *
    + * The message may be modified by a signal handler. For example, the default
    + * handler will output an internationalized string that describes what the
    + * response was.
    + *
    + * Returns: %TRUE if the request was handled and the message should not be
    + * echoed, otherwise %FALSE.
    + *
    + * Since: 3.0
    + */
    +G_GNUC_INTERNAL gboolean purple_ircv3_connection_emit_ctcp_response(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message, const char *command, const char *parameters);
    +
    +/**
    + * purple_ircv3_connection_writef:
    + * @connection: The instance.
    + * @format: The format string.
    + * @...: The arguments for @format.
    + *
    + * Similar to C `printf()` but writes the format string out to @connection.
    + *
    + * This will add the proper line termination, so you do not need to worry about
    + * that.
    + */
    +void purple_ircv3_connection_writef(PurpleIRCv3Connection *connection, const char *format, ...) G_GNUC_PRINTF(2, 3);
    +
    +/**
    + * purple_ircv3_connection_get_capabilities:
    + * @connection: The instance.
    + *
    + * Gets the list of capabilities that the server supplied during registration.
    + *
    + * Returns: (transfer none): The list of capabilities that the server supports.
    + */
    +PurpleIRCv3Capabilities *purple_ircv3_connection_get_capabilities(PurpleIRCv3Connection *connection);
    +
    +/**
    + * purple_ircv3_connection_get_registered:
    + * @connection: The instance.
    + *
    + * Gets whether or not the connection has finished the registration process.
    + *
    + * Returns: %TRUE if registration has been completed otherwise %FALSE.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +gboolean purple_ircv3_connection_get_registered(PurpleIRCv3Connection *connection);
    +
    +/**
    + * purple_ircv3_connection_add_status_message:
    + * @connection: The instance.
    + * @message: The message.
    + *
    + * Adds a message to the status conversation/window for @connection.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_connection_add_status_message(PurpleIRCv3Connection *connection, PurpleIRCv3Message *message);
    +
    +/**
    + * purple_ircv3_connection_is_channel:
    + * @connection: The instance.
    + * @id: The id to check.
    + *
    + * Checks if @id is a channel.
    + *
    + * Right now this just checks if @id starts with a `#` but in the future this
    + * will be updated to check all channel prefixes that the connection supports.
    + *
    + * Returns: %TRUE if @id is a channel otherwise %FALSE.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +gboolean purple_ircv3_connection_is_channel(PurpleIRCv3Connection *connection, const char *id);
    +
    +/**
    + * purple_ircv3_connection_find_or_create_conversation:
    + * @connection: The instance.
    + * @id: The id of the conversation.
    + *
    + * Looks for an existing conversation belonging to @connection and returns it
    + * if found. If not found a new conversation will be created.
    + *
    + * This will only ever return %NULL if @connection is invalid or @id is %NULL.
    + *
    + * Note that ownership of the conversation remains with the default
    + * [class@Purple.ConversationManager].
    + *
    + * Returns: (transfer none) (nullable): The conversation.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +PurpleConversation *purple_ircv3_connection_find_or_create_conversation(PurpleIRCv3Connection *connection, const char *id);
    +
    +/**
    + * purple_ircv3_connection_find_or_create_contact:
    + * @connection: The instance.
    + * @nick: The nickname of the user.
    + *
    + * Looks for an existing contact belonging to @connection and returns it if
    + * found. If not a new contact will be created.
    + *
    + * This will only ever return %NULL if @connection is invalid or @nick is
    + * %NULL.
    + *
    + * Note that the ownership of the contact remains with the default
    + * [class@Purple.ContactManager].
    + *
    + * Returns: (transfer none) (nullable): The contact.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +PurpleContact *purple_ircv3_connection_find_or_create_contact(PurpleIRCv3Connection *connection, const char *nick);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_CONNECTION_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3constants.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,328 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_CONSTANTS_H
    +#define PURPLE_IRCV3_CONSTANTS_H
    +
    +/**
    + * PURPLE_IRCV3_CTCP_ACTION:
    + *
    + * A constant representing the CTCP ACTION command.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_CTCP_ACTION ("ACTION") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_CTCP_DELIMITER:
    + *
    + * The delimiter used for CTCP messages.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_CTCP_DELIMITER (0x1) PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_CTCP_VERSION:
    + *
    + * A constant representing the CTCP VERSION command.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_CTCP_VERSION ("VERSION") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_ERR_NICKLOCKED:
    + *
    + * A constant for the IRC %NICKLOCKED error.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_ERR_NICKLOCKED ("902") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_ERR_SASLABORTED:
    + *
    + * A constant for the IRC %SASLABORTED error.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_ERR_SASLABORTED ("906") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_ERR_SASLALREADY:
    + *
    + * A constant for the IRC %SASLALREADY error.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_ERR_SASLALREADY ("907") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_ERR_SASLFAIL:
    + *
    + * A constant for the IRC %SASLFAIL error.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_ERR_SASLFAIL ("904") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_ERR_SASLTOOLONG:
    + *
    + * A constant for the IRC %SASLTOOLONG error.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_ERR_SASLTOOLONG ("905") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_MSG_AUTHENTICATE:
    + *
    + * A constant for the IRC %AUTHENTICATE message.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_MSG_AUTHENTICATE ("AUTHENTICATE") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_MSG_CAP:
    + *
    + * A constant for the IRC %CAP message.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_MSG_CAP ("CAP") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_MSG_JOIN:
    + *
    + * A constant for the IRC %JOIN message.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_MSG_JOIN ("JOIN") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_MSG_NOTICE:
    + *
    + * A constant for the IRC %NOTICE message.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_MSG_NOTICE ("NOTICE") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_MSG_PING:
    + *
    + * A constant for the IRC %PING message.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_MSG_PING ("PING") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_MSG_PRIVMSG:
    + *
    + * A constant for the IRC %PRIVMSG message.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_MSG_PRIVMSG ("PRIVMSG") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_MSG_TOPIC:
    + *
    + * A constant for the IRC %TOPIC message.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_MSG_TOPIC ("TOPIC") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_CREATED:
    + *
    + * A constant for the IRC %CREATED reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_CREATED ("003") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_ENDOFMOTD:
    + *
    + * A constant for the IRC %ENDOFMOTD reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_ENDOFMOTD ("376") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_LOGGEDIN:
    + *
    + * A constant for the IRC %LOGGEDIN reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_LOGGEDIN ("900") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_LOGGEDOUT:
    + *
    + * A constant for the IRC %LOGGEDOUT reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_LOGGEDOUT ("901") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_LUSERCHANNELS:
    + *
    + * A constant for the IRC %LUSERCHANNELS reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_LUSERCHANNELS ("254") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_LUSERCLIENT:
    + *
    + * A constant for the IRC %LUSERCLIENT reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_LUSERCLIENT ("251") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_LUSERME:
    + *
    + * A constant for the IRC %LUSERME reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_LUSERME ("255") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_LUSEROP:
    + *
    + * A constant for the IRC %LUSEROP reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_LUSEROP ("252") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_LUSERUNKNOWN:
    + *
    + * A constant for the IRC %LUSERUNKNOWN reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_LUSERUNKNOWN ("253") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_MYINFO:
    + *
    + * A constant for the IRC %MYINFO reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_MYINFO ("004") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_MOTD:
    + *
    + * A constant for the IRC %MOTD reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_MOTD ("372") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_MOTDSTART:
    + *
    + * A constant for the IRC %MOTDSTART reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_MOTDSTART ("375") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_NOTOPIC:
    + *
    + * A constant for the IRC %NOTOPIC reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_NOTOPIC ("331") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_SASLMECHS:
    + *
    + * A constant for the IRC %SASLMECHS reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_SASLMECHS ("908") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_SASLSUCCESS:
    + *
    + * A constant for the IRC %SASLSUCCESS reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_SASLSUCCESS ("903") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_TOPIC:
    + *
    + * A constant for the IRC %TOPIC reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_TOPIC ("332") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_WELCOME:
    + *
    + * A constant for the IRC %WELCOME reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_WELCOME ("001") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +/**
    + * PURPLE_IRCV3_RPL_YOURHOST:
    + *
    + * A constant for the IRC %YOURHOST reply.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_RPL_YOURHOST ("002") PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +
    +#endif /* PURPLE_IRCV3_CONSTANTS_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3core.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,128 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib.h>
    +#include <glib/gi18n-lib.h>
    +
    +#include <gplugin.h>
    +#include <gplugin-native.h>
    +
    +#include <purple.h>
    +
    +#include "purpleircv3core.h"
    +
    +#include "purpleircv3capabilities.h"
    +#include "purpleircv3connection.h"
    +#include "purpleircv3protocol.h"
    +
    +/******************************************************************************
    + * Globals
    + *****************************************************************************/
    +static PurpleProtocol *ircv3_protocol = NULL;
    +
    +/******************************************************************************
    + * GPlugin Exports
    + *****************************************************************************/
    +static GPluginPluginInfo *
    +purple_ircv3_query(G_GNUC_UNUSED GError **error) {
    + PurplePluginInfoFlags flags = PURPLE_PLUGIN_INFO_FLAGS_INTERNAL |
    + PURPLE_PLUGIN_INFO_FLAGS_AUTO_LOAD;
    + const gchar * const authors[] = {
    + "Pidgin Developers <devel@pidgin.im>",
    + NULL
    + };
    +
    + return purple_plugin_info_new(
    + "id", "prpl-ircv3",
    + "name", "IRCv3 Protocol",
    + "authors", authors,
    + "version", DISPLAY_VERSION,
    + "category", N_("Protocol"),
    + "summary", N_("IRCv3 Protocol Plugin"),
    + "description", N_("Modern IRC Support"),
    + "website", PURPLE_WEBSITE,
    + "abi-version", PURPLE_ABI_VERSION,
    + "flags", flags,
    + "bind-global", TRUE,
    + NULL);
    +}
    +
    +static gboolean
    +purple_ircv3_load(GPluginPlugin *plugin, GError **error) {
    + PurpleProtocolManager *manager = NULL;
    +
    + if(PURPLE_IS_PROTOCOL(ircv3_protocol)) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "plugin was not cleaned up properly");
    +
    + return FALSE;
    + }
    +
    + purple_ircv3_capabilities_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    + purple_ircv3_connection_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    + purple_ircv3_protocol_register(GPLUGIN_NATIVE_PLUGIN(plugin));
    +
    + ircv3_protocol = purple_ircv3_protocol_new();
    +
    + manager = purple_protocol_manager_get_default();
    + /* Manager can be NULL when we're generating the GIR stuff. */
    + if(PURPLE_IS_PROTOCOL_MANAGER(manager)) {
    + if(!purple_protocol_manager_register(manager, ircv3_protocol, error)) {
    + g_clear_object(&ircv3_protocol);
    +
    + return FALSE;
    + }
    + }
    +
    + return TRUE;
    +}
    +
    +static gboolean
    +purple_ircv3_unload(G_GNUC_UNUSED GPluginPlugin *plugin,
    + G_GNUC_UNUSED gboolean shutdown,
    + GError **error)
    +{
    + PurpleProtocolManager *manager = NULL;
    +
    + if(!PURPLE_IS_PROTOCOL(ircv3_protocol)) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "plugin was not setup properly");
    +
    + return FALSE;
    + }
    +
    + manager = purple_protocol_manager_get_default();
    + /* Manager can be NULL when we're generating the GIR stuff. */
    + if(PURPLE_IS_PROTOCOL_MANAGER(manager)) {
    + if(!purple_protocol_manager_unregister(manager, ircv3_protocol,
    + error))
    + {
    + return FALSE;
    + }
    + }
    +
    + g_clear_object(&ircv3_protocol);
    +
    + return TRUE;
    +}
    +
    +GPLUGIN_NATIVE_PLUGIN_DECLARE(purple_ircv3)
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3core.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,39 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_CORE_H
    +#define PURPLE_IRCV3_CORE_H
    +
    +#include <glib.h>
    +
    +#define PURPLE_IRCV3_DEFAULT_SERVER "irc.libera.chat"
    +#define PURPLE_IRCV3_DEFAULT_PLAIN_PORT 6667
    +#define PURPLE_IRCV3_DEFAULT_TLS_PORT 6697
    +
    +#define PURPLE_IRCV3_DOMAIN (g_quark_from_static_string("ircv3-plugin"))
    +
    +#endif /* PURPLE_IRCV3_CORE_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3ctcp.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,160 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib/gi18n-lib.h>
    +
    +#include "purpleircv3ctcp.h"
    +
    +#include "purpleircv3constants.h"
    +#include "purpleircv3source.h"
    +
    +/******************************************************************************
    + * Helpers
    + *****************************************************************************/
    +static void
    +purple_ircv3_ctcp_respond(PurpleIRCv3Connection *connection,
    + PurpleMessage *message,
    + const char *response)
    +{
    + const char *author = NULL;
    + char *nick = NULL;
    +
    + author = purple_message_get_author(message);
    + purple_ircv3_source_parse(author, &nick, NULL, NULL);
    + if(!purple_strempty(nick)) {
    + PurpleContact *contact = NULL;
    +
    + contact = purple_ircv3_connection_find_or_create_contact(connection,
    + nick);
    +
    + if(PURPLE_IS_CONTACT(contact)) {
    + const char *id = NULL;
    +
    + id = purple_contact_info_get_id(PURPLE_CONTACT_INFO(contact));
    +
    + purple_ircv3_connection_writef(connection, "%s %s :%s",
    + PURPLE_IRCV3_MSG_NOTICE,
    + id,
    + response);
    + }
    + }
    +
    + g_clear_pointer(&nick, g_free);
    +}
    +
    +/******************************************************************************
    + * Callbacks
    + *****************************************************************************/
    +static gboolean
    +purple_ircv3_ctcp_handler(PurpleIRCv3Connection *connection,
    + PurpleConversation *conversation,
    + PurpleMessage *message,
    + const char *command,
    + const char *params,
    + G_GNUC_UNUSED gpointer data)
    +{
    + if(purple_strequal(command, PURPLE_IRCV3_CTCP_ACTION)) {
    + purple_message_set_contents(message, params);
    + purple_message_set_action(message, TRUE);
    +
    + purple_conversation_write_message(conversation, message);
    +
    + return TRUE;
    + } else if(purple_strequal(command, PURPLE_IRCV3_CTCP_VERSION)) {
    + purple_ircv3_ctcp_respond(connection, message, "Purple3 IRCv3");
    + }
    +
    + return FALSE;
    +}
    +
    +/******************************************************************************
    + * Internal API
    + *****************************************************************************/
    +gboolean
    +purple_ircv3_ctcp_handle(PurpleIRCv3Connection *connection,
    + PurpleConversation *conversation,
    + PurpleMessage *message)
    +{
    + PurpleMessageFlags flags;
    + char *command = NULL;
    + char *params = NULL;
    + char *ptr = NULL;
    + const char *contents = NULL;
    + gboolean ret = FALSE;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection), FALSE);
    + g_return_val_if_fail(PURPLE_IS_CONVERSATION(conversation), FALSE);
    + g_return_val_if_fail(PURPLE_IS_MESSAGE(message), FALSE);
    +
    + contents = purple_message_get_contents(message);
    + if(contents == NULL || contents[0] != PURPLE_IRCV3_CTCP_DELIMITER) {
    + return FALSE;
    + }
    +
    + /* Move past the delimiter. */
    + contents += 1;
    +
    + /* Find the delimiter for the command. */
    + ptr = g_strstr_len(contents, -1, " ");
    +
    + /* If we don't have a space, then we have no parameters. */
    + if(ptr == NULL) {
    + size_t len = strlen(contents);
    +
    + command = g_strdup(contents);
    + if(command[len - 1] == PURPLE_IRCV3_CTCP_DELIMITER) {
    + command[len - 1] = '\0';
    + }
    + } else {
    + size_t len = 0;
    +
    + command = g_strndup(contents, ptr - contents);
    +
    + params = g_strdup(ptr + 1);
    + len = strlen(params);
    + if(params[len - 1] == PURPLE_IRCV3_CTCP_DELIMITER) {
    + params[len - 1] = '\0';
    + }
    + }
    +
    + flags = purple_message_get_flags(message);
    + if(flags & PURPLE_MESSAGE_NOTIFY) {
    + ret = purple_ircv3_connection_emit_ctcp_response(connection,
    + conversation, message,
    + command, params);
    + } else {
    + ret = purple_ircv3_connection_emit_ctcp_request(connection, conversation,
    + message, command,
    + params);
    + }
    +
    + g_clear_pointer(&command, g_free);
    + g_clear_pointer(&params, g_free);
    +
    + return ret;
    +}
    +
    +void
    +purple_ircv3_ctcp_add_default_handlers(PurpleIRCv3Connection *connection) {
    + g_signal_connect(connection, "ctcp-request",
    + G_CALLBACK(purple_ircv3_ctcp_handler), NULL);
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3ctcp.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,68 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_CTCP_H
    +#define PURPLE_IRCV3_CTCP_H
    +
    +#include <glib.h>
    +#include <glib-object.h>
    +
    +#include <purple.h>
    +
    +#include "purpleircv3connection.h"
    +
    +G_BEGIN_DECLS
    +
    +/**
    + * purple_ircv3_ctcp_handle: (skip)
    + * @connection: The connection.
    + * @conversation: The conversation.
    + * @message: The message.
    + *
    + * Check if @message is a CTCP message and handles it accordingly.
    + *
    + * Returns: %TRUE if the message was a CTCP message and %FALSE otherwise.
    + *
    + * Since: 3.0
    + */
    +G_GNUC_INTERNAL
    +gboolean purple_ircv3_ctcp_handle(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message);
    +
    +/**
    + * purple_ircv3_ctcp_add_default_handlers: (skip)
    + * @connection: The connection.
    + *
    + * Adds handlers for the CTCP commands that we support directly.
    + *
    + * Since: 3.0
    + */
    +G_GNUC_INTERNAL
    +void purple_ircv3_ctcp_add_default_handlers(PurpleIRCv3Connection *connection);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_CTCP_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3formatting.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,118 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include "purpleircv3formatting.h"
    +
    +#define IRCV3_FORMAT_BOLD (0x02)
    +#define IRCV3_FORMAT_COLOR (0x03)
    +#define IRCV3_FORMAT_HEX_COLOR (0x04)
    +#define IRCV3_FORMAT_ITALIC (0x1d)
    +#define IRCV3_FORMAT_MONOSPACE (0x11)
    +#define IRCV3_FORMAT_RESET (0x0f)
    +#define IRCV3_FORMAT_REVERSE (0x16)
    +#define IRCV3_FORMAT_STRIKETHROUGH (0x1e)
    +#define IRCV3_FORMAT_UNDERLINE (0x1f)
    +
    +/******************************************************************************
    + * Helpers
    + *****************************************************************************/
    +static inline gboolean
    +purple_ircv3_formatting_is_hex_color(const char *text) {
    + for(int i = 0; i < 6; i++) {
    + if(text[i] == '\0') {
    + return FALSE;
    + }
    +
    + if(!g_ascii_isxdigit(text[i])) {
    + return FALSE;
    + }
    + }
    +
    + return TRUE;
    +}
    +
    +/******************************************************************************
    + * Public API
    + *****************************************************************************/
    +char *
    +purple_ircv3_formatting_strip(const char *text) {
    + GString *str = NULL;
    +
    + /* We don't use purple_strempty here because if we're passed an empty
    + * string, we should return a newly allocated empty string.
    + */
    + if(text == NULL) {
    + return NULL;
    + }
    +
    + str = g_string_new("");
    +
    + for(int i = 0; text[i] != '\0'; i++) {
    + switch(text[i]) {
    + case IRCV3_FORMAT_BOLD:
    + case IRCV3_FORMAT_ITALIC:
    + case IRCV3_FORMAT_MONOSPACE:
    + case IRCV3_FORMAT_RESET:
    + case IRCV3_FORMAT_REVERSE:
    + case IRCV3_FORMAT_STRIKETHROUGH:
    + case IRCV3_FORMAT_UNDERLINE:
    + continue;
    + break;
    + case IRCV3_FORMAT_COLOR:
    + if(g_ascii_isdigit(text[i + 1])) {
    + i += 1;
    +
    + if(g_ascii_isdigit(text[i + 1])) {
    + i += 1;
    + }
    +
    + if(text[i + 1] == ',' && g_ascii_isdigit(text[i + 2])) {
    + i += 2;
    +
    + if(g_ascii_isdigit(text[i + 1])) {
    + i += 1;
    + }
    + }
    + }
    +
    + break;
    + case IRCV3_FORMAT_HEX_COLOR:
    + if(purple_ircv3_formatting_is_hex_color(&text[i + 1])) {
    + i += 6;
    + }
    +
    + if(text[i + 1] == ',' &&
    + purple_ircv3_formatting_is_hex_color(&text[i + 2]))
    + {
    + i += 7;
    + }
    +
    + break;
    + default:
    + g_string_append_c(str, text[i]);
    + break;
    + }
    +
    + }
    +
    + return g_string_free(str, FALSE);
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3formatting.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,52 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_FORMATTING_H
    +#define PURPLE_IRCV3_FORMATTING_H
    +
    +#include <glib.h>
    +
    +#include "purpleircv3version.h"
    +
    +G_BEGIN_DECLS
    +
    +/**
    + * purple_ircv3_formatting_strip:
    + * @text: (nullable): The text to strip.
    + *
    + * Removes all formatting from @text and returns the result.
    + *
    + * Returns: (transfer full) (nullable): The result of stripping @text.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +char *purple_ircv3_formatting_strip(const char *text);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_FORMATTING_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3message.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,284 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib/gi18n-lib.h>
    +
    +#include "purpleircv3message.h"
    +
    +enum {
    + PROP_0,
    + PROP_COMMAND,
    + PROP_SOURCE,
    + PROP_PARAMS,
    + PROP_TAGS,
    + N_PROPERTIES,
    +};
    +static GParamSpec *properties[N_PROPERTIES] = {NULL, };
    +
    +struct _PurpleIRCv3Message {
    + GObject parent;
    +
    + char *command;
    + GStrv params;
    + char *source;
    + GHashTable *tags;
    +};
    +
    +/******************************************************************************
    + * GObject Implementation
    + *****************************************************************************/
    +G_DEFINE_FINAL_TYPE(PurpleIRCv3Message, purple_ircv3_message, G_TYPE_OBJECT)
    +
    +static void
    +purple_ircv3_message_finalize(GObject *obj) {
    + PurpleIRCv3Message *message = PURPLE_IRCV3_MESSAGE(obj);
    +
    + g_clear_pointer(&message->command, g_free);
    + g_clear_pointer(&message->params, g_strfreev);
    + g_clear_pointer(&message->source, g_free);
    + g_clear_pointer(&message->tags, g_hash_table_unref);
    +
    + G_OBJECT_CLASS(purple_ircv3_message_parent_class)->finalize(obj);
    +}
    +
    +static void
    +purple_ircv3_message_get_property(GObject *obj, guint param_id, GValue *value,
    + GParamSpec *pspec)
    +{
    + PurpleIRCv3Message *message = PURPLE_IRCV3_MESSAGE(obj);
    +
    + switch(param_id) {
    + case PROP_COMMAND:
    + g_value_set_string(value, purple_ircv3_message_get_command(message));
    + break;
    + case PROP_PARAMS:
    + g_value_set_boxed(value, purple_ircv3_message_get_params(message));
    + break;
    + case PROP_SOURCE:
    + g_value_set_string(value, purple_ircv3_message_get_source(message));
    + break;
    + case PROP_TAGS:
    + g_value_set_boxed(value, purple_ircv3_message_get_tags(message));
    + break;
    + default:
    + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    + break;
    + }
    +}
    +
    +static void
    +purple_ircv3_message_set_property(GObject *obj, guint param_id,
    + const GValue *value, GParamSpec *pspec)
    +{
    + PurpleIRCv3Message *message = PURPLE_IRCV3_MESSAGE(obj);
    +
    + switch(param_id) {
    + case PROP_COMMAND:
    + purple_ircv3_message_set_command(message, g_value_get_string(value));
    + break;
    + case PROP_PARAMS:
    + purple_ircv3_message_set_params(message, g_value_get_boxed(value));
    + break;
    + case PROP_SOURCE:
    + purple_ircv3_message_set_source(message, g_value_get_string(value));
    + break;
    + case PROP_TAGS:
    + purple_ircv3_message_set_tags(message, g_value_get_boxed(value));
    + break;
    + default:
    + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, param_id, pspec);
    + break;
    + }
    +}
    +
    +static void
    +purple_ircv3_message_init(G_GNUC_UNUSED PurpleIRCv3Message *message) {
    +}
    +
    +static void
    +purple_ircv3_message_class_init(PurpleIRCv3MessageClass *klass) {
    + GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    +
    + obj_class->finalize = purple_ircv3_message_finalize;
    + obj_class->get_property = purple_ircv3_message_get_property;
    + obj_class->set_property = purple_ircv3_message_set_property;
    +
    + /**
    + * PurpleIRCv3Message:command:
    + *
    + * The command of this message.
    + *
    + * This could be something like JOIN or a server reply numeric like 005.
    + *
    + * Since: 3.0
    + */
    + properties[PROP_COMMAND] = g_param_spec_string(
    + "command", "command",
    + "The command of the message.",
    + NULL,
    + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    +
    + /**
    + * PurpleIRCv3Message:params:
    + *
    + * The parameters of the message.
    + *
    + * When serialized, the last item will be prefixed with a :.
    + *
    + * Since: 3.0
    + */
    + properties[PROP_PARAMS] = g_param_spec_boxed(
    + "params", "params",
    + "The parameters of this message.",
    + G_TYPE_STRV,
    + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    +
    + /**
    + * PurpleIRCv3Message:source:
    + *
    + * The source of the message.
    + *
    + * This could be a nickname, a full nick!ident@server, a server name, or
    + * %NULL.
    + *
    + * Since: 3.0
    + */
    + properties[PROP_SOURCE] = g_param_spec_string(
    + "source", "source",
    + "The source of the message.",
    + NULL,
    + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    +
    + /**
    + * PurpleIRCv3Message:tags:
    + *
    + * The [ircv3 message tags](https://ircv3.net/specs/extensions/message-tags)
    + * for the message.
    + *
    + * Since: 3.0
    + */
    + properties[PROP_TAGS] = g_param_spec_boxed(
    + "tags", "tags",
    + "The ircv3 message tags from the message.",
    + G_TYPE_HASH_TABLE,
    + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
    +
    + g_object_class_install_properties(obj_class, N_PROPERTIES, properties);
    +}
    +
    +/******************************************************************************
    + * Public API
    + *****************************************************************************/
    +PurpleIRCv3Message *
    +purple_ircv3_message_new(const char *command) {
    + return g_object_new(
    + PURPLE_IRCV3_TYPE_MESSAGE,
    + "command", command,
    + NULL);
    +}
    +
    +const char *
    +purple_ircv3_message_get_command(PurpleIRCv3Message *message) {
    + g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
    +
    + return message->command;
    +}
    +
    +void
    +purple_ircv3_message_set_command(PurpleIRCv3Message *message,
    + const char *command)
    +{
    + g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
    + g_return_if_fail(!purple_strempty(command));
    +
    + if(!purple_strequal(message->command, command)) {
    + g_free(message->command);
    + message->command = g_strdup(command);
    +
    + g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_COMMAND]);
    + }
    +}
    +
    +const char *
    +purple_ircv3_message_get_source(PurpleIRCv3Message *message) {
    + g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
    +
    + return message->source;
    +}
    +
    +void
    +purple_ircv3_message_set_source(PurpleIRCv3Message *message,
    + const char *source)
    +{
    + g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
    +
    + if(!purple_strequal(message->source, source)) {
    + g_free(message->source);
    + message->source = g_strdup(source);
    +
    + g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_SOURCE]);
    + }
    +}
    +
    +GHashTable *
    +purple_ircv3_message_get_tags(PurpleIRCv3Message *message) {
    + g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
    +
    + return message->tags;
    +}
    +
    +void
    +purple_ircv3_message_set_tags(PurpleIRCv3Message *message, GHashTable *tags) {
    + g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
    +
    + if(message->tags != tags) {
    + g_clear_pointer(&message->tags, g_hash_table_unref);
    +
    + if(tags != NULL) {
    + message->tags = g_hash_table_ref(tags);
    + }
    +
    + g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_TAGS]);
    + }
    +}
    +
    +GStrv
    +purple_ircv3_message_get_params(PurpleIRCv3Message *message) {
    + g_return_val_if_fail(PURPLE_IRCV3_IS_MESSAGE(message), NULL);
    +
    + return message->params;
    +}
    +
    +void
    +purple_ircv3_message_set_params(PurpleIRCv3Message *message, GStrv params) {
    + g_return_if_fail(PURPLE_IRCV3_IS_MESSAGE(message));
    +
    + if(message->params != params) {
    + g_clear_pointer(&message->params, g_strfreev);
    +
    + if(params != NULL) {
    + message->params = g_strdupv(params);
    + }
    +
    + g_object_notify_by_pspec(G_OBJECT(message), properties[PROP_PARAMS]);
    + }
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3message.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,164 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_MESSAGE_H
    +#define PURPLE_IRCV3_MESSAGE_H
    +
    +#include <glib.h>
    +
    +#include <gplugin.h>
    +#include <gplugin-native.h>
    +
    +#include <purple.h>
    +
    +#include "purpleircv3version.h"
    +
    +G_BEGIN_DECLS
    +
    +#define PURPLE_IRCV3_TYPE_MESSAGE (purple_ircv3_message_get_type())
    +
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +G_DECLARE_FINAL_TYPE(PurpleIRCv3Message, purple_ircv3_message, PURPLE_IRCV3,
    + MESSAGE, GObject)
    +
    +/**
    + * purple_ircv3_message_new:
    + * @command: (not nullable): The command of the message.
    + *
    + * Creates a new message with @command.
    + *
    + * Returns: (transfer full): The new message.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +PurpleIRCv3Message *purple_ircv3_message_new(const char *command);
    +
    +/**
    + * purple_ircv3_message_get_command:
    + * @message: The instance.
    + *
    + * Gets the command of the message.
    + *
    + * Returns: The command of the message.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +const char *purple_ircv3_message_get_command(PurpleIRCv3Message *message);
    +
    +/**
    + * purple_ircv3_message_set_command:
    + * @message: The instance.
    + * @command: The new command.
    + *
    + * Sets the command for @message to @command.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_message_set_command(PurpleIRCv3Message *message, const char *command);
    +
    +/**
    + * purple_ircv3_message_get_params:
    + * @message: The instance.
    + *
    + * Gets the parameters from @message.
    + *
    + * Returns: (transfer none) (nullable): The parameters from @message.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +GStrv purple_ircv3_message_get_params(PurpleIRCv3Message *message);
    +
    +/**
    + * purple_ircv3_message_set_params:
    + * @message: The instance.
    + * @params: (transfer none) (nullable): The new parameters.
    + *
    + * Sets the parameters of @message to @params.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_message_set_params(PurpleIRCv3Message *message, GStrv params);
    +
    +/**
    + * purple_ircv3_message_get_source:
    + * @message: The instance.
    + *
    + * Gets the the source of @message.
    + *
    + * Returns: (nullable): The source of @message.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +const char *purple_ircv3_message_get_source(PurpleIRCv3Message *message);
    +
    +/**
    + * purple_ircv3_message_set_source:
    + * @message: The instance.
    + * @source: (nullable): The new source.
    + *
    + * Sets the source of @message to @source.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_message_set_source(PurpleIRCv3Message *message, const char *source);
    +
    +/**
    + * purple_ircv3_message_get_tags:
    + * @message: The instance.
    + *
    + * Gets the tags from @message.
    + *
    + * Returns: (transfer none) (element-type utf8 utf8) (nullable): The tags from
    + * @message.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +GHashTable *purple_ircv3_message_get_tags(PurpleIRCv3Message *message);
    +
    +/**
    + * purple_ircv3_message_set_tags:
    + * @message: The instance.
    + * @tags: (transfer none) (element-type utf8 utf8) (nullable): The new tags.
    + *
    + * Sets the tags of @message to @tags.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_message_set_tags(PurpleIRCv3Message *message, GHashTable *tags);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_MESSAGE_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3messagehandlers.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,315 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib/gi18n-lib.h>
    +
    +#include "purpleircv3messagehandlers.h"
    +
    +#include "purpleircv3connection.h"
    +#include "purpleircv3constants.h"
    +#include "purpleircv3core.h"
    +#include "purpleircv3ctcp.h"
    +#include "purpleircv3formatting.h"
    +#include "purpleircv3source.h"
    +
    +/******************************************************************************
    + * Fallback
    + *****************************************************************************/
    +gboolean
    +purple_ircv3_message_handler_fallback(PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *connection = data;
    + char *new_command = NULL;
    + const char *command = NULL;
    +
    + command = purple_ircv3_message_get_command(message);
    +
    + new_command = g_strdup_printf(_("unknown command '%s'"), command);
    + purple_ircv3_message_set_command(message, new_command);
    + purple_ircv3_connection_add_status_message(connection, message);
    +
    + g_clear_pointer(&new_command, g_free);
    +
    + return TRUE;
    +}
    +
    +/******************************************************************************
    + * Status Messages
    + *****************************************************************************/
    +gboolean
    +purple_ircv3_message_handler_status(PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer data)
    +{
    + purple_ircv3_connection_add_status_message(data, message);
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_message_handler_status_ignore_param0(PurpleIRCv3Message *message,
    + GError **error,
    + gpointer data)
    +{
    + GStrv params = NULL;
    + GStrv new_params = NULL;
    + guint n_params = 0;
    +
    + params = purple_ircv3_message_get_params(message);
    + if(params != NULL) {
    + n_params = g_strv_length(params);
    + }
    +
    + if(n_params <= 1) {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "expected n_params > 1, got %u", n_params);
    +
    + return FALSE;
    + }
    +
    + /* We need to make a copy because otherwise we'd get a use after free in
    + * set_params.
    + */
    + new_params = g_strdupv(params + 1);
    + purple_ircv3_message_set_params(message, new_params);
    + g_clear_pointer(&new_params, g_strfreev);
    +
    + purple_ircv3_connection_add_status_message(data, message);
    +
    + return TRUE;
    +}
    +
    +/******************************************************************************
    + * General Commands
    + *****************************************************************************/
    +gboolean
    +purple_ircv3_message_handler_ping(PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *connection = data;
    + GStrv params = NULL;
    +
    + params = purple_ircv3_message_get_params(message);
    +
    + if(params != NULL && g_strv_length(params) == 1) {
    + purple_ircv3_connection_writef(connection, "PONG %s", params[0]);
    + } else {
    + purple_ircv3_connection_writef(connection, "PONG");
    + }
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_message_handler_privmsg(PurpleIRCv3Message *v3_message,
    + G_GNUC_UNUSED GError **error,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *connection = data;
    + PurpleContact *contact = NULL;
    + PurpleConversation *conversation = NULL;
    + PurpleMessage *message = NULL;
    + PurpleMessageFlags flags = PURPLE_MESSAGE_RECV;
    + GDateTime *dt = NULL;
    + GHashTable *tags = NULL;
    + GStrv params = NULL;
    + gpointer raw_id = NULL;
    + gpointer raw_timestamp = NULL;
    + char *nick = NULL;
    + char *stripped = NULL;
    + const char *command = NULL;
    + const char *id = NULL;
    + const char *source = NULL;
    + const char *target = NULL;
    +
    + command = purple_ircv3_message_get_command(v3_message);
    + params = purple_ircv3_message_get_params(v3_message);
    + source = purple_ircv3_message_get_source(v3_message);
    + tags = purple_ircv3_message_get_tags(v3_message);
    +
    + if(params == NULL) {
    + g_warning("privmsg received with no parameters");
    +
    + return FALSE;
    + }
    +
    + if(g_strv_length(params) != 2) {
    + char *body = g_strjoinv(" ", params);
    + g_warning("unknown privmsg message format: '%s'", body);
    + g_free(body);
    +
    + return FALSE;
    + }
    +
    + purple_ircv3_source_parse(source, &nick, NULL, NULL);
    +
    + /* Find or create the conversation. */
    + target = params[0];
    + if(!purple_ircv3_connection_is_channel(connection, target)) {
    + target = nick;
    + }
    + conversation = purple_ircv3_connection_find_or_create_conversation(connection,
    + target);
    + /* Find or create the contact. */
    + contact = purple_ircv3_connection_find_or_create_contact(connection, nick);
    + if(PURPLE_IS_CONTACT(contact)) {
    + PurpleConversationMember *member = NULL;
    +
    + /* Update the contact's sid as it may have changed. */
    + purple_contact_info_set_sid(PURPLE_CONTACT_INFO(contact), source);
    +
    + /* Make sure the contact is in the conversation. */
    + member = purple_conversation_find_member(conversation,
    + PURPLE_CONTACT_INFO(contact));
    + if(!PURPLE_IS_CONVERSATION_MEMBER(member)) {
    + purple_conversation_add_member(conversation,
    + PURPLE_CONTACT_INFO(contact),
    + FALSE, NULL);
    + }
    + }
    +
    + /* Grab the msgid if one was provided. */
    + if(g_hash_table_lookup_extended(tags, "msgid", NULL, &raw_id)) {
    + if(!purple_strempty(raw_id)) {
    + id = raw_id;
    + }
    + }
    +
    + if(purple_strequal(command, PURPLE_IRCV3_MSG_NOTICE)) {
    + flags |= PURPLE_MESSAGE_NOTIFY;
    + }
    +
    + /* Determine the timestamp of the message. */
    + if(g_hash_table_lookup_extended(tags, "time", NULL, &raw_timestamp)) {
    + const char *timestamp = raw_timestamp;
    +
    + if(!purple_strempty(timestamp)) {
    + GTimeZone *tz = g_time_zone_new_utc();
    +
    + dt = g_date_time_new_from_iso8601(timestamp, tz);
    +
    + g_time_zone_unref(tz);
    + }
    + }
    +
    + /* If the server didn't provide a time, use the current local time. */
    + if(dt == NULL) {
    + dt = g_date_time_new_now_local();
    + }
    +
    + stripped = purple_ircv3_formatting_strip(params[1]);
    + message = g_object_new(
    + PURPLE_TYPE_MESSAGE,
    + "author", source,
    + "contents", stripped,
    + "flags", flags,
    + "id", id,
    + "timestamp", dt,
    + NULL);
    + g_free(stripped);
    +
    + g_date_time_unref(dt);
    +
    + /* Check if this is a CTCP message. */
    + if(!purple_ircv3_ctcp_handle(connection, conversation, message)) {
    + purple_conversation_write_message(conversation, message);
    + }
    +
    + g_clear_pointer(&nick, g_free);
    + g_clear_object(&message);
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_message_handler_topic(PurpleIRCv3Message *message,
    + GError **error,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *connection = data;
    + PurpleConversation *conversation = NULL;
    + GStrv params = NULL;
    + const char *channel = NULL;
    + const char *command = NULL;
    + const char *topic = NULL;
    + guint n_params = 0;
    +
    + command = purple_ircv3_message_get_command(message);
    + params = purple_ircv3_message_get_params(message);
    + n_params = g_strv_length(params);
    +
    + if(purple_strequal(command, PURPLE_IRCV3_MSG_TOPIC)) {
    + if(n_params != 2) {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "received TOPIC with %u parameters, expected 2",
    + n_params);
    +
    + return FALSE;
    + }
    +
    + channel = params[0];
    + topic = params[1];
    + } else if(purple_strequal(command, PURPLE_IRCV3_RPL_NOTOPIC)) {
    + if(n_params != 3) {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "received RPL_NOTOPIC with %u parameters, expected 3",
    + n_params);
    +
    + return FALSE;
    + }
    +
    + channel = params[1];
    + topic = "";
    + } else if(purple_strequal(command, PURPLE_IRCV3_RPL_TOPIC)) {
    + if(n_params != 3) {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "received RPL_TOPIC with %u parameters, expected 3",
    + n_params);
    +
    + return FALSE;
    + }
    +
    + channel = params[1];
    + topic = params[2];
    + } else {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "unexpected command %s",
    + command);
    +
    + return FALSE;
    + }
    +
    + conversation = purple_ircv3_connection_find_or_create_conversation(connection,
    + channel);
    + if(!PURPLE_IS_CONVERSATION(conversation)) {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "failed to find or create channel '%s'", channel);
    +
    + return FALSE;
    + }
    +
    + purple_conversation_set_topic(conversation, topic);
    +
    + return TRUE;
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3messagehandlers.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,67 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_MESSAGE_HANDLERS_H
    +#define PURPLE_IRCV3_MESSAGE_HANDLERS_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +#include "purpleircv3message.h"
    +#include "purpleircv3version.h"
    +
    +G_BEGIN_DECLS
    +
    +/**
    + * PurpleIRCv3MessageHandler:
    + * @message: The [class@Message] to handle.
    + * @error: (nullable): A return address for a [type@GLib.Error].
    + * @data: The user data that was passed to [method@PurpleIRCv3.Parser.parse].
    + *
    + * Defines the type of a function that will be called to handle @message.
    + *
    + * Returns: %TRUE if the command was handled properly, otherwise %FALSE and
    + * @error may be set.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_TYPE_IN_3_0
    +typedef gboolean (*PurpleIRCv3MessageHandler)(PurpleIRCv3Message *message,
    + GError **error,
    + gpointer data);
    +
    +G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_fallback(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_status(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_status_ignore_param0(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_ping(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_privmsg(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_message_handler_topic(PurpleIRCv3Message *message, GError **error, gpointer data);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_MESSAGE_HANDLERS_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3parser.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,507 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include "purpleircv3parser.h"
    +
    +#include "purpleircv3capabilities.h"
    +#include "purpleircv3constants.h"
    +#include "purpleircv3core.h"
    +#include "purpleircv3message.h"
    +#include "purpleircv3messagehandlers.h"
    +#include "purpleircv3sasl.h"
    +
    +struct _PurpleIRCv3Parser {
    + GObject parent;
    +
    + GRegex *regex_message;
    + GRegex *regex_tags;
    +
    + PurpleIRCv3MessageHandler fallback_handler;
    + GHashTable *handlers;
    +};
    +
    +G_DEFINE_FINAL_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, G_TYPE_OBJECT)
    +
    +/******************************************************************************
    + * Helpers
    + *****************************************************************************/
    +static char *
    +purple_ircv3_parser_unescape_tag_value(const char *value) {
    + GString *unescaped = g_string_new("");
    + gboolean escaping = FALSE;
    +
    + /* Walk the string and replace escaped values according to
    + * https://ircv3.net/specs/extensions/message-tags.html#escaping-values
    + */
    + for(int i = 0; value[i] != '\0'; i++) {
    + if(escaping) {
    + /* Set the replacement to the current character which will fall
    + * through for everything, including '\\'
    + */
    + char replacement = value[i];
    +
    + if(value[i] == ':') {
    + replacement = ';';
    + } else if(value[i] == 's') {
    + replacement = ' ';
    + } else if(value[i] == 'r') {
    + replacement = '\r';
    + } else if(value[i] == 'n') {
    + replacement = '\n';
    + }
    +
    + g_string_append_c(unescaped, replacement);
    + escaping = FALSE;
    + } else {
    + if(value[i] == '\\') {
    + escaping = TRUE;
    + } else {
    + g_string_append_c(unescaped, value[i]);
    + }
    + }
    + }
    +
    + return g_string_free(unescaped, FALSE);
    +}
    +
    +static GHashTable *
    +purple_ircv3_parser_parse_tags(PurpleIRCv3Parser *parser,
    + const gchar *tags_string, GError **error)
    +{
    + GError *local_error = NULL;
    + GHashTable *tags = NULL;
    + GMatchInfo *info = NULL;
    + gboolean matches = FALSE;
    +
    + tags = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
    +
    + /* tags_string can never be NULL, because g_match_info_fetch_named always
    + * returns a string. So if we were passed an empty string, just return the
    + * empty hash table.
    + */
    + if(*tags_string == '\0') {
    + return tags;
    + }
    +
    + matches = g_regex_match_full(parser->regex_tags, tags_string, -1, 0, 0,
    + &info, &local_error);
    +
    + if(local_error != NULL) {
    + g_propagate_error(error, local_error);
    +
    + g_match_info_unref(info);
    +
    + return tags;
    + }
    +
    + if(!matches) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "failed to parse tags: unknown error");
    +
    + g_match_info_unref(info);
    +
    + return tags;
    + }
    +
    + while(g_match_info_matches(info)) {
    + char *key = NULL;
    + char *value = NULL;
    + char *unescaped = NULL;
    +
    + key = g_match_info_fetch_named(info, "key");
    + value = g_match_info_fetch_named(info, "value");
    +
    + unescaped = purple_ircv3_parser_unescape_tag_value(value);
    + g_free(value);
    +
    + /* the hash table is created with destroy notifies for both key and
    + * value, so there's no need to free the allocated memory right now.
    + */
    + g_hash_table_insert(tags, key, unescaped);
    + g_match_info_next(info, &local_error);
    +
    + if(local_error != NULL) {
    + g_propagate_error(error, local_error);
    +
    + break;
    + }
    + }
    +
    + g_match_info_unref(info);
    +
    + return tags;
    +}
    +
    +static guint
    +purple_ircv3_parser_extract_params(G_GNUC_UNUSED PurpleIRCv3Parser *parser,
    + GStrvBuilder *builder, const gchar *str)
    +{
    + gchar *ptr = NULL;
    + guint count = 0;
    +
    + /* Loop through str finding each space separated string. */
    + while(str != NULL && *str != '\0') {
    + /* Look for a space. */
    + ptr = strchr(str, ' ');
    +
    + /* If we found one, set it to null terminator and add the string to our
    + * builder.
    + */
    + if(ptr != NULL) {
    + *ptr = '\0';
    + g_strv_builder_add(builder, str);
    +
    + /* Move str to the next character as we know there's another
    + * character which might be another null terminator.
    + */
    + str = ptr + 1;
    +
    + /* And don't forget to increment the count... ah ah ah! */
    + count++;
    + } else {
    + /* Add the remaining string. */
    + g_strv_builder_add(builder, str);
    +
    + /* Give the count another one, ah ah ah! */
    + count++;
    +
    + /* Finally break out of the loop. */
    + break;
    + }
    + }
    +
    + return count;
    +}
    +
    +static GStrv
    +purple_ircv3_parser_build_params(PurpleIRCv3Parser *parser,
    + const gchar *middle, const gchar *coda,
    + const gchar *trailing, guint *n_params)
    +{
    + GStrvBuilder *builder = g_strv_builder_new();
    + GStrv result = NULL;
    +
    + purple_ircv3_parser_extract_params(parser, builder, middle);
    +
    + if(*coda != '\0') {
    + g_strv_builder_add(builder, trailing);
    + }
    +
    + result = g_strv_builder_end(builder);
    +
    + g_strv_builder_unref(builder);
    +
    + if(result != NULL && n_params != NULL) {
    + *n_params = g_strv_length(result);
    + }
    +
    + return result;
    +}
    +
    +/******************************************************************************
    + * Handlers
    + *****************************************************************************/
    +static gboolean
    +purple_ircv3_fallback_handler(PurpleIRCv3Message *message,
    + GError **error,
    + G_GNUC_UNUSED gpointer data)
    +{
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "no handler for command %s",
    + purple_ircv3_message_get_command(message));
    +
    + return FALSE;
    +}
    +
    +/******************************************************************************
    + * GObject Implementation
    + *****************************************************************************/
    +static void
    +purple_ircv3_parser_finalize(GObject *obj) {
    + PurpleIRCv3Parser *parser = PURPLE_IRCV3_PARSER(obj);
    +
    + g_clear_pointer(&parser->regex_message, g_regex_unref);
    + g_clear_pointer(&parser->regex_tags, g_regex_unref);
    +
    + g_hash_table_destroy(parser->handlers);
    +
    + G_OBJECT_CLASS(purple_ircv3_parser_parent_class)->finalize(obj);
    +}
    +
    +static void
    +purple_ircv3_parser_init(PurpleIRCv3Parser *parser) {
    + parser->regex_message = g_regex_new("(?:@(?<tags>[^ ]+) )?"
    + "(?::(?<source>[^ ]+) +)?"
    + "(?<command>[^ :]+)"
    + "(?: +(?<middle>(?:[^ :]+(?: +[^ :]+)*)))*"
    + "(?<coda> +:(?<trailing>.*)?)?",
    + 0, 0, NULL);
    + g_assert(parser->regex_message != NULL);
    +
    + parser->regex_tags = g_regex_new("(?<key>(?<client_prefix>\\+?)"
    + "(?:(?<vendor>[A-Za-z0-9-\\.]+)\\/)?"
    + "(?<key_name>[A-Za-z0-9-]+)"
    + ")"
    + "(?:=(?<value>[^\r\n;]*))?(?:;|$)",
    + 0, 0, NULL);
    + g_assert(parser->regex_tags != NULL);
    +
    + parser->fallback_handler = purple_ircv3_fallback_handler;
    + parser->handlers = g_hash_table_new_full(g_str_hash, g_str_equal, g_free,
    + NULL);
    +}
    +
    +static void
    +purple_ircv3_parser_class_init(PurpleIRCv3ParserClass *klass) {
    + GObjectClass *obj_class = G_OBJECT_CLASS(klass);
    +
    + obj_class->finalize = purple_ircv3_parser_finalize;
    +}
    +
    +/******************************************************************************
    + * Public API
    + *****************************************************************************/
    +PurpleIRCv3Parser *
    +purple_ircv3_parser_new(void) {
    + return g_object_new(PURPLE_IRCV3_TYPE_PARSER, NULL);
    +}
    +
    +void
    +purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser,
    + PurpleIRCv3MessageHandler handler)
    +{
    + g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
    +
    + parser->fallback_handler = handler;
    +}
    +
    +gboolean
    +purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer,
    + GError **error, gpointer data)
    +{
    + PurpleIRCv3Message *message = NULL;
    + PurpleIRCv3MessageHandler handler = NULL;
    + GError *local_error = NULL;
    + GHashTable *tags = NULL;
    + GMatchInfo *info = NULL;
    + GStrv params = NULL;
    + gchar *coda = NULL;
    + gchar *command = NULL;
    + gchar *middle = NULL;
    + gchar *source = NULL;
    + gchar *tags_string = NULL;
    + gchar *trailing = NULL;
    + gboolean matches = FALSE;
    + gboolean result = FALSE;
    +
    + g_return_val_if_fail(PURPLE_IRCV3_IS_PARSER(parser), FALSE);
    + g_return_val_if_fail(buffer != NULL, FALSE);
    +
    + /* Check if the buffer matches our regex for messages. */
    + matches = g_regex_match(parser->regex_message, buffer, 0, &info);
    + if(!matches) {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "failed to parser buffer '%s'", buffer);
    +
    + g_match_info_unref(info);
    +
    + return FALSE;
    + }
    +
    + /* Extract the command from the buffer, so we can find the handler. */
    + command = g_match_info_fetch_named(info, "command");
    + handler = g_hash_table_lookup(parser->handlers, command);
    + if(handler == NULL) {
    + if(parser->fallback_handler == NULL) {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "no handler found for command %s and no default "
    + "handler set.", command);
    +
    + g_free(command);
    + g_match_info_unref(info);
    +
    + return FALSE;
    + }
    +
    + handler = parser->fallback_handler;
    + }
    +
    + /* If we made it this far, we have our handler, so lets get the rest of the
    + * parameters and call the handler.
    + */
    + message = purple_ircv3_message_new(command);
    +
    + tags_string = g_match_info_fetch_named(info, "tags");
    + tags = purple_ircv3_parser_parse_tags(parser, tags_string, &local_error);
    + g_free(tags_string);
    + if(local_error != NULL) {
    + g_propagate_error(error, local_error);
    +
    + g_free(command);
    + g_clear_object(&message);
    + g_hash_table_destroy(tags);
    + g_match_info_unref(info);
    +
    + return FALSE;
    + }
    + if(tags != NULL) {
    + purple_ircv3_message_set_tags(message, tags);
    + g_hash_table_unref(tags);
    + }
    +
    + source = g_match_info_fetch_named(info, "source");
    + if(!purple_strempty(source)) {
    + purple_ircv3_message_set_source(message, source);
    + }
    + g_free(source);
    +
    + middle = g_match_info_fetch_named(info, "middle");
    + coda = g_match_info_fetch_named(info, "coda");
    + trailing = g_match_info_fetch_named(info, "trailing");
    + params = purple_ircv3_parser_build_params(parser, middle, coda, trailing,
    + NULL);
    + if(params != NULL) {
    + purple_ircv3_message_set_params(message, params);
    + }
    +
    + g_free(command);
    + g_free(middle);
    + g_free(coda);
    + g_free(trailing);
    + g_strfreev(params);
    +
    + /* Call the handler. */
    + result = handler(message, error, data);
    +
    + /* Cleanup the left overs. */
    + g_match_info_unref(info);
    + g_clear_object(&message);
    +
    + return result;
    +}
    +
    +void
    +purple_ircv3_parser_add_handler(PurpleIRCv3Parser *parser,
    + const char *command,
    + PurpleIRCv3MessageHandler handler)
    +{
    + g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
    + g_return_if_fail(command != NULL);
    + g_return_if_fail(handler != NULL);
    +
    + g_hash_table_insert(parser->handlers, g_strdup(command), handler);
    +}
    +
    +void
    +purple_ircv3_parser_add_handlers(PurpleIRCv3Parser *parser,
    + PurpleIRCv3MessageHandler handler,
    + ...)
    +{
    + va_list vargs;
    + const char *command = NULL;
    +
    + g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
    + g_return_if_fail(handler != NULL);
    +
    + va_start(vargs, handler);
    +
    + while((command = va_arg(vargs, const char *)) != NULL) {
    + purple_ircv3_parser_add_handler(parser, command, handler);
    + }
    +
    + va_end(vargs);
    +}
    +
    +void
    +purple_ircv3_parser_add_default_handlers(PurpleIRCv3Parser *parser) {
    + g_return_if_fail(PURPLE_IRCV3_IS_PARSER(parser));
    +
    + purple_ircv3_parser_set_fallback_handler(parser,
    + purple_ircv3_message_handler_fallback);
    +
    + /* Core functionality. */
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_CAP,
    + purple_ircv3_capabilities_message_handler);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_NOTICE,
    + purple_ircv3_message_handler_privmsg);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_PING,
    + purple_ircv3_message_handler_ping);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_PRIVMSG,
    + purple_ircv3_message_handler_privmsg);
    +
    + /* Topic stuff. */
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_TOPIC,
    + purple_ircv3_message_handler_topic);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_NOTOPIC,
    + purple_ircv3_message_handler_topic);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_TOPIC,
    + purple_ircv3_message_handler_topic);
    +
    + /* Post Registration Greetings */
    + purple_ircv3_parser_add_handlers(parser,
    + purple_ircv3_message_handler_status_ignore_param0,
    + PURPLE_IRCV3_RPL_WELCOME,
    + PURPLE_IRCV3_RPL_YOURHOST,
    + PURPLE_IRCV3_RPL_CREATED,
    + PURPLE_IRCV3_RPL_MYINFO,
    + NULL);
    +
    + /* Luser's */
    + purple_ircv3_parser_add_handlers(parser,
    + purple_ircv3_message_handler_status_ignore_param0,
    + PURPLE_IRCV3_RPL_LUSERCLIENT,
    + PURPLE_IRCV3_RPL_LUSEROP,
    + PURPLE_IRCV3_RPL_LUSERUNKNOWN,
    + PURPLE_IRCV3_RPL_LUSERCHANNELS,
    + PURPLE_IRCV3_RPL_LUSERME,
    + NULL);
    +
    + /* MOTD */
    + purple_ircv3_parser_add_handlers(parser,
    + purple_ircv3_message_handler_status_ignore_param0,
    + PURPLE_IRCV3_RPL_MOTD,
    + PURPLE_IRCV3_RPL_MOTDSTART,
    + PURPLE_IRCV3_RPL_ENDOFMOTD,
    + NULL);
    +
    + /* SASL stuff. */
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_LOGGEDIN,
    + purple_ircv3_sasl_logged_in);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_LOGGEDOUT,
    + purple_ircv3_sasl_logged_out);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_NICKLOCKED,
    + purple_ircv3_sasl_nick_locked);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_SASLSUCCESS,
    + purple_ircv3_sasl_success);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLFAIL,
    + purple_ircv3_sasl_failed);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLTOOLONG,
    + purple_ircv3_sasl_message_too_long);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLABORTED,
    + purple_ircv3_sasl_aborted);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_ERR_SASLALREADY,
    + purple_ircv3_sasl_already_authed);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_RPL_SASLMECHS,
    + purple_ircv3_sasl_mechanisms);
    + purple_ircv3_parser_add_handler(parser, PURPLE_IRCV3_MSG_AUTHENTICATE,
    + purple_ircv3_sasl_authenticate);
    +
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3parser.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,126 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_PARSER_H
    +#define PURPLE_IRCV3_PARSER_H
    +
    +#include <glib.h>
    +#include <glib-object.h>
    +
    +#include <purple.h>
    +
    +#include "purpleircv3messagehandlers.h"
    +#include "purpleircv3version.h"
    +
    +G_BEGIN_DECLS
    +
    +#define PURPLE_IRCV3_TYPE_PARSER (purple_ircv3_parser_get_type())
    +
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +G_DECLARE_FINAL_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, PURPLE_IRCV3,
    + PARSER, GObject)
    +
    +/**
    + * purple_ircv3_parser_new:
    + *
    + * Creates a new instance.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +PurpleIRCv3Parser *purple_ircv3_parser_new(void);
    +
    +/**
    + * purple_ircv3_parser_set_fallback_handler: (skip):
    + * @parser: The instance.
    + * @handler: A [func@PurpleIRCv3.MessageHandler].
    + *
    + * Sets @handler to be called for any messages that @parser doesn't know how to
    + * handle.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_parser_set_fallback_handler(PurpleIRCv3Parser *parser, PurpleIRCv3MessageHandler handler);
    +
    +/**
    + * purple_ircv3_parser_parse:
    + * @parser: The instance.
    + * @buffer: The buffer to parse.
    + * @error: Return address for a #GError, or %NULL.
    + * @data: (nullable): Optional data to pass to the handler.
    + *
    + * Parses @buffer with @parser.
    + *
    + * Returns: %TRUE if the buffer was parsed correctly or %FALSE with @error set.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +gboolean purple_ircv3_parser_parse(PurpleIRCv3Parser *parser, const gchar *buffer, GError **error, gpointer data);
    +
    +/**
    + * purple_ircv3_parser_add_handler:
    + * @parser: The instance.
    + * @command: The command string.
    + * @handler: (scope forever): The handler to call.
    + *
    + * Calls @handler every time @parser finds the command named @command.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_parser_add_handler(PurpleIRCv3Parser *parser, const char *command, PurpleIRCv3MessageHandler handler);
    +
    +/**
    + * purple_ircv3_parser_add_handlers:
    + * @parser: The instance.
    + * @handler: (scope forever): The handler to call when the command is received.
    + * @...: A %NULL terminated list of string command names.
    + *
    + * Like [method@Parser.add_handler] but allows you to add multiple commands at
    + * once that share a handler.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_parser_add_handlers(PurpleIRCv3Parser *parser, PurpleIRCv3MessageHandler handler, ...) G_GNUC_NULL_TERMINATED;
    +
    +/**
    + * purple_ircv3_parser_add_default_handlers:
    + * @parser: The instance.
    + *
    + * Adds all of the default handlers to @parser.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_parser_add_default_handlers(PurpleIRCv3Parser *parser);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_PARSER_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3protocol.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,279 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib/gi18n-lib.h>
    +
    +#include "purpleircv3protocol.h"
    +
    +#include "purpleircv3connection.h"
    +#include "purpleircv3core.h"
    +#include "purpleircv3protocolconversation.h"
    +
    +/******************************************************************************
    + * Callbacks
    + *****************************************************************************/
    +static void
    +purple_ircv3_protocol_can_reach_cb(GObject *self, GAsyncResult *result,
    + gpointer data)
    +{
    + GError *error = NULL;
    + GTask *task = data;
    + gboolean can_reach = FALSE;
    +
    + /* task and result share a cancellable, so either can be used here. */
    + if(g_task_return_error_if_cancelled(task)) {
    + g_clear_object(&task);
    +
    + return;
    + }
    +
    + can_reach = g_network_monitor_can_reach_finish(G_NETWORK_MONITOR(self), result,
    + &error);
    +
    + if(error != NULL) {
    + g_task_return_error(task, error);
    + } else if(!can_reach) {
    + g_task_return_new_error(task, PURPLE_IRCV3_DOMAIN, 0,
    + _("Unknown network error."));
    + } else {
    + g_task_return_boolean(task, TRUE);
    + }
    +
    + g_clear_object(&task);
    +}
    +
    +/******************************************************************************
    + * PurpleProtocol Implementation
    + *****************************************************************************/
    +static GList *
    +purple_ircv3_protocol_get_user_splits(G_GNUC_UNUSED PurpleProtocol *protocol) {
    + PurpleAccountUserSplit *split = NULL;
    + GList *splits = NULL;
    +
    + split = purple_account_user_split_new(_("Server"),
    + PURPLE_IRCV3_DEFAULT_SERVER,
    + '@');
    + splits = g_list_append(splits, split);
    +
    + return splits;
    +}
    +
    +static GList *
    +purple_ircv3_protocol_get_account_options(G_GNUC_UNUSED PurpleProtocol *protocol)
    +{
    + PurpleAccountOption *option;
    + GList *options = NULL;
    +
    + option = purple_account_option_int_new(_("Port"), "port",
    + PURPLE_IRCV3_DEFAULT_TLS_PORT);
    + options = g_list_append(options, option);
    +
    + option = purple_account_option_bool_new(_("Use TLS"), "use-tls", TRUE);
    + options = g_list_append(options, option);
    +
    + option = purple_account_option_string_new(_("Server password"),
    + "server-password", "");
    + purple_account_option_string_set_masked(option, TRUE);
    + options = g_list_append(options, option);
    +
    + option = purple_account_option_string_new(_("Ident name"), "ident", "");
    + options = g_list_append(options, option);
    +
    + option = purple_account_option_string_new(_("Real name"), "real-name", "");
    + options = g_list_append(options, option);
    +
    + option = purple_account_option_string_new(_("SASL login name"),
    + "sasl-login-name", "");
    + options = g_list_append(options, option);
    +
    + option = purple_account_option_string_new(_("SASL mechanisms"),
    + "sasl-mechanisms", "");
    + options = g_list_append(options, option);
    +
    + option = purple_account_option_bool_new(_("Allow plaintext SASL auth over "
    + "unencrypted connection"),
    + "plain-sasl-in-clear", FALSE);
    + options = g_list_append(options, option);
    +
    + option = purple_account_option_int_new(_("Seconds between sending "
    + "messages"),
    + "rate-limit-interval", 2);
    + options = g_list_append(options, option);
    +
    + option = purple_account_option_int_new(_("Maximum messages to send at "
    + "once"),
    + "rate-limit-burst", 5);
    + options = g_list_append(options, option);
    +
    + return options;
    +}
    +
    +static PurpleConnection *
    +purple_ircv3_protocol_create_connection(PurpleProtocol *protocol,
    + PurpleAccount *account,
    + const char *password,
    + GError **error)
    +{
    + const char *username = NULL;
    +
    + g_return_val_if_fail(PURPLE_IS_PROTOCOL(protocol), NULL);
    + g_return_val_if_fail(PURPLE_IS_ACCOUNT(account), NULL);
    +
    + /* Make sure the username (which includes the servername via usersplits),
    + * does not contain any whitespace.
    + */
    + username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
    + if(strpbrk(username, " \t\v\r\n") != NULL) {
    + g_set_error(error,
    + PURPLE_CONNECTION_ERROR,
    + PURPLE_CONNECTION_ERROR_INVALID_SETTINGS,
    + _("IRC nick and server may not contain whitespace"));
    +
    + return NULL;
    + }
    +
    + return g_object_new(
    + PURPLE_IRCV3_TYPE_CONNECTION,
    + "protocol", protocol,
    + "account", account,
    + "password", password,
    + NULL);
    +}
    +
    +static GList *
    +purple_ircv3_protocol_status_types(G_GNUC_UNUSED PurpleProtocol *protocol,
    + G_GNUC_UNUSED PurpleAccount *account)
    +{
    + PurpleStatusType *type = NULL;
    + GList *types = NULL;
    +
    + type = purple_status_type_new(PURPLE_STATUS_AVAILABLE, NULL, NULL, TRUE);
    + types = g_list_append(types, type);
    +
    + type = purple_status_type_new_with_attrs(
    + PURPLE_STATUS_AWAY, NULL, NULL, TRUE, TRUE, FALSE,
    + "message", _("Message"), purple_value_new(G_TYPE_STRING),
    + NULL);
    + types = g_list_append(types, type);
    +
    + type = purple_status_type_new(PURPLE_STATUS_OFFLINE, NULL, NULL, TRUE);
    + types = g_list_append(types, type);
    +
    + return types;
    +}
    +
    +static void
    +purple_ircv3_protocol_can_connect_async(PurpleProtocol *protocol,
    + PurpleAccount *account,
    + GCancellable *cancellable,
    + GAsyncReadyCallback callback,
    + gpointer data)
    +{
    + GNetworkMonitor *monitor = NULL;
    + GSocketConnectable *connectable = NULL;
    + GStrv parts = NULL;
    + GTask *task = NULL;
    + const char *username = NULL;
    + gint port = 0;
    +
    + task = g_task_new(protocol, cancellable, callback, data);
    +
    + monitor = g_network_monitor_get_default();
    +
    + username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
    + parts = g_strsplit(username, "@", 2);
    + port = purple_account_get_int(account, "port",
    + PURPLE_IRCV3_DEFAULT_TLS_PORT);
    +
    + connectable = g_network_address_new(parts[1], (guint16)port);
    + g_strfreev(parts);
    +
    + g_network_monitor_can_reach_async(monitor, connectable, cancellable,
    + purple_ircv3_protocol_can_reach_cb,
    + task);
    + g_clear_object(&connectable);
    +}
    +
    +static gboolean
    +purple_ircv3_protocol_can_connect_finish(G_GNUC_UNUSED PurpleProtocol *protocol,
    + GAsyncResult *result,
    + GError **error)
    +{
    + return g_task_propagate_boolean(G_TASK(result), error);
    +}
    +
    +/******************************************************************************
    + * GObject Implementation
    + *****************************************************************************/
    +G_DEFINE_DYNAMIC_TYPE_EXTENDED(
    + PurpleIRCv3Protocol,
    + purple_ircv3_protocol,
    + PURPLE_TYPE_PROTOCOL,
    + 0,
    + G_IMPLEMENT_INTERFACE_DYNAMIC(PURPLE_TYPE_PROTOCOL_CONVERSATION,
    + purple_ircv3_protocol_conversation_init))
    +
    +static void
    +purple_ircv3_protocol_init(G_GNUC_UNUSED PurpleIRCv3Protocol *protocol) {
    +}
    +
    +static void
    +purple_ircv3_protocol_class_finalize(G_GNUC_UNUSED PurpleIRCv3ProtocolClass *klass) {
    +}
    +
    +static void
    +purple_ircv3_protocol_class_init(PurpleIRCv3ProtocolClass *klass) {
    + PurpleProtocolClass *protocol_class = PURPLE_PROTOCOL_CLASS(klass);
    +
    + protocol_class->get_user_splits = purple_ircv3_protocol_get_user_splits;
    + protocol_class->get_account_options =
    + purple_ircv3_protocol_get_account_options;
    + protocol_class->create_connection =
    + purple_ircv3_protocol_create_connection;
    + protocol_class->status_types = purple_ircv3_protocol_status_types;
    + protocol_class->can_connect_async =
    + purple_ircv3_protocol_can_connect_async;
    + protocol_class->can_connect_finish =
    + purple_ircv3_protocol_can_connect_finish;
    +}
    +
    +/******************************************************************************
    + * GObject Implementation
    + *****************************************************************************/
    +void
    +purple_ircv3_protocol_register(GPluginNativePlugin *plugin) {
    + purple_ircv3_protocol_register_type(G_TYPE_MODULE(plugin));
    +}
    +
    +PurpleProtocol *
    +purple_ircv3_protocol_new(void) {
    + return g_object_new(
    + PURPLE_IRCV3_TYPE_PROTOCOL,
    + "id", "prpl-ircv3",
    + "name", "IRCv3",
    + "description", _("Version 3 of Internet Relay Chat (IRC)."),
    + "icon-name", "im-ircv3",
    + "icon-resource-path", "/im/pidgin/libpurple/ircv3/icons",
    + "options", OPT_PROTO_CHAT_TOPIC | OPT_PROTO_PASSWORD_OPTIONAL |
    + OPT_PROTO_SLASH_COMMANDS_NATIVE,
    + NULL);
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3protocol.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,71 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_PROTOCOL_H
    +#define PURPLE_IRCV3_PROTOCOL_H
    +
    +#include <glib.h>
    +#include <glib-object.h>
    +
    +#include <gplugin.h>
    +#include <gplugin-native.h>
    +
    +#include <purple.h>
    +
    +#include "purpleircv3version.h"
    +
    +G_BEGIN_DECLS
    +
    +#define PURPLE_IRCV3_TYPE_PROTOCOL (purple_ircv3_protocol_get_type())
    +
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +G_DECLARE_DERIVABLE_TYPE(PurpleIRCv3Protocol, purple_ircv3_protocol,
    + PURPLE_IRCV3, PROTOCOL, PurpleProtocol)
    +
    +struct _PurpleIRCv3ProtocolClass {
    + /*< private >*/
    + PurpleProtocolClass parent;
    +
    + /*< private >*/
    + gpointer reserved[4];
    +};
    +
    +/**
    + * purple_ircv3_protocol_register: (skip)
    + * @plugin: The GTypeModule
    + *
    + * Registers the dynamic type using @plugin.
    + *
    + * Since: 3.0
    + */
    +G_GNUC_INTERNAL void purple_ircv3_protocol_register(GPluginNativePlugin *plugin);
    +
    +G_GNUC_INTERNAL PurpleProtocol *purple_ircv3_protocol_new(void);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_PROTOCOL_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3protocolconversation.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,180 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib/gi18n-lib.h>
    +
    +#include "purpleircv3protocolconversation.h"
    +
    +#include "purpleircv3connection.h"
    +#include "purpleircv3constants.h"
    +#include "purpleircv3core.h"
    +
    +/******************************************************************************
    + * PurpleProtocolConversation Implementation
    + *****************************************************************************/
    +static void
    +purple_ircv3_protocol_conversation_send_message_async(PurpleProtocolConversation *protocol,
    + PurpleConversation *conversation,
    + PurpleMessage *message,
    + GCancellable *cancellable,
    + GAsyncReadyCallback callback,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *v3_connection = NULL;
    + PurpleAccount *account = NULL;
    + PurpleConnection *connection = NULL;
    + GTask *task = NULL;
    + const char *id = NULL;
    +
    + account = purple_conversation_get_account(conversation);
    + connection = purple_account_get_connection(account);
    + v3_connection = PURPLE_IRCV3_CONNECTION(connection);
    +
    + id = purple_conversation_get_id(conversation);
    + /* TODO: the new message dialog sets the name but not the id and we want to
    + * use the id only, so for now if id is NULL we grab the name.
    + */
    + if(purple_strempty(id)) {
    + id = purple_conversation_get_name(conversation);
    + }
    +
    + purple_ircv3_connection_writef(v3_connection, "PRIVMSG %s :%s", id,
    + purple_message_get_contents(message));
    +
    + task = g_task_new(protocol, cancellable, callback, data);
    + g_task_return_boolean(task, TRUE);
    + g_clear_object(&task);
    +
    + /* This will be made conditional when we add echo-message support.
    + * https://ircv3.net/specs/extensions/echo-message
    + */
    + purple_conversation_write_message(conversation, message);
    +}
    +
    +static gboolean
    +purple_ircv3_protocol_conversation_send_message_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
    + GAsyncResult *result,
    + GError **error)
    +{
    + return g_task_propagate_boolean(G_TASK(result), error);
    +}
    +
    +static PurpleChannelJoinDetails *
    +purple_ircv3_protocol_conversation_get_channel_join_details(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
    + G_GNUC_UNUSED PurpleAccount *account)
    +{
    + return purple_channel_join_details_new(FALSE, TRUE);
    +}
    +
    +static void
    +purple_ircv3_protocol_conversation_join_channel_async(PurpleProtocolConversation *protocol,
    + PurpleAccount *account,
    + PurpleChannelJoinDetails* details,
    + GCancellable* cancellable,
    + GAsyncReadyCallback callback,
    + gpointer data)
    +{
    + PurpleIRCv3Connection *v3_connection = NULL;
    + PurpleConnection *connection = NULL;
    + PurpleConversation *conversation = NULL;
    + PurpleConversationManager *manager = NULL;
    + GString *cmd = NULL;
    + GTask *task = NULL;
    + const char *name = NULL;
    + const char *password = NULL;
    +
    + connection = purple_account_get_connection(account);
    + v3_connection = PURPLE_IRCV3_CONNECTION(connection);
    +
    + task = g_task_new(protocol, cancellable, callback, data);
    +
    + /* Validate that the name isn't empty. */
    + /* TODO: check that name match the ISUPPORT channel prefixes. */
    + name = purple_channel_join_details_get_name(details);
    + if(purple_strempty(name)) {
    + g_task_return_new_error(task, PURPLE_IRCV3_DOMAIN, 0,
    + "channel name is empty");
    + g_clear_object(&task);
    +
    + return;
    + }
    +
    + manager = purple_conversation_manager_get_default();
    + conversation = purple_conversation_manager_find_with_id(manager, account,
    + name);
    +
    + /* If the conversation already exists, just return TRUE. */
    + if(PURPLE_IS_CONVERSATION(conversation)) {
    + g_task_return_boolean(task, TRUE);
    + g_clear_object(&task);
    +
    + return;
    + }
    +
    + /* Build our join string. */
    + cmd = g_string_new(NULL);
    + g_string_append_printf(cmd, "%s %s", PURPLE_IRCV3_MSG_JOIN, name);
    +
    + password = purple_channel_join_details_get_password(details);
    + if(!purple_strempty(password)) {
    + g_string_append_printf(cmd, " %s", password);
    + }
    +
    + conversation = g_object_new(
    + PURPLE_TYPE_CONVERSATION,
    + "account", account,
    + "type", PurpleConversationTypeChannel,
    + "id", name,
    + "name", name,
    + NULL);
    + purple_conversation_manager_register(manager, conversation);
    + g_clear_object(&conversation);
    +
    + purple_ircv3_connection_writef(v3_connection, "%s", cmd->str);
    + g_string_free(cmd, TRUE);
    +
    + g_task_return_boolean(task, TRUE);
    + g_clear_object(&task);
    +}
    +
    +static gboolean
    +purple_ircv3_protocol_conversation_join_channel_finish(G_GNUC_UNUSED PurpleProtocolConversation *protocol,
    + GAsyncResult *result,
    + GError **error)
    +{
    + return g_task_propagate_boolean(G_TASK(result), error);
    +}
    +
    +void
    +purple_ircv3_protocol_conversation_init(PurpleProtocolConversationInterface *iface) {
    + iface->send_message_async =
    + purple_ircv3_protocol_conversation_send_message_async;
    + iface->send_message_finish =
    + purple_ircv3_protocol_conversation_send_message_finish;
    +
    + iface->get_channel_join_details =
    + purple_ircv3_protocol_conversation_get_channel_join_details;
    + iface->join_channel_async =
    + purple_ircv3_protocol_conversation_join_channel_async;
    + iface->join_channel_finish =
    + purple_ircv3_protocol_conversation_join_channel_finish;
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3protocolconversation.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,41 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_PROTOCOL_CONVERSATION_H
    +#define PURPLE_IRCV3_PROTOCOL_CONVERSATION_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +G_BEGIN_DECLS
    +
    +G_GNUC_INTERNAL void purple_ircv3_protocol_conversation_init(PurpleProtocolConversationInterface *iface);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_PROTOCOL_CONVERSATION_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3sasl.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,563 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <glib/gi18n-lib.h>
    +
    +#include <hasl.h>
    +
    +#include "purpleircv3sasl.h"
    +
    +#include "purpleircv3capabilities.h"
    +#include "purpleircv3connection.h"
    +#include "purpleircv3constants.h"
    +#include "purpleircv3core.h"
    +
    +#define PURPLE_IRCV3_SASL_DATA_KEY ("sasl-data")
    +
    +typedef struct {
    + PurpleConnection *connection;
    +
    + HaslContext *ctx;
    +
    + GString *server_in_buffer;
    +} PurpleIRCv3SASLData;
    +
    +/******************************************************************************
    + * Helpers
    + *****************************************************************************/
    +static const char *
    +purple_ircv3_sasl_get_username(PurpleConnection *connection) {
    + PurpleAccount *account = NULL;
    + const char *username = NULL;
    +
    + account = purple_connection_get_account(connection);
    +
    + username = purple_account_get_string(account, "sasl-login-name", "");
    + if(username != NULL && username[0] != '\0') {
    + return username;
    + }
    +
    + return purple_connection_get_display_name(connection);
    +}
    +
    +/******************************************************************************
    + * SASL Helpers
    + *****************************************************************************/
    +static void
    +purple_ircv3_sasl_data_free(PurpleIRCv3SASLData *data) {
    + g_clear_object(&data->ctx);
    +
    + g_string_free(data->server_in_buffer, TRUE);
    +
    + g_free(data);
    +}
    +
    +static void
    +purple_ircv3_sasl_data_add(PurpleConnection *connection, HaslContext *ctx) {
    + PurpleIRCv3SASLData *data = NULL;
    +
    + data = g_new0(PurpleIRCv3SASLData, 1);
    + g_object_set_data_full(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY,
    + data, (GDestroyNotify)purple_ircv3_sasl_data_free);
    +
    + /* We don't reference this because the life cycle of this data is tied
    + * directly to the connection and adding a reference to the connection
    + * would keep both alive forever.
    + */
    + data->connection = connection;
    + data->ctx = ctx;
    +
    + /* We truncate the server_in_buffer when we need to so that we can minimize
    + * allocations and simplify the logic involved with it.
    + */
    + data->server_in_buffer = g_string_new("");
    +}
    +
    +static void
    +purple_ircv3_sasl_attempt(PurpleIRCv3Connection *connection) {
    + PurpleIRCv3SASLData *data = NULL;
    + const char *next_mechanism = NULL;
    + const char *current = NULL;
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    +
    + current = hasl_context_get_current_mechanism(data->ctx);
    + if(current != NULL) {
    + g_message("SASL '%s' mechanism failed", current);
    + }
    +
    + next_mechanism = hasl_context_next(data->ctx);
    + if(next_mechanism == NULL) {
    + GError *error = g_error_new(PURPLE_CONNECTION_ERROR,
    + PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
    + _("No valid SASL mechanisms found"));
    +
    + purple_connection_take_error(PURPLE_CONNECTION(connection), error);
    +
    + return;
    + }
    +
    + g_message("trying SASL '%s' mechanism", next_mechanism);
    +
    + purple_ircv3_connection_writef(connection, "%s %s",
    + PURPLE_IRCV3_MSG_AUTHENTICATE,
    + next_mechanism);
    +}
    +
    +static void
    +purple_ircv3_sasl_start(PurpleIRCv3Capabilities *caps) {
    + PurpleIRCv3Connection *connection = NULL;
    + PurpleAccount *account = NULL;
    + PurpleConnection *purple_connection = NULL;
    + HaslContext *ctx = NULL;
    + const char *mechanisms = NULL;
    + gboolean toggle = FALSE;
    +
    + connection = purple_ircv3_capabilities_get_connection(caps);
    + purple_connection = PURPLE_CONNECTION(connection);
    + account = purple_connection_get_account(purple_connection);
    +
    + ctx = hasl_context_new();
    +
    + /* At this point we are ready to start our SASL negotiation, so add a wait
    + * counter to the capabilities and start the negotiations!
    + */
    + purple_ircv3_capabilities_add_wait(caps);
    +
    + /* Determine what mechanisms we're allowing and tell the context. */
    + mechanisms = purple_account_get_string(account, "sasl-mechanisms", "");
    + if(purple_strempty(mechanisms)) {
    + /* If the user didn't specify any mechanisms, grab the mechanisms that
    + * the server advertised.
    + */
    + mechanisms = purple_ircv3_capabilities_lookup(caps, "sasl", NULL);
    + }
    + hasl_context_set_allowed_mechanisms(ctx, mechanisms);
    +
    + /* Add the values we know to the context. */
    + hasl_context_set_username(ctx, purple_ircv3_sasl_get_username(purple_connection));
    + hasl_context_set_password(ctx, purple_connection_get_password(purple_connection));
    +
    + toggle = purple_account_get_bool(account, "use-tls", TRUE);
    + hasl_context_set_tls(ctx, toggle);
    +
    + toggle = purple_account_get_bool(account, "plain-sasl-in-clear", FALSE);
    + hasl_context_set_allow_clear_text(ctx, toggle);
    +
    + /* Create our SASLData object, add it to the connection. */
    + purple_ircv3_sasl_data_add(purple_connection, ctx);
    +
    + /* Make it go! */
    + purple_ircv3_sasl_attempt(connection);
    +}
    +
    +/******************************************************************************
    + * Callbacks
    + *****************************************************************************/
    +static void
    +purple_ircv3_sasl_ack_cb(PurpleIRCv3Capabilities *caps,
    + G_GNUC_UNUSED const char *capability,
    + G_GNUC_UNUSED gpointer data)
    +{
    + purple_ircv3_sasl_start(caps);
    +}
    +
    +/******************************************************************************
    + * Internal API
    + *****************************************************************************/
    +void
    +purple_ircv3_sasl_request(PurpleIRCv3Capabilities *capabilities) {
    + purple_ircv3_capabilities_request(capabilities,
    + PURPLE_IRCV3_CAPABILITY_SASL);
    +
    + g_signal_connect(capabilities, "ack::" PURPLE_IRCV3_CAPABILITY_SASL,
    + G_CALLBACK(purple_ircv3_sasl_ack_cb), NULL);
    +}
    +
    +gboolean
    +purple_ircv3_sasl_logged_in(G_GNUC_UNUSED PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    + PurpleAccount *account = NULL;
    + const char *sasl_name = NULL;
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "RPL_LOGGEDIN received with no SASL data "
    + "present");
    +
    + return FALSE;
    + }
    +
    + /* Check if the SASL login name is not set. If it is not set, set it to the
    + * current nick as it was successful.
    + */
    + account = purple_connection_get_account(PURPLE_CONNECTION(connection));
    + sasl_name = purple_account_get_string(account, "sasl-login-name", "");
    + if(purple_strempty(sasl_name)) {
    + char **userparts = NULL;
    + const char *username = NULL;
    +
    + username = purple_contact_info_get_username(PURPLE_CONTACT_INFO(account));
    + userparts = g_strsplit(username, "@", 2);
    +
    + purple_account_set_string(account, "sasl-login-name", userparts[0]);
    +
    + g_strfreev(userparts);
    + }
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_sasl_logged_out(G_GNUC_UNUSED PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "RPL_LOGGEDOUT received with no SASL data "
    + "present");
    +
    + return FALSE;
    + }
    +
    + /* Not sure how to trigger this or what we should do in this case to be
    + * honest, so just note it for now.
    + * -- GK 2023-01-12
    + */
    + g_warning("Server sent SASL logged out");
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_sasl_nick_locked(PurpleIRCv3Message *v3_message,
    + GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    + GStrv params = NULL;
    + char *message = NULL;
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "ERR_NICKLOCKED received with no SASL data "
    + "present");
    +
    + return FALSE;
    + }
    +
    + params = purple_ircv3_message_get_params(v3_message);
    + message = g_strjoinv(" ", params);
    +
    + g_set_error(error, PURPLE_CONNECTION_ERROR,
    + PURPLE_CONNECTION_ERROR_AUTHENTICATION_IMPOSSIBLE,
    + _("Nick name is locked: %s"), message);
    +
    + g_free(message);
    +
    + return FALSE;
    +}
    +
    +
    +gboolean
    +purple_ircv3_sasl_success(G_GNUC_UNUSED PurpleIRCv3Message *message,
    + GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Capabilities *capabilities = NULL;
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    +
    + capabilities = purple_ircv3_connection_get_capabilities(connection);
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "RPL_SASLSUCCESS received with no SASL data "
    + "present");
    +
    + return FALSE;
    + }
    +
    + /* This needs to be after we've checked the SASL data otherwise we might
    + * end up removing a wait that we don't own.
    + */
    + purple_ircv3_capabilities_remove_wait(capabilities);
    +
    + g_message("successfully authenticated with SASL '%s' mechanism.",
    + hasl_context_get_current_mechanism(data->ctx));
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_sasl_failed(G_GNUC_UNUSED PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "ERR_SASLFAIL received with no SASL data present");
    +
    + return FALSE;
    + }
    +
    + purple_ircv3_sasl_attempt(connection);
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_sasl_message_too_long(G_GNUC_UNUSED PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "ERR_SASLTOOLONG received with no SASL data "
    + "present");
    +
    + return FALSE;
    + }
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_sasl_aborted(G_GNUC_UNUSED PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "ERR_SASLABORTED received with no SASL data "
    + "present");
    +
    + return FALSE;
    + }
    +
    + /* This is supposed to get sent, when the client sends `AUTHENTICATE *`,
    + * but we don't do this, so I guess we'll note it for now...?
    + * --GK 2023-01-12
    + */
    + g_warning("The server claims we aborted SASL authentication.");
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_sasl_already_authed(G_GNUC_UNUSED PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "ERR_SASLALREADY received with no SASL data "
    + "present");
    +
    + return FALSE;
    + }
    +
    + /* Similar to aborted above, we don't allow this, so just note that it
    + * happened.
    + * -- GK 2023-01-12
    + */
    + g_warning("Server claims we tried to SASL authenticate again.");
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_sasl_mechanisms(PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    + GStrv params = NULL;
    + guint n_params = 0;
    +
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "RPL_SASLMECHS received with no SASL data "
    + "present");
    +
    + return FALSE;
    + }
    +
    + params = purple_ircv3_message_get_params(message);
    + if(params != NULL) {
    + n_params = g_strv_length(params);
    + }
    +
    + /* We need to find a server that sends this message. The specification says
    + * it _may_ be sent when the client sends AUTHENTICATE with an unknown
    + * mechanism, but ergo doesn't.
    + *
    + * We can't just blindly accept the new list either, as depending on how
    + * the server implements it, we'll need to remove mechanisms we've already
    + * tried in the event the server just dumps the entire list. As we're not
    + * currently tracking which mechanisms we've tried, this will have to be
    + * addressed as well.
    + */
    + if(n_params > 0) {
    + char *message = g_strjoinv(" ", params);
    +
    + g_message("Server sent the following SASL mechanisms: %s", message);
    +
    + g_free(message);
    + } else {
    + g_message("Server sent an empty list of SASL mechanisms");
    + }
    +
    + return TRUE;
    +}
    +
    +gboolean
    +purple_ircv3_sasl_authenticate(PurpleIRCv3Message *message,
    + GError **error,
    + gpointer user_data)
    +{
    + PurpleIRCv3Connection *connection = user_data;
    + PurpleIRCv3SASLData *data = NULL;
    + GStrv params = NULL;
    + char *payload = NULL;
    + gboolean done = FALSE;
    + guint n_params = 0;
    +
    + params = purple_ircv3_message_get_params(message);
    + if(params != NULL) {
    + n_params = g_strv_length(params);
    + }
    +
    + if(n_params != 1) {
    + g_set_error(error, PURPLE_IRCV3_DOMAIN, 0,
    + "ignoring AUTHENTICATE with %d parameters", n_params);
    +
    + return FALSE;
    + }
    +
    + payload = params[0];
    + data = g_object_get_data(G_OBJECT(connection), PURPLE_IRCV3_SASL_DATA_KEY);
    + if(data == NULL) {
    + g_set_error_literal(error, PURPLE_IRCV3_DOMAIN, 0,
    + "AUTHENTICATE received with no SASL data present");
    +
    + return FALSE;
    + }
    +
    + /* If the server sent us a payload, combine the chunks. */
    + if(payload[0] != '+') {
    + g_string_append(data->server_in_buffer, payload);
    +
    + if(strlen(payload) < 400) {
    + done = TRUE;
    + }
    + } else {
    + /* The server sent a + which is an empty message or the final message
    + * ended on a 400 byte barrier. */
    + done = TRUE;
    + }
    +
    + if(done) {
    + HaslMechanismResult res = 0;
    + GError *local_error = NULL;
    + guint8 *server_in = NULL;
    + guint8 *client_out = NULL;
    + gsize server_in_length = 0;
    + size_t client_out_length = 0;
    +
    + /* If we have a buffer, base64 decode it, and then truncate it. */
    + if(data->server_in_buffer->len > 0) {
    + server_in = g_base64_decode(data->server_in_buffer->str,
    + &server_in_length);
    + g_string_truncate(data->server_in_buffer, 0);
    + }
    +
    + /* Try to move to the next step of the sasl client. */
    + res = hasl_context_step(data->ctx, server_in, server_in_length,
    + &client_out, &client_out_length, &local_error);
    +
    + /* We should be done with server_in, so free it.*/
    + g_clear_pointer(&server_in, g_free);
    +
    + if(res == HASL_MECHANISM_RESULT_ERROR) {
    + g_propagate_error(error, local_error);
    +
    + return FALSE;
    + }
    +
    + if(local_error != NULL) {
    + g_warning("hasl_context_step returned an error without an error "
    + "status: %s", local_error->message);
    +
    + g_clear_error(&local_error);
    + }
    +
    + /* If we got an output for the client, write it out. */
    + if(client_out_length > 0) {
    + char *encoded = NULL;
    +
    + encoded = g_base64_encode(client_out, client_out_length);
    + g_clear_pointer(&client_out, g_free);
    +
    + purple_ircv3_connection_writef(connection, "%s %s",
    + PURPLE_IRCV3_MSG_AUTHENTICATE,
    + encoded);
    + g_free(encoded);
    + } else {
    + purple_ircv3_connection_writef(connection, "%s +",
    + PURPLE_IRCV3_MSG_AUTHENTICATE);
    + }
    + }
    +
    + return TRUE;
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3sasl.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,55 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_SASL_H
    +#define PURPLE_IRCV3_SASL_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +#include "purpleircv3capabilities.h"
    +#include "purpleircv3message.h"
    +
    +G_BEGIN_DECLS
    +
    +G_GNUC_INTERNAL void purple_ircv3_sasl_request(PurpleIRCv3Capabilities *capabilities);
    +
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_logged_in(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_logged_out(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_nick_locked(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_success(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_failed(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_message_too_long(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_aborted(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_already_authed(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_mechanisms(PurpleIRCv3Message *message, GError **error, gpointer data);
    +G_GNUC_INTERNAL gboolean purple_ircv3_sasl_authenticate(PurpleIRCv3Message *message, GError **error, gpointer data);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_SASL_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3source.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,91 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#include <purple.h>
    +
    +#include "purpleircv3source.h"
    +
    +/******************************************************************************
    + * Public API
    + *****************************************************************************/
    +void
    +purple_ircv3_source_parse(const char *source, char **nick, char **user,
    + char **host)
    +{
    + GRegex *regex = NULL;
    + GMatchInfo *info = NULL;
    + gboolean matched = FALSE;
    +
    + g_return_if_fail(!purple_strempty(source));
    + g_return_if_fail(nick != NULL || user != NULL || host != NULL);
    +
    + /* If we find any \r \n or spaces in our source, it's invalid so we just
    + * bail immediately.
    + */
    + matched = g_regex_match_simple("[\r\n ]", source, G_REGEX_DEFAULT,
    + G_REGEX_MATCH_DEFAULT);
    + if(matched) {
    + return;
    + }
    +
    + regex = g_regex_new("^(?P<nick>[^ \r\n!]+)(?:!(?P<user>[^@]+)(?:@(?P<host>[^!@]+))?)?$",
    + G_REGEX_DEFAULT, G_REGEX_MATCH_DEFAULT, NULL);
    +
    + matched = g_regex_match(regex, source, G_REGEX_MATCH_DEFAULT, &info);
    + if(!matched) {
    + if(nick != NULL) {
    + *nick = g_strdup(source);
    + }
    +
    + g_clear_pointer(&info, g_match_info_unref);
    + g_clear_pointer(&regex, g_regex_unref);
    +
    + return;
    + }
    +
    + if(nick != NULL) {
    + *nick = g_match_info_fetch_named(info, "nick");
    + }
    +
    + if(user != NULL) {
    + char *tmp = g_match_info_fetch_named(info, "user");
    +
    + if(!purple_strempty(tmp)) {
    + *user = tmp;
    + } else {
    + g_free(tmp);
    + }
    + }
    +
    + if(host != NULL) {
    + char *tmp = g_match_info_fetch_named(info, "host");
    +
    + if(!purple_strempty(tmp)) {
    + *host = tmp;
    + } else {
    + g_free(tmp);
    + }
    + }
    +
    + g_clear_pointer(&info, g_match_info_unref);
    + g_clear_pointer(&regex, g_regex_unref);
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3source.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,58 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_SOURCE_H
    +#define PURPLE_IRCV3_SOURCE_H
    +
    +#include <glib.h>
    +#include <glib-object.h>
    +
    +#include "purpleircv3version.h"
    +
    +G_BEGIN_DECLS
    +
    +/**
    + * purple_ircv3_source_parse:
    + * @source: The source to parse.
    + * @nick: (out) (transfer full): A return address for the nick.
    + * @user: (out) (transfer full): A return address for the user.
    + * @host: (out) (transfer full): A return address for the host.
    + *
    + * Parses a `source` string like `pidgy!~u@53unc8n42i868.irc` into its nick,
    + * user, and host parts per https://modern.ircdocs.horse/#source.
    + *
    + * If the user or host aren't present in @source, but a return address is
    + * provided for them, that pointer will be set to %NULL.
    + *
    + * Since: 3.0
    + */
    +PURPLE_IRCV3_AVAILABLE_IN_ALL
    +void purple_ircv3_source_parse(const char *source, char **nick, char **user, char **host);
    +
    +G_END_DECLS
    +
    +#endif /* PURPLE_IRCV3_SOURCE_H */
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/purpleircv3version.h Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,133 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * Purple is the legal property of its developers, whose names are too numerous
    + * to list here. Please refer to the COPYRIGHT file distributed with this
    + * source distribution.
    + *
    + * This library is free software; you can redistribute it and/or modify
    + * it under the terms of the GNU 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 General Public License for more details.
    + *
    + * You should have received a copy of the GNU General Public License
    + * along with this library; if not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +#if !defined(PURPLE_IRCV3_GLOBAL_HEADER_INSIDE) && \
    + !defined(PURPLE_IRCV3_COMPILATION)
    +# error "only <libpurple/protocols/ircv3.h> may be included directly"
    +#endif
    +
    +#ifndef PURPLE_IRCV3_VERSION_H
    +#define PURPLE_IRCV3_VERSION_H
    +
    +#include <glib.h>
    +
    +#include <purple.h>
    +
    +#if (defined(_WIN32) || defined(__CYGWIN__)) && \
    + !defined(PURPLE_IRCV3_STATIC_COMPILATION)
    +#define _PURPLE_IRCV3_EXPORT __declspec(dllexport)
    +#define _PURPLE_IRCV3_IMPORT __declspec(dllimport)
    +#elif __GNUC__ >= 4
    +#define _PURPLE_IRCV3_EXPORT __attribute__((visibility("default")))
    +#define _PURPLE_IRCV3_IMPORT
    +#else
    +#define _PURPLE_IRCV3_EXPORT
    +#define _PURPLE_IRCV3_IMPORT
    +#endif
    +#ifdef PURPLE_IRCV3_COMPILATION
    +#define _PURPLE_IRCV3_API _PURPLE_IRCV3_EXPORT
    +#else
    +#define _PURPLE_IRCV3_API _PURPLE_IRCV3_IMPORT
    +#endif
    +
    +#define _PURPLE_IRCV3_EXTERN _PURPLE_IRCV3_API extern
    +
    +#ifdef PURPLE_IRCV3_DISABLE_DEPRECATION_WARNINGS
    +#define PURPLE_IRCV3_DEPRECATED _PURPLE_IRCV3_EXTERN
    +#define PURPLE_IRCV3_DEPRECATED_FOR(f) _PURPLE_IRCV3_EXTERN
    +#define PURPLE_IRCV3_UNAVAILABLE(maj, min) _PURPLE_IRCV3_EXTERN
    +#define PURPLE_IRCV3_UNAVAILABLE_STATIC_INLINE(maj, min)
    +#define PURPLE_IRCV3_UNAVAILABLE_TYPE(maj, min)
    +#else
    +#define PURPLE_IRCV3_DEPRECATED G_DEPRECATED _PURPLE_IRCV3_EXTERN
    +#define PURPLE_IRCV3_DEPRECATED_FOR(f) G_DEPRECATED_FOR(f) _PURPLE_IRCV3_EXTERN
    +#define PURPLE_IRCV3_UNAVAILABLE(maj, min) G_UNAVAILABLE(maj, min) _PURPLE_IRCV3_EXTERN
    +#define PURPLE_IRCV3_UNAVAILABLE_STATIC_INLINE(maj, min) G_UNAVAILABLE(maj, min)
    +#define PURPLE_IRCV3_UNAVAILABLE_TYPE(maj, min) G_UNAVAILABLE(maj, min)
    +#endif
    +
    +/**
    + * PURPLE_IRCV3_VERSION_CUR_STABLE:
    + *
    + * A macro that evaluates to the current stable version of the IRCv3 protocol
    + * plugin, in a format that can be used by the C pre-processor.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_VERSION_CUR_STABLE \
    + (G_ENCODE_VERSION(PURPLE_MAJOR_VERSION, PURPLE_MINOR_VERSION))
    +
    +/* If the package sets PURPLE_IRCV3_VERSION_MIN_REQUIRED to some future
    + * PURPLE_IRCV3_VERSION_X_Y value that we don't know about, it will compare as 0 in
    + * preprocessor tests.
    + */
    +#ifndef PURPLE_IRCV3_VERSION_MIN_REQUIRED
    +#define PURPLE_IRCV3_VERSION_MIN_REQUIRED (PURPLE_IRCV3_VERSION_CUR_STABLE)
    +#elif PURPLE_IRCV3_VERSION_MIN_REQUIRED == 0
    +#undef PURPLE_IRCV3_VERSION_MIN_REQUIRED
    +#define PURPLE_IRCV3_VERSION_MIN_REQUIRED (PURPLE_IRCV3_VERSION_CUR_STABLE + 1)
    +#endif /* PURPLE_IRCV3_VERSION_MIN_REQUIRED */
    +
    +#if !defined(PURPLE_IRCV3_VERSION_MAX_ALLOWED) || (PURPLE_IRCV3_VERSION_MAX_ALLOWED == 0)
    +#undef PURPLE_IRCV3_VERSION_MAX_ALLOWED
    +#define PURPLE_IRCV3_VERSION_MAX_ALLOWED (PURPLE_IRCV3_VERSION_CUR_STABLE)
    +#endif /* PURPLE_IRCV3_VERSION_MAX_ALLOWED */
    +
    +/* sanity checks */
    +#if PURPLE_IRCV3_VERSION_MIN_REQUIRED > PURPLE_IRCV3_VERSION_CUR_STABLE
    +#error "PURPLE_IRCV3_VERSION_MIN_REQUIRED must be <= PURPLE_IRCV3_VERSION_CUR_STABLE"
    +#endif
    +#if PURPLE_IRCV3_VERSION_MAX_ALLOWED < PURPLE_IRCV3_VERSION_MIN_REQUIRED
    +#error "PURPLE_IRCV3_VERSION_MAX_ALLOWED must be >= PURPLE_IRCV3_VERSION_MIN_REQUIRED"
    +#endif
    +#if PURPLE_IRCV3_VERSION_MIN_REQUIRED < G_ENCODE_VERSION(3, 0)
    +#error "PURPLE_IRCV3_VERSION_MIN_REQUIRED must be >= PURPLE_IRCV3_VERSION_3_0"
    +#endif
    +
    +#define PURPLE_IRCV3_VAR _PURPLE_IRCV3_EXTERN
    +#define PURPLE_IRCV3_AVAILABLE_IN_ALL _PURPLE_IRCV3_EXTERN
    +
    +/**
    + * PURPLE_IRCV3_VERSION_3_0:
    + *
    + * A macro that evaluates to the 3.0 version of the IRCv3 protocol plugin, in a
    + * format that can be used by the C pre-processor.
    + *
    + * Since: 3.0
    + */
    +#define PURPLE_IRCV3_VERSION_3_0 (G_ENCODE_VERSION(3, 0))
    +
    +#if PURPLE_IRCV3_VERSION_MAX_ALLOWED < PURPLE_IRCV3_VERSION_3_0
    +#define PURPLE_IRCV3_AVAILABLE_IN_3_0 PURPLE_IRCV3_UNAVAILABLE(3, 0)
    +#define PURPLE_IRCV3_AVAILABLE_STATIC_INLINE_IN_3_0 PURPLE_IRCV3_UNAVAILABLE_STATIC_INLINE(3, 0)
    +#define PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0 PURPLE_IRCV3_UNAVAILABLE_MACRO(3, 0)
    +#define PURPLE_IRCV3_AVAILABLE_ENUMERATOR_IN_3_0 PURPLE_IRCV3_UNAVAILABLE_ENUMERATOR(3, 0)
    +#define PURPLE_IRCV3_AVAILABLE_TYPE_IN_3_0 PURPLE_IRCV3_UNAVAILABLE_TYPE(3, 0)
    +#else
    +#define PURPLE_IRCV3_AVAILABLE_IN_3_0 _PURPLE_IRCV3_EXTERN
    +#define PURPLE_IRCV3_AVAILABLE_STATIC_INLINE_IN_3_0
    +#define PURPLE_IRCV3_AVAILABLE_MACRO_IN_3_0
    +#define PURPLE_IRCV3_AVAILABLE_ENUMERATOR_IN_3_0
    +#define PURPLE_IRCV3_AVAILABLE_TYPE_IN_3_0
    +#endif
    +
    +#endif /* PURPLE_IRCV3_VERSION_H */
    Binary file protocols/ircv3/resources/icons/16x16/apps/im-ircv3.png has changed
    Binary file protocols/ircv3/resources/icons/22x22/apps/im-ircv3.png has changed
    Binary file protocols/ircv3/resources/icons/48x48/apps/im-ircv3.png has changed
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/resources/ircv3.gresource.xml Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,8 @@
    +<?xml version="1.0" encoding="UTF-8"?>
    +<gresources>
    + <gresource prefix="/im/pidgin/libpurple/ircv3">
    + <file>icons/16x16/apps/im-ircv3.png</file>
    + <file>icons/22x22/apps/im-ircv3.png</file>
    + <file>icons/48x48/apps/im-ircv3.png</file>
    + </gresource>
    +</gresources>
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/tests/meson.build Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,15 @@
    +TESTS = [
    + 'formatting',
    + 'parser',
    + 'source',
    +]
    +
    +foreach prog : TESTS
    + e = executable(
    + f'test_ircv3_@prog@', f'test_ircv3_@prog@.c',
    + dependencies : [birb_dep, libpurple_dep, glib, hasl],
    + objects : ircv3_prpl.extract_all_objects(),
    + c_args : ['-DPURPLE_IRCV3_COMPILATION'])
    +
    + test(f'ircv3_@prog@', e)
    +endforeach
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/tests/test_ircv3_formatting.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,232 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 <purple.h>
    +
    +#include "../purpleircv3formatting.h"
    +
    +/******************************************************************************
    + * Strip Tests
    + *****************************************************************************/
    +static void
    +test_ircv3_formatting_strip(const char *input, const char *expected) {
    + char *actual = NULL;
    +
    + actual = purple_ircv3_formatting_strip(input);
    + g_assert_cmpstr(actual, ==, expected);
    + g_clear_pointer(&actual, g_free);
    +}
    +
    +static void
    +test_ircv3_formatting_strip_null(void) {
    + test_ircv3_formatting_strip(NULL, NULL);
    +}
    +
    +static void
    +test_ircv3_formatting_strip_empty(void) {
    + test_ircv3_formatting_strip("", "");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_color_comma(void) {
    + test_ircv3_formatting_strip("\003,", ",");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_color_foreground_comma(void) {
    + test_ircv3_formatting_strip("\0033,", ",");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_color_full(void) {
    + test_ircv3_formatting_strip("\0033,9wee", "wee");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_color_foreground_3_digit(void) {
    + test_ircv3_formatting_strip("\003314", "4");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_color_background_3_digit(void) {
    + test_ircv3_formatting_strip("\0031,234", "4");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_hex_color(void) {
    + test_ircv3_formatting_strip("\004FF00FFwoo!", "woo!");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_hex_color_full(void) {
    + test_ircv3_formatting_strip("\004FF00FF,00FF00woo!", "woo!");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_hex_color_comma(void) {
    + test_ircv3_formatting_strip("\004,", ",");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_hex_color_foreground_comma(void) {
    + test_ircv3_formatting_strip("\004FEFEFE,", ",");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_hex_color_foreground_7_characters(void) {
    + test_ircv3_formatting_strip("\004FEFEFEF", "F");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_hex_color_background_7_characters(void) {
    + test_ircv3_formatting_strip("\004FEFEFE,2222223", "3");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_bold(void) {
    + test_ircv3_formatting_strip("this is \002bold\002!", "this is bold!");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_italic(void) {
    + test_ircv3_formatting_strip("what do you \035mean\035?!",
    + "what do you mean?!");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_monospace(void) {
    + test_ircv3_formatting_strip("\021i++;\021", "i++;");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_reset(void) {
    + test_ircv3_formatting_strip("end of formatting\017", "end of formatting");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_reverse(void) {
    + test_ircv3_formatting_strip("re\026ver\026se", "reverse");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_strikethrough(void) {
    + test_ircv3_formatting_strip("\036I could be wrong\036",
    + "I could be wrong");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_underline(void) {
    + test_ircv3_formatting_strip("You can't handle the \037truth\037!",
    + "You can't handle the truth!");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_spec_example1(void) {
    + test_ircv3_formatting_strip(
    + "I love \0033IRC! \003It is the \0037best protocol ever!",
    + "I love IRC! It is the best protocol ever!");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_spec_example2(void) {
    + test_ircv3_formatting_strip(
    + "This is a \035\00313,9cool \003message",
    + "This is a cool message");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_spec_example3(void) {
    + test_ircv3_formatting_strip(
    + "IRC \002is \0034,12so \003great\017!",
    + "IRC is so great!");
    +}
    +
    +static void
    +test_ircv3_formatting_strip_spec_example4(void) {
    + test_ircv3_formatting_strip(
    + "Rules: Don't spam 5\00313,8,6\003,7,8, and especially not \0029\002\035!",
    + "Rules: Don't spam 5,6,7,8, and especially not 9!");
    +}
    +
    +/******************************************************************************
    + * Main
    + *****************************************************************************/
    +int
    +main(int argc, char *argv[]) {
    + g_test_init(&argc, &argv, NULL);
    +
    + g_test_add_func("/ircv3/formatting/strip/null",
    + test_ircv3_formatting_strip_null);
    + g_test_add_func("/ircv3/formatting/strip/empty",
    + test_ircv3_formatting_strip_empty);
    +
    + g_test_add_func("/ircv3/formatting/strip/color-comma",
    + test_ircv3_formatting_strip_color_comma);
    + g_test_add_func("/ircv3/formatting/strip/color-full",
    + test_ircv3_formatting_strip_color_full);
    + g_test_add_func("/ircv3/formatting/strip/color-foreground-comma",
    + test_ircv3_formatting_strip_color_foreground_comma);
    + g_test_add_func("/ircv3/formatting/strip/color-foreground-3-digit",
    + test_ircv3_formatting_strip_color_foreground_3_digit);
    + g_test_add_func("/ircv3/formatting/strip/color-background-3-digit",
    + test_ircv3_formatting_strip_color_background_3_digit);
    +
    + g_test_add_func("/ircv3/formatting/strip/hex-color",
    + test_ircv3_formatting_strip_hex_color);
    + g_test_add_func("/ircv3/formatting/strip/hex-color-full",
    + test_ircv3_formatting_strip_hex_color_full);
    + g_test_add_func("/ircv3/formatting/strip/hex-color-comma",
    + test_ircv3_formatting_strip_hex_color_comma);
    + g_test_add_func("/ircv3/formatting/strip/hex-color-foreground-comma",
    + test_ircv3_formatting_strip_hex_color_foreground_comma);
    + g_test_add_func("/ircv3/formatting/strip/hex-color-foreground-7-characters",
    + test_ircv3_formatting_strip_hex_color_foreground_7_characters);
    + g_test_add_func("/ircv3/formatting/strip/hex-color-background-7-characters",
    + test_ircv3_formatting_strip_hex_color_background_7_characters);
    +
    + g_test_add_func("/ircv3/formatting/strip/bold",
    + test_ircv3_formatting_strip_bold);
    + g_test_add_func("/ircv3/formatting/strip/italic",
    + test_ircv3_formatting_strip_italic);
    + g_test_add_func("/ircv3/formatting/strip/monospace",
    + test_ircv3_formatting_strip_monospace);
    + g_test_add_func("/ircv3/formatting/strip/reset",
    + test_ircv3_formatting_strip_reset);
    + g_test_add_func("/ircv3/formatting/strip/reverse",
    + test_ircv3_formatting_strip_reverse);
    + g_test_add_func("/ircv3/formatting/strip/strikethrough",
    + test_ircv3_formatting_strip_strikethrough);
    + g_test_add_func("/ircv3/formatting/strip/underline",
    + test_ircv3_formatting_strip_underline);
    +
    + /* These tests are based on the examples here
    + * https://modern.ircdocs.horse/formatting.html#examples
    + */
    + g_test_add_func("/ircv3/formatting/strip/spec-example1",
    + test_ircv3_formatting_strip_spec_example1);
    + g_test_add_func("/ircv3/formatting/strip/spec-example2",
    + test_ircv3_formatting_strip_spec_example2);
    + g_test_add_func("/ircv3/formatting/strip/spec-example3",
    + test_ircv3_formatting_strip_spec_example3);
    + g_test_add_func("/ircv3/formatting/strip/spec-example4",
    + test_ircv3_formatting_strip_spec_example4);
    +
    + return g_test_run();
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/tests/test_ircv3_parser.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,822 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 <purple.h>
    +
    +#include "../purpleircv3constants.h"
    +#include "../purpleircv3message.h"
    +#include "../purpleircv3parser.h"
    +
    +#define TEST_IRCV3_PARSER_DOMAIN (g_quark_from_static_string("test-ircv3-parser"))
    +
    +typedef struct {
    + GHashTable *tags;
    + gchar *source;
    + gchar *command;
    + guint n_params;
    + const gchar * const params[16];
    +} TestPurpleIRCv3ParserData;
    +
    +/******************************************************************************
    + * Handlers
    + *****************************************************************************/
    +static gboolean
    +test_purple_ircv3_test_handler(PurpleIRCv3Message *message,
    + G_GNUC_UNUSED GError **error,
    + gpointer data)
    +{
    + TestPurpleIRCv3ParserData *d = data;
    + GHashTable *tags = NULL;
    + GHashTableIter iter;
    + GStrv params = NULL;
    + const char *command = NULL;
    + const char *source = NULL;
    + guint n_params = 0;
    +
    + command = purple_ircv3_message_get_command(message);
    + params = purple_ircv3_message_get_params(message);
    + source = purple_ircv3_message_get_source(message);
    + tags = purple_ircv3_message_get_tags(message);
    +
    + if(params != NULL) {
    + n_params = g_strv_length(params);
    + }
    +
    + /* Make sure we have an expected tags hash table before checking them. */
    + if(d->tags != NULL) {
    + gpointer expected_key;
    + gpointer expected_value;
    + guint actual_size;
    + guint expected_size;
    +
    + /* Make sure the tag hash tables have the same size. */
    + expected_size = g_hash_table_size(d->tags);
    + actual_size = g_hash_table_size(tags);
    + g_assert_cmpuint(actual_size, ==, expected_size);
    +
    + /* Since the tables have the same size, we can walk through the expected
    + * table and use it to verify the actual table.
    + */
    + g_hash_table_iter_init(&iter, d->tags);
    + while(g_hash_table_iter_next(&iter, &expected_key, &expected_value)) {
    + gpointer actual_value = NULL;
    + gboolean found = FALSE;
    +
    + found = g_hash_table_lookup_extended(tags, expected_key, NULL,
    + &actual_value);
    + g_assert_true(found);
    + g_assert_cmpstr(actual_value, ==, expected_value);
    + }
    + }
    +
    + /* Walk through the params checking against the expected values. */
    + if(d->n_params > 0) {
    + g_assert_cmpuint(n_params, ==, d->n_params);
    +
    + for(guint i = 0; i < d->n_params; i++) {
    + g_assert_cmpstr(params[i], ==, d->params[i]);
    + }
    + }
    +
    + /* Validate all the string parameters. */
    + g_assert_cmpstr(source, ==, d->source);
    + g_assert_cmpstr(command, ==, d->command);
    +
    + /* Cleanup everything the caller allocated. */
    + g_clear_pointer(&d->tags, g_hash_table_destroy);
    +
    + /* Return the return value the caller asked for. */
    + return TRUE;
    +}
    +
    +static gboolean
    +test_purple_ircv3_test_handler_error(G_GNUC_UNUSED PurpleIRCv3Message *message,
    + GError **error,
    + G_GNUC_UNUSED gpointer data)
    +{
    + g_set_error(error, TEST_IRCV3_PARSER_DOMAIN, 0, "test error");
    +
    + return FALSE;
    +}
    +
    +/******************************************************************************
    + * Helpers
    + *****************************************************************************/
    +static void
    +test_purple_ircv3_parser(const gchar *source, TestPurpleIRCv3ParserData *d) {
    + PurpleIRCv3Parser *parser = purple_ircv3_parser_new();
    + GError *error = NULL;
    + gboolean result = FALSE;
    +
    + purple_ircv3_parser_set_fallback_handler(parser,
    + test_purple_ircv3_test_handler);
    +
    + result = purple_ircv3_parser_parse(parser, source, &error, d);
    +
    + g_assert_no_error(error);
    + g_assert_true(result);
    +
    + g_clear_object(&parser);
    +}
    +
    +/******************************************************************************
    + * Tests
    + *****************************************************************************/
    +static void
    +test_purple_ircv3_parser_propagates_errors(void) {
    + PurpleIRCv3Parser *parser = purple_ircv3_parser_new();
    + GError *error = NULL;
    + gboolean result = FALSE;
    +
    + purple_ircv3_parser_set_fallback_handler(parser,
    + test_purple_ircv3_test_handler_error);
    +
    + result = purple_ircv3_parser_parse(parser, "COMMAND", &error, NULL);
    + g_assert_error(error, TEST_IRCV3_PARSER_DOMAIN, 0);
    + g_clear_error(&error);
    +
    + g_assert_false(result);
    +
    + g_clear_object(&parser);
    +}
    +
    +static void
    +test_purple_ircv3_parser_simple(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "foo",
    + .n_params = 3,
    + .params = {"bar", "baz", "asdf"},
    + };
    +
    + test_purple_ircv3_parser("foo bar baz asdf", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_source(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "coolguy",
    + .command = "foo",
    + .n_params = 3,
    + .params = {"bar", "baz", "asdf"},
    + };
    +
    + test_purple_ircv3_parser(":coolguy foo bar baz asdf", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_trailing(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "foo",
    + .n_params = 3,
    + .params = {"bar", "baz", "asdf quux"},
    + };
    +
    + test_purple_ircv3_parser("foo bar baz :asdf quux", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_empty_trailing(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "foo",
    + .n_params = 3,
    + .params = {"bar", "baz", ""},
    + };
    +
    + test_purple_ircv3_parser("foo bar baz :", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_trailing_starting_colon(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "foo",
    + .n_params = 3,
    + .params = {"bar", "baz", ":asdf"},
    + };
    +
    + test_purple_ircv3_parser("foo bar baz ::asdf", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_source_and_trailing(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "coolguy",
    + .command = "foo",
    + .n_params = 3,
    + .params = {"bar", "baz", "asdf quux"},
    + };
    +
    + test_purple_ircv3_parser(":coolguy foo bar baz :asdf quux", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_source_and_trailing_whitespace(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "coolguy",
    + .command = "foo",
    + .n_params = 3,
    + .params = {"bar", "baz", " asdf quux "},
    + };
    +
    + test_purple_ircv3_parser(":coolguy foo bar baz : asdf quux ", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_source_and_trailing_colon(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "coolguy",
    + .command = PURPLE_IRCV3_MSG_PRIVMSG,
    + .n_params = 2,
    + .params = {"bar", "lol :) "},
    + };
    +
    + test_purple_ircv3_parser(":coolguy PRIVMSG bar :lol :) ", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_source_and_empty_trailing(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "coolguy",
    + .command = "foo",
    + .n_params = 3,
    + .params = {"bar", "baz", ""},
    + };
    +
    + test_purple_ircv3_parser(":coolguy foo bar baz :", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_source_and_trailing_only_whitespace(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "coolguy",
    + .command = "foo",
    + .n_params = 3,
    + .params = {"bar", "baz", " "},
    + };
    +
    + test_purple_ircv3_parser(":coolguy foo bar baz : ", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_tags(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "foo",
    + };
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "a", "b");
    + g_hash_table_insert(data.tags, "c", "32");
    + g_hash_table_insert(data.tags, "k", "");
    + g_hash_table_insert(data.tags, "rt", "ql7");
    +
    + test_purple_ircv3_parser("@a=b;c=32;k;rt=ql7 foo", &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_with_escaped_tags(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "foo",
    + };
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "a", "b\\and\nk");
    + g_hash_table_insert(data.tags, "c", "72 45");
    + g_hash_table_insert(data.tags, "d", "gh;764");
    +
    + test_purple_ircv3_parser("@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo",
    + &data);
    +}
    +
    +static void
    +test_purple_ircv3_with_tags_and_source(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "quux",
    + .command = "ab",
    + .n_params = 1,
    + .params = {"cd"},
    + };
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "c", "");
    + g_hash_table_insert(data.tags, "h", "");
    + g_hash_table_insert(data.tags, "a", "b");
    +
    + test_purple_ircv3_parser("@c;h=;a=b :quux ab cd", &data);
    +}
    +
    +static void
    +test_purple_ircv3_last_param_no_colon(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "src",
    + .command = PURPLE_IRCV3_MSG_JOIN,
    + .n_params = 1,
    + .params = {"#chan"},
    + };
    +
    + test_purple_ircv3_parser(":src JOIN #chan", &data);
    +}
    +
    +static void
    +test_purple_ircv3_last_param_with_colon(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "src",
    + .command = PURPLE_IRCV3_MSG_JOIN,
    + .n_params = 1,
    + .params = {"#chan"},
    + };
    +
    + test_purple_ircv3_parser(":src JOIN :#chan", &data);
    +}
    +
    +static void
    +test_purple_ircv3_without_last_param(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "src",
    + .command = "AWAY",
    + };
    +
    + test_purple_ircv3_parser(":src AWAY", &data);
    +}
    +
    +static void
    +test_purple_ircv3_with_last_param(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "src",
    + .command = "AWAY",
    + };
    +
    + test_purple_ircv3_parser(":src AWAY ", &data);
    +}
    +
    +static void
    +test_purple_ircv3_tab_is_not_space(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "cool\tguy",
    + .command = "foo",
    + .n_params = 2,
    + .params = {"bar", "baz"},
    + };
    +
    + test_purple_ircv3_parser(":cool\tguy foo bar baz", &data);
    +}
    +
    +static void
    +test_purple_ircv3_source_control_characters_1(void) {
    + /* Break each string after the hex escape as they are supposed to only be
    + * a single byte, but the c compiler will keep unescaping unless we break
    + * the string.
    + */
    + TestPurpleIRCv3ParserData data = {
    + .source = "coolguy!ag@net\x03" "5w\x03" "ork.admin",
    + .command = PURPLE_IRCV3_MSG_PRIVMSG,
    + .n_params = 2,
    + .params = {"foo", "bar baz"},
    + };
    + const gchar *msg = NULL;
    +
    + msg = ":coolguy!ag@net\x03" "5w\x03" "ork.admin PRIVMSG foo :bar baz";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_source_control_characters_2(void) {
    + /* Break each string after the hex escape as they are supposed to only be
    + * a single byte, but the c compiler will keep unescaping unless we break
    + * the string.
    + */
    + TestPurpleIRCv3ParserData data = {
    + .source = "coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin",
    + .command = PURPLE_IRCV3_MSG_PRIVMSG,
    + .n_params = 2,
    + .params = {"foo", "bar baz"},
    + };
    + const gchar *msg = NULL;
    +
    + msg = ":coolguy!~ag@n\x02" "et\x03" "05w\x0f" "ork.admin PRIVMSG foo :bar "
    + "baz";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_everything(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "irc.example.com",
    + .command = "COMMAND",
    + .n_params = 3,
    + .params = {"param1", "param2", "param3 param3"},
    + };
    + const gchar *msg = NULL;
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "tag1", "value1");
    + g_hash_table_insert(data.tags, "tag2", "");
    + g_hash_table_insert(data.tags, "vendor1/tag3", "value2");
    + g_hash_table_insert(data.tags, "vendor2/tag4", "");
    +
    + msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= "
    + ":irc.example.com COMMAND param1 param2 :param3 param3";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_everything_but_tags(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "irc.example.com",
    + .command = "COMMAND",
    + .n_params = 3,
    + .params = {"param1", "param2", "param3 param3"},
    + };
    + const gchar *msg = NULL;
    +
    + msg = ":irc.example.com COMMAND param1 param2 :param3 param3";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_everything_but_source(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "COMMAND",
    + .n_params = 3,
    + .params = {"param1", "param2", "param3 param3"},
    + };
    + const gchar *msg = NULL;
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "tag1", "value1");
    + g_hash_table_insert(data.tags, "tag2", "");
    + g_hash_table_insert(data.tags, "vendor1/tag3", "value2");
    + g_hash_table_insert(data.tags, "vendor2/tag4", "");
    +
    + msg = "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4 "
    + "COMMAND param1 param2 :param3 param3";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_command_only(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "COMMAND",
    + };
    +
    + test_purple_ircv3_parser("COMMAND", &data);
    +}
    +
    +static void
    +test_purple_ircv3_slashes_are_fun(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "COMMAND",
    + };
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "foo", "\\\\;\\s \r\n");
    +
    + test_purple_ircv3_parser("@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND", &data);
    +}
    +
    +static void
    +test_purple_ircv3_unreal_broken_1(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "gravel.mozilla.org",
    + .command = "432",
    + .n_params = 2,
    + .params = {"#momo", "Erroneous Nickname: Illegal characters"},
    + };
    + const gchar *msg = NULL;
    +
    + msg = ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal "
    + "characters";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_unreal_broken_2(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "gravel.mozilla.org",
    + .command = "MODE",
    + .n_params = 2,
    + .params = {"#tckk", "+n"},
    + };
    +
    + test_purple_ircv3_parser(":gravel.mozilla.org MODE #tckk +n ", &data);
    +}
    +
    +static void
    +test_purple_ircv3_unreal_broken_3(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "services.esper.net",
    + .command = "MODE",
    + .n_params = 3,
    + .params = {"#foo-bar", "+o", "foobar"},
    + };
    +
    + test_purple_ircv3_parser(":services.esper.net MODE #foo-bar +o foobar ",
    + &data);
    +}
    +
    +static void
    +test_purple_ircv3_tag_escape_char_at_a_time(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "COMMAND",
    + };
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "tag1", "value\\ntest");
    +
    + test_purple_ircv3_parser("@tag1=value\\\\ntest COMMAND", &data);
    +}
    +
    +static void
    +test_purple_ircv3_tag_drop_unnecessary_escapes(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "COMMAND",
    + };
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "tag1", "value1");
    +
    + test_purple_ircv3_parser("@tag1=value\\1 COMMAND", &data);
    +}
    +
    +static void
    +test_purple_ircv3_tag_drop_trailing_slash(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "COMMAND",
    + };
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "tag1", "value1");
    +
    + test_purple_ircv3_parser("@tag1=value1\\ COMMAND", &data);
    +}
    +
    +static void
    +test_purple_ircv3_duplicate_tags(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "COMMAND",
    + };
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "tag1", "5");
    + g_hash_table_insert(data.tags, "tag2", "3");
    + g_hash_table_insert(data.tags, "tag3", "4");
    +
    + test_purple_ircv3_parser("@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND", &data);
    +}
    +
    +static void
    +test_purple_ircv3_vendor_tags_are_namespaced(void) {
    + TestPurpleIRCv3ParserData data = {
    + .command = "COMMAND",
    + };
    + const gchar *msg = NULL;
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "tag1", "5");
    + g_hash_table_insert(data.tags, "tag2", "3");
    + g_hash_table_insert(data.tags, "tag3", "4");
    + g_hash_table_insert(data.tags, "vendor/tag2", "8");
    +
    + msg = "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_special_mode_1(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "SomeOp",
    + .command = "MODE",
    + .n_params = 2,
    + .params = {"#channel", "+i"},
    + };
    +
    + test_purple_ircv3_parser(":SomeOp MODE #channel :+i", &data);
    +}
    +
    +static void
    +test_purple_ircv3_special_mode_2(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "SomeOp",
    + .command = "MODE",
    + .n_params = 4,
    + .params = {"#channel", "+oo", "SomeUser", "AnotherUser"},
    + };
    +
    + test_purple_ircv3_parser(":SomeOp MODE #channel +oo SomeUser :AnotherUser",
    + &data);
    +}
    +
    +/******************************************************************************
    + * Message tags examples
    + *****************************************************************************/
    +static void
    +test_purple_ircv3_parser_message_tags_none(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "nick!ident@host.com",
    + .command = PURPLE_IRCV3_MSG_PRIVMSG,
    + .n_params = 2,
    + .params = {"me", "Hello"},
    + };
    + const char *msg = NULL;
    +
    + msg = ":nick!ident@host.com PRIVMSG me :Hello";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_message_tags_3_tags(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "nick!ident@host.com",
    + .command = PURPLE_IRCV3_MSG_PRIVMSG,
    + .n_params = 2,
    + .params = {"me", "Hello"},
    + };
    + const char *msg = NULL;
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "aaa", "bbb");
    + g_hash_table_insert(data.tags, "ccc", "");
    + g_hash_table_insert(data.tags, "example.com/ddd", "eee");
    +
    + msg = "@aaa=bbb;ccc;example.com/ddd=eee :nick!ident@host.com PRIVMSG me "
    + ":Hello";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_message_tags_client_only(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "url_bot!bot@example.com",
    + .command = PURPLE_IRCV3_MSG_PRIVMSG,
    + .n_params = 2,
    + .params = {"#channel", "Example.com: A News Story"},
    + };
    + const char *msg = NULL;
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "+icon", "https://example.com/favicon.png");
    +
    + msg = "@+icon=https://example.com/favicon.png :url_bot!bot@example.com "
    + "PRIVMSG #channel :Example.com: A News Story";
    +
    + test_purple_ircv3_parser(msg, &data);
    +}
    +
    +static void
    +test_purple_ircv3_parser_message_tags_labeled_response(void) {
    + TestPurpleIRCv3ParserData data = {
    + .source = "nick!user@example.com",
    + .command = "TAGMSG",
    + .n_params = 1,
    + .params = {"#channel"},
    + };
    + const char *msg = NULL;
    +
    + data.tags = g_hash_table_new(g_str_hash, g_str_equal);
    + g_hash_table_insert(data.tags, "label", "123");
    + g_hash_table_insert(data.tags, "msgid", "abc");
    + g_hash_table_insert(data.tags, "+example-client-tag", "example-value");
    +
    + msg = "@label=123;msgid=abc;+example-client-tag=example-value "
    + ":nick!user@example.com TAGMSG #channel";
    +
    + test_purple_ircv3_parser(msg, &data);
    +
    +}
    +/******************************************************************************
    + * Main
    + *****************************************************************************/
    +gint
    +main(gint argc, gchar *argv[]) {
    + g_test_init(&argc, &argv, NULL);
    +
    + /* Make sure an error in the handler is propagated back up to the calling
    + * function.
    + */
    + g_test_add_func("/ircv3/parser/propagates-errors",
    + test_purple_ircv3_parser_propagates_errors);
    +
    + /* These tests are based on the msg-split tests from
    + * https://github.com/ircdocs/parser-tests/blob/master/tests/msg-split.yaml
    + */
    + g_test_add_func("/ircv3/parser/simple",
    + test_purple_ircv3_parser_simple);
    + g_test_add_func("/ircv3/parser/with-source",
    + test_purple_ircv3_parser_with_source);
    + g_test_add_func("/ircv3/parser/with-trailing",
    + test_purple_ircv3_parser_with_trailing);
    + g_test_add_func("/ircv3/parser/with-empty-trailing",
    + test_purple_ircv3_parser_with_empty_trailing);
    + g_test_add_func("/ircv3/parser/with-trailing-starting-colon",
    + test_purple_ircv3_parser_with_trailing_starting_colon);
    + g_test_add_func("/ircv3/parser/with-source-and-trailing",
    + test_purple_ircv3_parser_with_source_and_trailing);
    + g_test_add_func("/ircv3/parser/with-source-and-trailing-whitespace",
    + test_purple_ircv3_parser_with_source_and_trailing_whitespace);
    + g_test_add_func("/ircv3/parser/with-source-and-trailing-colon",
    + test_purple_ircv3_parser_with_source_and_trailing_colon);
    + g_test_add_func("/ircv3/parser/with-source-and-empty-trailing",
    + test_purple_ircv3_parser_with_source_and_empty_trailing);
    + g_test_add_func("/ircv3/parser/with-source-and-trailing-only-whitespace",
    + test_purple_ircv3_parser_with_source_and_trailing_only_whitespace);
    +
    + g_test_add_func("/ircv3/parser/with-tags",
    + test_purple_ircv3_parser_with_tags);
    + g_test_add_func("/ircv3/parser/with-escaped-tags",
    + test_purple_ircv3_parser_with_escaped_tags);
    + g_test_add_func("/ircv3/parser/with-tags-and-source",
    + test_purple_ircv3_with_tags_and_source);
    +
    + g_test_add_func("/ircv3/parser/last-param-no-colon",
    + test_purple_ircv3_last_param_no_colon);
    + g_test_add_func("/ircv3/parser/last-param-with-colon",
    + test_purple_ircv3_last_param_with_colon);
    +
    + g_test_add_func("/ircv3/parser/without-last-param",
    + test_purple_ircv3_without_last_param);
    + g_test_add_func("/ircv3/parser/with-last-parsm",
    + test_purple_ircv3_with_last_param);
    +
    + g_test_add_func("/ircv3/parser/tab-is-not-space",
    + test_purple_ircv3_tab_is_not_space);
    +
    + g_test_add_func("/ircv3/parser/source_control_characters_1",
    + test_purple_ircv3_source_control_characters_1);
    + g_test_add_func("/ircv3/parser/source_control_characters_2",
    + test_purple_ircv3_source_control_characters_2);
    +
    + g_test_add_func("/ircv3/parser/everything",
    + test_purple_ircv3_everything);
    + g_test_add_func("/ircv3/parser/everything-but-tags",
    + test_purple_ircv3_everything_but_tags);
    + g_test_add_func("/ircv3/parser/everything-but-source",
    + test_purple_ircv3_everything_but_source);
    +
    + g_test_add_func("/ircv3/parser/command-only",
    + test_purple_ircv3_command_only);
    +
    + g_test_add_func("/ircv3/parser/slashes-are-fun",
    + test_purple_ircv3_slashes_are_fun);
    +
    + g_test_add_func("/ircv3/parser/unreal-broken-1",
    + test_purple_ircv3_unreal_broken_1);
    + g_test_add_func("/ircv3/parser/unreal-broken-2",
    + test_purple_ircv3_unreal_broken_2);
    + g_test_add_func("/ircv3/parser/unreal-broken-3",
    + test_purple_ircv3_unreal_broken_3);
    +
    + g_test_add_func("/ircv3/parser/tag-escape-char-at-a-time",
    + test_purple_ircv3_tag_escape_char_at_a_time);
    + g_test_add_func("/ircv3/parser/tag-drop-unnecessary-escapes",
    + test_purple_ircv3_tag_drop_unnecessary_escapes);
    + g_test_add_func("/ircv3/parser/tag-drop-trailing-slash",
    + test_purple_ircv3_tag_drop_trailing_slash);
    +
    + g_test_add_func("/ircv3/parser/duplicate-tags",
    + test_purple_ircv3_duplicate_tags);
    + g_test_add_func("/ircv3/parser/vendor-tags-are-namespaced",
    + test_purple_ircv3_vendor_tags_are_namespaced);
    +
    + g_test_add_func("/ircv3/parser/special-mode-1",
    + test_purple_ircv3_special_mode_1);
    + g_test_add_func("/ircv3/parser/special-mode-2",
    + test_purple_ircv3_special_mode_2);
    +
    + /* These tests are the examples from the message-tags specification,
    + * https://ircv3.net/specs/extensions/message-tags.html.
    + */
    + g_test_add_func("/ircv3/parser/message-tags/none",
    + test_purple_ircv3_parser_message_tags_none);
    + g_test_add_func("/ircv3/parser/message-tags/3-tags",
    + test_purple_ircv3_parser_message_tags_3_tags);
    + g_test_add_func("/ircv3/parser/message-tags/client-only",
    + test_purple_ircv3_parser_message_tags_client_only);
    + g_test_add_func("/ircv3/parser/message-tags/labeled-message",
    + test_purple_ircv3_parser_message_tags_labeled_response);
    +
    + return g_test_run();
    +}
    --- /dev/null Thu Jan 01 00:00:00 1970 +0000
    +++ b/protocols/ircv3/tests/test_ircv3_source.c Mon Mar 25 21:43:28 2024 -0500
    @@ -0,0 +1,158 @@
    +/*
    + * Purple - Internet Messaging Library
    + * Copyright (C) Pidgin Developers <devel@pidgin.im>
    + *
    + * 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 <purple.h>
    +
    +#include "../purpleircv3source.h"
    +
    +/******************************************************************************
    + * Tests
    + *****************************************************************************/
    +static void
    +test_ircv3_source_parse_return_address_required(void) {
    + if(g_test_subprocess()) {
    + purple_ircv3_source_parse("pidgy", NULL, NULL, NULL);
    + }
    +
    + g_test_trap_subprocess(NULL, 0, G_TEST_SUBPROCESS_DEFAULT);
    + g_test_trap_assert_stderr("*nick != NULL || user != NULL || host != NULL' failed*");
    +}
    +
    +static void
    +test_ircv3_source_parse_nick(void) {
    + char *nick = NULL;
    + char *user = NULL;
    + char *host = NULL;
    +
    + /* Once to make sure that user and host are null. */
    + purple_ircv3_source_parse("pidgy", &nick, &user, &host);
    + g_assert_cmpstr(nick, ==, "pidgy");
    + g_clear_pointer(&nick, g_free);
    +
    + g_assert_null(user);
    + g_assert_null(host);
    +
    + /* Again to make sure it works without user and host. */
    + purple_ircv3_source_parse("pidgy", &nick, NULL, NULL);
    + g_assert_cmpstr(nick, ==, "pidgy");
    + g_clear_pointer(&nick, g_free);
    +}
    +
    +static void
    +test_ircv3_source_parse_user(void) {
    + char *nick = NULL;
    + char *user = NULL;
    + char *host = NULL;
    +
    + /* Once to make sure host is null. */
    + purple_ircv3_source_parse("pidgy!~u", &nick, &user, &host);
    + g_assert_cmpstr(nick, ==, "pidgy");
    + g_clear_pointer(&nick, g_free);
    +
    + g_assert_cmpstr(user, ==, "~u");
    + g_clear_pointer(&user, g_free);
    +
    + g_assert_null(host);
    +
    + /* Again to make sure nick and host aren't required. */
    + purple_ircv3_source_parse("pidgy!~u", NULL, &user, NULL);
    + g_assert_cmpstr(user, ==, "~u");
    + g_clear_pointer(&user, g_free);
    +}
    +
    +static void
    +test_ircv3_source_parse_host(void) {
    + char *nick = NULL;
    + char *user = NULL;
    + char *host = NULL;
    +
    + /* Once to make sure everything works. */
    + purple_ircv3_source_parse("pidgy!~u@53unc8n42i868.irc", &nick, &user,
    + &host);
    + g_assert_cmpstr(nick, ==, "pidgy");
    + g_clear_pointer(&nick, g_free);
    +
    + g_assert_cmpstr(user, ==, "~u");
    + g_clear_pointer(&user, g_free);
    +
    + g_assert_cmpstr(host, ==, "53unc8n42i868.irc");
    + g_clear_pointer(&host, g_free);
    +
    + /* Again to make sure nick and host aren't required. */
    + purple_ircv3_source_parse("pidgy!~u@53unc8n42i868.irc", NULL, NULL, &host);
    + g_assert_cmpstr(host, ==, "53unc8n42i868.irc");
    + g_clear_pointer(&host, g_free);
    +}
    +
    +static void
    +test_ircv3_source_parse_baddies(void) {
    + char *nick = NULL;
    + char *user = NULL;
    + char *server = NULL;
    +
    + purple_ircv3_source_parse("\n", &nick, NULL, NULL);
    + g_assert_null(nick);
    +
    + purple_ircv3_source_parse("a\nb", &nick, NULL, NULL);
    + g_assert_null(nick);
    +
    + purple_ircv3_source_parse("\r", &nick, NULL, NULL);
    + g_assert_null(nick);
    +
    + purple_ircv3_source_parse("c\rd", &nick, NULL, NULL);
    + g_assert_null(nick);
    +
    + purple_ircv3_source_parse(" ", &nick, NULL, NULL);
    + g_assert_null(nick);
    +
    + purple_ircv3_source_parse("e f", &nick, NULL, NULL);
    + g_assert_null(nick);
    +
    + purple_ircv3_source_parse("nick@foo!user@server", &nick, &user, &server);
    + g_assert_cmpstr(nick, ==, "nick@foo");
    + g_clear_pointer(&nick, g_free);
    + g_assert_cmpstr(user, ==, "user");
    + g_clear_pointer(&user, g_free);
    + g_assert_cmpstr(server, ==, "server");
    + g_clear_pointer(&server, g_free);
    +
    + purple_ircv3_source_parse("nick!user@server!foo", &nick, &user, &server);
    + g_assert_cmpstr(nick, ==, "nick!user@server!foo");
    + g_assert_null(user);
    + g_assert_null(server);
    +}
    +
    +/******************************************************************************
    + * Main
    + *****************************************************************************/
    +int
    +main(int argc, char *argv[]) {
    + g_test_init(&argc, &argv, NULL);
    +
    + g_test_add_func("/ircv3/source/parse/return-address-required",
    + test_ircv3_source_parse_return_address_required);
    + g_test_add_func("/ircv3/source/parse/nick", test_ircv3_source_parse_nick);
    + g_test_add_func("/ircv3/source/parse/user", test_ircv3_source_parse_user);
    + g_test_add_func("/ircv3/source/parse/host", test_ircv3_source_parse_host);
    + g_test_add_func("/ircv3/source/parse/baddies",
    + test_ircv3_source_parse_baddies);
    +
    + return g_test_run();
    +}
    \ No newline at end of file
    --- a/protocols/meson.build Thu Mar 21 22:20:50 2024 -0500
    +++ b/protocols/meson.build Mon Mar 25 21:43:28 2024 -0500
    @@ -1,2 +1,4 @@
    subdir('bonjour')
    +subdir('demo')
    +subdir('ircv3')
    subdir('xmpp')