Skip to content

Commit 5b41936

Browse files
Adding support for the XEP-0147 (Protocol handler)
1 parent 9c5f20d commit 5b41936

File tree

3 files changed

+129
-17
lines changed

3 files changed

+129
-17
lines changed

src/headless/plugins/chat/utils.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,7 @@ export function routeToChat (event) {
2222
return;
2323
}
2424
event?.preventDefault();
25-
let jid = location.hash.split('=').pop();
26-
// decodeURIComponent is needed in case the JID contains special characters
27-
// that were URL-encoded, e.g. `user%40domain` instead of `user@domain`.
28-
jid = decodeURIComponent(jid);
29-
30-
// Remove xmpp: prefix if present
31-
if (jid.startsWith('xmpp:')) {
32-
jid = jid.slice(5);
33-
}
34-
25+
const jid = location.hash.split('=').pop();
3526
if (!u.isValidJID(jid)) {
3627
return log.warn(`Invalid JID "${jid}" provided in URL fragment`);
3728
}

src/plugins/chatview/index.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import 'shared/chat/help-messages.js';
88
import 'shared/chat/toolbar.js';
99
import ChatView from './chat.js';
1010
import { _converse, api, converse } from '@converse/headless';
11-
import { clearHistory } from './utils.js';
11+
import { clearHistory,routeToQueryAction } from './utils.js';
1212

1313
import './styles/index.scss';
1414

@@ -66,13 +66,14 @@ converse.plugins.add('converse-chatview', {
6666
Object.assign(_converse.exports, exports);
6767

6868
if ('registerProtocolHandler' in navigator) {
69-
try {
70-
const handlerUrl = `${window.location.origin}${window.location.pathname}#converse/chat?jid=%s`;
71-
navigator.registerProtocolHandler('xmpp', handlerUrl);
72-
} catch (error) {
73-
console.warn('Failed to register protocol handler:', error);
69+
try {
70+
const handlerUrl = `${window.location.origin}${window.location.pathname}#converse/action?uri=%s`;
71+
navigator.registerProtocolHandler('xmpp', handlerUrl);
72+
} catch (error) {
73+
console.warn('Failed to register protocol handler:', error);
74+
}
7475
}
75-
}
76+
routeToQueryAction();
7677
api.listen.on('connected', () => api.disco.own.features.add(Strophe.NS.SPOILER));
7778
api.listen.on('chatBoxClosed', (model) => clearHistory(model.get('jid')));
7879
}

src/plugins/chatview/utils.js

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { __ } from 'i18n';
22
import { _converse, api } from '@converse/headless';
3+
import log from "@converse/log";
4+
35

46
export function clearHistory (jid) {
57
if (location.hash === `converse/chat?jid=${jid}`) {
@@ -71,3 +73,121 @@ export function resetElementHeight (ev) {
7173
ev.target.style = '';
7274
}
7375
}
76+
77+
78+
/**
79+
* Handle XEP-0147 "query actions" invoked via xmpp: URIs.
80+
* Supports message sending, roster management, and future actions.
81+
*
82+
* Example URIs:
83+
* xmpp:user@example.com?action=message&body=Hello
84+
* xmpp:user@example.com?action=add-roster&name=John&group=Friends
85+
*/
86+
export async function routeToQueryAction(event) {
87+
const { u } = _converse.env;
88+
89+
try {
90+
const uri = extractXMPPURI(event);
91+
if (!uri) return;
92+
93+
const { jid, queryParams } = parseXMPPURI(uri);
94+
if (!u.isValidJID(jid)) {
95+
return log.warn(`Invalid JID: "${jid}"`);
96+
}
97+
98+
const action = queryParams.get('action');
99+
if (!action) {
100+
log.debug(`No action specified, opening chat for "${jid}"`);
101+
return api.chats.open(jid);
102+
}
103+
104+
switch (action) {
105+
case 'message':
106+
await handleMessageAction(jid, queryParams);
107+
break;
108+
109+
case 'add-roster':
110+
await handleRosterAction(jid, queryParams);
111+
break;
112+
113+
default:
114+
log.warn(`Unsupported XEP-0147 action: "${action}"`);
115+
await api.chats.open(jid);
116+
}
117+
} catch (error) {
118+
log.error('Failed to process XMPP query action:', error);
119+
}
120+
}
121+
122+
/**
123+
* Extracts and decodes the xmpp: URI from the window location or hash.
124+
*/
125+
function extractXMPPURI(event) {
126+
let uri = null;
127+
128+
// Case 1: protocol handler (?uri=...)
129+
const searchParams = new URLSearchParams(window.location.search);
130+
uri = searchParams.get('uri');
131+
132+
// Case 2: hash-based (#converse/action?uri=...)
133+
if (!uri && location.hash.startsWith('#converse/action?uri=')) {
134+
event?.preventDefault();
135+
uri = location.hash.split('uri=').pop();
136+
}
137+
138+
if (!uri) return null;
139+
140+
// Decode URI and remove xmpp: prefix
141+
uri = decodeURIComponent(uri);
142+
if (uri.startsWith('xmpp:')) uri = uri.slice(5);
143+
144+
// Clean up URL (remove ?uri=... for a clean view)
145+
const cleanUrl = `${window.location.origin}${window.location.pathname}`;
146+
window.history.replaceState({}, document.title, cleanUrl);
147+
148+
return uri;
149+
}
150+
151+
/**
152+
* Splits an xmpp: URI into a JID and query parameters.
153+
*/
154+
function parseXMPPURI(uri) {
155+
const [jid, query] = uri.split('?');
156+
const queryParams = new URLSearchParams(query);
157+
return { jid, queryParams };
158+
}
159+
160+
/**
161+
* Handles the `action=message` case.
162+
*/
163+
async function handleMessageAction(jid, params) {
164+
const body = params.get('body') || '';
165+
const chat = await api.chats.open(jid);
166+
167+
if (body && chat) {
168+
await chat.sendMessage({ body });
169+
}
170+
}
171+
172+
/**
173+
* Handles the `action=add-roster` case.
174+
*/
175+
async function handleRosterAction(jid, params) {
176+
await api.waitUntil('connected');
177+
await api.waitUntil('rosterContactsFetched');
178+
179+
const name = params.get('name') || jid.split('@')[0];
180+
const group = params.get('group');
181+
const groups = group ? [group] : [];
182+
183+
try {
184+
await api.contacts.add(
185+
{ jid, name, groups },
186+
true, // persist on server
187+
true, // subscribe to presence
188+
'' // no custom message
189+
);
190+
} catch (err) {
191+
log.error(`Failed to add "${jid}" to roster:`, err);
192+
}
193+
}

0 commit comments

Comments
 (0)