import dayjs from 'dayjs'; import log from '@converse/headless/log'; import u from '@converse/headless/utils/core'; import { _converse, api, converse } from '@converse/headless/core'; import { rejectMessage } from '@converse/headless/shared/actions'; import { StanzaParseError, getChatMarker, getChatState, getCorrectionAttributes, getEncryptionAttributes, getErrorAttributes, getMediaURLsMetadata, getOutOfBandAttributes, getReceiptId, getReferences, getRetractionAttributes, getSpoilerAttributes, getStanzaIDs, isArchived, isCarbon, isHeadline, isServerMessage, isValidReceiptRequest, throwErrorIfInvalidForward, } from '@converse/headless/shared/parsers'; const { Strophe, sizzle } = converse.env; /** * Parses a passed in message stanza and returns an object of attributes. * @method st#parseMessage * @param { Element } stanza - The message stanza * @param { _converse } _converse * @returns { (MessageAttributes|Error) } */ export async function parseMessage (stanza) { throwErrorIfInvalidForward(stanza); let to_jid = stanza.getAttribute('to'); const to_resource = Strophe.getResourceFromJid(to_jid); if (api.settings.get('filter_by_resource') && to_resource && to_resource !== _converse.resource) { return new StanzaParseError( `Ignoring incoming message intended for a different resource: ${to_jid}`, stanza ); } const original_stanza = stanza; let from_jid = stanza.getAttribute('from') || _converse.bare_jid; if (isCarbon(stanza)) { if (from_jid === _converse.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(`Rejecting carbon from invalid JID ${to_jid}`, stanza); } } const is_archived = isArchived(stanza); if (is_archived) { if (from_jid === _converse.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( `Invalid Stanza: alleged MAM message from ${stanza.getAttribute('from')}`, stanza ); } } const from_bare_jid = Strophe.getBareJidFromJid(from_jid); const is_me = from_bare_jid === _converse.bare_jid; if (is_me && to_jid === null) { return new StanzaParseError( `Don't know how to handle message stanza without 'to' attribute. ${stanza.outerHTML}`, stanza ); } 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( `Blocking messaging with a JID not in our roster because allow_non_roster_messaging is false.`, stanza ); } } /** * @typedef { Object } MessageAttributes * The object which {@link parseMessage} returns * @property { ('me'|'them') } sender - Whether the message was sent by the current user or someone else * @property { Array } references - A list of objects representing XEP-0372 references * @property { Boolean } editable - Is this message editable via XEP-0308? * @property { Boolean } is_archived - Is this message from a XEP-0313 MAM archive? * @property { Boolean } is_carbon - Is this message a XEP-0280 Carbon? * @property { Boolean } is_delayed - Was delivery of this message was delayed as per XEP-0203? * @property { Boolean } is_encrypted - Is this message XEP-0384 encrypted? * @property { Boolean } is_error - Whether an error was received for this message * @property { Boolean } is_headline - Is this a "headline" message? * @property { Boolean } is_markable - Can this message be marked with a XEP-0333 chat marker? * @property { Boolean } is_marker - Is this message a XEP-0333 Chat Marker? * @property { Boolean } is_only_emojis - Does the message body contain only emojis? * @property { Boolean } is_spoiler - Is this a XEP-0382 spoiler message? * @property { Boolean } is_tombstone - Is this a XEP-0424 tombstone? * @property { Boolean } is_unstyled - Whether XEP-0393 styling hints should be ignored * @property { Boolean } is_valid_receipt_request - Does this message request a XEP-0184 receipt (and is not from us or a carbon or archived message) * @property { Object } encrypted - XEP-0384 encryption payload attributes * @property { String } body - The contents of the tag of the message stanza * @property { String } chat_state - The XEP-0085 chat state notification contained in this message * @property { String } contact_jid - The JID of the other person or entity * @property { String } edited - An ISO8601 string recording the time that the message was edited per XEP-0308 * @property { String } error_condition - The defined error condition * @property { String } error_text - The error text received from the server * @property { String } error_type - The type of error received from the server * @property { String } from - The sender JID * @property { String } fullname - The full name of the sender * @property { String } marker - The XEP-0333 Chat Marker value * @property { String } marker_id - The `id` attribute of a XEP-0333 chat marker * @property { String } msgid - The root `id` attribute of the stanza * @property { String } nick - The roster nickname of the sender * @property { String } oob_desc - The description of the XEP-0066 out of band data * @property { String } oob_url - The URL of the XEP-0066 out of band data * @property { String } origin_id - The XEP-0359 Origin ID * @property { String } receipt_id - The `id` attribute of a XEP-0184 element * @property { String } received - An ISO8601 string recording the time that the message was received * @property { String } replace_id - The `id` attribute of a XEP-0308 element * @property { String } retracted - An ISO8601 string recording the time that the message was retracted * @property { String } retracted_id - The `id` attribute of a XEP-424 element * @property { String } spoiler_hint The XEP-0382 spoiler hint * @property { String } stanza_id - The XEP-0359 Stanza ID. Note: the key is actualy `stanza_id ${by_jid}` and there can be multiple. * @property { String } subject - The element value * @property { String } thread - The element value * @property { String } time - The time (in ISO8601 format), either given by the XEP-0203 element, or of receipt. * @property { String } to - The recipient JID * @property { String } type - The type of message */ 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': Strophe.getBareJidFromJid(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'), 'msgid': stanza.getAttribute('id') || original_stanza.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, _converse) ); if (attrs.is_archived) { const from = original_stanza.getAttribute('from'); if (from && from !== _converse.bare_jid) { return new StanzaParseError(`Invalid Stanza: Forged MAM message from ${from}`, stanza); } } await api.emojis.initialize(); attrs = Object.assign( { 'message': attrs.body || attrs.error, // TODO: Remove and use body and error attributes instead 'is_only_emojis': attrs.body ? u.isOnlyEmojis(attrs.body) : false, '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. return Object.assign(attrs, getMediaURLsMetadata(attrs.is_encrypted ? attrs.plaintext : attrs.body)); }