Tue, 23 Jan 2024 00:03:38 -0600
Make sure all of the final types in the protocols are defined as such
Testing Done:
Consulted with the turtles and verified the demo protocol plugin could send messages.
Reviewed at https://reviews.imfreedom.org/r/2920/
/* * 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.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.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.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.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.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.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.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); } }