Converse converse.js

Source: headless/converse-status.js

/**
 * @module converse-status
 * @copyright The Converse.js contributors
 * @license Mozilla Public License (MPLv2)
 */
import { isNaN, isObject } from "lodash-es";
import { Model } from '@converse/skeletor/src/model.js';
import { _converse, api, converse } from "@converse/headless/converse-core";

const { Strophe, $build, $pres } = converse.env;


converse.plugins.add('converse-status', {

    initialize () {

        api.settings.extend({
            auto_away: 0, // Seconds after which user status is set to 'away'
            auto_xa: 0, // Seconds after which user status is set to 'xa'
            csi_waiting_time: 0, // Support for XEP-0352. Seconds before client is considered idle and CSI is sent out.
            default_state: 'online',
            priority: 0,
        });
        api.promises.add(['statusInitialized']);

        _converse.XMPPStatus = Model.extend({
            defaults () {
                return {"status":  api.settings.get("default_state")}
            },

            initialize () {
                this.on('change', item => {
                    if (!isObject(item.changed)) {
                        return;
                    }
                    if ('status' in item.changed || 'status_message' in item.changed) {
                        api.user.presence.send(this.get('status'), null, this.get('status_message'));
                    }
                });
            },

            getNickname () {
                return _converse.nickname;
            },

            getFullname () {
                // Gets overridden in converse-vcard
                return '';
            },

            constructPresence (type, to=null, status_message) {
                type = typeof type === 'string' ? type : (this.get('status') || api.settings.get("default_state"));
                status_message = typeof status_message === 'string' ? status_message : this.get('status_message');
                let presence;
                const attrs = {to};
                if ((type === 'unavailable') ||
                        (type === 'probe') ||
                        (type === 'error') ||
                        (type === 'unsubscribe') ||
                        (type === 'unsubscribed') ||
                        (type === 'subscribe') ||
                        (type === 'subscribed')) {
                    attrs['type'] = type;
                    presence = $pres(attrs);
                } else if (type === 'offline') {
                    attrs['type'] = 'unavailable';
                    presence = $pres(attrs);
                } else if (type === 'online') {
                    presence = $pres(attrs);
                } else {
                    presence = $pres(attrs).c('show').t(type).up();
                }

                if (status_message) {
                    presence.c('status').t(status_message).up();
                }

                const priority = api.settings.get("priority");
                presence.c('priority').t(isNaN(Number(priority)) ? 0 : priority).up();
                if (_converse.idle) {
                    const idle_since = new Date();
                    idle_since.setSeconds(idle_since.getSeconds() - _converse.idle_seconds);
                    presence.c('idle', {xmlns: Strophe.NS.IDLE, since: idle_since.toISOString()});
                }
                return presence;
            }
        });


        /**
         * Send out a Client State Indication (XEP-0352)
         * @private
         * @method sendCSI
         * @memberOf _converse
         * @param { String } stat - The user's chat status
         */
        _converse.sendCSI = function (stat) {
            api.send($build(stat, {xmlns: Strophe.NS.CSI}));
            _converse.inactive = (stat === _converse.INACTIVE) ? true : false;
        };


        _converse.onUserActivity = function () {
            /* Resets counters and flags relating to CSI and auto_away/auto_xa */
            if (_converse.idle_seconds > 0) {
                _converse.idle_seconds = 0;
            }
            if (!_converse.connection?.authenticated) {
                // We can't send out any stanzas when there's no authenticated connection.
                // This can happen when the connection reconnects.
                return;
            }
            if (_converse.inactive) {
                _converse.sendCSI(_converse.ACTIVE);
            }
            if (_converse.idle) {
                _converse.idle = false;
                api.user.presence.send();
            }
            if (_converse.auto_changed_status === true) {
                _converse.auto_changed_status = false;
                // XXX: we should really remember the original state here, and
                // then set it back to that...
                _converse.xmppstatus.set('status', api.settings.get("default_state"));
            }
        };

        _converse.onEverySecond = function () {
            /* An interval handler running every second.
             * Used for CSI and the auto_away and auto_xa features.
             */
            if (!_converse.connection?.authenticated) {
                // We can't send out any stanzas when there's no authenticated connection.
                // This can happen when the connection reconnects.
                return;
            }
            const stat = _converse.xmppstatus.get('status');
            _converse.idle_seconds++;
            if (api.settings.get("csi_waiting_time") > 0 &&
                    _converse.idle_seconds > api.settings.get("csi_waiting_time") &&
                    !_converse.inactive) {
                _converse.sendCSI(_converse.INACTIVE);
            }
            if (api.settings.get("idle_presence_timeout") > 0 &&
                    _converse.idle_seconds > api.settings.get("idle_presence_timeout") &&
                    !_converse.idle) {
                _converse.idle = true;
                api.user.presence.send();
            }
            if (api.settings.get("auto_away") > 0 &&
                    _converse.idle_seconds > api.settings.get("auto_away") &&
                    stat !== 'away' && stat !== 'xa' && stat !== 'dnd') {
                _converse.auto_changed_status = true;
                _converse.xmppstatus.set('status', 'away');
            } else if (api.settings.get("auto_xa") > 0 &&
                    _converse.idle_seconds > api.settings.get("auto_xa") &&
                    stat !== 'xa' && stat !== 'dnd') {
                _converse.auto_changed_status = true;
                _converse.xmppstatus.set('status', 'xa');
            }
        };

        _converse.registerIntervalHandler = function () {
            /* Set an interval of one second and register a handler for it.
             * Required for the auto_away, auto_xa and csi_waiting_time features.
             */
            if (
                api.settings.get("auto_away") < 1 &&
                api.settings.get("auto_xa") < 1 &&
                api.settings.get("csi_waiting_time") < 1 &&
                api.settings.get("idle_presence_timeout") < 1
            ) {
                // Waiting time of less then one second means features aren't used.
                return;
            }
            _converse.idle_seconds = 0;
            _converse.auto_changed_status = false; // Was the user's status changed by Converse?

            const { unloadevent } = _converse;
            window.addEventListener('click', _converse.onUserActivity);
            window.addEventListener('focus', _converse.onUserActivity);
            window.addEventListener('keypress', _converse.onUserActivity);
            window.addEventListener('mousemove', _converse.onUserActivity);
            window.addEventListener(unloadevent, _converse.onUserActivity, {'once': true, 'passive': true});
            window.addEventListener(unloadevent, () => _converse.session?.save('active', false));
            _converse.everySecondTrigger = window.setInterval(_converse.onEverySecond, 1000);
        };


        api.listen.on('presencesInitialized', (reconnecting) => {
            if (!reconnecting) {
                _converse.registerIntervalHandler();
            }
        });


        function onStatusInitialized (reconnecting) {
            /**
             * Triggered when the user's own chat status has been initialized.
             * @event _converse#statusInitialized
             * @example _converse.api.listen.on('statusInitialized', status => { ... });
             * @example _converse.api.waitUntil('statusInitialized').then(() => { ... });
             */
            api.trigger('statusInitialized', reconnecting);
        }


        function initStatus (reconnecting) {
            // If there's no xmppstatus obj, then we were never connected to
            // begin with, so we set reconnecting to false.
            reconnecting = _converse.xmppstatus === undefined ? false : reconnecting;
            if (reconnecting) {
                onStatusInitialized(reconnecting);
            } else {
                const id = `converse.xmppstatus-${_converse.bare_jid}`;
                _converse.xmppstatus = new _converse.XMPPStatus({'id': id});
                _converse.xmppstatus.browserStorage = _converse.createStore(id, "session");
                _converse.xmppstatus.fetch({
                    'success': () => onStatusInitialized(reconnecting),
                    'error': () => onStatusInitialized(reconnecting),
                    'silent': true
                });
            }
        }


        /************************ BEGIN Event Handlers ************************/
        api.listen.on('clearSession', () => {
            if (_converse.shouldClearCache() && _converse.xmppstatus) {
                _converse.xmppstatus.destroy();
                delete _converse.xmppstatus;
                api.promises.add(['statusInitialized']);
            }
        });

        api.listen.on('connected', () => initStatus(false));
        api.listen.on('reconnected', () => initStatus(true));
        /************************ END Event Handlers ************************/


        /************************ BEGIN API ************************/
        Object.assign(_converse.api.user, {
            /**
             * @namespace _converse.api.user.presence
             * @memberOf _converse.api.user
             */
            presence: {
                /**
                 * Send out a presence stanza
                 * @method _converse.api.user.presence.send
                 * @param { String } type
                 * @param { String } to
                 * @param { String } [status] - An optional status message
                 */
                async send (type, to, status) {
                    await api.waitUntil('statusInitialized');
                    api.send(_converse.xmppstatus.constructPresence(type, to, status));
                }
            },

            /**
             * Set and get the user's chat status, also called their *availability*.
             * @namespace _converse.api.user.status
             * @memberOf _converse.api.user
             */
            status: {
                /**
                 * Return the current user's availability status.
                 * @async
                 * @method _converse.api.user.status.get
                 * @example _converse.api.user.status.get();
                 */
                async get () {
                    await api.waitUntil('statusInitialized');
                    return _converse.xmppstatus.get('status');
                },

                /**
                 * The user's status can be set to one of the following values:
                 *
                 * @async
                 * @method _converse.api.user.status.set
                 * @param {string} value The user's chat status (e.g. 'away', 'dnd', 'offline', 'online', 'unavailable' or 'xa')
                 * @param {string} [message] A custom status message
                 *
                 * @example _converse.api.user.status.set('dnd');
                 * @example _converse.api.user.status.set('dnd', 'In a meeting');
                 */
                async set (value, message) {
                    const data = {'status': value};
                    if (!Object.keys(_converse.STATUS_WEIGHTS).includes(value)) {
                        throw new Error(
                            'Invalid availability value. See https://xmpp.org/rfcs/rfc3921.html#rfc.section.2.2.2.1'
                        );
                    }
                    if (typeof message === 'string') {
                        data.status_message = message;
                    }
                    await api.waitUntil('statusInitialized');
                    _converse.xmppstatus.save(data);
                },

                /**
                 * Set and retrieve the user's custom status message.
                 *
                 * @namespace _converse.api.user.status.message
                 * @memberOf _converse.api.user.status
                 */
                message: {
                    /**
                     * @async
                     * @method _converse.api.user.status.message.get
                     * @returns {string} The status message
                     * @example const message = _converse.api.user.status.message.get()
                     */
                    async get () {
                        await api.waitUntil('statusInitialized');
                        return _converse.xmppstatus.get('status_message');
                    },
                    /**
                     * @async
                     * @method _converse.api.user.status.message.set
                     * @param {string} status The status message
                     * @example _converse.api.user.status.message.set('In a meeting');
                     */
                    async set (status) {
                        await api.waitUntil('statusInitialized');
                        _converse.xmppstatus.save({ status_message: status });
                    }
                }
            }
        });
    }
});