Tue, 03 Dec 2024 13:53:04 -0600
IRCv3: Fix kick detection and handle online conversation states
As we're starting to implement conversation online status in Pidgin we need to
make sure IRCv3 is accounting for it properly.
Testing Done:
Enabled and disabled accounts a bunch and kicked the Pidgin 3 account to verify everything via an upcoming Pidgin review request.
Reviewed at https://reviews.imfreedom.org/r/3690/
/* * 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 "purpleircv3core.h" #include "purpleircv3ctcphandlers.h" /****************************************************************************** * Helpers *****************************************************************************/ static void purple_ircv3_add_contact_to_conversation(PurpleContact *contact, PurpleConversation *conversation, gboolean announce) { PurpleContactInfo *info = PURPLE_CONTACT_INFO(contact); PurpleConversationMember *member = NULL; PurpleConversationMembers *members = NULL; members = purple_conversation_get_members(conversation); member = purple_conversation_members_find_member(members, info); if(!PURPLE_IS_CONVERSATION_MEMBER(member)) { char *message = NULL; if(announce) { message = g_strdup_printf(_("%s has joined %s"), purple_contact_info_get_sid(info), purple_conversation_get_title_for_display(conversation)); } purple_conversation_members_add_member(members, info, announce, message); g_clear_pointer(&message, g_free); } } /****************************************************************************** * General Commands *****************************************************************************/ gboolean purple_ircv3_message_handler_join(G_GNUC_UNUSED IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleContact *contact = NULL; PurpleConversation *conversation = NULL; GStrv params = NULL; const char *conversation_name = NULL; contact = purple_ircv3_connection_find_or_create_contact(connection, message); params = ibis_message_get_params(message); /* A normal join command has the channel as the only parameter. */ if(g_strv_length(params) == 1) { conversation_name = params[0]; } else { /* TODO: write this to join to the status window saying we didn't know * how to parse it. */ return TRUE; } conversation = purple_ircv3_connection_find_or_create_conversation(connection, conversation_name); purple_ircv3_add_contact_to_conversation(contact, conversation, TRUE); return TRUE; } gboolean purple_ircv3_message_handler_part(G_GNUC_UNUSED IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleAccount *account = NULL; PurpleContact *contact = NULL; PurpleConversation *conversation = NULL; PurpleConversationManager *manager = NULL; PurpleConversationMembers *members = NULL; GStrv params = NULL; guint n_params = 0; char *reason = NULL; const char *conversation_name = NULL; params = ibis_message_get_params(message); n_params = g_strv_length(params); if(n_params == 0) { /* TODO: mention unparsable message in the status window. */ return TRUE; } /* TODO: The spec says servers _SHOULD NOT_ send a comma separated list of * channels, but we should support that at some point just in case. */ conversation_name = params[0]; account = purple_connection_get_account(PURPLE_CONNECTION(connection)); manager = purple_conversation_manager_get_default(); conversation = purple_conversation_manager_find_with_id(manager, account, conversation_name); if(!PURPLE_IS_CONVERSATION(conversation)) { /* TODO: write status message unknown channel. */ return TRUE; } members = purple_conversation_get_members(conversation); /* We do want to find or create the contact, even on a part, because we * could have connected to a BNC and we weren't told about the contact yet. */ contact = purple_ircv3_connection_find_or_create_contact(connection, message); /* If a part message was given, join the remaining parameters with a space. */ if(n_params > 1) { char *part_message = NULL; part_message = g_strjoinv(" ", params + 1); reason = g_strdup_printf(_("%s has left %s (%s)"), purple_contact_info_get_sid(PURPLE_CONTACT_INFO(contact)), purple_conversation_get_title_for_display(conversation), part_message); } else { reason = g_strdup_printf(_("%s has left %s"), purple_contact_info_get_sid(PURPLE_CONTACT_INFO(contact)), purple_conversation_get_title_for_display(conversation)); } purple_conversation_members_remove_member(members, PURPLE_CONTACT_INFO(contact), TRUE, reason); g_clear_pointer(&reason, g_free); return TRUE; } gboolean purple_ircv3_message_handler_namreply(IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleConversation *conversation = NULL; GStrv params = NULL; GStrv nicks = NULL; const char *target = NULL; params = ibis_message_get_params(message); if(params == NULL) { g_warning("namreply received with no parameters"); return FALSE; } if(g_strv_length(params) != 4) { char *body = g_strjoinv(" ", params); g_warning("unknown namreply format: '%s'", body); g_free(body); return FALSE; } /* params[0] holds nick of the user and params[1] holds the channel type * (public/private) but we don't care about either of these. */ target = params[2]; if(!ibis_client_is_channel(client, target)) { g_warning("received namreply for '%s' which is not a channel.", target); return FALSE; } conversation = purple_ircv3_connection_find_or_create_conversation(connection, target); /* Split the last parameter on space to get a list of all the nicks. */ nicks = g_strsplit(params[3], " ", -1); if(nicks != NULL) { PurpleAccount *account = NULL; PurpleConnection *purple_connection = NULL; PurpleContactManager *manager = purple_contact_manager_get_default(); PurpleConversationMembers *members = NULL; purple_connection = PURPLE_CONNECTION(connection); account = purple_connection_get_account(purple_connection); members = purple_conversation_get_members(conversation); for(guint i = 0; i < g_strv_length(nicks); i++) { PurpleContact *contact = NULL; const char *nick = nicks[i]; char *stripped = NULL; stripped = ibis_client_strip_source_prefix(client, nick); contact = purple_contact_manager_find_with_id(manager, account, stripped); if(!PURPLE_IS_CONTACT(contact)) { contact = purple_contact_new(account, stripped); purple_contact_info_set_username(PURPLE_CONTACT_INFO(contact), stripped); purple_contact_manager_add(manager, contact); } purple_conversation_members_add_member(members, PURPLE_CONTACT_INFO(contact), FALSE, NULL); g_free(stripped); } } g_strfreev(nicks); return TRUE; } gboolean purple_ircv3_message_handler_tagmsg(IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *ibis_message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleContact *contact = NULL; PurpleConversation *conversation = NULL; GStrv params = NULL; IbisTags *tags = NULL; const char *target = NULL; const char *value = NULL; params = ibis_message_get_params(ibis_message); tags = ibis_message_get_tags(ibis_message); if(params == NULL) { g_warning("tagmsg received with no parameters"); return FALSE; } if(g_strv_length(params) != 1) { char *body = g_strjoinv(" ", params); g_warning("unknown tagmsg message format: '%s'", body); g_free(body); return FALSE; } /* Find or create the contact. */ contact = purple_ircv3_connection_find_or_create_contact(connection, ibis_message); /* Find or create the conversation. */ target = params[0]; if(!ibis_client_is_channel(client, target)) { target = purple_contact_info_get_id(PURPLE_CONTACT_INFO(contact)); } conversation = purple_ircv3_connection_find_or_create_conversation(connection, target); purple_ircv3_add_contact_to_conversation(contact, conversation, FALSE); /* Handle typing notifications. */ value = ibis_tags_lookup(tags, IBIS_TAG_TYPING); if(!purple_strempty(value)) { PurpleConversationMember *member = NULL; PurpleConversationMembers *members = NULL; PurpleTypingState state = PURPLE_TYPING_STATE_NONE; guint timeout = 1; members = purple_conversation_get_members(conversation); member = purple_conversation_members_find_member(members, PURPLE_CONTACT_INFO(contact)); if(purple_strequal(value, IBIS_TYPING_ACTIVE)) { state = PURPLE_TYPING_STATE_TYPING; timeout = 6; } else if(purple_strequal(value, IBIS_TYPING_PAUSED)) { state = PURPLE_TYPING_STATE_PAUSED; timeout = 30; } purple_conversation_member_set_typing_state(member, state, timeout); } return TRUE; } gboolean purple_ircv3_message_handler_privmsg(IbisClient *client, const char *command, IbisMessage *ibis_message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleContact *contact = NULL; PurpleConversation *conversation = NULL; PurpleMessage *message = NULL; GDateTime *dt = NULL; IbisCTCPMessage *ctcp_message = NULL; IbisTags *tags = NULL; GStrv params = NULL; const char *target = NULL; gboolean announce = TRUE; params = ibis_message_get_params(ibis_message); ctcp_message = ibis_message_get_ctcp_message(ibis_message); tags = ibis_message_get_tags(ibis_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; } /* Find or create the contact. */ contact = purple_ircv3_connection_find_or_create_contact(connection, ibis_message); /* Find or create the conversation. */ target = params[0]; if(!ibis_client_is_channel(client, target)) { target = purple_contact_info_get_id(PURPLE_CONTACT_INFO(contact)); } if(!ibis_client_get_registered(client)) { conversation = purple_ircv3_connection_get_status_conversation(connection); announce = FALSE; } else if(IBIS_IS_CTCP_MESSAGE(ctcp_message)) { conversation = purple_ircv3_connection_get_status_conversation(connection); announce = FALSE; } else { conversation = purple_ircv3_connection_find_or_create_conversation(connection, target); } purple_ircv3_add_contact_to_conversation(contact, conversation, announce); if(IBIS_IS_CTCP_MESSAGE(ctcp_message)) { if(ibis_ctcp_message_is_command(ctcp_message, IBIS_CTCP_ACTION)) { GStrv ctcp_params = NULL; char *ctcp_body = NULL; char *stripped = NULL; ctcp_params = ibis_ctcp_message_get_params(ctcp_message); ctcp_body = g_strjoinv(" ", ctcp_params); stripped = ibis_formatting_strip(ctcp_body); g_free(ctcp_body); message = purple_message_new(PURPLE_CONTACT_INFO(contact), stripped); g_free(stripped); purple_message_set_action(message, TRUE); } else { char *body = NULL; body = g_strdup_printf(_("requested CTCP '%s' (to %s) from %s"), ibis_ctcp_message_get_command(ctcp_message), params[0], purple_contact_info_get_id(PURPLE_CONTACT_INFO(contact))); message = purple_message_new(PURPLE_CONTACT_INFO(contact), body); g_free(body); } } if(!PURPLE_IS_MESSAGE(message)) { char *stripped = NULL; stripped = ibis_formatting_strip(params[1]); message = purple_message_new(PURPLE_CONTACT_INFO(contact), stripped); g_clear_pointer(&stripped, g_free); } if(purple_strequal(command, IBIS_MSG_NOTICE)) { purple_message_set_notice(message, TRUE); } if(IBIS_IS_TAGS(tags)) { const char *raw_tag = NULL; /* Grab the msgid if one was provided. */ raw_tag = ibis_tags_lookup(tags, "msgid"); if(!purple_strempty(raw_tag)) { purple_message_set_id(message, raw_tag); } /* Determine the timestamp of the message. */ raw_tag = ibis_tags_lookup(tags, "time"); if(!purple_strempty(raw_tag)) { GTimeZone *tz = g_time_zone_new_utc(); dt = g_date_time_new_from_iso8601(raw_tag, tz); g_time_zone_unref(tz); purple_message_set_timestamp(message, dt); g_date_time_unref(dt); } } /* If the server didn't provide a time, use the current local time. */ if(dt == NULL) { purple_message_set_timestamp_now(message); } purple_conversation_write_message(conversation, message); g_clear_object(&message); /* If the message contained a CTCP message and was a PRIVMSG, we then need * to attempt to handle it. */ if(IBIS_IS_CTCP_MESSAGE(ctcp_message) && purple_strequal(command, IBIS_MSG_PRIVMSG)) { purple_ircv3_ctcp_handler(connection, client, ibis_message); } return TRUE; } gboolean purple_ircv3_message_handler_topic(G_GNUC_UNUSED IbisClient *client, const char *command, IbisMessage *message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleConversation *conversation = NULL; GStrv params = NULL; const char *channel = NULL; const char *topic = NULL; guint n_params = 0; params = ibis_message_get_params(message); n_params = g_strv_length(params); if(purple_strequal(command, IBIS_MSG_TOPIC)) { if(n_params != 2) { g_message("received TOPIC with %u parameters, expected 2", n_params); return FALSE; } channel = params[0]; topic = params[1]; } else if(purple_strequal(command, IBIS_RPL_NOTOPIC)) { if(n_params != 3) { g_message("received RPL_NOTOPIC with %u parameters, expected 3", n_params); return FALSE; } channel = params[1]; topic = ""; } else if(purple_strequal(command, IBIS_RPL_TOPIC)) { if(n_params != 3) { g_message("received RPL_TOPIC with %u parameters, expected 3", n_params); return FALSE; } channel = params[1]; topic = params[2]; } else { g_message("unexpected command %s", command); return FALSE; } conversation = purple_ircv3_connection_find_or_create_conversation(connection, channel); if(!PURPLE_IS_CONVERSATION(conversation)) { g_message("failed to find or create channel '%s'", channel); return FALSE; } purple_conversation_set_topic(conversation, topic); return TRUE; } gboolean purple_ircv3_message_handler_quit(G_GNUC_UNUSED IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *ibis_message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleContact *contact = NULL; PurpleContactInfo *info = NULL; PurpleConversationManager *manager = NULL; GList *conversations = NULL; GStrv params = NULL; guint n_params = 0; char *message = NULL; params = ibis_message_get_params(ibis_message); n_params = g_strv_length(params); contact = purple_ircv3_connection_find_or_create_contact(connection, ibis_message); info = PURPLE_CONTACT_INFO(contact); if(n_params > 0) { char *reason = NULL; reason = g_strjoinv(" ", params); message = g_strdup_printf("%s has quit (%s)", purple_contact_info_get_sid(info), reason); g_free(reason); } else { message = g_strdup_printf("%s has quit", purple_contact_info_get_sid(info)); } manager = purple_conversation_manager_get_default(); conversations = purple_conversation_manager_get_all(manager); while(conversations != NULL) { PurpleConversation *conversation = conversations->data; PurpleConversationMembers *members = NULL; members = purple_conversation_get_members(conversation); purple_conversation_members_remove_member(members, info, TRUE, message); conversations = g_list_delete_link(conversations, conversations); } g_free(message); return TRUE; } gboolean purple_ircv3_message_handler_nick(G_GNUC_UNUSED IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *ibis_message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleContact *contact = NULL; PurpleContactInfo *info = NULL; IbisTags *tags = NULL; GStrv params = NULL; guint n_params = 0; char *new_source = NULL; char *user = NULL; char *host = NULL; const char *source = NULL; const char *nick = NULL; params = ibis_message_get_params(ibis_message); n_params = g_strv_length(params); if(n_params != 1) { g_message("received NICK with %d params, expected 1", n_params); return FALSE; } nick = params[0]; source = ibis_message_get_source(ibis_message); ibis_source_parse(source, NULL, &user, &host); new_source = ibis_source_serialize(nick, user, host); g_clear_pointer(&user, g_free); g_clear_pointer(&host, g_free); contact = purple_ircv3_connection_find_or_create_contact(connection, ibis_message); info = PURPLE_CONTACT_INFO(contact); /* If the account tag doesn't exist, we need to update the id property of * the contact. */ tags = ibis_message_get_tags(ibis_message); if(!ibis_tags_exists(tags, IBIS_TAG_ACCOUNT)) { purple_contact_info_set_id(info, nick); } purple_contact_info_set_display_name(info, nick); purple_contact_info_set_sid(info, new_source); g_clear_pointer(&new_source, g_free); return TRUE; } gboolean purple_ircv3_message_handler_error(IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *ibis_message, gpointer data) { PurpleIRCv3Connection *v3_connection = data; PurpleAccount *account = NULL; PurpleConnection *connection = data; GError *error = NULL; GStrv params = NULL; guint n_params = 0; char *reason = NULL; account = purple_connection_get_account(connection); params = ibis_message_get_params(ibis_message); n_params = g_strv_length(params); if(n_params > 0) { reason = g_strjoinv(" ", params); } else { reason = g_strdup(_("unknown error")); } purple_ircv3_connection_write_status_message(v3_connection, ibis_message, TRUE); error = g_error_new_literal(PURPLE_IRCV3_DOMAIN, 0, reason); g_clear_pointer(&reason, g_free); ibis_client_stop(client, NULL); purple_account_disconnect_with_error(account, error); return TRUE; } gboolean purple_ircv3_message_handler_wallops(G_GNUC_UNUSED IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *ibis_message, gpointer data) { PurpleIRCv3Connection *v3_connection = data; PurpleAccount *account = NULL; PurpleConnection *connection = data; PurpleContact *contact = NULL; PurpleContactInfo *info = NULL; PurpleNotification *notification = NULL; PurpleNotificationManager *manager = NULL; GStrv params = NULL; char *wallops_title = NULL; guint n_params = 0; params = ibis_message_get_params(ibis_message); n_params = g_strv_length(params); if(n_params != 1) { g_message("received WALLOPS with %u params, expected 1", n_params); return FALSE; } contact = purple_ircv3_connection_find_or_create_contact(v3_connection, ibis_message); info = PURPLE_CONTACT_INFO(contact); wallops_title = g_strdup_printf(_("WALLOPS from %s"), purple_contact_info_get_name_for_display(info)); notification = purple_notification_new(NULL, wallops_title); g_free(wallops_title); account = purple_connection_get_account(connection); purple_notification_set_account(notification, account); purple_notification_set_subtitle(notification, params[0]); purple_notification_set_icon_name(notification, PURPLE_IRCV3_ICON_NAME); manager = purple_notification_manager_get_default(); purple_notification_manager_add(manager, notification); g_clear_object(¬ification); return TRUE; } gboolean purple_ircv3_message_handler_kick(G_GNUC_UNUSED IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *ibis_message, gpointer data) { PurpleIRCv3Connection *v3_connection = data; PurpleAccount *account = NULL; PurpleContact *kicker = NULL; PurpleContact *kickee = NULL; PurpleContactInfo *me = NULL; PurpleConversation *conversation = NULL; GStrv params = NULL; char *reason = NULL; const char *kickee_id = NULL; const char *me_id = NULL; guint n_params = 0; params = ibis_message_get_params(ibis_message); n_params = g_strv_length(params); if(n_params < 2) { g_message("received KICK with %u params, need at least 2", n_params); return FALSE; } account = purple_connection_get_account(PURPLE_CONNECTION(v3_connection)); me = purple_account_get_contact_info(account); me_id = purple_contact_info_get_id(me); kicker = purple_ircv3_connection_find_or_create_contact(v3_connection, ibis_message); conversation = purple_ircv3_connection_find_or_create_conversation(v3_connection, params[0]); kickee = purple_ircv3_connection_find_or_create_contact_from_nick(v3_connection, params[1]); kickee_id = purple_contact_info_get_id(PURPLE_CONTACT_INFO(kickee)); if(n_params > 2) { reason = g_strjoinv(" ", params + 2); } else { reason = g_strdup(_("no reason")); } if(purple_strequal(kickee_id, me_id)) { GError *error = NULL; error = g_error_new(PURPLE_IRCV3_DOMAIN, 0, _("You were kicked from %s by %s: %s"), params[0], purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(kicker)), reason); purple_conversation_set_error(conversation, error); g_clear_error(&error); purple_conversation_set_online(conversation, FALSE); } else { PurpleConversationMembers *members = NULL; char *contents = NULL; contents = g_strdup_printf(_("%s kicked %s from %s: %s"), purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(kicker)), purple_contact_info_get_name_for_display(PURPLE_CONTACT_INFO(kickee)), params[0], reason); members = purple_conversation_get_members(conversation); purple_conversation_members_remove_member(members, PURPLE_CONTACT_INFO(kickee), TRUE, contents); g_free(contents); } g_free(reason); return TRUE; }