UNPKG

converse.js

Version:
174 lines (162 loc) 6.9 kB
/** * @module:plugin-chat-parsers */ import _converse from '../../shared/_converse.js'; import api from '../../shared/api/index.js'; import converse from '../../shared/api/public.js'; import dayjs from 'dayjs'; import log from "@converse/log"; import u from '../../utils/index.js'; import { rejectMessage } from '../../shared/actions.js'; import { StanzaParseError } from '../../shared/errors.js'; import { getChatMarker, getChatState, getCorrectionAttributes, getEncryptionAttributes, getErrorAttributes, getOutOfBandAttributes, getReceiptId, getReferences, getRetractionAttributes, getSpoilerAttributes, getStanzaIDs, isArchived, isCarbon, isHeadline, isServerMessage, isValidReceiptRequest, throwErrorIfInvalidForward, } from '../../shared/parsers'; const { Strophe, sizzle } = converse.env; /** * Parses a passed in message stanza and returns an object of attributes. * @param {Element} stanza - The message stanza * @returns {Promise<import('../../shared/types.ts').MessageAttributes|StanzaParseError>} */ export async function parseMessage (stanza) { throwErrorIfInvalidForward(stanza); let to_jid = stanza.getAttribute('to'); const to_resource = Strophe.getResourceFromJid(to_jid); const resource = _converse.session.get('resource'); if (api.settings.get('filter_by_resource') && to_resource && to_resource !== resource) { return new StanzaParseError( stanza, `Ignoring incoming message intended for a different resource: ${to_jid}`, ); } const bare_jid = _converse.session.get('bare_jid'); const original_stanza = stanza; let from_jid = stanza.getAttribute('from') || bare_jid; if (isCarbon(stanza)) { if (from_jid === bare_jid) { const selector = `[xmlns="${Strophe.NS.CARBONS}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`; stanza = sizzle(selector, stanza).pop(); to_jid = stanza.getAttribute('to'); from_jid = stanza.getAttribute('from'); } else { // Prevent message forging via carbons: https://xmpp.org/extensions/xep-0280.html#security rejectMessage(stanza, 'Rejecting carbon from invalid JID'); return new StanzaParseError(stanza, `Rejecting carbon from invalid JID ${to_jid}`); } } const is_archived = isArchived(stanza); if (is_archived) { if (from_jid === bare_jid) { const selector = `[xmlns="${Strophe.NS.MAM}"] > forwarded[xmlns="${Strophe.NS.FORWARD}"] > message`; stanza = sizzle(selector, stanza).pop(); to_jid = stanza.getAttribute('to'); from_jid = stanza.getAttribute('from'); } else { return new StanzaParseError( stanza, `Invalid Stanza: alleged MAM message from ${stanza.getAttribute('from')}`, ); } } const from_bare_jid = Strophe.getBareJidFromJid(from_jid); const is_me = from_bare_jid === bare_jid; if (is_me && to_jid === null) { return new StanzaParseError( stanza, `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`, ); } const is_headline = isHeadline(stanza); const is_server_message = isServerMessage(stanza); let contact, contact_jid; if (!is_headline && !is_server_message) { contact_jid = is_me ? Strophe.getBareJidFromJid(to_jid) : from_bare_jid; contact = await api.contacts.get(contact_jid); if (contact === undefined && !api.settings.get('allow_non_roster_messaging')) { log.error(stanza); return new StanzaParseError( stanza, `Blocking messaging with a JID not in our roster because allow_non_roster_messaging is false.`, ); } } const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop(); const marker = getChatMarker(stanza); const now = new Date().toISOString(); let attrs = Object.assign( { contact_jid, is_archived, is_headline, is_server_message, 'body': stanza.querySelector('body')?.textContent?.trim(), 'chat_state': getChatState(stanza), 'from': stanza.getAttribute('from'), 'is_carbon': isCarbon(original_stanza), 'is_delayed': !!delay, 'is_markable': !!sizzle(`markable[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length, 'is_marker': !!marker, 'is_unstyled': !!sizzle(`unstyled[xmlns="${Strophe.NS.STYLING}"]`, stanza).length, 'marker_id': marker && marker.getAttribute('id'), 'nick': contact?.attributes?.nickname, 'receipt_id': getReceiptId(stanza), 'received': new Date().toISOString(), 'references': getReferences(stanza), 'sender': is_me ? 'me' : 'them', 'subject': stanza.querySelector('subject')?.textContent, 'thread': stanza.querySelector('thread')?.textContent, 'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : now, 'to': stanza.getAttribute('to'), 'type': stanza.getAttribute('type') || 'normal' }, getErrorAttributes(stanza), getOutOfBandAttributes(stanza), getSpoilerAttributes(stanza), getCorrectionAttributes(stanza, original_stanza), getStanzaIDs(stanza, original_stanza), getRetractionAttributes(stanza, original_stanza), getEncryptionAttributes(stanza) ); if (attrs.is_archived) { const from = original_stanza.getAttribute('from'); if (from && from !== bare_jid) { return new StanzaParseError(stanza, `Invalid Stanza: Forged MAM message from ${from}`); } } attrs = Object.assign( { 'message': attrs.body || attrs.error, // TODO: Remove and use body and error attributes instead 'is_valid_receipt_request': isValidReceiptRequest(stanza, attrs) }, attrs ); // We prefer to use one of the XEP-0359 unique and stable stanza IDs // as the Model id, to avoid duplicates. attrs['id'] = attrs['origin_id'] || attrs[`stanza_id ${attrs.from}`] || u.getUniqueId(); /** * *Hook* which allows plugins to add additional parsing * @event _converse#parseMessage */ attrs = await api.hook('parseMessage', stanza, attrs); // We call this after the hook, to allow plugins (like omemo) to decrypt encrypted // messages, since we need to parse the message text to determine whether // there are media urls. const metadata = await u.getMediaURLsMetadata(attrs.is_encrypted ? attrs.plaintext : attrs.body); return Object.assign(attrs, metadata); }