UNPKG

converse.js

Version:
199 lines (181 loc) 6.69 kB
/** * @module:headless-plugins-chat-utils * @typedef {import('./model.js').default} ChatBox * @typedef {import('../../shared/types.ts').MessageAttributes} MessageAttributes * @typedef {import('../../shared/errors').StanzaParseError} StanzaParseError * @typedef {import('strophe.js').Builder} Builder */ import sizzle from "sizzle"; import { Model } from '@converse/skeletor'; import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import converse from "../../shared/api/public.js"; import log from "@converse/log"; import { isArchived, isHeadline, isMUCPrivateMessage, isServerMessage, } from '../../shared/parsers'; import { parseMessage } from './parsers.js'; import { PRIVATE_CHAT_TYPE } from "../../shared/constants.js"; const { Strophe, u } = converse.env; export function routeToChat (event) { if (!location.hash.startsWith('#converse/chat?jid=')) { return; } event?.preventDefault(); const jid = location.hash.split('=').pop(); if (!u.isValidJID(jid)) { return log.warn(`Invalid JID "${jid}" provided in URL fragment`); } api.chats.open(jid); } /** * Given a stanza, determine whether it's a new * message, i.e. not a MAM archived one. * @param {Element|Model|object} message */ export function isNewMessage (message) { if (message instanceof Element) { return !( sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, message).length && sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, message).length ); } else if (message instanceof Model) { message = message.attributes; } return !(message['is_delayed'] && message['is_archived']); } /** * @param {Element} stanza */ async function handleErrorMessage (stanza) { const from_jid = Strophe.getBareJidFromJid(stanza.getAttribute('from')); const bare_jid = _converse.session.get('bare_jid'); if (u.isSameBareJID(from_jid, bare_jid)) { return; } const chatbox = await api.chatboxes.get(from_jid); if (chatbox?.get('type') === PRIVATE_CHAT_TYPE) { chatbox?.handleErrorMessageStanza(stanza); } } export function autoJoinChats () { // Automatically join private chats, based on the // "auto_join_private_chats" configuration setting. api.settings.get('auto_join_private_chats').forEach(/** @param {string} jid */(jid) => { if (_converse.state.chatboxes.where({ 'jid': jid }).length) { return; } if (typeof jid === 'string') { api.chats.open(jid); } else { log.error('Invalid jid criteria specified for "auto_join_private_chats"'); } }); /** * Triggered once any private chats have been automatically joined as * specified by the `auto_join_private_chats` setting. * See: https://conversejs.org/docs/html/configuration.html#auto-join-private-chats * @event _converse#privateChatsAutoJoined * @example _converse.api.listen.on('privateChatsAutoJoined', () => { ... }); * @example _converse.api.waitUntil('privateChatsAutoJoined').then(() => { ... }); */ api.trigger('privateChatsAutoJoined'); } export function registerMessageHandlers () { api.connection.get().addHandler( /** @param {Element} stanza */ (stanza) => { if ( ['groupchat', 'error'].includes(stanza.getAttribute('type')) || isHeadline(stanza) || isServerMessage(stanza) || isArchived(stanza) ) { return true; } return _converse.exports.handleMessageStanza(stanza) || true; }, null, 'message', ); api.connection.get().addHandler( /** @param {Element} stanza */ (stanza) => { handleErrorMessage(stanza); return true; }, null, 'message', 'error' ); } /** * Handler method for all incoming single-user chat "message" stanzas. * @param {Element|Builder} stanza */ export async function handleMessageStanza (stanza) { stanza = (stanza instanceof Element) ? stanza : stanza.tree(); if (isServerMessage(stanza)) { // Prosody sends headline messages with type `chat`, so we need to filter them out here. const from = stanza.getAttribute('from'); return log.info(`handleMessageStanza: Ignoring incoming server message from JID: ${from}`); } if (await isMUCPrivateMessage(stanza)) { return true; } let attrs; try { attrs = await parseMessage(stanza); } catch (e) { return log.error(e); } if (u.isErrorObject(attrs)) { const { stanza, message } = /** @type {StanzaParseError} */(attrs); if (stanza) log.error(stanza); return log.error(message); } const { body, plaintext, contact_jid, nick } = /** @type {MessageAttributes} */(attrs); // XXX: Need to take XEP-428 <fallback> into consideration const has_body = !!(body || plaintext); const chatbox = await api.chats.get(contact_jid, { nickname: nick }, has_body); await chatbox?.queueMessage(attrs); /** * @typedef {Object} MessageData * An object containing the original message stanza, as well as the * parsed attributes. * @property {Element} stanza * @property {MessageAttributes} stanza * @property {ChatBox} chatbox */ const data = { stanza, attrs, chatbox }; /** * Triggered when a message stanza is been received and processed. * @event _converse#message * @type {MessageData} data */ api.trigger('message', data); } /** * Ask the XMPP server to enable Message Carbons * See [XEP-0280](https://xmpp.org/extensions/xep-0280.html#enabling) */ export async function enableCarbons () { const bare_jid = _converse.session.get('bare_jid'); const domain = Strophe.getDomainFromJid(bare_jid); const supported = await api.disco.supports(Strophe.NS.CARBONS, domain); if (!supported) { log.warn("Not enabling carbons because it's not supported!"); return; } const iq = new Strophe.Builder('iq', { 'from': api.connection.get().jid, 'type': 'set' }).c('enable', {xmlns: Strophe.NS.CARBONS}); const result = await api.sendIQ(iq, null, false); if (result === null) { log.warn(`A timeout occurred while trying to enable carbons`); } else if (u.isErrorStanza(result)) { log.warn('An error occurred while trying to enable message carbons.'); log.error(result); } else { log.debug('Message carbons have been enabled.'); } }