diff --git a/libpurple/protocols/ircv3/purpleircv3connection.h b/libpurple/protocols/ircv3/purpleircv3connection.h
--- a/libpurple/protocols/ircv3/purpleircv3connection.h
+++ b/libpurple/protocols/ircv3/purpleircv3connection.h
@@ -77,28 +77,46 @@
 /**
  * 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.
+ * 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.0
  */
-G_GNUC_INTERNAL void purple_ircv3_connection_emit_ctcp_request(PurpleIRCv3Connection *connection, const char *command, const char *parameters);
+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.
+ * 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.0
  */
-G_GNUC_INTERNAL void purple_ircv3_connection_emit_ctcp_response(PurpleIRCv3Connection *connection, const char *command, const char *parameters);
+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:
# HG changeset patch
# Node ID fd6b7cd4b3fd
# Parent  c5bcc44520c3
diff --git a/libpurple/protocols/ircv3/purpleircv3connection.c b/libpurple/protocols/ircv3/purpleircv3connection.c
--- a/libpurple/protocols/ircv3/purpleircv3connection.c
+++ b/libpurple/protocols/ircv3/purpleircv3connection.c
@@ -397,6 +397,57 @@
 }
 
 /******************************************************************************
+ * 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
@@ -612,46 +663,67 @@
 	/**
 	 * 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 it's message contents replaced with just the CTCP parameters
+	 * and sets the [property@Message:action] to %TRUE and then returns %TRUE.
+	 *
 	 * Since: 3.0.0
 	 */
 	signals[SIG_CTCP_REQUEST] = g_signal_new_class_handler(
 		"ctcp-request",
 		G_OBJECT_CLASS_TYPE(klass),
 		G_SIGNAL_RUN_LAST,
-		NULL,
+		G_CALLBACK(purple_ircv3_connection_ctcp_request_default_handler),
+		g_signal_accumulator_true_handled,
 		NULL,
 		NULL,
-		NULL,
-		G_TYPE_NONE,
-		2,
+		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.0
 	 */
 	signals[SIG_CTCP_RESPONSE] = g_signal_new_class_handler(
 		"ctcp-response",
 		G_OBJECT_CLASS_TYPE(klass),
 		G_SIGNAL_RUN_LAST,
-		NULL,
+		G_CALLBACK(purple_ircv3_connection_ctcp_response_default_handler),
+		g_signal_accumulator_true_handled,
 		NULL,
 		NULL,
-		NULL,
-		G_TYPE_NONE,
-		2,
+		G_TYPE_BOOLEAN,
+		4,
+		PURPLE_TYPE_CONVERSATION,
+		PURPLE_TYPE_MESSAGE,
 		G_TYPE_STRING,
 		G_TYPE_STRING);
 }
@@ -687,28 +759,44 @@
 	return priv->cancellable;
 }
 
-void
+gboolean
 purple_ircv3_connection_emit_ctcp_request(PurpleIRCv3Connection *connection,
+                                          PurpleConversation *conversation,
+                                          PurpleMessage *message,
                                           const char *command,
                                           const char *parameters)
 {
-	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
-	g_return_if_fail(!purple_strempty(command));
+	gboolean ret = FALSE;
 
-	g_signal_emit(connection, signals[SIG_CTCP_REQUEST], 0, command,
-	              parameters);
+	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;
 }
 
-void
+gboolean
 purple_ircv3_connection_emit_ctcp_response(PurpleIRCv3Connection *connection,
+                                           PurpleConversation *conversation,
+                                           PurpleMessage *message,
                                            const char *command,
                                            const char *parameters)
 {
-	g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
-	g_return_if_fail(!purple_strempty(command));
+	gboolean ret = FALSE;
 
-	g_signal_emit(connection, signals[SIG_CTCP_RESPONSE], 0, command,
-	              parameters);
+	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;
 }
 
 /******************************************************************************
diff --git a/libpurple/protocols/ircv3/purpleircv3ctcp.h b/libpurple/protocols/ircv3/purpleircv3ctcp.h
--- a/libpurple/protocols/ircv3/purpleircv3ctcp.h
+++ b/libpurple/protocols/ircv3/purpleircv3ctcp.h
@@ -34,15 +34,18 @@
 
 /**
  * purple_ircv3_ctcp_handle: (skip)
- * @connection: The connection instance.
- * @message: The message instance.
+ * @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.0
  */
 G_GNUC_INTERNAL
-void purple_ircv3_ctcp_handle(PurpleIRCv3Connection *connection, PurpleMessage *message);
+gboolean purple_ircv3_ctcp_handle(PurpleIRCv3Connection *connection, PurpleConversation *conversation, PurpleMessage *message);
 
 G_END_DECLS
 
diff --git a/libpurple/protocols/ircv3/purpleircv3ctcp.c b/libpurple/protocols/ircv3/purpleircv3ctcp.c
--- a/libpurple/protocols/ircv3/purpleircv3ctcp.c
+++ b/libpurple/protocols/ircv3/purpleircv3ctcp.c
@@ -25,79 +25,66 @@
 /******************************************************************************
  * Internal API
  *****************************************************************************/
-void
+gboolean
 purple_ircv3_ctcp_handle(PurpleIRCv3Connection *connection,
+                         PurpleConversation *conversation,
                          PurpleMessage *message)
 {
-    const char *contents = NULL;
-
-    g_return_if_fail(PURPLE_IRCV3_IS_CONNECTION(connection));
-    g_return_if_fail(PURPLE_IS_MESSAGE(message));
+	PurpleMessageFlags flags;
+	char *command = NULL;
+	char *params = NULL;
+	char *ptr = NULL;
+	const char *contents = NULL;
+	gboolean ret = FALSE;
 
-    contents = purple_message_get_contents(message);
-    if(contents != NULL && contents[0] == PURPLE_IRCV3_CTCP_DELIMITER) {
-        PurpleMessageFlags flags;
-        char *command = NULL;
-        char *contents = NULL;
-        char *params = NULL;
-        char *ptr = NULL;
-
-        /* Move past the delimiter. */
-        contents += 1;
+	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);
 
-        /* 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);
+	contents = purple_message_get_contents(message);
+	if(contents == NULL || contents[0] != PURPLE_IRCV3_CTCP_DELIMITER) {
+		return FALSE;
+	}
 
-            command = g_strdup(contents);
-            if(command[len - 1] == PURPLE_IRCV3_CTCP_DELIMITER) {
-                command[len - 1] = '\0';
-            }
-        } else {
-            size_t len = 0;
+	/* Move past the delimiter. */
+	contents += 1;
 
-            command = g_strndup(contents, ptr - contents);
+	/* 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);
 
-            params = g_strdup(ptr + 1);
-            len = strlen(params);
-            if(params[len - 1] == PURPLE_IRCV3_CTCP_DELIMITER) {
-                params[len - 1] = '\0';
-            }
-        }
+		command = g_strdup(contents);
+		if(command[len - 1] == PURPLE_IRCV3_CTCP_DELIMITER) {
+			command[len - 1] = '\0';
+		}
+	} else {
+		size_t len = 0;
 
-        flags = purple_message_get_flags(message);
-        if(flags & PURPLE_MESSAGE_NOTIFY) {
-            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);
-            }
+		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';
+		}
+	}
 
-            purple_ircv3_connection_emit_ctcp_response(connection, command,
-                                                       params);
-        } else {
-            if(!purple_strempty(params)) {
-                contents = g_strdup_printf(_("requested CTCP %s: %s"), command,
-                                           params);
-            } else {
-                contents = g_strdup_printf(_("requested CTCP %s"), command);
-            }
+	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);
+	}
 
-            purple_ircv3_connection_emit_ctcp_request(connection, command,
-                                                      params);
-        }
+	g_clear_pointer(&command, g_free);
+	g_clear_pointer(&params, g_free);
 
-        if(!purple_strempty(contents)) {
-           purple_message_set_contents(message, contents);
-        }
-
-        g_clear_pointer(&contents, g_free);
-        g_clear_pointer(&command, g_free);
-        g_clear_pointer(&params, g_free);
-    }
+	return ret;
 }
diff --git a/libpurple/protocols/ircv3/purpleircv3messagehandlers.c b/libpurple/protocols/ircv3/purpleircv3messagehandlers.c
--- a/libpurple/protocols/ircv3/purpleircv3messagehandlers.c
+++ b/libpurple/protocols/ircv3/purpleircv3messagehandlers.c
@@ -229,9 +229,9 @@
 	g_date_time_unref(dt);
 
 	/* Check if this is a CTCP message. */
-	purple_ircv3_ctcp_handle(connection, message);
-
-	purple_conversation_write_message(conversation, 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);
