Quellcode durchsuchen

Use stx and add types

JC Brand vor 4 Monaten
Ursprung
Commit
1630f58663

+ 1 - 2
src/headless/plugins/muc/types.ts

@@ -70,8 +70,7 @@ export type MUCMessageAttributes = MessageAttributes & ExtraMUCAttributes;
 export type MUCAffiliation = 'owner'|'admin'|'member'|'outcast'|'none';
 export type MUCRole = 'moderator'|'participant'|'visitor'|'none';
 
-
- export type NonOutcastAffiliation = 'admin' | 'owner' | 'member';
+export type NonOutcastAffiliation = 'admin' | 'owner' | 'member';
 
 /**
  * Either the JID or the nickname (or both) will be available.

+ 90 - 81
src/headless/shared/types.ts

@@ -1,4 +1,4 @@
-import { Collection, Model } from '@converse/skeletor';
+import { Collection, Model } from "@converse/skeletor";
 
 export type ModelAttributes = Record<string, any>;
 
@@ -24,6 +24,7 @@ type Constructor<T = {}> = new (...args: any[]) => T;
 export type ModelExtender = Constructor<Model>;
 
 type EncryptionPayloadAttrs = {
+    key?: string;
     prekey?: boolean;
     device_id: string;
 };
@@ -34,7 +35,7 @@ export type RetractionAttrs = {
     retracted: string;
     retracted_id?: string; // ID of the message being retracted
     retraction_id?: string; // ID of the retraction message
-}
+};
 
 export type EncryptionAttrs = {
     encrypted?: EncryptionPayloadAttrs; //  XEP-0384 encryption payload attributes
@@ -64,19 +65,19 @@ export type XFormCaptchaURI = {
     data: string;
 };
 
-type XFormListTypes = 'list-single' | 'list-multi';
-type XFormJIDTypes = 'jid-single' | 'jid-multi';
-type XFormTextTypes = 'text-multi' | 'text-private' | 'text-single';
-type XFormDateTypes = 'date' | 'datetime';
+type XFormListTypes = "list-single" | "list-multi";
+type XFormJIDTypes = "jid-single" | "jid-multi";
+type XFormTextTypes = "text-multi" | "text-private" | "text-single";
+type XFormDateTypes = "date" | "datetime";
 type XFormFieldTypes =
     | XFormListTypes
     | XFormJIDTypes
     | XFormTextTypes
     | XFormDateTypes
-    | 'fixed'
-    | 'boolean'
-    | 'url'
-    | 'hidden';
+    | "fixed"
+    | "boolean"
+    | "url"
+    | "hidden";
 
 export type XFormField = {
     var: string;
@@ -91,7 +92,7 @@ export type XFormField = {
     readonly: boolean;
 };
 
-export type XFormResponseType = 'result' | 'form';
+export type XFormResponseType = "result" | "form";
 
 export type XForm = {
     type: XFormResponseType;
@@ -115,30 +116,30 @@ export type ErrorExtra = Record<string, string>;
 
 // https://datatracker.ietf.org/doc/html/rfc6120#section-8.3
 export type ErrorName =
-    | 'bad-request'
-    | 'conflict'
-    | 'feature-not-implemented'
-    | 'forbidden'
-    | 'gone'
-    | 'internal-server-error'
-    | 'item-not-found'
-    | 'jid-malformed'
-    | 'not-acceptable'
-    | 'not-allowed'
-    | 'not-authorized'
-    | 'payment-required'
-    | 'recipient-unavailable'
-    | 'redirect'
-    | 'registration-required'
-    | 'remote-server-not-found'
-    | 'remote-server-timeout'
-    | 'resource-constraint'
-    | 'service-unavailable'
-    | 'subscription-required'
-    | 'undefined-condition'
-    | 'unexpected-request';
-
-export type ErrorType = 'auth' | 'cancel' | 'continue' | 'modify' | 'wait';
+    | "bad-request"
+    | "conflict"
+    | "feature-not-implemented"
+    | "forbidden"
+    | "gone"
+    | "internal-server-error"
+    | "item-not-found"
+    | "jid-malformed"
+    | "not-acceptable"
+    | "not-allowed"
+    | "not-authorized"
+    | "payment-required"
+    | "recipient-unavailable"
+    | "redirect"
+    | "registration-required"
+    | "remote-server-not-found"
+    | "remote-server-timeout"
+    | "resource-constraint"
+    | "service-unavailable"
+    | "subscription-required"
+    | "undefined-condition"
+    | "unexpected-request";
+
+export type ErrorType = "auth" | "cancel" | "continue" | "modify" | "wait";
 
 // Represents a XEP-0372 reference
 export type Reference = {
@@ -146,7 +147,7 @@ export type Reference = {
     end: number;
     type: string;
     uri: string;
-}
+};
 
 export type MessageErrorAttributes = {
     is_error: boolean; // Whether an error was received for this message
@@ -155,50 +156,58 @@ export type MessageErrorAttributes = {
     error_condition: string; // The defined error condition
     error_text: string; // The error text received from the server
     error_type: string; // The type of error received from the server
-}
+};
 
-export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & {
-    body: string; // The contents of the <body> tag of the message stanza
-    chat_state: string; // The XEP-0085 chat state notification contained in this message
-    contact_jid: string; // The JID of the other person or entity
-    editable: boolean; // Is this message editable via XEP-0308?
-    edited: string; // An ISO8601 string recording the time that the message was edited per XEP-0308
-    from: string; // The sender JID
-    message?: string; // Used with info and error messages
-    fullname: string; // The full name of the sender
-    is_archived: boolean; //  Is this message from a XEP-0313 MAM archive?
-    is_carbon: boolean; // Is this message a XEP-0280 Carbon?
-    is_delayed: boolean; // Was delivery of this message was delayed as per XEP-0203?
-    is_encrypted: boolean; //  Is this message XEP-0384  encrypted?
-    is_headline: boolean; // Is this a "headline" message?
-    is_markable: boolean; // Can this message be marked with a XEP-0333 chat marker?
-    is_marker: boolean; // Is this message a XEP-0333 Chat Marker?
-    is_only_emojis: boolean; // Does the message body contain only emojis?
-    is_spoiler: boolean; // Is this a XEP-0382 spoiler message?
-    is_tombstone: boolean; // Is this a XEP-0424 tombstone?
-    is_unstyled: boolean; // Whether XEP-0393 styling hints should be ignored
-    is_valid_receipt_request: boolean; // Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
-    marker: string; // The XEP-0333 Chat Marker value
-    marker_id: string; // The `id` attribute of a XEP-0333 chat marker
-    msgid: string; // The root `id` attribute of the stanza
-    nick: string; // The roster nickname of the sender
-    ogp_for_id?: string; // Used for Open Graph Metadata support for unfurls
-    oob_desc: string; // The description of the XEP-0066 out of band data
-    oob_url: string; // The URL of the XEP-0066 out of band data
-    origin_id: string; // The XEP-0359 Origin ID
-    plaintext: string; // The decrypted text of this message, in case it was encrypted.
-    receipt_id: string; // The `id` attribute of a XEP-0184 <receipt> element
-    received: string; // An ISO8601 string recording the time that the message was received
-    references: Array<Reference>; // A list of objects representing XEP-0372 references
-    replace_id: string; // The `id` attribute of a XEP-0308 <replace> element
-    retracted: string; // An ISO8601 string recording the time that the message was retracted
-    retracted_id: string; // The `id` attribute of a XEP-424 <retracted> element
-    sender: 'me' | 'them'; // Whether the message was sent by the current user or someone else
-    spoiler_hint: string; //  The XEP-0382 spoiler hint
-    stanza_id: string; // The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
-    subject: string; // The <subject> element value
-    thread: string; // The <thread> element value
-    time: string; // The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
-    to: string; // The recipient JID
-    type: string; // The type of message
+export type MessageAttributes = EncryptionAttrs &
+    MessageErrorAttributes & {
+        body: string; // The contents of the <body> tag of the message stanza
+        chat_state: string; // The XEP-0085 chat state notification contained in this message
+        contact_jid: string; // The JID of the other person or entity
+        editable: boolean; // Is this message editable via XEP-0308?
+        edited: string; // An ISO8601 string recording the time that the message was edited per XEP-0308
+        from: string; // The sender JID
+        message?: string; // Used with info and error messages
+        fullname: string; // The full name of the sender
+        is_archived: boolean; //  Is this message from a XEP-0313 MAM archive?
+        is_carbon: boolean; // Is this message a XEP-0280 Carbon?
+        is_delayed: boolean; // Was delivery of this message was delayed as per XEP-0203?
+        is_encrypted: boolean; //  Is this message XEP-0384  encrypted?
+        is_headline: boolean; // Is this a "headline" message?
+        is_markable: boolean; // Can this message be marked with a XEP-0333 chat marker?
+        is_marker: boolean; // Is this message a XEP-0333 Chat Marker?
+        is_only_emojis: boolean; // Does the message body contain only emojis?
+        is_spoiler: boolean; // Is this a XEP-0382 spoiler message?
+        is_tombstone: boolean; // Is this a XEP-0424 tombstone?
+        is_unstyled: boolean; // Whether XEP-0393 styling hints should be ignored
+        is_valid_receipt_request: boolean; // Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message)
+        marker: string; // The XEP-0333 Chat Marker value
+        marker_id: string; // The `id` attribute of a XEP-0333 chat marker
+        msgid: string; // The root `id` attribute of the stanza
+        nick: string; // The roster nickname of the sender
+        ogp_for_id?: string; // Used for Open Graph Metadata support for unfurls
+        oob_desc: string; // The description of the XEP-0066 out of band data
+        oob_url: string; // The URL of the XEP-0066 out of band data
+        origin_id: string; // The XEP-0359 Origin ID
+        plaintext: string; // The decrypted text of this message, in case it was encrypted.
+        receipt_id: string; // The `id` attribute of a XEP-0184 <receipt> element
+        received: string; // An ISO8601 string recording the time that the message was received
+        references: Array<Reference>; // A list of objects representing XEP-0372 references
+        replace_id: string; // The `id` attribute of a XEP-0308 <replace> element
+        retracted: string; // An ISO8601 string recording the time that the message was retracted
+        retracted_id: string; // The `id` attribute of a XEP-424 <retracted> element
+        sender: "me" | "them"; // Whether the message was sent by the current user or someone else
+        spoiler_hint: string; //  The XEP-0382 spoiler hint
+        stanza_id: string; // The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple.
+        subject: string; // The <subject> element value
+        thread: string; // The <thread> element value
+        time: string; // The time (in ISO8601 format), either given by the XEP-0203 <delay> element, or of receipt.
+        to: string; // The recipient JID
+        type: string; // The type of message
+    };
+
+export type FileUploadMessageAttributes = {
+    body: string;
+    message: string;
+    oob_url: string;
+    upload: 'success' | 'failure';
 };

+ 1 - 1
src/headless/types/shared/connection/index.d.ts

@@ -98,7 +98,6 @@ export class Connection extends Connection_base {
 }
 /**
  * The MockConnection class is used during testing, to mock an XMPP connection.
- * @class
  */
 export class MockConnection extends Connection {
     /**
@@ -110,6 +109,7 @@ export class MockConnection extends Connection {
     IQ_stanzas: any[];
     IQ_ids: any[];
     mock: boolean;
+    get _sasl_mechanism(): import("strophe.js/src/types/sasl-sha256.js").default;
     _processRequest(): void;
     sendIQ(iq: any, callback: any, errback: any): string;
     send(stanza: any): void;

+ 17 - 10
src/headless/types/shared/types.d.ts

@@ -1,4 +1,4 @@
-import { Collection, Model } from '@converse/skeletor';
+import { Collection, Model } from "@converse/skeletor";
 export type ModelAttributes = Record<string, any>;
 export interface ModelOptions {
     collection?: Collection;
@@ -14,6 +14,7 @@ export type RSMResult = {
 type Constructor<T = {}> = new (...args: any[]) => T;
 export type ModelExtender = Constructor<Model>;
 type EncryptionPayloadAttrs = {
+    key?: string;
     prekey?: boolean;
     device_id: string;
 };
@@ -47,11 +48,11 @@ export type XFormCaptchaURI = {
     type: string;
     data: string;
 };
-type XFormListTypes = 'list-single' | 'list-multi';
-type XFormJIDTypes = 'jid-single' | 'jid-multi';
-type XFormTextTypes = 'text-multi' | 'text-private' | 'text-single';
-type XFormDateTypes = 'date' | 'datetime';
-type XFormFieldTypes = XFormListTypes | XFormJIDTypes | XFormTextTypes | XFormDateTypes | 'fixed' | 'boolean' | 'url' | 'hidden';
+type XFormListTypes = "list-single" | "list-multi";
+type XFormJIDTypes = "jid-single" | "jid-multi";
+type XFormTextTypes = "text-multi" | "text-private" | "text-single";
+type XFormDateTypes = "date" | "datetime";
+type XFormFieldTypes = XFormListTypes | XFormJIDTypes | XFormTextTypes | XFormDateTypes | "fixed" | "boolean" | "url" | "hidden";
 export type XFormField = {
     var: string;
     label: string;
@@ -64,7 +65,7 @@ export type XFormField = {
     uri?: XFormCaptchaURI;
     readonly: boolean;
 };
-export type XFormResponseType = 'result' | 'form';
+export type XFormResponseType = "result" | "form";
 export type XForm = {
     type: XFormResponseType;
     title?: string;
@@ -81,8 +82,8 @@ export type XEP372Reference = {
     uri: string;
 };
 export type ErrorExtra = Record<string, string>;
-export type ErrorName = 'bad-request' | 'conflict' | 'feature-not-implemented' | 'forbidden' | 'gone' | 'internal-server-error' | 'item-not-found' | 'jid-malformed' | 'not-acceptable' | 'not-allowed' | 'not-authorized' | 'payment-required' | 'recipient-unavailable' | 'redirect' | 'registration-required' | 'remote-server-not-found' | 'remote-server-timeout' | 'resource-constraint' | 'service-unavailable' | 'subscription-required' | 'undefined-condition' | 'unexpected-request';
-export type ErrorType = 'auth' | 'cancel' | 'continue' | 'modify' | 'wait';
+export type ErrorName = "bad-request" | "conflict" | "feature-not-implemented" | "forbidden" | "gone" | "internal-server-error" | "item-not-found" | "jid-malformed" | "not-acceptable" | "not-allowed" | "not-authorized" | "payment-required" | "recipient-unavailable" | "redirect" | "registration-required" | "remote-server-not-found" | "remote-server-timeout" | "resource-constraint" | "service-unavailable" | "subscription-required" | "undefined-condition" | "unexpected-request";
+export type ErrorType = "auth" | "cancel" | "continue" | "modify" | "wait";
 export type Reference = {
     begin: number;
     end: number;
@@ -136,7 +137,7 @@ export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & {
     replace_id: string;
     retracted: string;
     retracted_id: string;
-    sender: 'me' | 'them';
+    sender: "me" | "them";
     spoiler_hint: string;
     stanza_id: string;
     subject: string;
@@ -145,5 +146,11 @@ export type MessageAttributes = EncryptionAttrs & MessageErrorAttributes & {
     to: string;
     type: string;
 };
+export type FileUploadMessageAttributes = {
+    body: string;
+    message: string;
+    oob_url: string;
+    upload: 'success' | 'failure';
+};
 export {};
 //# sourceMappingURL=types.d.ts.map

+ 23 - 17
src/plugins/omemo/device.js

@@ -1,37 +1,43 @@
-import { IQError } from 'shared/errors.js';
 import { Model } from '@converse/skeletor';
-import { UNDECIDED } from './consts.js';
 import { _converse, api, converse, log, u } from '@converse/headless';
+import { IQError } from 'shared/errors.js';
+import { UNDECIDED } from './consts.js';
 import { parseBundle } from './utils.js';
 
-const { Strophe, sizzle, $iq } = converse.env;
+const { Strophe, sizzle, stx } = converse.env;
 
-/**
- * @namespace _converse.Device
- * @memberOf _converse
- */
 class Device extends Model {
-    defaults () { // eslint-disable-line class-methods-use-this
+    defaults () {
         return {
-            'trusted': UNDECIDED,
-            'active': true
+            trusted: UNDECIDED,
+            active: true
         }
     }
 
+    /**
+     * @returns {import('./types').PreKey}
+     */
     getRandomPreKey () {
         // XXX: assumes that the bundle has already been fetched
         const bundle = this.get('bundle');
         return bundle.prekeys[u.getRandomInt(bundle.prekeys.length)];
     }
 
+    /**
+     * Fetch the device's OMEMO bundle from the server.
+     * A bundle is a collection of publicly accessible data that can
+     * be used to build a session with a device, namely its public IdentityKey,
+     * a signed PreKey with corresponding signature, and a list of (single use) PreKeys.
+     * @returns {Promise<import('./types').Bundle>}
+     */
     async fetchBundleFromServer () {
         const bare_jid = _converse.session.get('bare_jid');
-        const stanza = $iq({
-            'type': 'get',
-            'from': bare_jid,
-            'to': this.get('jid')
-        }).c('pubsub', { 'xmlns': Strophe.NS.PUBSUB })
-          .c('items', { 'node': `${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}` });
+        const stanza = stx`
+            <iq type="get" from="${bare_jid}" to="${this.get('jid')}" xmlns="jabber:client">
+                <pubsub xmlns="${Strophe.NS.PUBSUB}">
+                    <items node="${Strophe.NS.OMEMO_BUNDLES}:${this.get('id')}"/>
+                </pubsub>
+            </iq>`;
 
         let iq;
         try {
@@ -54,7 +60,7 @@ class Device extends Model {
     /**
      * Fetch and save the bundle information associated with
      * this device, if the information is not cached already.
-     * @method _converse.Device#getBundle
+     * @returns {Promise<import('./types').Bundle>}
      */
     getBundle () {
         if (this.get('bundle')) {

+ 34 - 22
src/plugins/omemo/devicelist.js

@@ -1,13 +1,9 @@
 import { Model } from '@converse/skeletor';
-import { _converse, api, converse, log, u } from '@converse/headless';
 import { getOpenPromise } from '@converse/openpromise';
+import { _converse, api, converse, log, u } from '@converse/headless';
 
-const { Strophe, $build, $iq, sizzle } = converse.env;
+const { Strophe, stx, sizzle } = converse.env;
 
-/**
- * @namespace _converse.DeviceList
- * @memberOf _converse
- */
 class DeviceList extends Model {
     get idAttribute () {
         return 'jid';
@@ -53,8 +49,8 @@ class DeviceList extends Model {
         if (this._devices_promise === undefined) {
             this._devices_promise = new Promise(resolve => {
                 this.devices.fetch({
-                    'success': c => resolve(this.onDevicesFound(c)),
-                    'error': (_, e) => {
+                    success: c => resolve(this.onDevicesFound(c)),
+                    error: (_, e) => {
                         log.error(e);
                         resolve();
                     }
@@ -64,6 +60,9 @@ class DeviceList extends Model {
         return this._devices_promise;
     }
 
+    /**
+     * @returns {Promise<string>}
+     */
     async getOwnDeviceId () {
         const { omemo_store } = _converse.state;
         let device_id = omemo_store.get('device_id');
@@ -75,6 +74,9 @@ class DeviceList extends Model {
         return device_id;
     }
 
+    /**
+     * @param {string[]} device_ids
+     */
     async publishCurrentDevice (device_ids) {
         const bare_jid = _converse.session.get('bare_jid');
         if (this.get('jid') !== bare_jid) {
@@ -85,7 +87,7 @@ class DeviceList extends Model {
         if (!_converse.state.omemo_store) {
             // Happens during tests. The connection gets torn down
             // before publishCurrentDevice has time to finish.
-            log.warn('publishCurrentDevice: omemo_store is not defined, likely a timing issue');
+            log.debug('publishCurrentDevice: omemo_store is not defined, likely a timing issue');
             return;
         }
         if (!device_ids.includes(await this.getOwnDeviceId())) {
@@ -93,34 +95,44 @@ class DeviceList extends Model {
         }
     }
 
+    /**
+     * @returns {Promise<import('./device').default[]>}
+     */
     async fetchDevicesFromServer () {
         const bare_jid = _converse.session.get('bare_jid');
-        const stanza = $iq({
-            'type': 'get',
-            'from': bare_jid,
-            'to': this.get('jid')
-        }).c('pubsub', { 'xmlns': Strophe.NS.PUBSUB })
-          .c('items', { 'node': Strophe.NS.OMEMO_DEVICELIST });
+        const stanza = stx`
+            <iq type='get' from='${bare_jid}' to='${this.get('jid')}'>
+                <pubsub xmlns='${Strophe.NS.PUBSUB}'>
+                    <items node='${Strophe.NS.OMEMO_DEVICELIST}'/>
+                </pubsub>
+            </iq>`;
 
         const iq = await api.sendIQ(stanza);
         const selector = `list[xmlns="${Strophe.NS.OMEMO}"] device`;
         const device_ids = sizzle(selector, iq).map(d => d.getAttribute('id'));
         const jid = this.get('jid');
-        return Promise.all(device_ids.map(id => this.devices.create({ id, jid }, { 'promise': true })));
+        return Promise.all(device_ids.map((id) => this.devices.create({ id, jid }, { promise: true })));
     }
 
     /**
-     * Send an IQ stanza to the current user's "devices" PEP node to
+     * Sends an IQ stanza to the current user's "devices" PEP node to
      * ensure that all devices are published for potential chat partners to see.
-     * See: https://xmpp.org/extensions/xep-0384.html#usecases-announcing
+     * See: https://xmpp.org/extensions/attic/xep-0384-0.3.0.html#usecases-announcing
      */
     publishDevices () {
-        const item = $build('item', { 'id': 'current' }).c('list', { 'xmlns': Strophe.NS.OMEMO });
-        this.devices.filter(d => d.get('active')).forEach(d => item.c('device', { 'id': d.get('id') }).up());
+        const item = stx`
+            <item id='current'>
+                <list xmlns='${Strophe.NS.OMEMO}'>
+                    ${this.devices.filter((d) => d.get('active')).map((d) => stx`<device id='${d.get('id')}'/>`)}
+                </list>
+            </item>`;
         const options = { access_model: 'open' };
         return api.pubsub.publish(null, Strophe.NS.OMEMO_DEVICELIST, item, options, false);
     }
 
+    /**
+     * @param {string[]} device_ids
+     */
     async removeOwnDevices (device_ids) {
         const bare_jid = _converse.session.get('bare_jid');
         if (this.get('jid') !== bare_jid) {
@@ -128,8 +140,8 @@ class DeviceList extends Model {
         }
         await Promise.all(device_ids.map(id => this.devices.get(id)).map(d =>
             new Promise(resolve => d.destroy({
-                'success': resolve,
-                'error': (_, e) => { log.error(e); resolve(); }
+                success: resolve,
+                error: (_, e) => { log.error(e); resolve(); }
             }))
         ));
         return this.publishDevices();

+ 1 - 1
src/plugins/omemo/devices.js

@@ -1,5 +1,5 @@
-import Device from './device.js';
 import { Collection } from '@converse/skeletor';
+import Device from './device.js';
 
 class Devices extends Collection {
 

+ 1 - 2
src/plugins/omemo/index.js

@@ -1,7 +1,6 @@
 /**
  * @copyright The Converse.js contributors
  * @license Mozilla Public License (MPLv2)
- *
  * @module plugins-omemo-index
  * @typedef {Window & globalThis & {libsignal: any} } WindowWithLibsignal
  */
@@ -58,7 +57,7 @@ converse.plugins.add('converse-omemo', {
     dependencies: ['converse-chatview', 'converse-pubsub', 'converse-profile'],
 
     initialize () {
-        api.settings.extend({ 'omemo_default': false });
+        api.settings.extend({ omemo_default: false });
         api.promises.add(['OMEMOInitialized']);
 
         Object.assign(_converse.api, omemo_api);

+ 34 - 0
src/plugins/omemo/types.ts

@@ -0,0 +1,34 @@
+import { MUCMessageAttributes, MessageAttributes } from "./utils";
+
+export type PreKey = {
+    id: number;
+    key: string;
+};
+
+export type Bundle = {
+    identity_key: string;
+    signed_prekey: {
+        id: number;
+        public_key: string;
+        signature: string;
+    };
+    prekeys: PreKey[];
+};
+
+export type EncryptedMessageAttributes = {
+    iv: string;
+    key: string;
+    payload: string | null;
+    prekey: boolean;
+};
+
+export type MUCMessageAttrsWithEncryption = MUCMessageAttributes & EncryptedMessageAttributes;
+export type MessageAttrsWithEncryption = MessageAttributes & EncryptedMessageAttributes;
+
+export type EncryptedMessage = {
+    key: ArrayBuffer;
+    tag: ArrayBuffer;
+    key_and_tag: ArrayBuffer;
+    payload: string;
+    iv: string;
+};

+ 206 - 82
src/plugins/omemo/utils.js

@@ -3,6 +3,7 @@
  * @typedef {import('@converse/headless/shared/types').MessageAttributes} MessageAttributes
  * @typedef {import('@converse/headless/plugins/muc/types').MUCMessageAttributes} MUCMessageAttributes
  * @typedef {import('@converse/headless').ChatBox} ChatBox
+ * @typedef {import('@converse/headless/types/shared/message').default} BaseMessage
  */
 import { html } from 'lit';
 import { __ } from 'i18n';
@@ -35,6 +36,9 @@ const {
     stringToArrayBuffer,
 } = u;
 
+/**
+ * @param {string} fp
+ */
 export function formatFingerprint (fp) {
     fp = fp.replace(/^05/, '');
     for (let i=1; i<8; i++) {
@@ -44,6 +48,9 @@ export function formatFingerprint (fp) {
     return fp;
 }
 
+/**
+ * @param {string} fp
+ */
 export function formatFingerprintForQRCode (fp) {
     const sid = _converse.state.omemo_store.get('device_id');
     const jid = _converse.session.get('bare_jid');
@@ -86,26 +93,38 @@ export function handleMessageSendError (e, chat) {
     throw e;
 }
 
+/**
+ * @param {string} jid
+ */
 export async function contactHasOMEMOSupport (jid) {
     /* Checks whether the contact advertises any OMEMO-compatible devices. */
     const devices = await getDevicesForContact(jid);
     return devices.length > 0;
 }
 
+/**
+ * @param {ChatBox|MUC} chat
+ * @param {MessageAttributes} attrs
+ * @return {MessageAttributes}
+ */
 export function getOutgoingMessageAttributes (chat, attrs) {
     if (chat.get('omemo_active') && attrs.body) {
-        attrs['is_encrypted'] = true;
-        attrs['plaintext'] = attrs.body;
-        attrs['body'] = __(
-            'This is an OMEMO encrypted message which your client doesn’t seem to support. ' +
-            'Find more information on https://conversations.im/omemo'
-        );
+        return {
+            ...attrs,
+            is_encrypted: true,
+            plaintext: attrs.body,
+            body: __(
+                'This is an OMEMO encrypted message which your client doesn’t seem to support. ' +
+                'Find more information on https://conversations.im/omemo'
+            )
+        }
     }
     return attrs;
 }
 
 /**
  * @param {string} plaintext
+ * @returns {Promise<import('./types').EncryptedMessage>}
  */
 async function encryptMessage (plaintext) {
     // The client MUST use fresh, randomly generated key/IV pairs
@@ -120,10 +139,10 @@ async function encryptMessage (plaintext) {
     // https://crypto.stackexchange.com/questions/26783/ciphertext-and-tag-size-and-iv-transmission-with-aes-in-gcm-mode
     const iv = crypto.getRandomValues(new window.Uint8Array(12));
     const key = await crypto.subtle.generateKey(KEY_ALGO, true, ['encrypt', 'decrypt']);
-    const algo = {
-            'name': 'AES-GCM',
-            'iv': iv,
-            'tagLength': TAG_LENGTH
+    const algo = /** @type {AesGcmParams} */{
+            iv,
+            name: 'AES-GCM',
+            tagLength: TAG_LENGTH
         };
     const encrypted = await crypto.subtle.encrypt(algo, key, stringToArrayBuffer(plaintext));
     const length = encrypted.byteLength - ((128 + 7) >> 3);
@@ -131,21 +150,25 @@ async function encryptMessage (plaintext) {
     const tag = encrypted.slice(length);
     const exported_key = await crypto.subtle.exportKey('raw', key);
     return {
-        'key': exported_key,
-        'tag': tag,
-        'key_and_tag': appendArrayBuffer(exported_key, tag),
-        'payload': arrayBufferToBase64(ciphertext),
-        'iv': arrayBufferToBase64(iv)
+        tag,
+        key: exported_key,
+        key_and_tag: appendArrayBuffer(exported_key, tag),
+        payload: arrayBufferToBase64(ciphertext),
+        iv: arrayBufferToBase64(iv)
     };
 }
 
+/**
+ * @param {import('./types').EncryptedMessage} obj
+ * @returns {Promise<string>}
+ */
 async function decryptMessage (obj) {
     const key_obj = await crypto.subtle.importKey('raw', obj.key, KEY_ALGO, true, ['encrypt', 'decrypt']);
     const cipher = appendArrayBuffer(base64ToArrayBuffer(obj.payload), obj.tag);
-    const algo = {
-        'name': 'AES-GCM',
-        'iv': base64ToArrayBuffer(obj.iv),
-        'tagLength': TAG_LENGTH
+    const algo = /** @type {AesGcmParams} */{
+        name: 'AES-GCM',
+        iv: base64ToArrayBuffer(obj.iv),
+        tagLength: TAG_LENGTH
     };
     return arrayBufferToString(await crypto.subtle.decrypt(algo, key_obj, cipher));
 }
@@ -165,24 +188,36 @@ export async function encryptFile (file) {
     return encrypted_file;
 }
 
+/**
+ * @param {import('@converse/headless/types/shared/message').default} message
+ * @param {import('@converse/headless/shared/types').FileUploadMessageAttributes} attrs
+ */
 export function setEncryptedFileURL (message, attrs) {
     const url = attrs.oob_url.replace(/^https?:/, 'aesgcm:') + '#' + message.file.xep454_ivkey;
     return Object.assign(attrs, {
-        'oob_url': null, // Since only the body gets encrypted, we don't set the oob_url
-        'message': url,
-        'body': url
+        oob_url: null, // Since only the body gets encrypted, we don't set the oob_url
+        message: url,
+        body: url
     });
 }
 
+/**
+ * @param {string} iv
+ * @param {string} key
+ * @param {ArrayBuffer} cipher
+ */
 async function decryptFile (iv, key, cipher) {
     const key_obj = await crypto.subtle.importKey('raw', hexToArrayBuffer(key), 'AES-GCM', false, ['decrypt']);
-    const algo = {
-        'name': 'AES-GCM',
-        'iv': hexToArrayBuffer(iv),
+    const algo = /** @type {AesGcmParams} */{
+        name: 'AES-GCM',
+        iv: hexToArrayBuffer(iv),
     };
     return crypto.subtle.decrypt(algo, key_obj, cipher);
 }
 
+/**
+ * @param {string} url
+ */
 async function downloadFile(url) {
     let response;
     try {
@@ -238,9 +273,9 @@ function getTemplateForObjectURL (uri, obj_url, richtext) {
     const file_url = uri.toString();
     if (isImageURL(file_url)) {
         return tplImage({
-            'src': obj_url,
-            'onClick': richtext.onImgClick,
-            'onLoad': richtext.onImgLoad
+            src: obj_url,
+            onClick: richtext.onImgClick,
+            onLoad: richtext.onImgLoad
         });
     } else if (isAudioURL(file_url)) {
         return tplAudio(obj_url);
@@ -252,12 +287,22 @@ function getTemplateForObjectURL (uri, obj_url, richtext) {
 
 }
 
+/**
+ * @param {string} text
+ * @param {number} offset
+ * @param {import('shared/texture/texture.js').Texture} richtext
+ */
 function addEncryptedFiles(text, offset, richtext) {
     const objs = [];
     try {
         const parse_options = { 'start': /\b(aesgcm:\/\/)/gi };
         URI.withinString(
             text,
+            /**
+             * @param {string} url
+             * @param {number} start
+             * @param {number} end
+             */
             (url, start, end) => {
                 objs.push({ url, start, end });
                 return url;
@@ -278,20 +323,29 @@ function addEncryptedFiles(text, offset, richtext) {
     });
 }
 
+/**
+ * @param {import('shared/texture/texture.js').Texture} richtext
+ */
 export function handleEncryptedFiles (richtext) {
     if (!_converse.state.config.get('trusted')) {
         return;
     }
-    richtext.addAnnotations((text, offset) => addEncryptedFiles(text, offset, richtext));
+    richtext.addAnnotations(
+        /**
+         * @param {string} text
+         * @param {number} offset
+         */
+        (text, offset) => addEncryptedFiles(text, offset, richtext));
 }
 
 /**
- * Hook handler for { @link parseMessage } and { @link parseMUCMessage }, which
+ * Hook handler for {@link parseMessage} and {@link parseMUCMessage}, which
  * parses the passed in `message` stanza for OMEMO attributes and then sets
  * them on the attrs object.
- * @param { Element } stanza - The message stanza
- * @param { (MUCMessageAttributes|MessageAttributes) } attrs
- * @returns (MUCMessageAttributes|MessageAttributes)
+ * @param {Element} stanza - The message stanza
+ * @param {MUCMessageAttributes|MessageAttributes} attrs
+ * @returns {Promise<MUCMessageAttributes| MessageAttributes|
+        import('./types').MUCMessageAttrsWithEncryption|import('./types').MessageAttrsWithEncryption>}
  */
 export async function parseEncryptedMessage (stanza, attrs) {
     if (api.settings.get('clear_cache_on_logout') ||
@@ -307,18 +361,18 @@ export async function parseEncryptedMessage (stanza, attrs) {
     const key = device_id && sizzle(`key[rid="${device_id}"]`, encrypted_el).pop();
     if (key) {
         Object.assign(attrs.encrypted, {
-            'iv': header.querySelector('iv').textContent,
-            'key': key.textContent,
-            'payload': encrypted_el.querySelector('payload')?.textContent || null,
-            'prekey': ['true', '1'].includes(key.getAttribute('prekey'))
+            iv: header.querySelector('iv').textContent,
+            key: key.textContent,
+            payload: encrypted_el.querySelector('payload')?.textContent || null,
+            prekey: ['true', '1'].includes(key.getAttribute('prekey'))
         });
     } else {
         return Object.assign(attrs, {
-            'error_condition': 'not-encrypted-for-this-device',
-            'error_type': 'Decryption',
-            'is_ephemeral': true,
-            'is_error': true,
-            'type': 'error'
+            error_condition: 'not-encrypted-for-this-device',
+            error_type: 'Decryption',
+            is_ephemeral: true,
+            is_error: true,
+            type: 'error'
         });
     }
     // https://xmpp.org/extensions/xep-0384.html#usecases-receiving
@@ -330,7 +384,7 @@ export async function parseEncryptedMessage (stanza, attrs) {
 }
 
 export function onChatBoxesInitialized () {
-    _converse.state.chatboxes.on('add', chatbox => {
+    _converse.state.chatboxes.on('add', (chatbox) => {
         checkOMEMOSupported(chatbox);
         if (chatbox.get('type') === CHATROOMS_TYPE) {
             chatbox.occupants.on('add', o => onOccupantAdded(chatbox, o));
@@ -340,7 +394,7 @@ export function onChatBoxesInitialized () {
 }
 
 export function onChatInitialized (el) {
-    el.listenTo(el.model.messages, 'add', message => {
+    el.listenTo(el.model.messages, 'add', (message) => {
         if (message.get('is_encrypted') && !message.get('is_error')) {
             el.model.save('omemo_supported', true);
         }
@@ -359,22 +413,29 @@ export function onChatInitialized (el) {
     });
 }
 
+/**
+ * @param {string} jid
+ * @param {number} id
+ */
 export function getSessionCipher (jid, id) {
     const { libsignal } = /** @type WindowWithLibsignal */(window);
     const address = new libsignal.SignalProtocolAddress(jid, id);
     return new libsignal.SessionCipher(_converse.state.omemo_store, address);
 }
 
+/**
+ * @param {MUCMessageAttributes|MessageAttributes} attrs
+ */
 function getJIDForDecryption (attrs) {
-    const from_jid = attrs.from_muc ? attrs.from_real_jid : attrs.from;
+    const from_jid = "from_muc" in attrs ? attrs.from_real_jid : attrs.from;
     if (!from_jid) {
         Object.assign(attrs, {
-            'error_text': __("Sorry, could not decrypt a received OMEMO "+
+            error_text: __("Sorry, could not decrypt a received OMEMO "+
                 "message because we don't have the XMPP address for that user."),
-            'error_type': 'Decryption',
-            'is_ephemeral': true,
-            'is_error': true,
-            'type': 'error'
+            error_type: 'Decryption',
+            is_ephemeral: true,
+            is_error: true,
+            type: 'error'
         });
         throw new Error("Could not find JID to decrypt OMEMO message for");
     }
@@ -392,7 +453,7 @@ async function handleDecryptedWhisperMessage (attrs, key_and_tag) {
     if (encrypted.payload) {
         const key = key_and_tag.slice(0, 16);
         const tag = key_and_tag.slice(16);
-        const result = await omemo.decryptMessage(Object.assign(encrypted, { 'key': key, 'tag': tag }));
+        const result = await omemo.decryptMessage(Object.assign(encrypted, { key, tag }));
         device.save('active', true);
         return result;
     }
@@ -411,6 +472,9 @@ function getDecryptionErrorAttributes (e) {
     };
 }
 
+/**
+ * @param {MUCMessageAttributes|MessageAttributes} attrs
+ */
 async function decryptPrekeyWhisperMessage (attrs) {
     const from_jid = getJIDForDecryption(attrs);
     const session_cipher = getSessionCipher(from_jid, parseInt(attrs.encrypted.device_id, 10));
@@ -460,6 +524,9 @@ async function decryptPrekeyWhisperMessage (attrs) {
     }
 }
 
+/**
+ * @param {MUCMessageAttributes|MessageAttributes} attrs
+ */
 async function decryptWhisperMessage (attrs) {
     const from_jid = getJIDForDecryption(attrs);
     const session_cipher = getSessionCipher(from_jid, parseInt(attrs.encrypted.device_id, 10));
@@ -477,30 +544,38 @@ async function decryptWhisperMessage (attrs) {
 /**
  * Given an XML element representing a user's OMEMO bundle, parse it
  * and return a map.
+ * @param {Element} bundle_el
+ * @returns {import('./types').Bundle}
  */
 export function parseBundle (bundle_el) {
     const signed_prekey_public_el = bundle_el.querySelector('signedPreKeyPublic');
     const signed_prekey_signature_el = bundle_el.querySelector('signedPreKeySignature');
-    const prekeys = sizzle(`prekeys > preKeyPublic`, bundle_el).map(el => ({
-        'id': parseInt(el.getAttribute('preKeyId'), 10),
-        'key': el.textContent
+    const prekeys = sizzle(`prekeys > preKeyPublic`, bundle_el).map(/** @param {Element} el */(el) => ({
+        id: parseInt(el.getAttribute('preKeyId'), 10),
+        key: el.textContent
     }));
     return {
-        'identity_key': bundle_el.querySelector('identityKey').textContent.trim(),
-        'signed_prekey': {
-            'id': parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10),
-            'public_key': signed_prekey_public_el.textContent,
-            'signature': signed_prekey_signature_el.textContent
+        identity_key: bundle_el.querySelector('identityKey').textContent.trim(),
+        signed_prekey: {
+            id: parseInt(signed_prekey_public_el.getAttribute('signedPreKeyId'), 10),
+            public_key: signed_prekey_public_el.textContent,
+            signature: signed_prekey_signature_el.textContent
         },
-        'prekeys': prekeys
+        prekeys
     };
 }
 
+/**
+ * @param {string} jid
+ */
 export async function generateFingerprints (jid) {
     const devices = await getDevicesForContact(jid);
     return Promise.all(devices.map(d => generateFingerprint(d)));
 }
 
+/**
+ * @param {import('./device.js').default} device
+ */
 export async function generateFingerprint (device) {
     if (device.get('bundle')?.fingerprint) {
         return;
@@ -511,6 +586,10 @@ export async function generateFingerprint (device) {
     device.trigger('change:bundle'); // Doesn't get triggered automatically due to pass-by-reference
 }
 
+/**
+ * @param {string} jid
+ * @returns {Promise<import('./devices.js').default>}
+ */
 export async function getDevicesForContact (jid) {
     await api.waitUntil('OMEMOInitialized');
     const devicelist = await api.omemo.devicelists.get(jid, true);
@@ -518,8 +597,14 @@ export async function getDevicesForContact (jid) {
     return devicelist.devices;
 }
 
-export function getDeviceForContact (jid, device_id) {
-    return getDevicesForContact(jid).then(devices => devices.get(device_id));
+/**
+ * @param {string} jid
+ * @param {string} device_id
+ * @returns {Promise<import('./device.js').default[]>}
+ */
+export async function getDeviceForContact (jid, device_id) {
+    const devices = await getDevicesForContact(jid);
+    return devices.get(device_id);
 }
 
 export async function generateDeviceID () {
@@ -544,34 +629,38 @@ export async function generateDeviceID () {
     return device_id.toString();
 }
 
+/**
+ * @param {import('./device.js').default} device
+ */
 async function buildSession (device) {
     const { libsignal } = /** @type WindowWithLibsignal */(window);
     const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
     const sessionBuilder = new libsignal.SessionBuilder(_converse.state.omemo_store, address);
     const prekey = device.getRandomPreKey();
     const bundle = await device.getBundle();
-
     return sessionBuilder.processPreKey({
-        'registrationId': parseInt(device.get('id'), 10),
-        'identityKey': base64ToArrayBuffer(bundle.identity_key),
-        'signedPreKey': {
-            'keyId': bundle.signed_prekey.id, // <Number>
-            'publicKey': base64ToArrayBuffer(bundle.signed_prekey.public_key),
-            'signature': base64ToArrayBuffer(bundle.signed_prekey.signature)
+        registrationId: parseInt(device.get('id'), 10),
+        identityKey: base64ToArrayBuffer(bundle.identity_key),
+        signedPreKey: {
+            keyId: bundle.signed_prekey.id, // <Number>
+            publicKey: base64ToArrayBuffer(bundle.signed_prekey.public_key),
+            signature: base64ToArrayBuffer(bundle.signed_prekey.signature)
         },
-        'preKey': {
-            'keyId': prekey.id, // <Number>
-            'publicKey': base64ToArrayBuffer(prekey.key)
+        preKey: {
+            keyId: prekey.id, // <Number>
+            publicKey: base64ToArrayBuffer(prekey.key)
         }
     });
 }
 
+/**
+ * @param {import('./device.js').default} device
+ */
 export async function getSession (device) {
     if (!device.get('bundle')) {
         log.error(`Could not build an OMEMO session for device ${device.get('id')} because we don't have its bundle`);
         return null;
     }
-
     const { libsignal } = /** @type WindowWithLibsignal */(window);
     const address = new libsignal.SignalProtocolAddress(device.get('jid'), device.get('id'));
     const session = await _converse.state.omemo_store.loadSession(address.toString());
@@ -579,8 +668,7 @@ export async function getSession (device) {
         return session;
     } else {
         try {
-            const session = await buildSession(device);
-            return session;
+            return await buildSession(device);
         } catch (e) {
             log.error(`Could not build an OMEMO session for device ${device.get('id')}`);
             log.error(e);
@@ -589,6 +677,9 @@ export async function getSession (device) {
     }
 }
 
+/**
+ * @param {Element} stanza
+ */
 async function updateBundleFromStanza (stanza) {
     const items_el = sizzle(`items`, stanza).pop();
     if (!items_el || !items_el.getAttribute('node').startsWith(Strophe.NS.OMEMO_BUNDLES)) {
@@ -602,6 +693,9 @@ async function updateBundleFromStanza (stanza) {
     device.save({ 'bundle': parseBundle(bundle_el) });
 }
 
+/**
+ * @param {Element} stanza
+ */
 async function updateDevicesFromStanza (stanza) {
     const items_el = sizzle(`items[node="${Strophe.NS.OMEMO_DEVICELIST}"]`, stanza).pop();
     if (!items_el) {
@@ -622,7 +716,7 @@ async function updateDevicesFromStanza (stanza) {
         }
         devices.get(id).save('active', false);
     });
-    device_ids.forEach(device_id => {
+    device_ids.forEach(/** @param {string} device_id */(device_id) => {
         const device = devices.get(device_id);
         if (device) {
             device.save('active', true);
@@ -640,6 +734,7 @@ async function updateDevicesFromStanza (stanza) {
 export function registerPEPPushHandler () {
     // Add a handler for devices pushed from other connected clients
     api.connection.get().addHandler(
+        /** @param {Element} message */
         async (message) => {
             try {
                 if (sizzle(`event[xmlns="${Strophe.NS.PUBSUB}#event"]`, message).length) {
@@ -669,8 +764,8 @@ async function fetchDeviceLists () {
     initStorage(_converse.state.devicelists, id);
     await new Promise(resolve => {
         _converse.state.devicelists.fetch({
-            'success': resolve,
-            'error': (_m, e) => { log.error(e); resolve(); }
+            success: resolve,
+            error: (_m, e) => { log.error(e); resolve(); }
         })
     });
     // Call API method to wait for our own device list to be fetched from the
@@ -679,6 +774,9 @@ async function fetchDeviceLists () {
     await api.omemo.devicelists.get(bare_jid, true);
 }
 
+/**
+ * @param {boolean} reconnecting
+ */
 export async function initOMEMO (reconnecting) {
     if (reconnecting) {
         return;
@@ -704,6 +802,10 @@ export async function initOMEMO (reconnecting) {
     api.trigger('OMEMOInitialized');
 }
 
+/**
+ * @param {MUC} chatroom
+ * @param {import('@converse/headless/types/plugins/muc/occupant').default} occupant
+ */
 async function onOccupantAdded (chatroom, occupant) {
     if (occupant.isSelf() || !chatroom.features.get('nonanonymous') || !chatroom.features.get('membersonly')) {
         return;
@@ -738,6 +840,9 @@ async function checkOMEMOSupported (chatbox) {
     }
 }
 
+/**
+ * @param {MouseEvent} ev
+ */
 function toggleOMEMO (ev) {
     ev.stopPropagation();
     ev.preventDefault();
@@ -764,6 +869,10 @@ function toggleOMEMO (ev) {
     toolbar_el.model.save({ 'omemo_active': !toolbar_el.model.get('omemo_active') });
 }
 
+/**
+ * @param {import('shared/chat/toolbar').ChatToolbar} toolbar_el
+ * @param {Array<import('lit').TemplateResult>} buttons
+ */
 export function getOMEMOToolbarButton (toolbar_el, buttons) {
     const model = toolbar_el.model;
     const is_muc = model.get('type') === CHATROOMS_TYPE;
@@ -811,13 +920,17 @@ export function getOMEMOToolbarButton (toolbar_el, buttons) {
 
 /**
  * @param {MUC|ChatBox} chatbox
+ * @returns {Promise<import('./device.js').default[]>}
  */
 async function getBundlesAndBuildSessions (chatbox) {
     const no_devices_err = __('Sorry, no devices found to which we can send an OMEMO encrypted message.');
     let devices;
     if (chatbox instanceof MUC) {
-        const collections = await Promise.all(chatbox.occupants.map(o => getDevicesForContact(o.get('jid'))));
+        const collections = await Promise.all(chatbox.occupants.map(
+            /** @param {import('@converse/headless/types/plugins/muc/occupant').default} o */
+            (o) => getDevicesForContact(o.get('jid'))));
         devices = collections.reduce((a, b) => a.concat(b.models), []);
+
     } else if (chatbox.get('type') === PRIVATE_CHAT_TYPE) {
         const their_devices = await getDevicesForContact(chatbox.get('jid'));
         if (their_devices.length === 0) {
@@ -830,9 +943,11 @@ async function getBundlesAndBuildSessions (chatbox) {
     }
     // Filter out our own device
     const id = _converse.state.omemo_store.get('device_id');
-    devices = devices.filter(d => d.get('id') !== id);
+    devices = devices.filter(
+        /** @param {import('./device.js').default} d */(d) => d.get('id') !== id);
+
     // Fetch bundles if necessary
-    await Promise.all(devices.map(d => d.getBundle()));
+    await Promise.all(devices.map((d) => d.getBundle()));
 
     const sessions = devices.filter(d => d).map(d => getSession(d));
     await Promise.all(sessions);
@@ -846,12 +961,21 @@ async function getBundlesAndBuildSessions (chatbox) {
     return devices;
 }
 
+/**
+ * @param {ArrayBuffer} key_and_tag
+ * @param {import('./device.js').default} device
+ */
 function encryptKey (key_and_tag, device) {
     return getSessionCipher(device.get('jid'), device.get('id'))
         .encrypt(key_and_tag)
-        .then(payload => ({ 'payload': payload, 'device': device }));
+        .then((payload) => ({ payload, device }));
 }
 
+/**
+ * @param {MUC|ChatBox} chat
+ * @param {{ message: BaseMessage, stanza: import('strophe.js').Builder }} data
+ * @return {Promise<{ message: BaseMessage, stanza: import('strophe.js').Builder }>}
+ */
 export async function createOMEMOMessageStanza (chat, data) {
     let { stanza } = data;
     const { message } = data;

+ 14 - 16
src/types/plugins/omemo/device.d.ts

@@ -1,29 +1,27 @@
 export default Device;
-/**
- * @namespace _converse.Device
- * @memberOf _converse
- */
 declare class Device extends Model {
     defaults(): {
         trusted: number;
         active: boolean;
     };
-    getRandomPreKey(): any;
-    fetchBundleFromServer(): Promise<{
-        identity_key: any;
-        signed_prekey: {
-            id: number;
-            public_key: any;
-            signature: any;
-        };
-        prekeys: any;
-    }>;
+    /**
+     * @returns {import('./types').PreKey}
+     */
+    getRandomPreKey(): import("./types").PreKey;
+    /**
+     * Fetch the device's OMEMO bundle from the server.
+     * A bundle is a collection of publicly accessible data that can
+     * be used to build a session with a device, namely its public IdentityKey,
+     * a signed PreKey with corresponding signature, and a list of (single use) PreKeys.
+     * @returns {Promise<import('./types').Bundle>}
+     */
+    fetchBundleFromServer(): Promise<import("./types").Bundle>;
     /**
      * Fetch and save the bundle information associated with
      * this device, if the information is not cached already.
-     * @method _converse.Device#getBundle
+     * @returns {Promise<import('./types').Bundle>}
      */
-    getBundle(): Promise<any>;
+    getBundle(): Promise<import("./types").Bundle>;
 }
 import { Model } from '@converse/skeletor';
 //# sourceMappingURL=device.d.ts.map

+ 18 - 10
src/types/plugins/omemo/devicelist.d.ts

@@ -1,8 +1,4 @@
 export default DeviceList;
-/**
- * @namespace _converse.DeviceList
- * @memberOf _converse
- */
 declare class DeviceList extends Model {
     initialize(): Promise<void>;
     initialized: any;
@@ -11,16 +7,28 @@ declare class DeviceList extends Model {
     onDevicesFound(collection: any): Promise<void>;
     fetchDevices(): Promise<any>;
     _devices_promise: Promise<any>;
-    getOwnDeviceId(): Promise<any>;
-    publishCurrentDevice(device_ids: any): Promise<any>;
-    fetchDevicesFromServer(): Promise<any[]>;
     /**
-     * Send an IQ stanza to the current user's "devices" PEP node to
+     * @returns {Promise<string>}
+     */
+    getOwnDeviceId(): Promise<string>;
+    /**
+     * @param {string[]} device_ids
+     */
+    publishCurrentDevice(device_ids: string[]): Promise<any>;
+    /**
+     * @returns {Promise<import('./device').default[]>}
+     */
+    fetchDevicesFromServer(): Promise<import("./device").default[]>;
+    /**
+     * Sends an IQ stanza to the current user's "devices" PEP node to
      * ensure that all devices are published for potential chat partners to see.
-     * See: https://xmpp.org/extensions/xep-0384.html#usecases-announcing
+     * See: https://xmpp.org/extensions/attic/xep-0384-0.3.0.html#usecases-announcing
      */
     publishDevices(): any;
-    removeOwnDevices(device_ids: any): Promise<any>;
+    /**
+     * @param {string[]} device_ids
+     */
+    removeOwnDevices(device_ids: string[]): Promise<any>;
 }
 import { Model } from '@converse/skeletor';
 //# sourceMappingURL=devicelist.d.ts.map

+ 30 - 0
src/types/plugins/omemo/types.d.ts

@@ -0,0 +1,30 @@
+import { MUCMessageAttributes, MessageAttributes } from "./utils";
+export type PreKey = {
+    id: number;
+    key: string;
+};
+export type Bundle = {
+    identity_key: string;
+    signed_prekey: {
+        id: number;
+        public_key: string;
+        signature: string;
+    };
+    prekeys: PreKey[];
+};
+export type EncryptedMessageAttributes = {
+    iv: string;
+    key: string;
+    payload: string | null;
+    prekey: boolean;
+};
+export type MUCMessageAttrsWithEncryption = MUCMessageAttributes & EncryptedMessageAttributes;
+export type MessageAttrsWithEncryption = MessageAttributes & EncryptedMessageAttributes;
+export type EncryptedMessage = {
+    key: ArrayBuffer;
+    tag: ArrayBuffer;
+    key_and_tag: ArrayBuffer;
+    payload: string;
+    iv: string;
+};
+//# sourceMappingURL=types.d.ts.map

+ 98 - 37
src/types/plugins/omemo/utils.d.ts

@@ -1,55 +1,115 @@
-export function formatFingerprint(fp: any): any;
-export function formatFingerprintForQRCode(fp: any): string;
+/**
+ * @param {string} fp
+ */
+export function formatFingerprint(fp: string): string;
+/**
+ * @param {string} fp
+ */
+export function formatFingerprintForQRCode(fp: string): string;
 /**
  * @param {Error|IQError|UserFacingError} e
  * @param {ChatBox} chat
  */
 export function handleMessageSendError(e: Error | IQError | UserFacingError, chat: ChatBox): void;
-export function contactHasOMEMOSupport(jid: any): Promise<boolean>;
-export function getOutgoingMessageAttributes(chat: any, attrs: any): any;
+/**
+ * @param {string} jid
+ */
+export function contactHasOMEMOSupport(jid: string): Promise<boolean>;
+/**
+ * @param {ChatBox|MUC} chat
+ * @param {MessageAttributes} attrs
+ * @return {MessageAttributes}
+ */
+export function getOutgoingMessageAttributes(chat: ChatBox | MUC, attrs: MessageAttributes): MessageAttributes;
 /**
  * @param {File} file
  * @returns {Promise<File>}
  */
 export function encryptFile(file: File): Promise<File>;
-export function setEncryptedFileURL(message: any, attrs: any): any;
-export function handleEncryptedFiles(richtext: any): void;
 /**
- * Hook handler for { @link parseMessage } and { @link parseMUCMessage }, which
+ * @param {import('@converse/headless/types/shared/message').default} message
+ * @param {import('@converse/headless/shared/types').FileUploadMessageAttributes} attrs
+ */
+export function setEncryptedFileURL(message: import("@converse/headless").BaseMessage<any>, attrs: import("@converse/headless/shared/types").FileUploadMessageAttributes): import("@converse/headless/shared/types").FileUploadMessageAttributes & {
+    oob_url: any;
+    message: string;
+    body: string;
+};
+/**
+ * @param {import('shared/texture/texture.js').Texture} richtext
+ */
+export function handleEncryptedFiles(richtext: import("shared/texture/texture.js").Texture): void;
+/**
+ * Hook handler for {@link parseMessage} and {@link parseMUCMessage}, which
  * parses the passed in `message` stanza for OMEMO attributes and then sets
  * them on the attrs object.
- * @param { Element } stanza - The message stanza
- * @param { (MUCMessageAttributes|MessageAttributes) } attrs
- * @returns (MUCMessageAttributes|MessageAttributes)
+ * @param {Element} stanza - The message stanza
+ * @param {MUCMessageAttributes|MessageAttributes} attrs
+ * @returns {Promise<MUCMessageAttributes| MessageAttributes|
+        import('./types').MUCMessageAttrsWithEncryption|import('./types').MessageAttrsWithEncryption>}
  */
-export function parseEncryptedMessage(stanza: Element, attrs: (MUCMessageAttributes | MessageAttributes)): Promise<any>;
+export function parseEncryptedMessage(stanza: Element, attrs: MUCMessageAttributes | MessageAttributes): Promise<MUCMessageAttributes | MessageAttributes | import("./types").MUCMessageAttrsWithEncryption | import("./types").MessageAttrsWithEncryption>;
 export function onChatBoxesInitialized(): void;
 export function onChatInitialized(el: any): void;
-export function getSessionCipher(jid: any, id: any): any;
+/**
+ * @param {string} jid
+ * @param {number} id
+ */
+export function getSessionCipher(jid: string, id: number): any;
 /**
  * Given an XML element representing a user's OMEMO bundle, parse it
  * and return a map.
+ * @param {Element} bundle_el
+ * @returns {import('./types').Bundle}
  */
-export function parseBundle(bundle_el: any): {
-    identity_key: any;
-    signed_prekey: {
-        id: number;
-        public_key: any;
-        signature: any;
-    };
-    prekeys: any;
-};
-export function generateFingerprints(jid: any): Promise<any[]>;
-export function generateFingerprint(device: any): Promise<void>;
-export function getDevicesForContact(jid: any): Promise<any>;
-export function getDeviceForContact(jid: any, device_id: any): Promise<any>;
+export function parseBundle(bundle_el: Element): import("./types").Bundle;
+/**
+ * @param {string} jid
+ */
+export function generateFingerprints(jid: string): Promise<any[]>;
+/**
+ * @param {import('./device.js').default} device
+ */
+export function generateFingerprint(device: import("./device.js").default): Promise<void>;
+/**
+ * @param {string} jid
+ * @returns {Promise<import('./devices.js').default>}
+ */
+export function getDevicesForContact(jid: string): Promise<import("./devices.js").default>;
+/**
+ * @param {string} jid
+ * @param {string} device_id
+ * @returns {Promise<import('./device.js').default[]>}
+ */
+export function getDeviceForContact(jid: string, device_id: string): Promise<import("./device.js").default[]>;
 export function generateDeviceID(): Promise<any>;
-export function getSession(device: any): Promise<any>;
+/**
+ * @param {import('./device.js').default} device
+ */
+export function getSession(device: import("./device.js").default): Promise<any>;
 export function registerPEPPushHandler(): void;
 export function restoreOMEMOSession(): Promise<void>;
-export function initOMEMO(reconnecting: any): Promise<void>;
-export function getOMEMOToolbarButton(toolbar_el: any, buttons: any): any;
-export function createOMEMOMessageStanza(chat: any, data: any): Promise<any>;
+/**
+ * @param {boolean} reconnecting
+ */
+export function initOMEMO(reconnecting: boolean): Promise<void>;
+/**
+ * @param {import('shared/chat/toolbar').ChatToolbar} toolbar_el
+ * @param {Array<import('lit').TemplateResult>} buttons
+ */
+export function getOMEMOToolbarButton(toolbar_el: import("shared/chat/toolbar").ChatToolbar, buttons: Array<import("lit").TemplateResult>): import("lit").TemplateResult<1 | 2>[];
+/**
+ * @param {MUC|ChatBox} chat
+ * @param {{ message: BaseMessage, stanza: import('strophe.js').Builder }} data
+ * @return {Promise<{ message: BaseMessage, stanza: import('strophe.js').Builder }>}
+ */
+export function createOMEMOMessageStanza(chat: MUC | ChatBox, data: {
+    message: BaseMessage;
+    stanza: import("strophe.js").Builder;
+}): Promise<{
+    message: BaseMessage;
+    stanza: import("strophe.js").Builder;
+}>;
 export namespace omemo {
     export { decryptMessage };
     export { encryptMessage };
@@ -59,18 +119,19 @@ export type WindowWithLibsignal = any;
 export type MessageAttributes = import("@converse/headless/shared/types").MessageAttributes;
 export type MUCMessageAttributes = import("@converse/headless/plugins/muc/types").MUCMessageAttributes;
 export type ChatBox = import("@converse/headless").ChatBox;
+export type BaseMessage = import("@converse/headless").BaseMessage<any>;
 import { IQError } from 'shared/errors.js';
 import { UserFacingError } from 'shared/errors.js';
-declare function decryptMessage(obj: any): Promise<string>;
+import { MUC } from '@converse/headless';
+/**
+ * @param {import('./types').EncryptedMessage} obj
+ * @returns {Promise<string>}
+ */
+declare function decryptMessage(obj: import("./types").EncryptedMessage): Promise<string>;
 /**
  * @param {string} plaintext
+ * @returns {Promise<import('./types').EncryptedMessage>}
  */
-declare function encryptMessage(plaintext: string): Promise<{
-    key: ArrayBuffer;
-    tag: ArrayBuffer;
-    key_and_tag: ArrayBufferLike;
-    payload: string;
-    iv: string;
-}>;
+declare function encryptMessage(plaintext: string): Promise<import("./types").EncryptedMessage>;
 export {};
 //# sourceMappingURL=utils.d.ts.map