11import { __ } from 'i18n' ;
22import { _converse , api } from '@converse/headless' ;
3+ import log from "@converse/log" ;
4+
35
46export 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