Thu, 24 Apr 2025 22:19:39 -0500
IRCv3: check if a member is in a conversation before adding them
Awhile ago we update the NAMREPLY handler to build a separate
Purple.ConversationMembers and then splice that onto the existing one to help
with sorting and other performance issues. However, we didn't check if the
users already existed in the existing list, so we would get duplicates. This
address that.
Also fixed a reference leak.
Testing Done:
Joined a channel and sent `/quote names #channel` multiple times and verified that the member list did not grow to include a bunch of duplicates.
Reviewed at https://reviews.imfreedom.org/r/3987/
/* * 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 <pango/pango.h> #include "purpleircv3messagehandlers.h" #include "purpleircv3connection.h" #include "purpleircv3core.h" #include "purpleircv3ctcphandlers.h" /****************************************************************************** * Helpers *****************************************************************************/ static PurpleConversationMember * 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)); } member = purple_conversation_members_add_member(members, info, announce, message); g_clear_pointer(&message, g_free); } return member; } static PurpleBadge * purple_ircv3_badge_for_prefix(const char prefix) { PurpleBadge *badge = NULL; PurpleBadgeManager *manager = NULL; const char *description = NULL; const char *id = NULL; const char *mnemonic = NULL; int priority = 0; manager = purple_badge_manager_get_default(); switch(prefix) { case '~': description = _("Founder"); id = "ircv3-badge-founder"; priority = 500; mnemonic = "~"; break; case '&': description = _("Protected"); id = "ircv3-badge-protected"; priority = 400; mnemonic = "&"; break; case '@': description = _("Operator"); id = "ircv3-badge-operator"; priority = 300; mnemonic = "@"; break; case '%': description = _("Half Operator"); id = "ircv3-badge-halfop"; priority = 200; mnemonic = "%%"; break; case '+': description = _("Voice"); id = "ircv3-badge-voice"; priority = 100; mnemonic = "+"; break; } if(id == NULL) { return NULL; } badge = purple_badge_manager_find(manager, id); if(!PURPLE_IS_BADGE(badge)) { badge = purple_badge_new(id, priority, id, mnemonic); if(!purple_strempty(description)) { purple_badge_set_description(badge, description); } purple_badge_manager_add(manager, badge); /* This is transfer none and the manager has a reference, so we unref * our reference to the badge. */ g_object_unref(badge); } return badge; } static void purple_ircv3_add_badge_to_member(PurpleConversationMember *member, IbisClient *client, const char prefix) { PurpleBadge *badge = NULL; g_return_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member)); g_return_if_fail(IBIS_IS_CLIENT(client)); badge = purple_ircv3_badge_for_prefix(prefix); if(PURPLE_IS_BADGE(badge)) { PurpleBadges *badges = NULL; badges = purple_conversation_member_get_badges(member); purple_badges_add_badge(badges, badge); } } static void purple_ircv3_remove_badge_from_member(PurpleConversationMember *member, IbisClient *client, const char prefix) { PurpleBadge *badge = NULL; g_return_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member)); g_return_if_fail(IBIS_IS_CLIENT(client)); badge = purple_ircv3_badge_for_prefix(prefix); if(PURPLE_IS_BADGE(badge)) { PurpleBadges *badges = purple_conversation_member_get_badges(member); /* I know this is gross, but we need to rework badges here and I wanted * to get this done sooner rather than later. * -- gk 2025-03-14 */ purple_badges_remove_badge(badges, purple_badge_get_id(badge)); } } static void purple_ircv3_add_badges_to_member(PurpleConversationMember *member, IbisClient *client, const char *nick) { char *prefixes = NULL; g_return_if_fail(PURPLE_IS_CONVERSATION_MEMBER(member)); g_return_if_fail(IBIS_IS_CLIENT(client)); prefixes = ibis_client_get_source_prefix(client, nick); if(purple_strempty(prefixes)) { return; } for(guint i = 0; prefixes[i] != '\0'; i++) { purple_ircv3_add_badge_to_member(member, client, prefixes[i]); } g_free(prefixes); } /****************************************************************************** * General Commands *****************************************************************************/ gboolean purple_ircv3_message_handler_away(G_GNUC_UNUSED IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleContact *contact = NULL; PurplePresence *presence = NULL; GStrv params = NULL; char *away_message = NULL; contact = purple_ircv3_connection_find_or_create_contact(connection, message); presence = purple_contact_info_get_presence(PURPLE_CONTACT_INFO(contact)); /* Figure out if we have a message. */ params = ibis_message_get_params(message); if(params != NULL) { away_message = g_strjoinv(" ", params); } /* We have a message so we need to set it and possibly set the presence to * away. */ if(!purple_strempty(away_message)) { purple_presence_set_message(presence, away_message); purple_presence_set_primitive(presence, PURPLE_PRESENCE_PRIMITIVE_AWAY); } else { purple_presence_set_message(presence, NULL); purple_presence_set_primitive(presence, PURPLE_PRESENCE_PRIMITIVE_AVAILABLE); } g_clear_pointer(&away_message, g_free); return TRUE; } gboolean purple_ircv3_message_handler_join(IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleContact *contact = NULL; PurpleConversation *conversation = NULL; IbisMessage *who_message = 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); /* Now fire off a who message to sync the conversation. */ who_message = ibis_message_new(IBIS_MSG_WHO); ibis_message_set_params(who_message, conversation_name, NULL); ibis_client_write(client, who_message); 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 *existing_members = NULL; PurpleConversationMembers *new_members = NULL; const char *active_nick = NULL; purple_connection = PURPLE_CONNECTION(connection); account = purple_connection_get_account(purple_connection); existing_members = purple_conversation_get_members(conversation); new_members = purple_conversation_members_new(); /* We don't want to add ourselves and we're already in the list. */ active_nick = ibis_client_get_active_nick(client); for(guint i = 0; i < g_strv_length(nicks); i++) { PurpleContact *contact = NULL; PurpleConversationMember *member = NULL; const char *nick = nicks[i]; char *stripped = NULL; stripped = ibis_client_strip_source_prefix(client, nick); if(purple_strequal(stripped, active_nick)) { g_free(stripped); continue; } 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); } /* Check if the member is already in the existing members list. * This can happen if the user sends a NAMES command and surely * other ways are possible. */ member = purple_conversation_members_find_member(existing_members, PURPLE_CONTACT_INFO(contact)); /* If the member doesn't exist, add them. */ if(!PURPLE_IS_CONVERSATION_MEMBER(member)) { member = purple_conversation_members_add_member(new_members, PURPLE_CONTACT_INFO(contact), FALSE, NULL); } purple_ircv3_add_badges_to_member(member, client, nick); g_free(stripped); g_clear_object(&contact); } purple_conversation_members_extend(existing_members, new_members); } 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; PurpleConversationMember *member = 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); 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; } 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; if(!PURPLE_IS_CONVERSATION(conversation)) { conversation = purple_ircv3_connection_find_or_create_conversation(connection, target); } member = purple_ircv3_add_contact_to_conversation(contact, conversation, announce); 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(member, stripped); g_free(stripped); purple_message_set_action(message, TRUE); } else { char *body = NULL; announce = FALSE; if(!PURPLE_IS_CONVERSATION(conversation)) { conversation = purple_ircv3_connection_get_status_conversation(connection); } member = purple_ircv3_add_contact_to_conversation(contact, conversation, announce); 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(member, body); purple_message_set_event(message, TRUE); g_free(body); } } else { PangoAttrList *attrs = NULL; char *stripped = NULL; /* If we received this message before registration has completed, * conversation will be set to the status conversation. */ if(!PURPLE_IS_CONVERSATION(conversation)) { conversation = purple_ircv3_connection_find_or_create_conversation(connection, target); } member = purple_ircv3_add_contact_to_conversation(contact, conversation, announce); stripped = ibis_formatting_parse(params[1], &attrs); message = purple_message_new(member, stripped); purple_message_set_attributes(message, attrs); g_clear_pointer(&stripped, g_free); g_clear_pointer(&attrs, pango_attr_list_unref); } 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_whotopic(G_GNUC_UNUSED IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleConversation *conversation = NULL; PurpleContact *contact = NULL; GDateTime *timestamp = NULL; GObject *obj = NULL; GStrv params = NULL; char *nick = NULL; guint n_params = 0; params = ibis_message_get_params(message); n_params = g_strv_length(params); /* The 333 whotopic message has parameters of `client channel source * timestamp`. */ if(n_params != 4) { g_message("received 333 with %u parameters, expected 4", n_params); return FALSE; } conversation = purple_ircv3_connection_find_or_create_conversation(connection, params[1]); if(!PURPLE_IS_CONVERSATION(conversation)) { g_message("failed to find or create channel '%s'", params[1]); return FALSE; } ibis_source_parse(params[2], &nick, NULL, NULL); contact = purple_ircv3_connection_find_or_create_contact_from_nick(connection, nick); g_free(nick); timestamp = g_date_time_new_from_unix_utc(atoi(params[3])); obj = G_OBJECT(conversation); g_object_freeze_notify(obj); purple_conversation_set_topic_author(conversation, PURPLE_CONTACT_INFO(contact)); purple_conversation_set_topic_updated(conversation, timestamp); g_object_thaw_notify(obj); g_date_time_unref(timestamp); return TRUE; } gboolean purple_ircv3_message_handler_channel_url(G_GNUC_UNUSED IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *message, gpointer data) { PurpleIRCv3Connection *connection = data; PurpleConversation *conversation = NULL; GStrv params = NULL; guint n_params = 0; params = ibis_message_get_params(message); n_params = g_strv_length(params); /* The 328 numeric has parameters of `client channel :url`. */ if(n_params != 3) { g_message("received 328 with %u parameters, expected 3", n_params); return FALSE; } conversation = purple_ircv3_connection_find_or_create_conversation(connection, params[1]); if(!PURPLE_IS_CONVERSATION(conversation)) { g_message("failed to find or create channel '%s'", params[1]); return FALSE; } purple_conversation_set_url(conversation, params[2]); 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; PurplePresence *presence = NULL; GList *conversations = NULL; GStrv params = NULL; guint n_params = 0; char *message = NULL; char *reason = 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) { reason = g_strjoinv(" ", params); message = g_strdup_printf("%s has quit (%s)", purple_contact_info_get_sid(info), reason); } else { message = g_strdup_printf("%s has quit", purple_contact_info_get_sid(info)); } /* Update the presence to offline and if they provided a quit message, set * it as the presence message. */ presence = purple_contact_info_get_presence(PURPLE_CONTACT_INFO(contact)); purple_presence_set_message(presence, reason); purple_presence_set_primitive(presence, PURPLE_PRESENCE_PRIMITIVE_OFFLINE); 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_clear_pointer(&message, g_free); g_clear_pointer(&reason, g_free); 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; GError *error = NULL; GStrv params = NULL; guint n_params = 0; char *reason = NULL; 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, FALSE); error = g_error_new_literal(PURPLE_IRCV3_DOMAIN, 0, reason); g_clear_pointer(&reason, g_free); ibis_client_stop(client, 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; } gboolean purple_ircv3_message_handler_mode(IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *ibis_message, gpointer data) { PurpleIRCv3Connection *v3_connection = data; PurpleContact *contact = NULL; PurpleConversation *conversation = NULL; PurpleConversationMember *author = NULL; PurpleMessage *message = NULL; IbisModeChange *mode_changes = NULL; GError *error = NULL; GStrv params = NULL; char *contents = NULL; char *parts = NULL; guint n_params = 0; guint n_mode_changes = 0; const char *target = NULL; params = ibis_message_get_params(ibis_message); n_params = g_strv_length(params); if(n_params < 2) { g_message("received MODE with %u params, need at least 2", n_params); return FALSE; } target = params[0]; if(!ibis_client_is_channel(client, target)) { return FALSE; } conversation = purple_ircv3_connection_find_or_create_conversation(v3_connection, target); contact = purple_ircv3_connection_find_or_create_contact(v3_connection, ibis_message); author = purple_conversation_find_or_add_member(conversation, PURPLE_CONTACT_INFO(contact), FALSE, NULL); mode_changes = ibis_client_parse_mode_string(client, params[1], params + 2, &n_mode_changes, &error); if(error != NULL) { g_warning("failed to parse mode string: %s", error->message); g_clear_error(&error); return FALSE; } for(guint i = 0; i < n_mode_changes; i++) { PurpleContact *subject = NULL; PurpleConversationMember *member = NULL; IbisModeChange change = mode_changes[i]; char prefix = '\0'; prefix = ibis_client_get_prefix_for_mode(client, change.mode); if(prefix == '\0') { continue; } subject = purple_ircv3_connection_find_or_create_contact_from_nick(v3_connection, change.parameter); member = purple_conversation_find_or_add_member(conversation, PURPLE_CONTACT_INFO(subject), FALSE, NULL); if(change.add) { purple_ircv3_add_badge_to_member(member, client, prefix); } else { purple_ircv3_remove_badge_from_member(member, client, prefix); } } parts = g_strjoinv(" ", params + 1); contents = g_strdup_printf(_("mode %s"), parts); g_free(parts); message = g_object_new( PURPLE_TYPE_MESSAGE, "author", author, "contents", contents, "event", TRUE, NULL); g_free(contents); purple_conversation_write_message(conversation, message); g_clear_object(&message); return TRUE; } gboolean purple_ircv3_message_handler_whoreply(IbisClient *client, G_GNUC_UNUSED const char *command, IbisMessage *ibis_message, gpointer data) { PurpleIRCv3Connection *v3_connection = data; PurpleContact *contact = NULL; PurplePresence *presence = NULL; GStrv params = NULL; char *sid = NULL; guint n_params = 0; const char *flags = NULL; params = ibis_message_get_params(ibis_message); n_params = g_strv_length(params); /* A standard RPL_WHOREPLY has 7 parameters: * * <client> <channel> <username> <host> <server> <nick> <flags> :<hopcount> <realname> * * We ignore hopcount and realname for now as we don't have a good use case * to use it. */ if(n_params < 7) { g_message("received RPL_WHOREPLY with %u params, need at least 7", n_params); return FALSE; } /* Find the contact and update everything for it. */ contact = purple_ircv3_connection_find_or_create_contact_from_nick(v3_connection, params[5]); sid = g_strdup_printf("%s!%s@%s", params[5], params[2], params[3]); purple_contact_info_set_sid(PURPLE_CONTACT_INFO(contact), sid); /* Process the flags starting with presence. */ flags = params[6]; presence = purple_contact_info_get_presence(PURPLE_CONTACT_INFO(contact)); if(flags[0] == 'G') { purple_presence_set_primitive(presence, PURPLE_PRESENCE_PRIMITIVE_AWAY); } else { purple_presence_set_primitive(presence, PURPLE_PRESENCE_PRIMITIVE_AVAILABLE); } /* Increment flags to the next value. */ flags += 1; /* Check if a server admin. */ if(flags[0] == '*') { /* We'll need to create badge for this at some point */ flags += 1; } /* If we got a channel update the member's prefix (badges). */ if(!purple_strequal(params[1], "*")) { PurpleConversation *conversation = NULL; PurpleConversationMember *member = NULL; conversation = purple_ircv3_connection_find_or_create_conversation(v3_connection, params[1]); member = purple_conversation_find_or_add_member(conversation, PURPLE_CONTACT_INFO(contact), FALSE, NULL); /* Now run through the remaining flags looking for channel member * prefixes and adding badges for the ones we find. */ for(guint i = 0; flags[i] != '\0'; i++) { /* We abuse the fact that if a badge can't be found it is ignored * to avoid having to check the prefix before adding the badge. */ purple_ircv3_add_badge_to_member(member, client, flags[i]); } } return TRUE; }