From 02e679c27707abbe7ba87ec45df1ab1a942a01c3 Mon Sep 17 00:00:00 2001 From: Jabber Developer Date: Tue, 23 Jun 2026 11:11:25 +0000 Subject: [PATCH] feat(autoping): autoping availability warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Introduced change A new warning that notifies users when the connected XMPP server advertises XEP-0199 (urn:xmpp:ping) support but the autoping feature is disabled in settings. The warning can be toggled with `/autoping warning on|off`. The warning fires during the on-connect disco#info exchange, only for responses from the server's own domain — responses from user JIDs or subdomain services (e.g., conference servers) are excluded. ### Capabilities - **On-connect detection**: Warning is emitted automatically when the server's disco#info response indicates `urn:xmpp:ping` support while autoping is disabled and the preference is enabled (default: on) - **`/autoping warning on|off`**: Toggle the warning via the existing autoping command - **Domain-scoped**: Only triggers for disco#info from the bound server domain; user JIDs and subdomain service JIDs are skipped - **Case-insensitive domain matching**: Uses `g_ascii_strcasecmp` instead of `g_strcmp0` to handle case differences between the stanza `from` field and the bound domain - **Display in settings**: Shown in both `/notify` and `/autoping` settings dumps, with consistent alignment and `(/autoping warning)` reference in each line - **Autocomplete**: Dedicated `_autoping_autocomplete` function handles the `warning` subcommand with `on|off` completion ## Reasoning behind the change Users connecting to servers that support XEP-0199 ping but have autoping disabled may experience poorer connection stability. The warning draws attention to this configuration mismatch without requiring users to read documentation or dig into settings. The warning is scoped to the server domain (not user JIDs or subdomain services) because autoping is a connection-level feature — it only makes sense in the context of the server's keepalive capabilities. The warning preference defaults to `on` so users are informed by default, but can be disabled with `/autoping warning off` if they prefer not to see it. ## Implementation details ### Warning logic (`src/xmpp/iq.c`) `_disco_autoping_warning_message()` is called from the on-connect disco#info handler when the `from` field matches the bound domain. It checks three conditions: 1. Server features contain `urn:xmpp:ping` 2. Autoping interval is 0 (disabled) 3. `PREF_AUTOPING_WARNING` is true All three must be true for the warning to display. ### Domain matching `g_ascii_strcasecmp(from, connection_get_domain())` is used instead of `g_strcmp0` to handle case differences. This prevents the warning from silently skipping if a server echoes the `from` field in a different case than the bound domain. ### Command integration (`src/command/cmd_*.c`) - `cmd_defs.c`: Added `/autoping warning on|off` syntax and argument description - `cmd_funcs.c`: Added `warning` subcommand handler that calls `_cmd_set_boolean_preference` with `PREF_AUTOPING_WARNING` - `cmd_ac.c`: Registered dedicated `_autoping_autocomplete` that handles the `warning` subcommand with `on|off` boolean completion ### Preference storage (`src/config/preferences.c`) - Group: `PREF_GROUP_NOTIFICATIONS` - Key: `autoping.warning` - Default: `TRUE` ### Settings display (`src/ui/console.c`) The autoping warning preference is shown in both `cons_notify_setting()` and `cons_autoping_setting()` with consistent column alignment and a `(/autoping warning)` reference in each line. ### Tests (`tests/functionaltests/test_autoping.c`) Six functional tests cover all condition combinations: | Test | Server ping | Autoping | Warning pref | Expected | |---|---|---|---|---| | `autoping_warning_shown_when_disabled` | yes | off | on | warning shown | | `autoping_warning_not_shown_when_server_unsupported` | no | off | on | no warning | | `autoping_warning_not_shown_when_autoping_enabled` | yes | on | on | no warning | | `autoping_warning_not_shown_when_user_disabled` | yes | off | off | no warning | | `autoping_warning_not_shown_for_user_jid` | yes (from user JID) | off | on | no warning | | `autoping_warning_not_shown_for_subdomain_service` | yes (from subdomain) | off | on | no warning | Co-authored-by: Jabber Developer2 --- src/command/cmd_ac.c | 18 ++- src/command/cmd_defs.c | 6 +- src/command/cmd_funcs.c | 2 + src/config/preferences.c | 4 + src/config/preferences.h | 1 + src/ui/console.c | 22 +++- src/xmpp/iq.c | 21 ++++ tests/functionaltests/functionaltests.c | 8 ++ tests/functionaltests/test_autoping.c | 141 ++++++++++++++++++++++++ tests/functionaltests/test_autoping.h | 6 + 10 files changed, 220 insertions(+), 9 deletions(-) diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index d7949bad..99c2ac36 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -47,6 +47,7 @@ static char* _notify_autocomplete(ProfWin* window, const char* const input, gboo static char* _theme_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _spellcheck_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _autoaway_autocomplete(ProfWin* window, const char* const input, gboolean previous); +static char* _autoping_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _autoconnect_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _account_autocomplete(ProfWin* window, const char* const input, gboolean previous); static char* _who_autocomplete(ProfWin* window, const char* const input, gboolean previous); @@ -1056,6 +1057,7 @@ cmd_ac_init(void) autocomplete_add(autoping_ac, "set"); autocomplete_add(autoping_ac, "timeout"); + autocomplete_add(autoping_ac, "warning"); autocomplete_add(plugins_ac, "install"); autocomplete_add(plugins_ac, "update"); @@ -1407,6 +1409,7 @@ cmd_ac_init(void) g_hash_table_insert(ac_funcs, "/alias", _alias_autocomplete); g_hash_table_insert(ac_funcs, "/autoaway", _autoaway_autocomplete); g_hash_table_insert(ac_funcs, "/autoconnect", _autoconnect_autocomplete); + g_hash_table_insert(ac_funcs, "/autoping", _autoping_autocomplete); g_hash_table_insert(ac_funcs, "/avatar", _avatar_autocomplete); g_hash_table_insert(ac_funcs, "/ban", _ban_autocomplete); g_hash_table_insert(ac_funcs, "/blocked", _blocked_autocomplete); @@ -1934,7 +1937,6 @@ _cmd_ac_complete_params(ProfWin* window, const char* const input, gboolean previ { "/prefs", prefs_ac }, { "/disco", disco_ac }, { "/room", room_ac }, - { "/autoping", autoping_ac }, { "/mainwin", winpos_ac }, { "/inputwin", winpos_ac }, }; @@ -4264,6 +4266,20 @@ _executable_autocomplete(ProfWin* window, const char* const input, gboolean prev return result; } +static char* +_autoping_autocomplete(ProfWin* window, const char* const input, gboolean previous) +{ + char* result = NULL; + + result = autocomplete_param_with_ac(input, "/autoping", autoping_ac, TRUE, previous); + if (result) { + return result; + } + + result = autocomplete_param_with_func(input, "/autoping warning", prefs_autocomplete_boolean_choice, previous, NULL); + return result; +} + static char* _lastactivity_autocomplete(ProfWin* window, const char* const input, gboolean previous) { diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index e3e26ddf..ccd9f1da 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -1964,12 +1964,14 @@ static const struct cmd_t command_defs[] = { CMD_TAG_CONNECTION) CMD_SYN( "/autoping set ", - "/autoping timeout ") + "/autoping timeout ", + "/autoping warning on|off") CMD_DESC( "Set the interval between sending ping requests to the server to ensure the connection is kept alive.") CMD_ARGS( { "set ", "Number of seconds between sending pings, a value of 0 disables autoping." }, - { "timeout ", "Seconds to wait for autoping responses, after which the connection is considered broken." }) + { "timeout ", "Seconds to wait for autoping responses, after which the connection is considered broken." }, + { "warning on|off", "Enable or disable autoping availability warning." }) }, { CMD_PREAMBLE("/ping", diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 3fad000a..7fbc5f05 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -6374,6 +6374,8 @@ cmd_autoping(ProfWin* window, const char* const command, gchar** args) cons_bad_cmd_usage(command); } + } else if (g_strcmp0(cmd, "warning") == 0) { + _cmd_set_boolean_preference(value, "Autoping availability warning", PREF_AUTOPING_WARNING); } else { cons_bad_cmd_usage(command); } diff --git a/src/config/preferences.c b/src/config/preferences.c index 9f9db2e8..04d3dda5 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -2290,6 +2290,7 @@ _get_group(preference_t pref) case PREF_TRAY: case PREF_TRAY_READ: case PREF_ADV_NOTIFY_DISCO_OR_VERSION: + case PREF_AUTOPING_WARNING: return PREF_GROUP_NOTIFICATIONS; case PREF_DBLOG: case PREF_CHLOG: @@ -2646,6 +2647,8 @@ _get_key(preference_t pref) return "enabled"; case PREF_SPELLCHECK_LANG: return "lang"; + case PREF_AUTOPING_WARNING: + return "autoping.warning"; default: return NULL; } @@ -2699,6 +2702,7 @@ _get_default_boolean(preference_t pref) case PREF_MOOD: case PREF_STROPHE_SM_ENABLED: case PREF_STROPHE_SM_RESEND: + case PREF_AUTOPING_WARNING: return TRUE; case PREF_SPELLCHECK_ENABLE: case PREF_PGP_PUBKEY_AUTOIMPORT: diff --git a/src/config/preferences.h b/src/config/preferences.h index 962e607b..4e422a05 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -169,6 +169,7 @@ typedef enum { PREF_FORCE_ENCRYPTION_MODE, PREF_SPELLCHECK_ENABLE, PREF_SPELLCHECK_LANG, + PREF_AUTOPING_WARNING } preference_t; typedef struct prof_alias_t diff --git a/src/ui/console.c b/src/ui/console.c index a98577a7..de565d38 100644 --- a/src/ui/console.c +++ b/src/ui/console.c @@ -1703,6 +1703,11 @@ cons_notify_setting(void) else cons_show("Subscription requests (/notify sub) : OFF"); + if (prefs_get_boolean(PREF_AUTOPING_WARNING)) + cons_show("Autoping warning (/autoping warning): ON"); + else + cons_show("Autoping warning (/autoping warning): OFF"); + gint remind_period = prefs_get_notify_remind(); if (remind_period == 0) { cons_show("Reminder period (/notify remind) : OFF"); @@ -2003,21 +2008,26 @@ cons_autoping_setting(void) { gint autoping_interval = prefs_get_autoping(); if (autoping_interval == 0) { - cons_show("Autoping interval (/autoping) : OFF"); + cons_show("Autoping interval (/autoping) : OFF"); } else if (autoping_interval == 1) { - cons_show("Autoping interval (/autoping) : 1 second"); + cons_show("Autoping interval (/autoping) : 1 second"); } else { - cons_show("Autoping interval (/autoping) : %d seconds", autoping_interval); + cons_show("Autoping interval (/autoping) : %d seconds", autoping_interval); } gint autoping_timeout = prefs_get_autoping_timeout(); if (autoping_timeout == 0) { - cons_show("Autoping timeout (/autoping) : OFF"); + cons_show("Autoping timeout (/autoping) : OFF"); } else if (autoping_timeout == 1) { - cons_show("Autoping timeout (/autoping) : 1 second"); + cons_show("Autoping timeout (/autoping) : 1 second"); } else { - cons_show("Autoping timeout (/autoping) : %d seconds", autoping_timeout); + cons_show("Autoping timeout (/autoping) : %d seconds", autoping_timeout); } + + if (prefs_get_boolean(PREF_AUTOPING_WARNING)) + cons_show("Autoping warning (/autoping warning): ON"); + else + cons_show("Autoping warning (/autoping warning): OFF"); } void diff --git a/src/xmpp/iq.c b/src/xmpp/iq.c index 377f444c..c3a2c13d 100644 --- a/src/xmpp/iq.c +++ b/src/xmpp/iq.c @@ -99,6 +99,7 @@ static void _disco_items_result_handler(xmpp_stanza_t* const stanza); static void _last_activity_get_handler(xmpp_stanza_t* const stanza); static void _version_get_handler(xmpp_stanza_t* const stanza); static void _ping_get_handler(xmpp_stanza_t* const stanza); +static void _disco_autoping_warning_message(GHashTable* features); static int _version_result_id_handler(xmpp_stanza_t* const stanza, void* const userdata); static int _disco_info_response_id_handler(xmpp_stanza_t* const stanza, void* const userdata); @@ -2429,6 +2430,11 @@ _disco_info_response_id_handler_onconnect(xmpp_stanza_t* const stanza, void* con } child = xmpp_stanza_get_next(child); } + + // Prevent repetitions by avoiding checks of disco items (from connection_set_disco_items) + if (from && g_ascii_strcasecmp(from, connection_get_domain()) == 0) { + _disco_autoping_warning_message(features); + } } connection_features_received(from); @@ -2436,6 +2442,21 @@ _disco_info_response_id_handler_onconnect(xmpp_stanza_t* const stanza, void* con return 0; } +static void +_disco_autoping_warning_message(GHashTable* features) +{ + gboolean server_supports_ping = g_hash_table_contains(features, "urn:xmpp:ping"); + gboolean user_prefers_warning = prefs_get_boolean(PREF_AUTOPING_WARNING); + gboolean is_autoping_enabled = prefs_get_autoping() != 0; + + if (!is_autoping_enabled && server_supports_ping && user_prefers_warning) { + cons_show("This server supports XEP-0199: XMPP Ping (better keepalive detection),\n" + "but autoping feature is currently disabled in settings.\n" + "Consider enabling it (e.g., `/autoping set 30`) for improved connection stability.\n" + "Use `/autoping warning off` to disable this message."); + } +} + static int _http_upload_response_id_handler(xmpp_stanza_t* const stanza, void* const userdata) { diff --git a/tests/functionaltests/functionaltests.c b/tests/functionaltests/functionaltests.c index 8102f928..394b31d9 100644 --- a/tests/functionaltests/functionaltests.c +++ b/tests/functionaltests/functionaltests.c @@ -150,6 +150,14 @@ main(int argc, char* argv[]) PROF_FUNC_TEST(autoping_sends_ping_after_interval), PROF_FUNC_TEST(autoping_server_not_supporting_ping), + /* Autoping availability warning - negative cases wait ~3s */ + PROF_FUNC_TEST(autoping_warning_shown_when_disabled), + PROF_FUNC_TEST(autoping_warning_not_shown_when_server_unsupported), + PROF_FUNC_TEST(autoping_warning_not_shown_when_autoping_enabled), + PROF_FUNC_TEST(autoping_warning_not_shown_when_user_disabled), + PROF_FUNC_TEST(autoping_warning_not_shown_for_user_jid), + PROF_FUNC_TEST(autoping_warning_not_shown_for_subdomain_service), + /* Service Discovery - XEP-0030 */ PROF_FUNC_TEST(disco_info_shows_identity), PROF_FUNC_TEST(disco_info_shows_features), diff --git a/tests/functionaltests/test_autoping.c b/tests/functionaltests/test_autoping.c index 1e1a1332..c8853af1 100644 --- a/tests/functionaltests/test_autoping.c +++ b/tests/functionaltests/test_autoping.c @@ -112,3 +112,144 @@ autoping_server_not_supporting_ping(void** state) // Should show error about ping not being supported assert_true(prof_output_regex("Server ping not supported")); } + +/* + * Autoping availability warning. + * + * The warning fires from the on-connect disco handler, so the three inputs + * (server ping support, autoping interval, warning preference) must all be + * set before prof_connect(). + */ + +/* Stable substring of the warning text emitted by iq.c. */ +#define AUTOPING_WARNING_MATCH "This server supports XEP-0199" + +static void +_stub_server_disco(gboolean with_ping) +{ + if (with_ping) { + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + ); + } else { + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + ); + } +} + +void +autoping_warning_shown_when_disabled(void** state) +{ + // All conditions met: server supports ping, autoping off, warning on (default). + _stub_server_disco(TRUE); + + prof_input("/autoping set 0"); + assert_true(prof_output_exact("Autoping disabled.")); + + prof_connect(); + + assert_true(prof_output_regex(AUTOPING_WARNING_MATCH)); +} + +void +autoping_warning_not_shown_when_server_unsupported(void** state) +{ + // Server does not advertise urn:xmpp:ping -> no warning even with autoping off. + _stub_server_disco(FALSE); + + prof_input("/autoping set 0"); + assert_true(prof_output_exact("Autoping disabled.")); + + prof_connect(); + + prof_timeout(NEGATIVE_ASSERT_TIMEOUT); + assert_false(prof_output_regex(AUTOPING_WARNING_MATCH)); +} + +void +autoping_warning_not_shown_when_autoping_enabled(void** state) +{ + // Warning is suppressed while autoping is enabled. + _stub_server_disco(TRUE); + + prof_input("/autoping set 30"); + assert_true(prof_output_exact("Autoping interval set to 30 seconds.")); + + prof_connect(); + + prof_timeout(NEGATIVE_ASSERT_TIMEOUT); + assert_false(prof_output_regex(AUTOPING_WARNING_MATCH)); +} + +void +autoping_warning_not_shown_when_user_disabled(void** state) +{ + // User opted out -> no warning. + _stub_server_disco(TRUE); + + prof_input("/autoping set 0"); + assert_true(prof_output_exact("Autoping disabled.")); + + prof_input("/autoping warning off"); + assert_true(prof_output_exact("Autoping availability warning disabled.")); + + prof_connect(); + + prof_timeout(NEGATIVE_ASSERT_TIMEOUT); + assert_false(prof_output_regex(AUTOPING_WARNING_MATCH)); +} + +void +autoping_warning_not_shown_for_user_jid(void** state) +{ + // disco#info from a user JID (with '@') should not trigger the warning, + // only server responses (no '@') should. + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + ); + + prof_input("/autoping set 0"); + assert_true(prof_output_exact("Autoping disabled.")); + + prof_connect(); + + prof_timeout(NEGATIVE_ASSERT_TIMEOUT); + assert_false(prof_output_regex(AUTOPING_WARNING_MATCH)); +} + +void +autoping_warning_not_shown_for_subdomain_service(void** state) +{ + // disco#info from a subdomain service should not trigger the warning, only the server's own domain should + stbbr_for_query("http://jabber.org/protocol/disco#info", + "" + "" + "" + "" + "" + "" + ); + + prof_input("/autoping set 0"); + assert_true(prof_output_exact("Autoping disabled.")); + + prof_connect(); + + prof_timeout(NEGATIVE_ASSERT_TIMEOUT); + assert_false(prof_output_regex(AUTOPING_WARNING_MATCH)); +} diff --git a/tests/functionaltests/test_autoping.h b/tests/functionaltests/test_autoping.h index 9456a50d..3226f9ee 100644 --- a/tests/functionaltests/test_autoping.h +++ b/tests/functionaltests/test_autoping.h @@ -4,3 +4,9 @@ void autoping_timeout_set(void** state); void autoping_timeout_zero_disables(void** state); void autoping_sends_ping_after_interval(void** state); void autoping_server_not_supporting_ping(void** state); +void autoping_warning_shown_when_disabled(void** state); +void autoping_warning_not_shown_when_server_unsupported(void** state); +void autoping_warning_not_shown_when_autoping_enabled(void** state); +void autoping_warning_not_shown_when_user_disabled(void** state); +void autoping_warning_not_shown_for_user_jid(void** state); +void autoping_warning_not_shown_for_subdomain_service(void** state);