From eb6f485e6daadb7330fc34edc316f09d29fcb818 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 09:07:24 +0200 Subject: [PATCH 01/14] feat: Add configuration settings for XEP-0410 MUC Self-Ping Introduce PREF_GROUP_CONNECTION for controlling the MUC selfping behavior: * muc.ping.interval (default 600s): Inactivity time before pinging. * muc.ping.timeout (default 60s): Timeout to wait for a reply. See-also:: https://github.com/profanity-im/profanity/issues/1491 Signed-off-by: Michael Vetter --- src/config/preferences.c | 32 ++++++++++++++++++++++++++++++++ src/config/preferences.h | 4 ++++ 2 files changed, 36 insertions(+) diff --git a/src/config/preferences.c b/src/config/preferences.c index aa59fa592..f92d49524 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -836,6 +836,38 @@ prefs_set_autoping_timeout(gint value) g_key_file_set_integer(prefs, PREF_GROUP_CONNECTION, "autoping.timeout", value); } +gint +prefs_get_muc_ping_interval(void) +{ + if (!g_key_file_has_key(prefs, PREF_GROUP_CONNECTION, "muc.ping.interval", NULL)) { + return 600; + } else { + return g_key_file_get_integer(prefs, PREF_GROUP_CONNECTION, "muc.ping.interval", NULL); + } +} + +void +prefs_set_muc_ping_interval(gint value) +{ + g_key_file_set_integer(prefs, PREF_GROUP_CONNECTION, "muc.ping.interval", value); +} + +gint +prefs_get_muc_ping_timeout(void) +{ + if (!g_key_file_has_key(prefs, PREF_GROUP_CONNECTION, "muc.ping.timeout", NULL)) { + return 60; + } else { + return g_key_file_get_integer(prefs, PREF_GROUP_CONNECTION, "muc.ping.timeout", NULL); + } +} + +void +prefs_set_muc_ping_timeout(gint value) +{ + g_key_file_set_integer(prefs, PREF_GROUP_CONNECTION, "muc.ping.timeout", value); +} + gint prefs_get_autoaway_time(void) { diff --git a/src/config/preferences.h b/src/config/preferences.h index 1708e4428..76934bbf3 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -206,6 +206,10 @@ void prefs_set_autoping(gint value); gint prefs_get_autoping(void); void prefs_set_autoping_timeout(gint value); gint prefs_get_autoping_timeout(void); +void prefs_set_muc_ping_interval(gint value); +gint prefs_get_muc_ping_interval(void); +void prefs_set_muc_ping_timeout(gint value); +gint prefs_get_muc_ping_timeout(void); gint prefs_get_inpblock(void); void prefs_set_inpblock(gint value); From 6c335fe2f7a09b4791db2552d11034f76a976483 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 09:12:02 +0200 Subject: [PATCH 02/14] feat(muc): add state management for activity aware selfpings Track two key timestamps needed for XEP-0410 activity aware selfpings: * last_activity: Time when normal room activity was last seen. * ping_sent_time: Monotonic time when a self-ping was sent, or 0 if none is in flight. See-also: https://xmpp.org/extensions/xep-0410.html See-also: https://github.com/profanity-im/profanity/issues/1491 Signed-off-by: Michael Vetter --- src/xmpp/muc.c | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/xmpp/muc.h | 5 +++++ 2 files changed, 61 insertions(+) diff --git a/src/xmpp/muc.c b/src/xmpp/muc.c index 472c7ee31..c5fedf696 100644 --- a/src/xmpp/muc.c +++ b/src/xmpp/muc.c @@ -48,6 +48,8 @@ typedef struct _muc_room_t gboolean roster_received; muc_member_type_t member_type; muc_anonymity_type_t anonymity_type; + gint64 last_activity; + gint64 ping_sent_time; } ChatRoom; GHashTable* rooms = NULL; @@ -212,6 +214,8 @@ muc_join(const char* const room, const char* const nick, const char* const passw new_room->autojoin = autojoin; new_room->member_type = MUC_MEMBER_TYPE_UNKNOWN; new_room->anonymity_type = MUC_ANONYMITY_TYPE_UNKNOWN; + new_room->last_activity = g_get_monotonic_time() / G_TIME_SPAN_SECOND; + new_room->ping_sent_time = 0; g_hash_table_insert(rooms, strdup(room), new_room); } @@ -222,6 +226,58 @@ muc_leave(const char* const room) g_hash_table_remove(rooms, room); } +void +muc_update_activity(const char* const room) +{ + if (room == NULL) { + return; + } + ChatRoom* chat_room = g_hash_table_lookup(rooms, room); + if (chat_room) { + chat_room->last_activity = g_get_monotonic_time() / G_TIME_SPAN_SECOND; + } +} + +gint64 +muc_last_activity(const char* const room) +{ + if (room == NULL) { + return 0; + } + ChatRoom* chat_room = g_hash_table_lookup(rooms, room); + if (chat_room) { + return chat_room->last_activity; + } else { + return 0; + } +} + +void +muc_set_ping_sent_time(const char* const room, gint64 time) +{ + if (room == NULL) { + return; + } + ChatRoom* chat_room = g_hash_table_lookup(rooms, room); + if (chat_room) { + chat_room->ping_sent_time = time; + } +} + +gint64 +muc_ping_sent_time(const char* const room) +{ + if (room == NULL) { + return 0; + } + ChatRoom* chat_room = g_hash_table_lookup(rooms, room); + if (chat_room) { + return chat_room->ping_sent_time; + } else { + return 0; + } +} + gboolean muc_requires_config(const char* const room) { diff --git a/src/xmpp/muc.h b/src/xmpp/muc.h index 715fdd886..8885d06f6 100644 --- a/src/xmpp/muc.h +++ b/src/xmpp/muc.h @@ -60,6 +60,11 @@ void muc_init(void); void muc_join(const char* const room, const char* const nick, const char* const password, gboolean autojoin); void muc_leave(const char* const room); +void muc_update_activity(const char* const room); +gint64 muc_last_activity(const char* const room); +void muc_set_ping_sent_time(const char* const room, gint64 time); +gint64 muc_ping_sent_time(const char* const room); + gboolean muc_active(const char* const room); gboolean muc_autojoin(const char* const room); From 3b0610132d6951d7ab611c50d32d3f997b07a87a Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 09:20:37 +0200 Subject: [PATCH 03/14] feat(muc): update room activity on incoming messages and presence Update the MUC last_activity timestamp on relevant incoming stanzas. This ensures we only selfping rooms that have actually gone idle. See-also: https://xmpp.org/extensions/xep-0410.html See-also: https://github.com/profanity-im/profanity/issues/1491 Signed-off-by: Michael Vetter --- src/xmpp/message.c | 4 ++++ src/xmpp/presence.c | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/xmpp/message.c b/src/xmpp/message.c index 130cace03..232457905 100644 --- a/src/xmpp/message.c +++ b/src/xmpp/message.c @@ -997,6 +997,8 @@ _handle_groupchat(xmpp_stanza_t* const stanza) return; } + muc_update_activity(from_jid->barejid); + // handle room subject xmpp_stanza_t* subject = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_SUBJECT); if (subject) { @@ -1245,6 +1247,8 @@ _handle_muc_private_message(xmpp_stanza_t* const stanza) goto out; } + muc_update_activity(message->from_jid->barejid); + // message stanza id const char* id = xmpp_stanza_get_id(stanza); if (id) { diff --git a/src/xmpp/presence.c b/src/xmpp/presence.c index f1aed9acb..c62d862ac 100644 --- a/src/xmpp/presence.c +++ b/src/xmpp/presence.c @@ -793,6 +793,7 @@ _muc_user_self_handler(xmpp_stanza_t* stanza) } if (muc_active(room)) { + muc_update_activity(room); sv_ev_muc_self_online(room, nick, config_required, role, affiliation, actor, reason, jid, show_str, status_str); } else { log_debug("presence: self-presence received for untracked room: %s", room); @@ -817,6 +818,8 @@ _muc_user_occupant_handler(xmpp_stanza_t* stanza) return; } + muc_update_activity(room); + const char* type = xmpp_stanza_get_type(stanza); if (g_strcmp0(type, STANZA_TYPE_UNAVAILABLE) == 0) { From af9d8a884bf942118e0b32e78bf30eb3e76c0b0f Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 09:24:08 +0200 Subject: [PATCH 04/14] feat(muc): implement XEP-0410 timer loop Periodically verify whether the client is still joined. We check if the MUC has been idle for 10 minutes, every 10 seconds. If we later switch to GMainloop or use more threading we might want to change this code. See-also: https://xmpp.org/extensions/xep-0410.html See-also: https://github.com/profanity-im/profanity/issues/1491 See-also: https://github.com/profanity-im/profanity/issues/225 Signed-off-by: Michael Vetter --- src/xmpp/iq.c | 84 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c index 90107eac6..ac883a575 100644 --- a/src/xmpp/iq.c +++ b/src/xmpp/iq.c @@ -135,6 +135,8 @@ static void _iq_id_handler_free(ProfIqHandler* handler); // scheduled static int _autoping_timed_send(xmpp_conn_t* const conn, void* const userdata); +static int _muc_self_ping_timer_check(xmpp_conn_t* const conn, void* const userdata); +static int _muc_self_pong_handler(xmpp_stanza_t* const stanza, void* const userdata); static void _identity_destroy(DiscoIdentity* identity); static void _item_destroy(DiscoItem* item); @@ -244,6 +246,9 @@ iq_handlers_init(void) int millis = prefs_get_autoping() * 1000; xmpp_timed_handler_add(conn, _autoping_timed_send, millis, ctx); } + if (prefs_get_muc_ping_interval() != 0) { + xmpp_timed_handler_add(conn, _muc_self_ping_timer_check, 10000, ctx); + } received_disco_items = FALSE; iq_handlers_clear(); @@ -1452,6 +1457,85 @@ autoping_timer_extend(void) g_timer_start(autoping_time); } +static int +_muc_self_pong_handler(xmpp_stanza_t* const stanza, void* const userdata) +{ + char* room = (char*)userdata; + log_debug("MUC self-ping: Pong received for room %s", room); + + // Reset ping sent time + muc_set_ping_sent_time(room, 0); + // Update activity to reset timer + muc_update_activity(room); + + return 0; +} + +static int +_muc_self_ping_timer_check(xmpp_conn_t* const conn, void* const userdata) +{ + if (connection_get_status() != JABBER_CONNECTED) { + return 1; + } + + gint64 current_time = g_get_monotonic_time() / G_TIME_SPAN_SECOND; + gint interval = prefs_get_muc_ping_interval(); + gint timeout = prefs_get_muc_ping_timeout(); + + if (interval <= 0) { + return 1; + } + + GList* rooms_list = muc_rooms(); + GList* curr = rooms_list; + + while (curr) { + char* room = curr->data; + gint64 last_act = muc_last_activity(room); + gint64 sent_time = muc_ping_sent_time(room); + + if (sent_time > 0) { + // A ping is currently in flight for this room. Check for timeout. + if (current_time - sent_time > timeout) { + log_debug("MUC self-ping: Timeout for room %s", room); + muc_set_ping_sent_time(room, 0); + + ProfMucWin* mucwin = wins_get_muc(room); + if (mucwin) { + win_println((ProfWin*)mucwin, THEME_DEFAULT, "!", "MUC service not responding."); + } + } + } else { + // No ping is in flight. Check if room is idle. + if (current_time - last_act > interval) { + const char* nick = muc_nick(room); + if (nick) { + auto_gchar gchar* occupant_jid = create_fulljid(room, nick); + log_debug("MUC self-ping: Sending ping to %s", occupant_jid); + + xmpp_ctx_t* ctx = (xmpp_ctx_t*)userdata; + xmpp_stanza_t* iq = stanza_create_ping_iq(ctx, occupant_jid); + const char* id = xmpp_stanza_get_id(iq); + + // Add response handler + iq_id_handler_add(id, _muc_self_pong_handler, free, strdup(room)); + + iq_send_stanza(iq); + xmpp_stanza_release(iq); + + muc_set_ping_sent_time(room, current_time); + } + } + } + + curr = g_list_next(curr); + } + + g_list_free(rooms_list); + + return 1; +} + static int _auto_pong_id_handler(xmpp_stanza_t* const stanza, void* const userdata) { From 9653b4433209c5cb927dc2114f59a03747b9131b Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 09:30:43 +0200 Subject: [PATCH 05/14] feat(muc): implement pong handler and autorejoin recovery Parse incoming IQ response and distinguish success from error stanzas. On disconnect errors (not-acceptable, not-allowed, item-not-found), print a warning to the MUC window and silently trigger presence_join_room() to rejoin the stale chat room. See-also: https://xmpp.org/extensions/xep-0410.html See-also: https://github.com/profanity-im/profanity/issues/1491 Signed-off-by: Michael Vetter --- src/xmpp/iq.c | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c index ac883a575..dc13af3e7 100644 --- a/src/xmpp/iq.c +++ b/src/xmpp/iq.c @@ -1465,7 +1465,31 @@ _muc_self_pong_handler(xmpp_stanza_t* const stanza, void* const userdata) // Reset ping sent time muc_set_ping_sent_time(room, 0); - // Update activity to reset timer + + const char* type = xmpp_stanza_get_type(stanza); + if (g_strcmp0(type, STANZA_TYPE_ERROR) == 0) { + xmpp_stanza_t* error_stanza = xmpp_stanza_get_child_by_name(stanza, STANZA_NAME_ERROR); + if (error_stanza) { + if (xmpp_stanza_get_child_by_name(error_stanza, "not-acceptable") || xmpp_stanza_get_child_by_name(error_stanza, "not-allowed") || xmpp_stanza_get_child_by_name(error_stanza, "item-not-found")) { + + log_warning("MUC selfping: Disconnected from room %s (stale connection detected)", room); + + ProfMucWin* mucwin = wins_get_muc(room); + if (mucwin) { + win_println((ProfWin*)mucwin, THEME_DEFAULT, "!", "Connection to room lost. Attempting to rejoin..."); + } + + char* password = muc_password(room); + const char* nick = muc_nick(room); + if (nick) { + presence_join_room(room, nick, password); + } + return 0; + } + } + } + + // Still connected. Reset idle timer muc_update_activity(room); return 0; From 7f78e1ed62e9b3ad6b5c843bd18d4f096e66c568 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 14:52:18 +0200 Subject: [PATCH 06/14] feat(muc): validate MUC selfping interval and timeout range Enforce that the MUC selfping timeout is less than the ping interval. Signed-off-by: Michael Vetter --- src/config/preferences.c | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/config/preferences.c b/src/config/preferences.c index f92d49524..8d742e247 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -849,6 +849,11 @@ prefs_get_muc_ping_interval(void) void prefs_set_muc_ping_interval(gint value) { + gint timeout = prefs_get_muc_ping_timeout(); + if (value != 0 && value <= timeout) { + cons_show_error("MUC self-ping interval must be greater than timeout (%d seconds).", timeout); + return; + } g_key_file_set_integer(prefs, PREF_GROUP_CONNECTION, "muc.ping.interval", value); } @@ -865,6 +870,11 @@ prefs_get_muc_ping_timeout(void) void prefs_set_muc_ping_timeout(gint value) { + gint interval = prefs_get_muc_ping_interval(); + if (interval != 0 && value >= interval) { + cons_show_error("MUC self-ping timeout must be less than interval (%d seconds).", interval); + return; + } g_key_file_set_integer(prefs, PREF_GROUP_CONNECTION, "muc.ping.timeout", value); } From c6d9f7b508b803fd5585e779e9d8bec3fac952aa Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 16:07:06 +0200 Subject: [PATCH 07/14] feat(muc): Add `/mucping` command Add `/mucping set` and `/mucping timeout` commands. See-also: https://github.com/profanity-im/profanity/issues/1491 Signed-off-by: Michael Vetter --- src/command/cmd_defs.c | 15 ++++++++++++ src/command/cmd_funcs.c | 51 ++++++++++++++++++++++++++++++++++++++++ src/command/cmd_funcs.h | 1 + src/config/preferences.c | 10 -------- src/ui/console.c | 22 +++++++++++++++++ src/ui/ui.h | 1 + 6 files changed, 90 insertions(+), 10 deletions(-) diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index 9e13450d8..a584086f0 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -1963,6 +1963,21 @@ static const struct cmd_t command_defs[] = { { "timeout ", "Seconds to wait for autoping responses, after which the connection is considered broken." }) }, + { CMD_PREAMBLE("/mucping", + parse_args, 2, 2, &cons_mucping_setting) + CMD_MAINFUNC(cmd_mucping) + CMD_TAGS( + CMD_TAG_CONNECTION) + CMD_SYN( + "/mucping set ", + "/mucping timeout ") + CMD_DESC( + "Set the interval and timeout for MUC selfpings (XEP-0410). Recommended interval is between 300 and 900 seconds, with a timeout of 10 to 60 seconds.") + CMD_ARGS( + { "set ", "Number of seconds of idle room activity before sending a ping, a value of 0 disables MUC selfpings." }, + { "timeout ", "Seconds to wait for MUC selfping responses, after which the room connection is considered disconnected." }) + }, + { CMD_PREAMBLE("/ping", parse_args, 0, 1, NULL) CMD_MAINFUNC(cmd_ping) diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 2a9b47e2b..0276eeb42 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -6355,6 +6355,57 @@ cmd_autoping(ProfWin* window, const char* const command, gchar** args) return TRUE; } +gboolean +cmd_mucping(ProfWin* window, const char* const command, gchar** args) +{ + char* cmd = args[0]; + char* value = args[1]; + + if (g_strcmp0(cmd, "set") == 0) { + int intval = 0; + auto_char char* err_msg = NULL; + gboolean res = strtoi_range(value, &intval, 0, INT_MAX, &err_msg); + if (res) { + gint timeout = prefs_get_muc_ping_timeout(); + if (intval != 0 && intval <= timeout) { + cons_show_error("MUC self-ping interval must be greater than timeout (%d seconds).", timeout); + } else { + prefs_set_muc_ping_interval(intval); + if (intval == 0) { + cons_show("MUC self-ping disabled."); + } else { + cons_show("MUC self-ping interval set to %d seconds.", intval); + } + } + } else { + cons_show(err_msg); + cons_bad_cmd_usage(command); + } + + } else if (g_strcmp0(cmd, "timeout") == 0) { + int intval = 0; + auto_char char* err_msg = NULL; + gboolean res = strtoi_range(value, &intval, 1, INT_MAX, &err_msg); + if (res) { + gint interval = prefs_get_muc_ping_interval(); + if (interval != 0 && intval >= interval) { + cons_show_error("MUC self-ping timeout must be less than interval (%d seconds).", interval); + } else { + prefs_set_muc_ping_timeout(intval); + cons_show("MUC self-ping timeout set to %d seconds.", intval); + } + } else { + cons_show(err_msg); + cons_bad_cmd_usage(command); + } + + } else { + cons_bad_cmd_usage(command); + } + + return TRUE; +} + gboolean cmd_ping(ProfWin* window, const char* const command, gchar** args) { diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h index 10455dc07..882387d6b 100644 --- a/src/command/cmd_funcs.h +++ b/src/command/cmd_funcs.h @@ -58,6 +58,7 @@ gboolean cmd_about(ProfWin* window, const char* const command, gchar** args); gboolean cmd_autoaway(ProfWin* window, const char* const command, gchar** args); gboolean cmd_autoconnect(ProfWin* window, const char* const command, gchar** args); gboolean cmd_autoping(ProfWin* window, const char* const command, gchar** args); +gboolean cmd_mucping(ProfWin* window, const char* const command, gchar** args); gboolean cmd_beep(ProfWin* window, const char* const command, gchar** args); gboolean cmd_caps(ProfWin* window, const char* const command, gchar** args); gboolean cmd_logging(ProfWin* window, const char* const command, gchar** args); diff --git a/src/config/preferences.c b/src/config/preferences.c index 8d742e247..f92d49524 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -849,11 +849,6 @@ prefs_get_muc_ping_interval(void) void prefs_set_muc_ping_interval(gint value) { - gint timeout = prefs_get_muc_ping_timeout(); - if (value != 0 && value <= timeout) { - cons_show_error("MUC self-ping interval must be greater than timeout (%d seconds).", timeout); - return; - } g_key_file_set_integer(prefs, PREF_GROUP_CONNECTION, "muc.ping.interval", value); } @@ -870,11 +865,6 @@ prefs_get_muc_ping_timeout(void) void prefs_set_muc_ping_timeout(gint value) { - gint interval = prefs_get_muc_ping_interval(); - if (interval != 0 && value >= interval) { - cons_show_error("MUC self-ping timeout must be less than interval (%d seconds).", interval); - return; - } g_key_file_set_integer(prefs, PREF_GROUP_CONNECTION, "muc.ping.timeout", value); } diff --git a/src/ui/console.c b/src/ui/console.c index 57ca3fdad..3b2601ef1 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -2033,6 +2033,28 @@ cons_autoping_setting(void) } } +void +cons_mucping_setting(void) +{ + gint muc_ping_interval = prefs_get_muc_ping_interval(); + if (muc_ping_interval == 0) { + cons_show("MUC selfping interval (/mucping) : OFF"); + } else if (muc_ping_interval == 1) { + cons_show("MUC selfping interval (/mucping) : 1 second"); + } else { + cons_show("MUC selfping interval (/mucping) : %d seconds", muc_ping_interval); + } + + gint muc_ping_timeout = prefs_get_muc_ping_timeout(); + if (muc_ping_timeout == 0) { + cons_show("MUC selfping timeout (/mucping) : OFF"); + } else if (muc_ping_timeout == 1) { + cons_show("MUC selfping timeout (/mucping) : 1 second"); + } else { + cons_show("MUC selfping timeout (/mucping) : %d seconds", muc_ping_timeout); + } +} + void cons_color_setting(void) { diff --git a/src/ui/ui.h b/src/ui/ui.h index 63d8ab0b7..bcd2c428c 100644 --- a/src/ui/ui.h +++ b/src/ui/ui.h @@ -311,6 +311,7 @@ void cons_logging_setting(void); void cons_autoaway_setting(void); void cons_reconnect_setting(void); void cons_autoping_setting(void); +void cons_mucping_setting(void); void cons_autoconnect_setting(void); void cons_room_cache_setting(void); void cons_inpblock_setting(void); From 1562ad81af77bd6896be2cc90e582479297be340 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 16:37:28 +0200 Subject: [PATCH 08/14] tests: add unit tests for `/mucping` * Parsing validation: Rejection of non integer inputs * Range validation: Rejection of negative integer values. * Boundary constraints: Rejection of overlapping configurations, verifying that `interval <= timeout` and `timeout >= interval` are caught and reported as console errors. Signed-off-by: Michael Vetter --- meson.build | 1 + tests/unittests/command/test_cmd_mucping.c | 98 ++++++++++++++++++++++ tests/unittests/command/test_cmd_mucping.h | 11 +++ tests/unittests/ui/stub_ui.c | 4 + tests/unittests/unittests.c | 12 +++ 5 files changed, 126 insertions(+) create mode 100644 tests/unittests/command/test_cmd_mucping.c create mode 100644 tests/unittests/command/test_cmd_mucping.h diff --git a/meson.build b/meson.build index e10168b5d..ee10f68d0 100644 --- a/meson.build +++ b/meson.build @@ -682,6 +682,7 @@ if get_option('tests') 'tests/unittests/command/test_cmd_roster.c', 'tests/unittests/command/test_cmd_ac.c', 'tests/unittests/command/test_cmd_disconnect.c', + 'tests/unittests/command/test_cmd_mucping.c', 'tests/unittests/plugins/test_callbacks.c', 'tests/unittests/plugins/test_plugins_disco.c', 'tests/unittests/unittests.c', diff --git a/tests/unittests/command/test_cmd_mucping.c b/tests/unittests/command/test_cmd_mucping.c new file mode 100644 index 000000000..cae9a203d --- /dev/null +++ b/tests/unittests/command/test_cmd_mucping.c @@ -0,0 +1,98 @@ +#include "prof_cmocka.h" +#include +#include +#include + +#include "config/preferences.h" +#include "ui/ui.h" +#include "ui/stub_ui.h" +#include "command/cmd_funcs.h" + +#define CMD_MUCPING "/mucping" + +void +cmd_mucping__shows__usage_when_invalid_interval_chars(void** state) +{ + gchar* args[] = { "set", "abc", NULL }; + + expect_any_cons_show(); + expect_string(cons_bad_cmd_usage, cmd, CMD_MUCPING); + + gboolean result = cmd_mucping(NULL, CMD_MUCPING, args); + assert_true(result); +} + +void +cmd_mucping__shows__usage_when_invalid_interval_range(void** state) +{ + gchar* args[] = { "set", "-5", NULL }; + + expect_any_cons_show(); + expect_string(cons_bad_cmd_usage, cmd, CMD_MUCPING); + + gboolean result = cmd_mucping(NULL, CMD_MUCPING, args); + assert_true(result); +} + +void +cmd_mucping__shows__usage_when_invalid_timeout_chars(void** state) +{ + gchar* args[] = { "timeout", "xyz", NULL }; + + expect_any_cons_show(); + expect_string(cons_bad_cmd_usage, cmd, CMD_MUCPING); + + gboolean result = cmd_mucping(NULL, CMD_MUCPING, args); + assert_true(result); +} + +void +cmd_mucping__shows__usage_when_invalid_timeout_range(void** state) +{ + gchar* args[] = { "timeout", "-1", NULL }; + + expect_any_cons_show(); + expect_string(cons_bad_cmd_usage, cmd, CMD_MUCPING); + + gboolean result = cmd_mucping(NULL, CMD_MUCPING, args); + assert_true(result); +} + +void +cmd_mucping__shows__error_when_interval_smaller_than_timeout(void** state) +{ + // Default interval = 600, timeout = 60 + assert_int_equal(600, prefs_get_muc_ping_interval()); + assert_int_equal(60, prefs_get_muc_ping_timeout()); + + // Set timeout to 80 (valid) + gchar* args_timeout[] = { "timeout", "80", NULL }; + expect_cons_show("MUC self-ping timeout set to 80 seconds."); + gboolean result1 = cmd_mucping(NULL, CMD_MUCPING, args_timeout); + assert_true(result1); + assert_int_equal(80, prefs_get_muc_ping_timeout()); + + // Try setting interval to 50 (invalid: <= timeout of 80) + gchar* args_interval[] = { "set", "50", NULL }; + expect_cons_show_error("MUC self-ping interval must be greater than timeout (80 seconds)."); + gboolean result2 = cmd_mucping(NULL, CMD_MUCPING, args_interval); + assert_true(result2); + // Interval remains 600 + assert_int_equal(600, prefs_get_muc_ping_interval()); +} + +void +cmd_mucping__shows__error_when_timeout_greater_than_interval(void** state) +{ + // Reset interval and timeout + prefs_set_muc_ping_interval(100); + prefs_set_muc_ping_timeout(60); + + // Try setting timeout to 120 (invalid: >= interval of 100) + gchar* args_timeout[] = { "timeout", "120", NULL }; + expect_cons_show_error("MUC self-ping timeout must be less than interval (100 seconds)."); + gboolean result1 = cmd_mucping(NULL, CMD_MUCPING, args_timeout); + assert_true(result1); + // Timeout remains 60 + assert_int_equal(60, prefs_get_muc_ping_timeout()); +} diff --git a/tests/unittests/command/test_cmd_mucping.h b/tests/unittests/command/test_cmd_mucping.h new file mode 100644 index 000000000..be6f29bb6 --- /dev/null +++ b/tests/unittests/command/test_cmd_mucping.h @@ -0,0 +1,11 @@ +#ifndef TESTS_TEST_CMD_MUCPING_H +#define TESTS_TEST_CMD_MUCPING_H + +void cmd_mucping__shows__usage_when_invalid_interval_chars(void** state); +void cmd_mucping__shows__usage_when_invalid_interval_range(void** state); +void cmd_mucping__shows__usage_when_invalid_timeout_chars(void** state); +void cmd_mucping__shows__usage_when_invalid_timeout_range(void** state); +void cmd_mucping__shows__error_when_interval_smaller_than_timeout(void** state); +void cmd_mucping__shows__error_when_timeout_greater_than_interval(void** state); + +#endif diff --git a/tests/unittests/ui/stub_ui.c b/tests/unittests/ui/stub_ui.c index 7c90c480f..84de8aa72 100644 --- a/tests/unittests/ui/stub_ui.c +++ b/tests/unittests/ui/stub_ui.c @@ -1116,6 +1116,10 @@ cons_autoping_setting(void) { } void +cons_mucping_setting(void) +{ +} +void cons_autoconnect_setting(void) { } diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 17e3774e3..31a03cb95 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -27,6 +27,7 @@ #include "command/test_cmd_rooms.h" #include "command/test_cmd_sub.h" #include "command/test_cmd_presence.h" +#include "command/test_cmd_mucping.h" #include "command/test_cmd_otr.h" #include "command/test_cmd_pgp.h" #include "xmpp/test_jid.h" @@ -459,6 +460,17 @@ main(int argc, char* argv[]) cmocka_unit_test(p_contact_is_available__is__true_when_highest_priority_online), cmocka_unit_test(p_contact_is_available__is__true_when_highest_priority_chat), + cmocka_unit_test(cmd_mucping__shows__usage_when_invalid_interval_chars), + cmocka_unit_test(cmd_mucping__shows__usage_when_invalid_interval_range), + cmocka_unit_test(cmd_mucping__shows__usage_when_invalid_timeout_chars), + cmocka_unit_test(cmd_mucping__shows__usage_when_invalid_timeout_range), + cmocka_unit_test_setup_teardown(cmd_mucping__shows__error_when_interval_smaller_than_timeout, + load_preferences, + close_preferences), + cmocka_unit_test_setup_teardown(cmd_mucping__shows__error_when_timeout_greater_than_interval, + load_preferences, + close_preferences), + cmocka_unit_test(cmd_presence__shows__usage_when_bad_subcmd), cmocka_unit_test(cmd_presence__shows__usage_when_bad_console_setting), cmocka_unit_test(cmd_presence__shows__usage_when_bad_chat_setting), From f08bfb04dc991c15f7afc556854ce49422686490 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 21:33:46 +0200 Subject: [PATCH 09/14] chore: Add XEP-0410 to DOAP Signed-off-by: Michael Vetter --- profanity.doap | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/profanity.doap b/profanity.doap index fa93ec17d..bb188d403 100644 --- a/profanity.doap +++ b/profanity.doap @@ -463,7 +463,18 @@ - + + + + + complete + 1.1.0 + DEV + + + + + From 0001efcfba6c1eb4dfc39dabb2152555caaf3762 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Wed, 17 Jun 2026 23:08:09 +0200 Subject: [PATCH 10/14] feat: Add autocompleter for /mucping Signed-off-by: Michael Vetter --- src/command/cmd_ac.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index 44798f95b..f6ad8caa1 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -226,6 +226,7 @@ static Autocomplete script_show_ac; static Autocomplete console_ac; static Autocomplete console_msg_ac; static Autocomplete autoping_ac; +static Autocomplete mucping_ac; static Autocomplete plugins_ac; static Autocomplete plugins_load_ac; static Autocomplete plugins_unload_ac; @@ -386,6 +387,7 @@ static Autocomplete* all_acs[] = { &console_ac, &console_msg_ac, &autoping_ac, + &mucping_ac, &plugins_ac, &filepath_ac, &blocked_ac, @@ -1035,6 +1037,9 @@ cmd_ac_init(void) autocomplete_add(autoping_ac, "set"); autocomplete_add(autoping_ac, "timeout"); + autocomplete_add(mucping_ac, "set"); + autocomplete_add(mucping_ac, "timeout"); + autocomplete_add(plugins_ac, "install"); autocomplete_add(plugins_ac, "update"); autocomplete_add(plugins_ac, "uninstall"); @@ -1855,6 +1860,7 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ { "/disco", disco_ac }, { "/room", room_ac }, { "/autoping", autoping_ac }, + { "/mucping", mucping_ac }, { "/mainwin", winpos_ac }, { "/inputwin", winpos_ac }, }; From 74c8486577b80f4a8b51bd922624c9e84d65b6d2 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Fri, 19 Jun 2026 13:11:13 +0200 Subject: [PATCH 11/14] refactor: Remove not used parameter Ref: 2490f5b417 Signed-off-by: Michael Vetter --- src/command/cmd_funcs.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 0276eeb42..734a43025 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -6297,7 +6297,7 @@ cmd_reconnect(ProfWin* window, const char* const command, gchar** args) } else if (strtoi_range(value, &intval, 0, INT_MAX, &err_msg)) { prefs_set_reconnect(intval); if (intval == 0) { - cons_show("Reconnect disabled.", intval); + cons_show("Reconnect disabled."); } else { cons_show("Reconnect interval set to %d seconds.", intval); } From 781f439267471722e7b462e7e6902a5dfde2658c Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Fri, 19 Jun 2026 13:14:45 +0200 Subject: [PATCH 12/14] feat: Print mucping setting in /prefs conn Signed-off-by: Michael Vetter --- src/ui/console.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ui/console.c b/src/ui/console.c index 3b2601ef1..a5fdffddf 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -2168,6 +2168,7 @@ cons_show_connection_prefs(void) cons_show(""); cons_reconnect_setting(); cons_autoping_setting(); + cons_mucping_setting(); cons_autoconnect_setting(); cons_rooms_cache_setting(); cons_strophe_setting(); From 600b9670cce46460d72ec909f00c2440f72a24cd Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Fri, 19 Jun 2026 13:16:31 +0200 Subject: [PATCH 13/14] feat: Print MAM and Silence setting in /prefs chat Signed-off-by: Michael Vetter --- src/ui/console.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ui/console.c b/src/ui/console.c index a5fdffddf..a76520a55 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -1817,6 +1817,8 @@ cons_show_chat_prefs(void) cons_history_setting(); cons_carbons_setting(); cons_receipts_setting(); + cons_mam_setting(); + cons_silence_setting(); cons_alert(NULL); } From de6f5285e098143e59003a68bebc5a043c1aa11d Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Fri, 19 Jun 2026 13:22:00 +0200 Subject: [PATCH 14/14] refactor: Actually make it possible to disable MUC self-pings And use self-pings instead of selfpings. Like in the XEP. Also document the relation between timeout and interval. Signed-off-by: Michael Vetter --- src/command/cmd_defs.c | 7 ++++--- src/command/cmd_funcs.c | 25 +++++++++++++++++++------ src/ui/console.c | 12 ++++++------ 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index a584086f0..4fe028fdd 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -1972,10 +1972,11 @@ static const struct cmd_t command_defs[] = { "/mucping set ", "/mucping timeout ") CMD_DESC( - "Set the interval and timeout for MUC selfpings (XEP-0410). Recommended interval is between 300 and 900 seconds, with a timeout of 10 to 60 seconds.") + "Set the interval and timeout for MUC self-pings (XEP-0410). Recommended interval is between 300 and 900 seconds, with a timeout of 10 to 60 seconds. " + "Note that the interval must be greater than the timeout.") CMD_ARGS( - { "set ", "Number of seconds of idle room activity before sending a ping, a value of 0 disables MUC selfpings." }, - { "timeout ", "Seconds to wait for MUC selfping responses, after which the room connection is considered disconnected." }) + { "set ", "Number of seconds of idle room activity before sending a ping, a value of 0 disables MUC self-pings." }, + { "timeout ", "Seconds to wait for MUC self-ping responses, after which the room connection is considered disconnected. A value of 0 disables MUC self-pings entirely." }) }, { CMD_PREAMBLE("/ping", diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 734a43025..d7a31167a 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -6367,6 +6367,13 @@ cmd_mucping(ProfWin* window, const char* const command, gchar** args) gboolean res = strtoi_range(value, &intval, 0, INT_MAX, &err_msg); if (res) { gint timeout = prefs_get_muc_ping_timeout(); + if (intval != 0 && timeout == 0) { + timeout = (intval > 60) ? 60 : (intval / 2); + if (timeout == 0) { + timeout = 1; + } + prefs_set_muc_ping_timeout(timeout); + } if (intval != 0 && intval <= timeout) { cons_show_error("MUC self-ping interval must be greater than timeout (%d seconds).", timeout); } else { @@ -6385,14 +6392,20 @@ cmd_mucping(ProfWin* window, const char* const command, gchar** args) } else if (g_strcmp0(cmd, "timeout") == 0) { int intval = 0; auto_char char* err_msg = NULL; - gboolean res = strtoi_range(value, &intval, 1, INT_MAX, &err_msg); + gboolean res = strtoi_range(value, &intval, 0, INT_MAX, &err_msg); if (res) { - gint interval = prefs_get_muc_ping_interval(); - if (interval != 0 && intval >= interval) { - cons_show_error("MUC self-ping timeout must be less than interval (%d seconds).", interval); + if (intval == 0) { + prefs_set_muc_ping_interval(0); + prefs_set_muc_ping_timeout(0); + cons_show("MUC self-ping disabled."); } else { - prefs_set_muc_ping_timeout(intval); - cons_show("MUC self-ping timeout set to %d seconds.", intval); + gint interval = prefs_get_muc_ping_interval(); + if (interval != 0 && intval >= interval) { + cons_show_error("MUC self-ping timeout must be less than interval (%d seconds).", interval); + } else { + prefs_set_muc_ping_timeout(intval); + cons_show("MUC self-ping timeout set to %d seconds.", intval); + } } } else { cons_show(err_msg); diff --git a/src/ui/console.c b/src/ui/console.c index a76520a55..5ad9019dc 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -2040,20 +2040,20 @@ cons_mucping_setting(void) { gint muc_ping_interval = prefs_get_muc_ping_interval(); if (muc_ping_interval == 0) { - cons_show("MUC selfping interval (/mucping) : OFF"); + cons_show("MUC self-ping interval (/mucping) : OFF"); } else if (muc_ping_interval == 1) { - cons_show("MUC selfping interval (/mucping) : 1 second"); + cons_show("MUC self-ping interval (/mucping) : 1 second"); } else { - cons_show("MUC selfping interval (/mucping) : %d seconds", muc_ping_interval); + cons_show("MUC self-ping interval (/mucping) : %d seconds", muc_ping_interval); } gint muc_ping_timeout = prefs_get_muc_ping_timeout(); if (muc_ping_timeout == 0) { - cons_show("MUC selfping timeout (/mucping) : OFF"); + cons_show("MUC self-ping timeout (/mucping) : OFF"); } else if (muc_ping_timeout == 1) { - cons_show("MUC selfping timeout (/mucping) : 1 second"); + cons_show("MUC self-ping timeout (/mucping) : 1 second"); } else { - cons_show("MUC selfping timeout (/mucping) : %d seconds", muc_ping_timeout); + cons_show("MUC self-ping timeout (/mucping) : %d seconds", muc_ping_timeout); } }