Skip to content

Commit 87118db

Browse files
committed
feat: add support for XEP-0092 to show server version
1 parent 63553c4 commit 87118db

16 files changed

Lines changed: 293 additions & 2 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## 14.0.0 (Unreleased)
44

5+
- feat(version): Add XEP-0092 Software Version support and show the server's software version in the profile modal
56
- fix(omemo): detect omemo:2-only contacts as OMEMO-capable
67

78
## 14.0.0-beta.4 (2026-06-22)

conversejs.doap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@
8787
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0085.html"/>
8888
</xmpp:SupportedXep>
8989
</implements>
90+
<implements>
91+
<xmpp:SupportedXep>
92+
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0092.html"/>
93+
</xmpp:SupportedXep>
94+
</implements>
9095
<implements>
9196
<xmpp:SupportedXep>
9297
<xmpp:xep rdf:resource="https://xmpp.org/extensions/xep-0115.html"/>

src/headless/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export { MUCMessage, MUCMessages, MUC, MUCOccupant, MUCOccupants } from './plugi
3636
import './plugins/ping/index.js'; // XEP-0199 XMPP Ping
3737
import './plugins/pubsub/index.js'; // XEP-0060 Pubsub
3838
import './plugins/reactions/index.js'; // XEP-0444 Message Reactions
39+
import './plugins/version/index.js'; // XEP-0092 Software Version
3940

4041
// RFC-6121 Contacts Roster
4142
export { RosterContact, RosterContacts, RosterFilter, Presence, Presences } from './plugins/roster/index.js';
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @copyright The Converse.js contributors
3+
* @license Mozilla Public License (MPLv2)
4+
*/
5+
import _converse from '../../shared/_converse.js';
6+
import api from '../../shared/api/index.js';
7+
import converse from '../../shared/api/public.js';
8+
9+
const { Strophe, u, stx } = converse.env;
10+
11+
export default {
12+
/**
13+
* The XEP-0092 Software Version API
14+
*
15+
* This API lets you query an XMPP entity (such as your own server) for the
16+
* name, version and operating system of the software it's running.
17+
*
18+
* @namespace api.version
19+
* @memberOf api
20+
*/
21+
version: {
22+
/**
23+
* Queries an XMPP entity for its software version, as specified in
24+
* XEP-0092: Software Version.
25+
*
26+
* @method api.version.get
27+
* @param {string} [jid] The JID of the entity to query. If not provided,
28+
* the user's own server (the domain part of their JID) is queried.
29+
* @param {number} [timeout] The amount of time in milliseconds to wait
30+
* for a response.
31+
* @returns {Promise<import('./types').SoftwareVersion|null>} A promise
32+
* which resolves with the software version details, or with `null` if
33+
* the entity didn't respond or doesn't support XEP-0092.
34+
* @example
35+
* const version = await api.version.get();
36+
* // { name: 'Prosody', version: '0.12.0', os: 'Debian GNU/Linux' }
37+
*/
38+
async get(jid, timeout) {
39+
if (!api.connection.authenticated()) {
40+
return null;
41+
}
42+
43+
const bare_jid = _converse.session.get('bare_jid');
44+
jid = jid || Strophe.getDomainFromJid(bare_jid);
45+
46+
const iq = stx`
47+
<iq type="get" to="${jid}" xmlns="jabber:client">
48+
<query xmlns="${Strophe.NS.VERSION}"></query>
49+
</iq>`;
50+
51+
const result = await api.sendIQ(iq, timeout, false);
52+
if (result === null || u.isErrorStanza(result)) {
53+
return null;
54+
}
55+
56+
const query = result.querySelector(':scope > query');
57+
if (!query) {
58+
return null;
59+
}
60+
61+
return {
62+
name: query.querySelector(':scope > name')?.textContent || null,
63+
version: query.querySelector(':scope > version')?.textContent || null,
64+
os: query.querySelector(':scope > os')?.textContent || null,
65+
};
66+
},
67+
},
68+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @description
3+
* Converse.js plugin which adds support for querying the software version of
4+
* an XMPP entity, as specified in XEP-0092: Software Version.
5+
* @copyright The Converse.js contributors
6+
* @license Mozilla Public License (MPLv2)
7+
*/
8+
import api from '../../shared/api/index.js';
9+
import converse from '../../shared/api/public.js';
10+
import version_api from './api.js';
11+
12+
// Strophe already defines `Strophe.NS.VERSION` as 'jabber:iq:version'.
13+
14+
converse.plugins.add('converse-version', {
15+
initialize() {
16+
Object.assign(api, version_api);
17+
},
18+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import mock from '../../../tests/mock.js';
2+
import converse from '../../../dist/converse-headless.js';
3+
4+
const { sizzle, stx, u } = converse.env;
5+
6+
describe('XEP-0092 Software Version', function () {
7+
it(
8+
'can be queried via api.version.get()',
9+
mock.initConverse(converse, ['statusInitialized'], {}, async (_converse) => {
10+
const conn = _converse.api.connection.get();
11+
const promise = _converse.api.version.get();
12+
13+
const sent_iq = await u.waitUntil(() =>
14+
conn.IQ_stanzas.filter((iq) => sizzle('query[xmlns="jabber:iq:version"]', iq).length).pop()
15+
);
16+
expect(sent_iq).toEqualStanza(stx`
17+
<iq id="${sent_iq.getAttribute('id')}" to="montague.lit" type="get" xmlns="jabber:client">
18+
<query xmlns="jabber:iq:version"/>
19+
</iq>`);
20+
21+
const result = stx`
22+
<iq from="montague.lit" to="${_converse.jid}" id="${sent_iq.getAttribute('id')}" type="result" xmlns="jabber:client">
23+
<query xmlns="jabber:iq:version">
24+
<name>Prosody</name>
25+
<version>0.12.0</version>
26+
<os>Debian GNU/Linux</os>
27+
</query>
28+
</iq>`;
29+
conn._dataRecv(mock.createRequest(_converse, result));
30+
31+
const version = await promise;
32+
expect(version).toEqual({ name: 'Prosody', version: '0.12.0', os: 'Debian GNU/Linux' });
33+
})
34+
);
35+
36+
it(
37+
'returns null when the entity responds with an error',
38+
mock.initConverse(converse, ['statusInitialized'], {}, async (_converse) => {
39+
const conn = _converse.api.connection.get();
40+
const promise = _converse.api.version.get('shakespeare.lit');
41+
42+
const sent_iq = await u.waitUntil(() =>
43+
conn.IQ_stanzas
44+
.filter((iq) => iq.getAttribute('to') === 'shakespeare.lit')
45+
.filter((iq) => sizzle('query[xmlns="jabber:iq:version"]', iq).length)
46+
.pop()
47+
);
48+
49+
const error = stx`
50+
<iq from="shakespeare.lit" to="${_converse.jid}" id="${sent_iq.getAttribute('id')}" type="error" xmlns="jabber:client">
51+
<query xmlns="jabber:iq:version"/>
52+
<error type="cancel">
53+
<service-unavailable xmlns="urn:ietf:params:xml:ns:xmpp-stanzas"/>
54+
</error>
55+
</iq>`;
56+
conn._dataRecv(mock.createRequest(_converse, error));
57+
58+
expect(await promise).toBe(null);
59+
})
60+
);
61+
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export type SoftwareVersion = {
2+
name: string | null;
3+
version: string | null;
4+
os: string | null;
5+
};

src/headless/shared/constants.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export const CORE_PLUGINS = [
145145
'converse-smacks',
146146
'converse-status',
147147
'converse-vcard',
148+
'converse-version',
148149
'converse-omemo',
149150
];
150151

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
declare namespace _default {
2+
namespace version {
3+
/**
4+
* Queries an XMPP entity for its software version, as specified in
5+
* XEP-0092: Software Version.
6+
*
7+
* @method api.version.get
8+
* @param {string} [jid] The JID of the entity to query. If not provided,
9+
* the user's own server (the domain part of their JID) is queried.
10+
* @param {number} [timeout] The amount of time in milliseconds to wait
11+
* for a response.
12+
* @returns {Promise<import('./types').SoftwareVersion|null>} A promise
13+
* which resolves with the software version details, or with `null` if
14+
* the entity didn't respond or doesn't support XEP-0092.
15+
* @example
16+
* const version = await api.version.get();
17+
* // { name: 'Prosody', version: '0.12.0', os: 'Debian GNU/Linux' }
18+
*/
19+
function get(jid?: string, timeout?: number): Promise<import("./types").SoftwareVersion | null>;
20+
}
21+
}
22+
export default _default;
23+
//# sourceMappingURL=api.d.ts.map
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export {};
2+
//# sourceMappingURL=index.d.ts.map

0 commit comments

Comments
 (0)