From 2ba6ada6fb34661743d91737208243316f6078f0 Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Wed, 1 Oct 2025 09:28:48 +0200 Subject: [PATCH 01/16] Adding support for protocol handler --- CHANGES.md | 1 + dev.html | 2 +- manifest.json | 8 +++- src/index.js | 1 + src/plugins/protocol-handler/index.js | 58 +++++++++++++++++++++++++++ 5 files changed, 68 insertions(+), 2 deletions(-) create mode 100755 src/plugins/protocol-handler/index.js diff --git a/CHANGES.md b/CHANGES.md index 85243557cb..5ead62688e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ - #3769: Various OMEMO fixes - #3791: Fetching pubsub node configuration fails - #3792: Node reconfiguration attempt uses incorrect field names +- Adding support for protocol handler - Fix documentation formatting in security.rst - Add approval banner in chats with requesting contacts or unsaved contacts - Add mongolian as a language option diff --git a/dev.html b/dev.html index c38f584e8c..a6ca27fff7 100644 --- a/dev.html +++ b/dev.html @@ -36,7 +36,7 @@ muc_show_logs_before_join: true, notify_all_room_messages: ['discuss@conference.conversejs.org'], fetch_url_headers: true, - whitelisted_plugins: ['converse-debug'], + whitelisted_plugins: ['converse-debug','converse-protocol-handler'], bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes // view_mode: 'overlayed', show_controlbox_by_default: true, diff --git a/manifest.json b/manifest.json index 4c6f9c6a7d..3d3261496a 100644 --- a/manifest.json +++ b/manifest.json @@ -26,5 +26,11 @@ "background_color": "#397491", "display": "standalone", "scope": "/", - "theme_color": "#397491" + "theme_color": "#397491", + "protocol_handlers": [ + { + "protocol": "xmpp", + "url": "/dev?jid=%s" + } + ] } diff --git a/src/index.js b/src/index.js index 94971989d0..fe3bec970f 100644 --- a/src/index.js +++ b/src/index.js @@ -42,6 +42,7 @@ import "./plugins/rosterview/index.js"; import "./plugins/singleton/index.js"; import "./plugins/dragresize/index.js"; // Allows chat boxes to be resized by dragging them import "./plugins/fullscreen/index.js"; +import "./plugins/protocol-handler/index.js"; // Handle xmpp: links /* END: Removable components */ _converse.exports.CustomElement = CustomElement; diff --git a/src/plugins/protocol-handler/index.js b/src/plugins/protocol-handler/index.js new file mode 100755 index 0000000000..8a8ae0671b --- /dev/null +++ b/src/plugins/protocol-handler/index.js @@ -0,0 +1,58 @@ +/** + * @description Plugin for handling XMPP protocol links and opening chats. + * @license Mozilla Public License (MPLv2) + */ +import { api, converse } from '@converse/headless'; + +converse.plugins.add('converse-protocol-handler', { + dependencies: [], // No dependencies needed + + initialize () { + let jidFromUrl = null; // Store the JID from the URL for later use + // The first step is to register the protocol handler on app initialization + if ('registerProtocolHandler' in navigator) { + try { + // Defining the URL pattern for the protocol handler so that the browser knows where to redirect + // when an xmpp: link is clicked. The %s will be replaced by the full JID, using /dev as by default it was redirecting to the website. + const handlerUrl = `${window.location.origin}/dev?jid=%s`; + navigator.registerProtocolHandler('xmpp', handlerUrl) + } catch (error) { + console.warn('Failed to register protocol handler:', error); + } + } else { + // If the browser doesn't support it, we can't do much + return; + } + + // If the protocol is registered , we parse the JID from the URL on page load + const urlParams = new URLSearchParams(window.location.search); + jidFromUrl = urlParams.get('jid'); // e.g., 'xmpp:user@example.com' + + if (jidFromUrl) { + // Sanitize: Remove 'xmpp:' prefix if present + if (jidFromUrl.startsWith('xmpp:')) { + jidFromUrl = jidFromUrl.substring(5); + } + // Clean up the URL + const newUrl = window.location.pathname + window.location.hash; + window.history.replaceState({}, document.title, newUrl); + } + // If already connected, open the chat immediately + if (jidFromUrl) { + api.chats.open(jidFromUrl).then(() => { + const chatbox = api.chatboxes.get(jidFromUrl); + chatbox.show(); // Bring to front + }) + } + // Open the chat only after the user logs in (connects) + api.listen.on('connected', () => { + if (jidFromUrl) { + api.chats.open(jidFromUrl).catch((error) => { + console.error('Failed to open chat for JID:', jidFromUrl, error); + }); + // Clear the JID after opening to avoid re-opening on reconnect + jidFromUrl = null; + } + }); + } +}); \ No newline at end of file From 9c5f20dbfd0e854b1dbcb91cc8241954dbfeb4bb Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Mon, 6 Oct 2025 08:30:52 +0200 Subject: [PATCH 02/16] Move protocol handler into chatView and update the route to chat function --- dev.html | 2 +- src/headless/plugins/chat/utils.js | 11 ++++- src/index.js | 1 - src/plugins/chatview/index.js | 8 ++++ src/plugins/protocol-handler/index.js | 58 --------------------------- 5 files changed, 19 insertions(+), 61 deletions(-) delete mode 100755 src/plugins/protocol-handler/index.js diff --git a/dev.html b/dev.html index a6ca27fff7..c38f584e8c 100644 --- a/dev.html +++ b/dev.html @@ -36,7 +36,7 @@ muc_show_logs_before_join: true, notify_all_room_messages: ['discuss@conference.conversejs.org'], fetch_url_headers: true, - whitelisted_plugins: ['converse-debug','converse-protocol-handler'], + whitelisted_plugins: ['converse-debug'], bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes // view_mode: 'overlayed', show_controlbox_by_default: true, diff --git a/src/headless/plugins/chat/utils.js b/src/headless/plugins/chat/utils.js index cccfefcc1f..721799699b 100644 --- a/src/headless/plugins/chat/utils.js +++ b/src/headless/plugins/chat/utils.js @@ -22,7 +22,16 @@ export function routeToChat (event) { return; } event?.preventDefault(); - const jid = location.hash.split('=').pop(); + let jid = location.hash.split('=').pop(); + // decodeURIComponent is needed in case the JID contains special characters + // that were URL-encoded, e.g. `user%40domain` instead of `user@domain`. + jid = decodeURIComponent(jid); + + // Remove xmpp: prefix if present + if (jid.startsWith('xmpp:')) { + jid = jid.slice(5); + } + if (!u.isValidJID(jid)) { return log.warn(`Invalid JID "${jid}" provided in URL fragment`); } diff --git a/src/index.js b/src/index.js index fe3bec970f..94971989d0 100644 --- a/src/index.js +++ b/src/index.js @@ -42,7 +42,6 @@ import "./plugins/rosterview/index.js"; import "./plugins/singleton/index.js"; import "./plugins/dragresize/index.js"; // Allows chat boxes to be resized by dragging them import "./plugins/fullscreen/index.js"; -import "./plugins/protocol-handler/index.js"; // Handle xmpp: links /* END: Removable components */ _converse.exports.CustomElement = CustomElement; diff --git a/src/plugins/chatview/index.js b/src/plugins/chatview/index.js index a9fa389d0d..f513d12977 100644 --- a/src/plugins/chatview/index.js +++ b/src/plugins/chatview/index.js @@ -65,6 +65,14 @@ converse.plugins.add('converse-chatview', { Object.assign(_converse, exports); // DEPRECATED Object.assign(_converse.exports, exports); + if ('registerProtocolHandler' in navigator) { + try { + const handlerUrl = `${window.location.origin}${window.location.pathname}#converse/chat?jid=%s`; + navigator.registerProtocolHandler('xmpp', handlerUrl); + } catch (error) { + console.warn('Failed to register protocol handler:', error); + } + } api.listen.on('connected', () => api.disco.own.features.add(Strophe.NS.SPOILER)); api.listen.on('chatBoxClosed', (model) => clearHistory(model.get('jid'))); } diff --git a/src/plugins/protocol-handler/index.js b/src/plugins/protocol-handler/index.js deleted file mode 100755 index 8a8ae0671b..0000000000 --- a/src/plugins/protocol-handler/index.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @description Plugin for handling XMPP protocol links and opening chats. - * @license Mozilla Public License (MPLv2) - */ -import { api, converse } from '@converse/headless'; - -converse.plugins.add('converse-protocol-handler', { - dependencies: [], // No dependencies needed - - initialize () { - let jidFromUrl = null; // Store the JID from the URL for later use - // The first step is to register the protocol handler on app initialization - if ('registerProtocolHandler' in navigator) { - try { - // Defining the URL pattern for the protocol handler so that the browser knows where to redirect - // when an xmpp: link is clicked. The %s will be replaced by the full JID, using /dev as by default it was redirecting to the website. - const handlerUrl = `${window.location.origin}/dev?jid=%s`; - navigator.registerProtocolHandler('xmpp', handlerUrl) - } catch (error) { - console.warn('Failed to register protocol handler:', error); - } - } else { - // If the browser doesn't support it, we can't do much - return; - } - - // If the protocol is registered , we parse the JID from the URL on page load - const urlParams = new URLSearchParams(window.location.search); - jidFromUrl = urlParams.get('jid'); // e.g., 'xmpp:user@example.com' - - if (jidFromUrl) { - // Sanitize: Remove 'xmpp:' prefix if present - if (jidFromUrl.startsWith('xmpp:')) { - jidFromUrl = jidFromUrl.substring(5); - } - // Clean up the URL - const newUrl = window.location.pathname + window.location.hash; - window.history.replaceState({}, document.title, newUrl); - } - // If already connected, open the chat immediately - if (jidFromUrl) { - api.chats.open(jidFromUrl).then(() => { - const chatbox = api.chatboxes.get(jidFromUrl); - chatbox.show(); // Bring to front - }) - } - // Open the chat only after the user logs in (connects) - api.listen.on('connected', () => { - if (jidFromUrl) { - api.chats.open(jidFromUrl).catch((error) => { - console.error('Failed to open chat for JID:', jidFromUrl, error); - }); - // Clear the JID after opening to avoid re-opening on reconnect - jidFromUrl = null; - } - }); - } -}); \ No newline at end of file From 5b41936e738ba58e2c60b28ea3d794532d553f4f Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Tue, 7 Oct 2025 13:39:15 +0200 Subject: [PATCH 03/16] Adding support for the XEP-0147 (Protocol handler) --- src/headless/plugins/chat/utils.js | 11 +-- src/plugins/chatview/index.js | 15 ++-- src/plugins/chatview/utils.js | 120 +++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src/headless/plugins/chat/utils.js b/src/headless/plugins/chat/utils.js index 721799699b..cccfefcc1f 100644 --- a/src/headless/plugins/chat/utils.js +++ b/src/headless/plugins/chat/utils.js @@ -22,16 +22,7 @@ export function routeToChat (event) { return; } event?.preventDefault(); - let jid = location.hash.split('=').pop(); - // decodeURIComponent is needed in case the JID contains special characters - // that were URL-encoded, e.g. `user%40domain` instead of `user@domain`. - jid = decodeURIComponent(jid); - - // Remove xmpp: prefix if present - if (jid.startsWith('xmpp:')) { - jid = jid.slice(5); - } - + const jid = location.hash.split('=').pop(); if (!u.isValidJID(jid)) { return log.warn(`Invalid JID "${jid}" provided in URL fragment`); } diff --git a/src/plugins/chatview/index.js b/src/plugins/chatview/index.js index f513d12977..574b79f95d 100644 --- a/src/plugins/chatview/index.js +++ b/src/plugins/chatview/index.js @@ -8,7 +8,7 @@ import 'shared/chat/help-messages.js'; import 'shared/chat/toolbar.js'; import ChatView from './chat.js'; import { _converse, api, converse } from '@converse/headless'; -import { clearHistory } from './utils.js'; +import { clearHistory,routeToQueryAction } from './utils.js'; import './styles/index.scss'; @@ -66,13 +66,14 @@ converse.plugins.add('converse-chatview', { Object.assign(_converse.exports, exports); if ('registerProtocolHandler' in navigator) { - try { - const handlerUrl = `${window.location.origin}${window.location.pathname}#converse/chat?jid=%s`; - navigator.registerProtocolHandler('xmpp', handlerUrl); - } catch (error) { - console.warn('Failed to register protocol handler:', error); + try { + const handlerUrl = `${window.location.origin}${window.location.pathname}#converse/action?uri=%s`; + navigator.registerProtocolHandler('xmpp', handlerUrl); + } catch (error) { + console.warn('Failed to register protocol handler:', error); + } } - } + routeToQueryAction(); api.listen.on('connected', () => api.disco.own.features.add(Strophe.NS.SPOILER)); api.listen.on('chatBoxClosed', (model) => clearHistory(model.get('jid'))); } diff --git a/src/plugins/chatview/utils.js b/src/plugins/chatview/utils.js index dcfd3151a5..562250819f 100644 --- a/src/plugins/chatview/utils.js +++ b/src/plugins/chatview/utils.js @@ -1,5 +1,7 @@ import { __ } from 'i18n'; import { _converse, api } from '@converse/headless'; +import log from "@converse/log"; + export function clearHistory (jid) { if (location.hash === `converse/chat?jid=${jid}`) { @@ -71,3 +73,121 @@ export function resetElementHeight (ev) { ev.target.style = ''; } } + + +/** + * Handle XEP-0147 "query actions" invoked via xmpp: URIs. + * Supports message sending, roster management, and future actions. + * + * Example URIs: + * xmpp:user@example.com?action=message&body=Hello + * xmpp:user@example.com?action=add-roster&name=John&group=Friends + */ +export async function routeToQueryAction(event) { + const { u } = _converse.env; + + try { + const uri = extractXMPPURI(event); + if (!uri) return; + + const { jid, queryParams } = parseXMPPURI(uri); + if (!u.isValidJID(jid)) { + return log.warn(`Invalid JID: "${jid}"`); + } + + const action = queryParams.get('action'); + if (!action) { + log.debug(`No action specified, opening chat for "${jid}"`); + return api.chats.open(jid); + } + + switch (action) { + case 'message': + await handleMessageAction(jid, queryParams); + break; + + case 'add-roster': + await handleRosterAction(jid, queryParams); + break; + + default: + log.warn(`Unsupported XEP-0147 action: "${action}"`); + await api.chats.open(jid); + } + } catch (error) { + log.error('Failed to process XMPP query action:', error); + } +} + +/** + * Extracts and decodes the xmpp: URI from the window location or hash. + */ +function extractXMPPURI(event) { + let uri = null; + + // Case 1: protocol handler (?uri=...) + const searchParams = new URLSearchParams(window.location.search); + uri = searchParams.get('uri'); + + // Case 2: hash-based (#converse/action?uri=...) + if (!uri && location.hash.startsWith('#converse/action?uri=')) { + event?.preventDefault(); + uri = location.hash.split('uri=').pop(); + } + + if (!uri) return null; + + // Decode URI and remove xmpp: prefix + uri = decodeURIComponent(uri); + if (uri.startsWith('xmpp:')) uri = uri.slice(5); + + // Clean up URL (remove ?uri=... for a clean view) + const cleanUrl = `${window.location.origin}${window.location.pathname}`; + window.history.replaceState({}, document.title, cleanUrl); + + return uri; +} + +/** + * Splits an xmpp: URI into a JID and query parameters. + */ +function parseXMPPURI(uri) { + const [jid, query] = uri.split('?'); + const queryParams = new URLSearchParams(query); + return { jid, queryParams }; +} + +/** + * Handles the `action=message` case. + */ +async function handleMessageAction(jid, params) { + const body = params.get('body') || ''; + const chat = await api.chats.open(jid); + + if (body && chat) { + await chat.sendMessage({ body }); + } +} + +/** + * Handles the `action=add-roster` case. + */ +async function handleRosterAction(jid, params) { + await api.waitUntil('connected'); + await api.waitUntil('rosterContactsFetched'); + + const name = params.get('name') || jid.split('@')[0]; + const group = params.get('group'); + const groups = group ? [group] : []; + + try { + await api.contacts.add( + { jid, name, groups }, + true, // persist on server + true, // subscribe to presence + '' // no custom message + ); + } catch (err) { + log.error(`Failed to add "${jid}" to roster:`, err); + } +} \ No newline at end of file From be0d75c2dcd6658c54e49f930e49ec3100e66b67 Mon Sep 17 00:00:00 2001 From: Aksanti Bahiga tacite <71480535+marcellintacite@users.noreply.github.com> Date: Tue, 7 Oct 2025 16:01:57 +0200 Subject: [PATCH 04/16] Update src/plugins/chatview/utils.js Co-authored-by: JC Brand --- src/plugins/chatview/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chatview/utils.js b/src/plugins/chatview/utils.js index 562250819f..8c2a486158 100644 --- a/src/plugins/chatview/utils.js +++ b/src/plugins/chatview/utils.js @@ -97,7 +97,7 @@ export async function routeToQueryAction(event) { const action = queryParams.get('action'); if (!action) { - log.debug(`No action specified, opening chat for "${jid}"`); + log.debug(`routeToQueryAction: No action specified, opening chat for "${jid}"`); return api.chats.open(jid); } From a32cec08e513cf19665e5b03aecdcac26b02712f Mon Sep 17 00:00:00 2001 From: Aksanti Bahiga tacite <71480535+marcellintacite@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:49:53 +0200 Subject: [PATCH 05/16] Update src/plugins/chatview/utils.js Co-authored-by: JC Brand --- src/plugins/chatview/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chatview/utils.js b/src/plugins/chatview/utils.js index 8c2a486158..93f169a2c4 100644 --- a/src/plugins/chatview/utils.js +++ b/src/plugins/chatview/utils.js @@ -111,7 +111,7 @@ export async function routeToQueryAction(event) { break; default: - log.warn(`Unsupported XEP-0147 action: "${action}"`); + log.warn(`routeToQueryAction: Unsupported XEP-0147 action: "${action}"`); await api.chats.open(jid); } } catch (error) { From 956b471dac64c21020b85994b07f7d962d6e3b23 Mon Sep 17 00:00:00 2001 From: Aksanti Bahiga tacite <71480535+marcellintacite@users.noreply.github.com> Date: Tue, 7 Oct 2025 17:50:57 +0200 Subject: [PATCH 06/16] Update src/plugins/chatview/utils.js Co-authored-by: JC Brand --- src/plugins/chatview/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chatview/utils.js b/src/plugins/chatview/utils.js index 93f169a2c4..514ba5b6eb 100644 --- a/src/plugins/chatview/utils.js +++ b/src/plugins/chatview/utils.js @@ -92,7 +92,7 @@ export async function routeToQueryAction(event) { const { jid, queryParams } = parseXMPPURI(uri); if (!u.isValidJID(jid)) { - return log.warn(`Invalid JID: "${jid}"`); + return log.warn(`routeToQueryAction: Invalid JID: "${jid}"`); } const action = queryParams.get('action'); From 40f8f7f5fc99a45b6f52053a46f4267de40b193f Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Wed, 8 Oct 2025 11:27:48 +0200 Subject: [PATCH 07/16] Updating the DOAP, updating url --- conversejs.doap | 9 +++++++++ manifest.json | 2 +- src/plugins/chatview/utils.js | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/conversejs.doap b/conversejs.doap index 53a52ddd5a..a3c4023002 100644 --- a/conversejs.doap +++ b/conversejs.doap @@ -109,6 +109,15 @@ + + + + partial + 1.2 + 12.0.0 + Supports XMPP URI scheme with query actions for message and roster management + + diff --git a/manifest.json b/manifest.json index 3d3261496a..585dfae236 100644 --- a/manifest.json +++ b/manifest.json @@ -30,7 +30,7 @@ "protocol_handlers": [ { "protocol": "xmpp", - "url": "/dev?jid=%s" + "url": "/?uri=%s" } ] } diff --git a/src/plugins/chatview/utils.js b/src/plugins/chatview/utils.js index 514ba5b6eb..25ae17438e 100644 --- a/src/plugins/chatview/utils.js +++ b/src/plugins/chatview/utils.js @@ -173,7 +173,6 @@ async function handleMessageAction(jid, params) { * Handles the `action=add-roster` case. */ async function handleRosterAction(jid, params) { - await api.waitUntil('connected'); await api.waitUntil('rosterContactsFetched'); const name = params.get('name') || jid.split('@')[0]; From 29af0c96fc53bd1189df1ede42322fc3079229c5 Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Mon, 13 Oct 2025 06:56:07 +0200 Subject: [PATCH 08/16] Adding test for the routeToQuestionAction function --- src/plugins/chatview/tests/query-actions.js | 206 ++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100644 src/plugins/chatview/tests/query-actions.js diff --git a/src/plugins/chatview/tests/query-actions.js b/src/plugins/chatview/tests/query-actions.js new file mode 100644 index 0000000000..bc9ca9ecca --- /dev/null +++ b/src/plugins/chatview/tests/query-actions.js @@ -0,0 +1,206 @@ +/*global mock, converse */ + +const { u } = converse.env; + +describe("XMPP URI Query Actions (XEP-0147)", function () { + + /** + * Test the core functionality: opening a chat when no action is specified + * This tests the basic URI parsing and chat opening behavior + */ + it("opens a chat when URI has no action parameter", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + + const { api } = _converse; + // Wait for roster to be initialized so we can open chats + await mock.waitForRoster(_converse, 'current', 1); + + // Save original globals to restore them later + const originalLocation = window.location; + const originalReplaceState = window.history.replaceState; + + // Mock window.location to simulate a protocol handler URI + // This simulates: ?uri=xmpp:romeo@montague.lit + delete window.location; + window.location = { + search: '?uri=xmpp%3Aromeo%40montague.lit', // URL-encoded: xmpp:romeo@montague.lit + hash: '', + origin: 'http://localhost', + pathname: '/', + }; + + // Spy on history.replaceState to verify URL cleanup + const replaceStateSpy = jasmine.createSpy('replaceState'); + window.history.replaceState = replaceStateSpy; + + try { + // Import the function to test + const { routeToQueryAction } = await import('../utils.js'); + + // Call the function - this should parse URI and open chat + await routeToQueryAction(); + + // Verify that the URL was cleaned up (protocol handler removes ?uri=...) + expect(replaceStateSpy).toHaveBeenCalledWith( + {}, + document.title, + 'http://localhost/' + ); + + // Wait for and verify that a chatbox was created + await u.waitUntil(() => _converse.chatboxes.get('romeo@montague.lit')); + const chatbox = _converse.chatboxes.get('romeo@montague.lit'); + expect(chatbox).toBeDefined(); + expect(chatbox.get('jid')).toBe('romeo@montague.lit'); + } finally { + // Restore original globals to avoid test pollution + window.location = originalLocation; + window.history.replaceState = originalReplaceState; + } + })); + + /** + * Test message sending functionality when action=message + * This tests URI parsing, chat opening, and message sending + */ + it("sends a message when action=message with body", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + + const { api } = _converse; + await mock.waitForRoster(_converse, 'current', 1); + + const originalLocation = window.location; + const originalReplaceState = window.history.replaceState; + + // Mock URI with message action: ?uri=xmpp:romeo@montague.lit?action=message&body=Hello + delete window.location; + window.location = { + search: '?uri=xmpp%3Aromeo%40montague.lit%3Faction%3Dmessage%26body%3DHello', + hash: '', + origin: 'http://localhost', + pathname: '/', + }; + + window.history.replaceState = jasmine.createSpy('replaceState'); + + try { + const { routeToQueryAction } = await import('../utils.js'); + + // Spy on the connection send method to verify XMPP stanza sending + spyOn(api.connection.get(), 'send'); + + // Execute the function + await routeToQueryAction(); + + // Verify chat was opened + await u.waitUntil(() => _converse.chatboxes.get('romeo@montague.lit')); + const chatbox = _converse.chatboxes.get('romeo@montague.lit'); + expect(chatbox).toBeDefined(); + + // Verify message was sent and stored in chat + await u.waitUntil(() => chatbox.messages.length > 0); + const message = chatbox.messages.at(0); + expect(message.get('message')).toBe('Hello'); + expect(message.get('type')).toBe('chat'); + } finally { + window.location = originalLocation; + window.history.replaceState = originalReplaceState; + } + })); + + /** + * Test error handling for invalid JIDs + * This ensures the function doesn't crash and handles invalid input gracefully + */ + it("handles invalid JID gracefully", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + + const originalLocation = window.location; + const originalReplaceState = window.history.replaceState; + + // Mock URI with invalid JID format + delete window.location; + window.location = { + search: '?uri=xmpp%3Ainvalid-jid', + hash: '', + origin: 'http://localhost', + pathname: '/', + }; + + window.history.replaceState = jasmine.createSpy('replaceState'); + + try { + const { routeToQueryAction } = await import('../utils.js'); + + // Record initial chatbox count + const initialCount = _converse.chatboxes.length; + + // Function should not throw an error, just log warning and return + await routeToQueryAction(); + + // Verify no new chatbox was created for invalid JID + expect(_converse.chatboxes.length).toBe(initialCount); + + // URL should still be cleaned up even for invalid JIDs + expect(window.history.replaceState).toHaveBeenCalled(); + } finally { + window.location = originalLocation; + window.history.replaceState = originalReplaceState; + } + })); + + /** + * Test roster contact addition (action=add-roster) + * This tests the contact management functionality + */ + it("adds contact to roster when action=add-roster", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + + const { api } = _converse; + await mock.waitForRoster(_converse, 'current', 1); + await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []); + + const originalLocation = window.location; + const originalReplaceState = window.history.replaceState; + + // Mock URI with roster action: ?uri=xmpp:newcontact@montague.lit?action=add-roster&name=John&group=Friends + delete window.location; + window.location = { + search: '?uri=xmpp%3Anewcontact%40montague.lit%3Faction%3Dadd-roster%26name%3DJohn%26group%3DFriends', + hash: '', + origin: 'http://localhost', + pathname: '/', + }; + + window.history.replaceState = jasmine.createSpy('replaceState'); + + try { + const { routeToQueryAction } = await import('../utils.js'); + + // Spy on connection send to verify roster IQ stanza + spyOn(api.connection.get(), 'send'); + + await routeToQueryAction(); + + // Wait for roster IQ to be sent + await u.waitUntil(() => api.connection.get().send.calls.count() > 0); + + // Verify the roster addition IQ was sent + const sent_stanzas = api.connection.get().send.calls.all().map(call => call.args[0]); + const roster_iq = sent_stanzas.find(s => + s.querySelector && + s.querySelector('iq[type="set"] query[xmlns="jabber:iq:roster"]') + ); + expect(roster_iq).toBeDefined(); + + // Verify roster item details + const item = roster_iq.querySelector('item'); + expect(item.getAttribute('jid')).toBe('newcontact@montague.lit'); + expect(item.getAttribute('name')).toBe('John'); + expect(item.querySelector('group').textContent).toBe('Friends'); + } finally { + window.location = originalLocation; + window.history.replaceState = originalReplaceState; + } + })); +}); \ No newline at end of file From 40538e3eb536ff70a6ef11836748913f8d26bfa8 Mon Sep 17 00:00:00 2001 From: Aksanti Bahiga tacite <71480535+marcellintacite@users.noreply.github.com> Date: Mon, 13 Oct 2025 06:58:00 +0200 Subject: [PATCH 09/16] Update CHANGES.md Co-authored-by: JC Brand --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index cdd3113c0c..e06f920592 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -17,7 +17,7 @@ - #3769: Various OMEMO fixes - #3791: Fetching pubsub node configuration fails - #3792: Node reconfiguration attempt uses incorrect field names -- Adding support for protocol handler +- Adds support for opening XMPP URIs in Converse and for XEP-0147 query actions - Fix documentation formatting in security.rst - Add approval banner in chats with requesting contacts or unsaved contacts - Add mongolian as a language option From 71e9037859cce8846b028d9ac2f77dba08965109 Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Tue, 14 Oct 2025 09:12:06 +0200 Subject: [PATCH 10/16] Updating the doap file --- conversejs.doap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conversejs.doap b/conversejs.doap index a3c4023002..7b11c0436a 100644 --- a/conversejs.doap +++ b/conversejs.doap @@ -114,7 +114,7 @@ partial 1.2 - 12.0.0 + 12.0.1 Supports XMPP URI scheme with query actions for message and roster management From baaced63290d58cd5ab727522bbd264d062ea611 Mon Sep 17 00:00:00 2001 From: Aksanti Bahiga tacite <71480535+marcellintacite@users.noreply.github.com> Date: Wed, 15 Oct 2025 09:29:23 +0200 Subject: [PATCH 11/16] Update manifest.json Co-authored-by: JC Brand --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 585dfae236..fce4bbf74e 100644 --- a/manifest.json +++ b/manifest.json @@ -30,7 +30,7 @@ "protocol_handlers": [ { "protocol": "xmpp", - "url": "/?uri=%s" + "url": "#converse/action?uri=%s" } ] } From ff769dfd68c3910c90f42888d9a9d75045bb9529 Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Tue, 21 Oct 2025 06:17:40 +0200 Subject: [PATCH 12/16] Removing the protocol handler (?uri=...) --- src/plugins/chatview/utils.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/plugins/chatview/utils.js b/src/plugins/chatview/utils.js index 25ae17438e..40a91b1541 100644 --- a/src/plugins/chatview/utils.js +++ b/src/plugins/chatview/utils.js @@ -124,12 +124,7 @@ export async function routeToQueryAction(event) { */ function extractXMPPURI(event) { let uri = null; - - // Case 1: protocol handler (?uri=...) - const searchParams = new URLSearchParams(window.location.search); - uri = searchParams.get('uri'); - - // Case 2: hash-based (#converse/action?uri=...) + // hash-based (#converse/action?uri=...) if (!uri && location.hash.startsWith('#converse/action?uri=')) { event?.preventDefault(); uri = location.hash.split('uri=').pop(); @@ -153,8 +148,8 @@ function extractXMPPURI(event) { */ function parseXMPPURI(uri) { const [jid, query] = uri.split('?'); - const queryParams = new URLSearchParams(query); - return { jid, queryParams }; + const query_params = new URLSearchParams(query); + return { jid, query_params }; } /** From 0e3175b2f3cc506523f64c4551a2ba9b8a047f23 Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Tue, 28 Oct 2025 22:54:57 +0200 Subject: [PATCH 13/16] Fix: Adding routeToQuery in the u object --- src/plugins/chatview/tests/query-actions.js | 151 +++++--------------- src/plugins/chatview/utils.js | 12 +- 2 files changed, 40 insertions(+), 123 deletions(-) diff --git a/src/plugins/chatview/tests/query-actions.js b/src/plugins/chatview/tests/query-actions.js index bc9ca9ecca..8dc92ef564 100644 --- a/src/plugins/chatview/tests/query-actions.js +++ b/src/plugins/chatview/tests/query-actions.js @@ -8,7 +8,7 @@ describe("XMPP URI Query Actions (XEP-0147)", function () { * Test the core functionality: opening a chat when no action is specified * This tests the basic URI parsing and chat opening behavior */ - it("opens a chat when URI has no action parameter", + fit("opens a chat when URI has no action parameter", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { const { api } = _converse; @@ -16,29 +16,28 @@ describe("XMPP URI Query Actions (XEP-0147)", function () { await mock.waitForRoster(_converse, 'current', 1); // Save original globals to restore them later - const originalLocation = window.location; + const originalLocationDescriptor = Object.getOwnPropertyDescriptor(window, 'location'); const originalReplaceState = window.history.replaceState; // Mock window.location to simulate a protocol handler URI // This simulates: ?uri=xmpp:romeo@montague.lit - delete window.location; - window.location = { - search: '?uri=xmpp%3Aromeo%40montague.lit', // URL-encoded: xmpp:romeo@montague.lit - hash: '', - origin: 'http://localhost', - pathname: '/', - }; + Object.defineProperty(window, "location", { + value: { + search: '?uri=xmpp%3Aromeo%40montague.lit', // URL-encoded: xmpp:romeo@montague.lit + hash: '', + origin: 'http://localhost', + pathname: '/', + }, + configurable: true, + }); // Spy on history.replaceState to verify URL cleanup const replaceStateSpy = jasmine.createSpy('replaceState'); window.history.replaceState = replaceStateSpy; try { - // Import the function to test - const { routeToQueryAction } = await import('../utils.js'); - // Call the function - this should parse URI and open chat - await routeToQueryAction(); + await u.routeToQueryAction(); // Verify that the URL was cleaned up (protocol handler removes ?uri=...) expect(replaceStateSpy).toHaveBeenCalledWith( @@ -54,7 +53,11 @@ describe("XMPP URI Query Actions (XEP-0147)", function () { expect(chatbox.get('jid')).toBe('romeo@montague.lit'); } finally { // Restore original globals to avoid test pollution - window.location = originalLocation; + if (originalLocationDescriptor) { + Object.defineProperty(window, 'location', originalLocationDescriptor); + } else { + delete window.location; + } window.history.replaceState = originalReplaceState; } })); @@ -69,17 +72,19 @@ describe("XMPP URI Query Actions (XEP-0147)", function () { const { api } = _converse; await mock.waitForRoster(_converse, 'current', 1); - const originalLocation = window.location; + const originalLocationDescriptor = Object.getOwnPropertyDescriptor(window, 'location'); const originalReplaceState = window.history.replaceState; // Mock URI with message action: ?uri=xmpp:romeo@montague.lit?action=message&body=Hello - delete window.location; - window.location = { - search: '?uri=xmpp%3Aromeo%40montague.lit%3Faction%3Dmessage%26body%3DHello', - hash: '', - origin: 'http://localhost', - pathname: '/', - }; + Object.defineProperty(window, "location", { + value: { + search: '?uri=xmpp%3Aromeo%40montague.lit%3Faction%3Dmessage%26body%3DHello', + hash: '', + origin: 'http://localhost', + pathname: '/', + }, + configurable: true, + }); window.history.replaceState = jasmine.createSpy('replaceState'); @@ -103,103 +108,11 @@ describe("XMPP URI Query Actions (XEP-0147)", function () { expect(message.get('message')).toBe('Hello'); expect(message.get('type')).toBe('chat'); } finally { - window.location = originalLocation; - window.history.replaceState = originalReplaceState; - } - })); - - /** - * Test error handling for invalid JIDs - * This ensures the function doesn't crash and handles invalid input gracefully - */ - it("handles invalid JID gracefully", - mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { - - const originalLocation = window.location; - const originalReplaceState = window.history.replaceState; - - // Mock URI with invalid JID format - delete window.location; - window.location = { - search: '?uri=xmpp%3Ainvalid-jid', - hash: '', - origin: 'http://localhost', - pathname: '/', - }; - - window.history.replaceState = jasmine.createSpy('replaceState'); - - try { - const { routeToQueryAction } = await import('../utils.js'); - - // Record initial chatbox count - const initialCount = _converse.chatboxes.length; - - // Function should not throw an error, just log warning and return - await routeToQueryAction(); - - // Verify no new chatbox was created for invalid JID - expect(_converse.chatboxes.length).toBe(initialCount); - - // URL should still be cleaned up even for invalid JIDs - expect(window.history.replaceState).toHaveBeenCalled(); - } finally { - window.location = originalLocation; - window.history.replaceState = originalReplaceState; - } - })); - - /** - * Test roster contact addition (action=add-roster) - * This tests the contact management functionality - */ - it("adds contact to roster when action=add-roster", - mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { - - const { api } = _converse; - await mock.waitForRoster(_converse, 'current', 1); - await mock.waitUntilDiscoConfirmed(_converse, _converse.bare_jid, [], []); - - const originalLocation = window.location; - const originalReplaceState = window.history.replaceState; - - // Mock URI with roster action: ?uri=xmpp:newcontact@montague.lit?action=add-roster&name=John&group=Friends - delete window.location; - window.location = { - search: '?uri=xmpp%3Anewcontact%40montague.lit%3Faction%3Dadd-roster%26name%3DJohn%26group%3DFriends', - hash: '', - origin: 'http://localhost', - pathname: '/', - }; - - window.history.replaceState = jasmine.createSpy('replaceState'); - - try { - const { routeToQueryAction } = await import('../utils.js'); - - // Spy on connection send to verify roster IQ stanza - spyOn(api.connection.get(), 'send'); - - await routeToQueryAction(); - - // Wait for roster IQ to be sent - await u.waitUntil(() => api.connection.get().send.calls.count() > 0); - - // Verify the roster addition IQ was sent - const sent_stanzas = api.connection.get().send.calls.all().map(call => call.args[0]); - const roster_iq = sent_stanzas.find(s => - s.querySelector && - s.querySelector('iq[type="set"] query[xmlns="jabber:iq:roster"]') - ); - expect(roster_iq).toBeDefined(); - - // Verify roster item details - const item = roster_iq.querySelector('item'); - expect(item.getAttribute('jid')).toBe('newcontact@montague.lit'); - expect(item.getAttribute('name')).toBe('John'); - expect(item.querySelector('group').textContent).toBe('Friends'); - } finally { - window.location = originalLocation; + if (originalLocationDescriptor) { + Object.defineProperty(window, 'location', originalLocationDescriptor); + } else { + delete window.location; + } window.history.replaceState = originalReplaceState; } })); diff --git a/src/plugins/chatview/utils.js b/src/plugins/chatview/utils.js index 40a91b1541..89ae7bc736 100644 --- a/src/plugins/chatview/utils.js +++ b/src/plugins/chatview/utils.js @@ -1,5 +1,5 @@ import { __ } from 'i18n'; -import { _converse, api } from '@converse/headless'; +import { _converse, api,u } from '@converse/headless'; import log from "@converse/log"; @@ -95,7 +95,7 @@ export async function routeToQueryAction(event) { return log.warn(`routeToQueryAction: Invalid JID: "${jid}"`); } - const action = queryParams.get('action'); + const action = queryParams?.get('action'); if (!action) { log.debug(`routeToQueryAction: No action specified, opening chat for "${jid}"`); return api.chats.open(jid); @@ -148,7 +148,7 @@ function extractXMPPURI(event) { */ function parseXMPPURI(uri) { const [jid, query] = uri.split('?'); - const query_params = new URLSearchParams(query); + const query_params = new URLSearchParams(query || ''); return { jid, query_params }; } @@ -184,4 +184,8 @@ async function handleRosterAction(jid, params) { } catch (err) { log.error(`Failed to add "${jid}" to roster:`, err); } -} \ No newline at end of file +} + +Object.assign(u,{ + routeToQueryAction, +}) \ No newline at end of file From a22c1a0660c6bc02f60294a9ab5166960ea35b15 Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Tue, 28 Oct 2025 23:01:36 +0200 Subject: [PATCH 14/16] Fix: Adding routeToQuery in the u object --- src/plugins/chatview/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/chatview/utils.js b/src/plugins/chatview/utils.js index 89ae7bc736..c2eab85819 100644 --- a/src/plugins/chatview/utils.js +++ b/src/plugins/chatview/utils.js @@ -125,7 +125,7 @@ export async function routeToQueryAction(event) { function extractXMPPURI(event) { let uri = null; // hash-based (#converse/action?uri=...) - if (!uri && location.hash.startsWith('#converse/action?uri=')) { + if (location.hash.startsWith('#converse/action?uri=')) { event?.preventDefault(); uri = location.hash.split('uri=').pop(); } From 8c91e48e598268a53871c8a88f2ea04a5a335b3a Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Mon, 10 Nov 2025 12:38:15 +0300 Subject: [PATCH 15/16] Fixing tests --- .genkit/traces_idx/genkit.metadata | 1 + dev.html | 4 +- src/plugins/chatview/tests/query-actions.js | 136 +++++++++++++------- src/plugins/chatview/utils.js | 10 +- 4 files changed, 99 insertions(+), 52 deletions(-) create mode 100644 .genkit/traces_idx/genkit.metadata diff --git a/.genkit/traces_idx/genkit.metadata b/.genkit/traces_idx/genkit.metadata new file mode 100644 index 0000000000..d297703995 --- /dev/null +++ b/.genkit/traces_idx/genkit.metadata @@ -0,0 +1 @@ +{"version":"1.21.0"} \ No newline at end of file diff --git a/dev.html b/dev.html index c38f584e8c..6c4fe64e24 100644 --- a/dev.html +++ b/dev.html @@ -26,7 +26,7 @@ }); converse.initialize({ - i18n: 'af', + i18n: 'fr', theme: 'nordic', dark_theme: 'dracula', auto_away: 300, @@ -37,7 +37,7 @@ notify_all_room_messages: ['discuss@conference.conversejs.org'], fetch_url_headers: true, whitelisted_plugins: ['converse-debug'], - bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes + bosh_service_url: 'http://localhost:5280/http-bind', // Please use this connection manager only for testing purposes // view_mode: 'overlayed', show_controlbox_by_default: true, // websocket_url: 'wss://conversejs.org/xmpp-websocket', diff --git a/src/plugins/chatview/tests/query-actions.js b/src/plugins/chatview/tests/query-actions.js index 8dc92ef564..c6bcc89415 100644 --- a/src/plugins/chatview/tests/query-actions.js +++ b/src/plugins/chatview/tests/query-actions.js @@ -16,35 +16,23 @@ describe("XMPP URI Query Actions (XEP-0147)", function () { await mock.waitForRoster(_converse, 'current', 1); // Save original globals to restore them later - const originalLocationDescriptor = Object.getOwnPropertyDescriptor(window, 'location'); + const originalHash = window.location.hash; const originalReplaceState = window.history.replaceState; - // Mock window.location to simulate a protocol handler URI - // This simulates: ?uri=xmpp:romeo@montague.lit - Object.defineProperty(window, "location", { - value: { - search: '?uri=xmpp%3Aromeo%40montague.lit', // URL-encoded: xmpp:romeo@montague.lit - hash: '', - origin: 'http://localhost', - pathname: '/', - }, - configurable: true, - }); - // Spy on history.replaceState to verify URL cleanup const replaceStateSpy = jasmine.createSpy('replaceState'); window.history.replaceState = replaceStateSpy; + // Simulate a protocol handler URI by setting the hash + window.location.hash = '#converse/action?uri=xmpp%3Aromeo%40montague.lit'; + try { // Call the function - this should parse URI and open chat await u.routeToQueryAction(); // Verify that the URL was cleaned up (protocol handler removes ?uri=...) - expect(replaceStateSpy).toHaveBeenCalledWith( - {}, - document.title, - 'http://localhost/' - ); + const expected_url = `${window.location.origin}${window.location.pathname}`; + expect(replaceStateSpy).toHaveBeenCalledWith({}, document.title, expected_url); // Wait for and verify that a chatbox was created await u.waitUntil(() => _converse.chatboxes.get('romeo@montague.lit')); @@ -53,11 +41,7 @@ describe("XMPP URI Query Actions (XEP-0147)", function () { expect(chatbox.get('jid')).toBe('romeo@montague.lit'); } finally { // Restore original globals to avoid test pollution - if (originalLocationDescriptor) { - Object.defineProperty(window, 'location', originalLocationDescriptor); - } else { - delete window.location; - } + window.location.hash = originalHash; window.history.replaceState = originalReplaceState; } })); @@ -66,36 +50,26 @@ describe("XMPP URI Query Actions (XEP-0147)", function () { * Test message sending functionality when action=message * This tests URI parsing, chat opening, and message sending */ - it("sends a message when action=message with body", + fit("sends a message when action=message with body", mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { const { api } = _converse; await mock.waitForRoster(_converse, 'current', 1); - const originalLocationDescriptor = Object.getOwnPropertyDescriptor(window, 'location'); + const originalHash = window.location.hash; const originalReplaceState = window.history.replaceState; - // Mock URI with message action: ?uri=xmpp:romeo@montague.lit?action=message&body=Hello - Object.defineProperty(window, "location", { - value: { - search: '?uri=xmpp%3Aromeo%40montague.lit%3Faction%3Dmessage%26body%3DHello', - hash: '', - origin: 'http://localhost', - pathname: '/', - }, - configurable: true, - }); - window.history.replaceState = jasmine.createSpy('replaceState'); - try { - const { routeToQueryAction } = await import('../utils.js'); + // Mock URI with message action + window.location.hash = '#converse/action?uri=xmpp%3Aromeo%40montague.lit%3Faction%3Dmessage%26body%3DHello'; + try { // Spy on the connection send method to verify XMPP stanza sending spyOn(api.connection.get(), 'send'); // Execute the function - await routeToQueryAction(); + await u.routeToQueryAction(); // Verify chat was opened await u.waitUntil(() => _converse.chatboxes.get('romeo@montague.lit')); @@ -108,11 +82,85 @@ describe("XMPP URI Query Actions (XEP-0147)", function () { expect(message.get('message')).toBe('Hello'); expect(message.get('type')).toBe('chat'); } finally { - if (originalLocationDescriptor) { - Object.defineProperty(window, 'location', originalLocationDescriptor); - } else { - delete window.location; - } + window.location.hash = originalHash; + window.history.replaceState = originalReplaceState; + } + })); + + /** + * Test roster add functionality when action=add-roster + * This tests URI parsing and adding a contact to the roster + */ + fit("adds a contact to roster when action=add-roster", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + + const { api } = _converse; + await mock.waitForRoster(_converse, 'current', 1); + + const originalHash = window.location.hash; + const originalReplaceState = window.history.replaceState; + + window.history.replaceState = jasmine.createSpy('replaceState'); + + // Mock URI with add-roster action: ?uri=xmpp:juliet@capulet.lit?action=add-roster&name=Juliet&group=Friends + window.location.hash = '#converse/action?uri=xmpp%3Ajuliet%40capulet.lit%3Faction%3Dadd-roster%26name%3DJuliet%26group%3DFriends'; + + try { + // Spy on the contacts.add API method - return a resolved promise to avoid network calls + spyOn(api.contacts, 'add').and.returnValue(Promise.resolve()); + + // Execute the function + await u.routeToQueryAction(); + + // Verify that contacts.add was called with correct parameters + expect(api.contacts.add).toHaveBeenCalledWith( + { + jid: 'juliet@capulet.lit', + name: 'Juliet', + groups: ['Friends'] + }, + true, // persist on server + true, // subscribe to presence + '' // no custom message + ); + } finally { + window.location.hash = originalHash; + window.history.replaceState = originalReplaceState; + } + })); + + /** + * Test handling of invalid JIDs + * This ensures the function gracefully handles malformed JIDs + */ + fit("handles invalid JID gracefully", + mock.initConverse(['chatBoxesFetched'], {}, async function (_converse) { + + const { api } = _converse; + await mock.waitForRoster(_converse, 'current', 1); + + const originalHash = window.location.hash; + const originalReplaceState = window.history.replaceState; + + window.history.replaceState = jasmine.createSpy('replaceState'); + + // Mock URI with invalid JID (missing domain) + window.location.hash = '#converse/action?uri=xmpp%3Ainvalid-jid'; + + try { + // Spy on api.chats.open to ensure it's NOT called for invalid JID + spyOn(api.chats, 'open'); + + // Execute the function + await u.routeToQueryAction(); + + // Verify that no chat was opened for the invalid JID + expect(api.chats.open).not.toHaveBeenCalled(); + + // Verify no chatbox was created + expect(_converse.chatboxes.get('invalid-jid')).toBeUndefined(); + } finally { + window.location.hash = originalHash; window.history.replaceState = originalReplaceState; } })); diff --git a/src/plugins/chatview/utils.js b/src/plugins/chatview/utils.js index c2eab85819..e33082e24e 100644 --- a/src/plugins/chatview/utils.js +++ b/src/plugins/chatview/utils.js @@ -90,12 +90,12 @@ export async function routeToQueryAction(event) { const uri = extractXMPPURI(event); if (!uri) return; - const { jid, queryParams } = parseXMPPURI(uri); + const { jid, query_params } = parseXMPPURI(uri); if (!u.isValidJID(jid)) { return log.warn(`routeToQueryAction: Invalid JID: "${jid}"`); } - const action = queryParams?.get('action'); + const action = query_params?.get('action'); if (!action) { log.debug(`routeToQueryAction: No action specified, opening chat for "${jid}"`); return api.chats.open(jid); @@ -103,11 +103,11 @@ export async function routeToQueryAction(event) { switch (action) { case 'message': - await handleMessageAction(jid, queryParams); + await handleMessageAction(jid, query_params); break; case 'add-roster': - await handleRosterAction(jid, queryParams); + await handleRosterAction(jid, query_params); break; default: @@ -168,8 +168,6 @@ async function handleMessageAction(jid, params) { * Handles the `action=add-roster` case. */ async function handleRosterAction(jid, params) { - await api.waitUntil('rosterContactsFetched'); - const name = params.get('name') || jid.split('@')[0]; const group = params.get('group'); const groups = group ? [group] : []; From cd88326f7f99a32c6249f70e3a5d86b05cb55ae7 Mon Sep 17 00:00:00 2001 From: marcellintacite Date: Mon, 10 Nov 2025 12:42:50 +0300 Subject: [PATCH 16/16] chore: remove genkit metadata --- .genkit/traces_idx/genkit.metadata | 1 - dev.html | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 .genkit/traces_idx/genkit.metadata diff --git a/.genkit/traces_idx/genkit.metadata b/.genkit/traces_idx/genkit.metadata deleted file mode 100644 index d297703995..0000000000 --- a/.genkit/traces_idx/genkit.metadata +++ /dev/null @@ -1 +0,0 @@ -{"version":"1.21.0"} \ No newline at end of file diff --git a/dev.html b/dev.html index 6c4fe64e24..c38f584e8c 100644 --- a/dev.html +++ b/dev.html @@ -26,7 +26,7 @@ }); converse.initialize({ - i18n: 'fr', + i18n: 'af', theme: 'nordic', dark_theme: 'dracula', auto_away: 300, @@ -37,7 +37,7 @@ notify_all_room_messages: ['discuss@conference.conversejs.org'], fetch_url_headers: true, whitelisted_plugins: ['converse-debug'], - bosh_service_url: 'http://localhost:5280/http-bind', // Please use this connection manager only for testing purposes + bosh_service_url: 'https://conversejs.org/http-bind/', // Please use this connection manager only for testing purposes // view_mode: 'overlayed', show_controlbox_by_default: true, // websocket_url: 'wss://conversejs.org/xmpp-websocket',