Mon, 10 Oct 2022 02:46:34 -0500
Start adding capability support to the IRCv3 protocol
Right now, this just requests the list of capabilities and stores it. In the
future we'll add hooks to allow the protocol and plugins to request additional
capabilities.
Testing Done:
Connected to a local ergo and verified that the capabilties were output to the terminal.
Reviewed at https://reviews.imfreedom.org/r/1915/
/* * 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 "purpleircv3parser.h" #include "purpleircv3core.h" #include "purpleircv3messagehandlers.h" struct _PurpleIRCv3Parser { GObject parent; GRegex *regex_message; GRegex *regex_tags; PurpleIRCv3MessageHandler fallback_handler; GHashTable *handlers; }; G_DEFINE_TYPE(PurpleIRCv3Parser, purple_ircv3_parser, G_TYPE_OBJECT) /****************************************************************************** * Helpers *****************************************************************************/ 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)) { gchar *key = NULL; gchar *value = NULL; key = g_match_info_fetch_named(info, "key"); value = g_match_info_fetch_named(info, "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, value); 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(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; guint count = 0; *n_params = 0; count = purple_ircv3_parser_extract_params(parser, builder, middle); *n_params = *n_params + count; if(*coda != '\0') { g_strv_builder_add(builder, trailing); *n_params = *n_params + 1; } result = g_strv_builder_end(builder); g_strv_builder_unref(builder); return result; } static void purple_ircv3_parser_add_handler(PurpleIRCv3Parser *parser, const gchar *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); } /****************************************************************************** * Handlers *****************************************************************************/ static gboolean purple_ircv3_fallback_handler(G_GNUC_UNUSED GHashTable *tags, G_GNUC_UNUSED const gchar *source, G_GNUC_UNUSED const gchar *command, G_GNUC_UNUSED guint n_params, G_GNUC_UNUSED GStrv params, GError **error, G_GNUC_UNUSED gpointer data) { g_set_error(error, PURPLE_IRCV3_DOMAIN, 0, "no handler for command %s", command); 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>[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) { 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; guint n_params = 0; 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. */ 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_hash_table_destroy(tags); g_match_info_unref(info); return FALSE; } source = g_match_info_fetch_named(info, "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, &n_params); /* Call the handler. */ result = handler(tags, source, command, n_params, params, error, data); /* Cleanup everything. */ g_free(source); g_free(command); g_free(middle); g_free(coda); g_free(trailing); g_strfreev(params); g_hash_table_destroy(tags); g_match_info_unref(info); return result; } 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_messager_handler_fallback); purple_ircv3_parser_add_handler(parser, "PING", purple_ircv3_messager_handler_ping); purple_ircv3_parser_add_handler(parser, "CAP", purple_ircv3_messager_handler_cap); }