From 75c4a42e188b88b2b64ef6685c47a4d23dcba260 Mon Sep 17 00:00:00 2001 From: Michael Vetter Date: Mon, 6 Jul 2026 09:15:42 +0200 Subject: [PATCH] feat(muc): Support XMPP URI 'join' query type in `/join` Enable `/join` command to accept XMPP URIs of the 'join' query type (XEP-0045 and XEP-0147). Fixes: https://github.com/profanity-im/profanity/issues/1782 Signed-off-by: Michael Vetter --- src/command/cmd_funcs.c | 67 +++++++++++++++++++++++-- tests/unittests/command/test_cmd_join.c | 65 ++++++++++++++++++++++++ tests/unittests/command/test_cmd_join.h | 3 ++ tests/unittests/unittests.c | 3 ++ 4 files changed, 135 insertions(+), 3 deletions(-) diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index 84a0841f4..29c3ecaaf 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -3540,6 +3540,51 @@ cmd_serversoftware(ProfWin* window, const char* const command, gchar** args) return TRUE; } +static gboolean +_xmpp_uri_parse_join(const gchar* uri, gchar** out_room, gchar** out_password) +{ + if (g_ascii_strncasecmp(uri, "xmpp:", 5) != 0) { + return FALSE; + } + + const gchar* body = uri + 5; + const gchar* query = strchr(body, '?'); + + if (query == NULL) { + *out_room = g_uri_unescape_string(body, NULL); + *out_password = NULL; + return TRUE; + } + + gchar* room_escaped = g_strndup(body, query - body); + *out_room = g_uri_unescape_string(room_escaped, NULL); + g_free(room_escaped); + + const gchar* query_str = query + 1; + auto_gcharv gchar** parts = g_strsplit(query_str, ";", -1); + + if (parts[0] == NULL || g_strcmp0(parts[0], "join") != 0) { + g_free(*out_room); + *out_room = NULL; + return FALSE; + } + + gchar* password = NULL; + for (int i = 1; parts[i] != NULL; i++) { + gchar* eq = strchr(parts[i], '='); + if (eq != NULL) { + gchar* key = g_strndup(parts[i], eq - parts[i]); + if (g_strcmp0(key, "password") == 0) { + password = g_uri_unescape_string(eq + 1, NULL); + } + g_free(key); + } + } + + *out_password = password; + return TRUE; +} + gboolean cmd_join(ProfWin* window, const char* const command, gchar** args) { @@ -3566,7 +3611,19 @@ cmd_join(ProfWin* window, const char* const command, gchar** args) return TRUE; } - auto_jid Jid* room_arg = jid_create(args[0]); + auto_gchar gchar* room_jid = NULL; + auto_gchar gchar* uri_password = NULL; + + if (g_ascii_strncasecmp(args[0], "xmpp:", 5) == 0) { + if (!_xmpp_uri_parse_join(args[0], &room_jid, &uri_password)) { + cons_show_error("Unsupported or invalid XMPP URI query type. Only 'join' is supported."); + return TRUE; + } + } else { + room_jid = g_strdup(args[0]); + } + + auto_jid Jid* room_arg = jid_create(room_jid); if (room_arg == NULL) { cons_show_error("Specified room has incorrect format."); cons_show(""); @@ -3580,11 +3637,11 @@ cmd_join(ProfWin* window, const char* const command, gchar** args) // full room jid supplied (room@server) if (room_arg->localpart) { - room = g_strdup(args[0]); + room = g_strdup(room_jid); // server not supplied (room), use account preference } else if (account->muc_service) { - room = g_strdup_printf("%s@%s", args[0], account->muc_service); + room = g_strdup_printf("%s@%s", room_jid, account->muc_service); // no account preference } else { @@ -3613,6 +3670,10 @@ cmd_join(ProfWin* window, const char* const command, gchar** args) nick = account->muc_nick; } + if (!passwd) { + passwd = uri_password; + } + // When no password, check for invite with password if (!passwd) { passwd = muc_invite_password(room); diff --git a/tests/unittests/command/test_cmd_join.c b/tests/unittests/command/test_cmd_join.c index 563d7f143..a85f02342 100644 --- a/tests/unittests/command/test_cmd_join.c +++ b/tests/unittests/command/test_cmd_join.c @@ -158,3 +158,68 @@ cmd_join__tests__uses_password_when_supplied(void** state) gboolean result = cmd_join(NULL, CMD_JOIN, args); assert_true(result); } + +void +cmd_join__tests__xmpp_uri_join(void** state) +{ + gchar* account_name = g_strdup("an_account"); + char* room_uri = "xmpp:room@conf.server.org?join"; + char* expected_room = "room@conf.server.org"; + gchar* account_nick = g_strdup("a_nick"); + gchar* args[] = { room_uri, NULL }; + ProfAccount* account = account_new(account_name, g_strdup("user@server.org"), NULL, NULL, + TRUE, NULL, 0, g_strdup("laptop"), NULL, NULL, 0, 0, 0, 0, 0, NULL, account_nick, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0); + + will_return(connection_get_status, JABBER_CONNECTED); + will_return(session_get_account_name, account_name); + + expect_string(accounts_get_account, name, account_name); + will_return(accounts_get_account, account); + + expect_string(presence_join_room, room, expected_room); + expect_string(presence_join_room, nick, account_nick); + expect_value(presence_join_room, passwd, NULL); + + gboolean result = cmd_join(NULL, CMD_JOIN, args); + assert_true(result); +} + +void +cmd_join__tests__xmpp_uri_join_with_password(void** state) +{ + gchar* account_name = g_strdup("an_account"); + char* room_uri = "xmpp:room@conf.server.org?join;password=secret"; + char* expected_room = "room@conf.server.org"; + char* expected_password = "secret"; + gchar* account_nick = g_strdup("a_nick"); + gchar* args[] = { room_uri, NULL }; + ProfAccount* account = account_new(account_name, g_strdup("user@server.org"), NULL, NULL, + TRUE, NULL, 0, g_strdup("laptop"), NULL, NULL, 0, 0, 0, 0, 0, NULL, account_nick, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, 0); + + will_return(connection_get_status, JABBER_CONNECTED); + will_return(session_get_account_name, account_name); + + expect_string(accounts_get_account, name, account_name); + will_return(accounts_get_account, account); + + expect_string(presence_join_room, room, expected_room); + expect_string(presence_join_room, nick, account_nick); + expect_string(presence_join_room, passwd, expected_password); + + gboolean result = cmd_join(NULL, CMD_JOIN, args); + assert_true(result); +} + +void +cmd_join__tests__xmpp_uri_invalid_query_type(void** state) +{ + char* room_uri = "xmpp:room@conf.server.org?message"; + gchar* args[] = { room_uri, NULL }; + + will_return(connection_get_status, JABBER_CONNECTED); + + expect_cons_show_error("Unsupported or invalid XMPP URI query type. Only 'join' is supported."); + + gboolean result = cmd_join(NULL, CMD_JOIN, args); + assert_true(result); +} diff --git a/tests/unittests/command/test_cmd_join.h b/tests/unittests/command/test_cmd_join.h index d5b282a3c..bc982b187 100644 --- a/tests/unittests/command/test_cmd_join.h +++ b/tests/unittests/command/test_cmd_join.h @@ -10,5 +10,8 @@ void cmd_join__tests__uses_account_mucservice_when_no_service_specified(void** s void cmd_join__tests__uses_supplied_nick(void** state); void cmd_join__tests__uses_account_nick_when_not_supplied(void** state); void cmd_join__tests__uses_password_when_supplied(void** state); +void cmd_join__tests__xmpp_uri_join(void** state); +void cmd_join__tests__xmpp_uri_join_with_password(void** state); +void cmd_join__tests__xmpp_uri_invalid_query_type(void** state); #endif diff --git a/tests/unittests/unittests.c b/tests/unittests/unittests.c index 31a03cb95..189f817a0 100644 --- a/tests/unittests/unittests.c +++ b/tests/unittests/unittests.c @@ -642,6 +642,9 @@ main(int argc, char* argv[]) muc_unit_test(cmd_join__tests__uses_supplied_nick), muc_unit_test(cmd_join__tests__uses_account_nick_when_not_supplied), muc_unit_test(cmd_join__tests__uses_password_when_supplied), + muc_unit_test(cmd_join__tests__xmpp_uri_join), + muc_unit_test(cmd_join__tests__xmpp_uri_join_with_password), + cmocka_unit_test(cmd_join__tests__xmpp_uri_invalid_query_type), cmocka_unit_test(cmd_roster__shows__message_when_disconnecting), cmocka_unit_test(cmd_roster__shows__message_when_connecting),