xmpp.chapril.org-conversejs/src/headless/utils/stanza.js
JC Brand b4dafcc45b Add support for XEP-0424 and XEP-0425
- Add support for switching ephemerality after message creation
- Move more methods from ChatBox and ChatRoom to utils/stanza.js
- Rename 'ephemeral' to 'is_ephemeral' since it's a boolean
2019-11-22 13:50:36 +01:00

317 lines
13 KiB
JavaScript

import * as strophe from 'strophe.js/src/core';
import { get, propertyOf } from "lodash";
import dayjs from 'dayjs';
import log from '@converse/headless/log';
import sizzle from 'sizzle';
import u from '@converse/headless/utils/core';
const Strophe = strophe.default.Strophe;
/**
* The stanza utils object. Contains utility functions related to stanza
* processing.
* @namespace stanza_utils
*/
const stanza_utils = {
isReceipt (stanza) {
return sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).length > 0;
},
isChatMarker (stanza) {
return sizzle(
`received[xmlns="${Strophe.NS.MARKERS}"],
displayed[xmlns="${Strophe.NS.MARKERS}"],
acknowledged[xmlns="${Strophe.NS.MARKERS}"]`, stanza).length > 0;
},
/**
* Determines whether the passed in stanza represents a XEP-0313 MAM stanza
* @private
* @method stanza_utils#isArchived
* @param { XMLElement } stanza - The message stanza
* @returns { Boolean }
*/
isArchived (original_stanza) {
return !!sizzle(`result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
},
/**
* Extract the XEP-0359 stanza IDs from the passed in stanza
* and return a map containing them.
* @private
* @method stanza_utils#getStanzaIDs
* @param { XMLElement } stanza - The message stanza
* @returns { Object }
*/
getStanzaIDs (stanza, original_stanza) {
const attrs = {};
const stanza_ids = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza);
if (stanza_ids.length) {
stanza_ids.forEach(s => (attrs[`stanza_id ${s.getAttribute('by')}`] = s.getAttribute('id')));
}
const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
if (result) {
const by_jid = original_stanza.getAttribute('from');
attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
}
const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
if (origin_id) {
attrs['origin_id'] = origin_id.getAttribute('id');
}
// 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();
return attrs;
},
/** @method stanza_utils#getModerationAttributes
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @param { _converse.ChatRoom } room - The MUC in which the moderation stanza is received.
* @returns { Object }
*/
getModerationAttributes (stanza, original_stanza, room) {
const fastening = sizzle(`apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
if (fastening) {
const applies_to_id = fastening.getAttribute('id');
const moderated = sizzle(`moderated[xmlns="${Strophe.NS.MODERATE}"]`, fastening).pop();
if (moderated) {
const retracted = sizzle(`retract[xmlns="${Strophe.NS.RETRACT}"]`, moderated).pop();
if (retracted) {
const from = stanza.getAttribute('from');
if (from !== room.get('jid')) {
log.warn("getModerationAttributes: ignore moderation stanza that's not from the MUC!");
log.error(original_stanza);
return {};
}
return {
'moderated': 'retracted',
'moderated_by': moderated.getAttribute('by'),
'moderated_id': applies_to_id,
'moderation_reason': get(moderated.querySelector('reason'), 'textContent')
}
}
}
} else {
const tombstone = sizzle(`> moderated[xmlns="${Strophe.NS.MODERATE}"]`, stanza).pop();
if (tombstone) {
const retracted = sizzle(`retracted[xmlns="${Strophe.NS.RETRACT}"]`, tombstone).pop();
if (retracted) {
return {
'is_tombstone': true,
'retracted': tombstone.getAttribute('stamp'),
'moderated_by': tombstone.getAttribute('by'),
'moderation_reason': get(tombstone.querySelector('reason'), 'textContent')
}
}
}
}
return {};
},
/**
* @method stanza_utils#getRetractionAttributes
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @returns { Object }
*/
getRetractionAttributes (stanza, original_stanza) {
const fastening = sizzle(`> apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
if (fastening) {
const applies_to_id = fastening.getAttribute('id');
const retracted = sizzle(`> retract[xmlns="${Strophe.NS.RETRACT}"]`, fastening).pop();
if (retracted) {
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
const time = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString();
return {
'retracted': time,
'retracted_id': applies_to_id
}
}
} else {
const tombstone = sizzle(`> retracted[xmlns="${Strophe.NS.RETRACT}"]`, stanza).pop();
if (tombstone) {
return {
'retracted': tombstone.getAttribute('stamp'),
'is_tombstone': true
}
}
}
return {};
},
getReferences (stanza) {
const text = propertyOf(stanza.querySelector('body'))('textContent');
return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
const begin = ref.getAttribute('begin');
const end = ref.getAttribute('end');
return {
'begin': begin,
'end': end,
'type': ref.getAttribute('type'),
'value': text.slice(begin, end),
'uri': ref.getAttribute('uri')
};
});
},
getSenderAttributes (stanza, chatbox, _converse) {
if (u.isChatRoom(chatbox)) {
const from = stanza.getAttribute('from');
const nick = Strophe.unescapeNode(Strophe.getResourceFromJid(from));
return {
'from': from,
'nick': nick,
'sender': nick === chatbox.get('nick') ? 'me': 'them',
'received': (new Date()).toISOString(),
}
} else {
const from = Strophe.getBareJidFromJid(stanza.getAttribute('from'));
if (from === _converse.bare_jid) {
return {
from,
'sender': 'me',
'fullname': _converse.xmppstatus.get('fullname')
}
} else {
return {
from,
'sender': 'them',
'fullname': chatbox.get('fullname')
}
}
}
},
getSpoilerAttributes (stanza) {
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
return {
'is_spoiler': !!spoiler,
'spoiler_hint': get(spoiler, 'textContent')
}
},
getOutOfBandAttributes (stanza) {
const xform = sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza).pop();
if (xform) {
return {
'oob_url': get(xform.querySelector('url'), 'textContent'),
'oob_desc': get(xform.querySelector('desc'), 'textContent')
}
}
return {};
},
getCorrectionAttributes (stanza, original_stanza) {
const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
if (el) {
const replaced_id = el.getAttribute('id');
const msgid = replaced_id;
if (replaced_id) {
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
const time = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString();
return {
msgid,
replaced_id,
'edited': time
}
}
}
return {};
},
getErrorMessage (stanza, is_muc, _converse) {
const { __ } = _converse;
if (is_muc) {
if (sizzle(`forbidden[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
return __("Your message was not delivered because you're not allowed to send messages in this groupchat.");
} else if (sizzle(`not-acceptable[xmlns="${Strophe.NS.STANZAS}"]`, stanza).length) {
return __("Your message was not delivered because you're not present in the groupchat.");
}
}
const error = stanza.querySelector('error');
return propertyOf(error.querySelector('text'))('textContent') ||
__('Sorry, an error occurred:') + ' ' + error.innerHTML;
},
/**
* Given a message stanza, return the text contained in its body.
* @private
* @method stanza_utils#getMessageBody
* @param { XMLElement } stanza
* @param { Boolean } is_muc
* @param { _converse } _converse
*/
getMessageBody (stanza, is_muc, _converse) {
const type = stanza.getAttribute('type');
if (type === 'error') {
return stanza_utils.getErrorMessage(stanza, is_muc, _converse);
} else {
const body = stanza.querySelector('body');
if (body) {
return body.textContent.trim();
}
}
},
getChatState (stanza) {
return stanza.getElementsByTagName('composing').length && 'composing' ||
stanza.getElementsByTagName('paused').length && 'paused' ||
stanza.getElementsByTagName('inactive').length && 'inactive' ||
stanza.getElementsByTagName('active').length && 'active' ||
stanza.getElementsByTagName('gone').length && 'gone';
},
/**
* Parses a passed in message stanza and returns an object of attributes.
* @private
* @method stanza_utils#getMessageAttributesFromStanza
* @param { XMLElement } stanza - The message stanza
* @param { XMLElement } original_stanza - The original stanza, that contains the
* message stanza, if it was contained, otherwise it's the message stanza itself.
* @param { _converse.ChatBox|_converse.ChatRoom } chatbox
* @param { _converse } _converse
* @returns { Object }
*/
async getMessageAttributesFromStanza (stanza, original_stanza, chatbox, _converse) {
const is_muc = u.isChatRoom(chatbox);
let attrs = Object.assign(
stanza_utils.getStanzaIDs(stanza, original_stanza),
stanza_utils.getRetractionAttributes(stanza, original_stanza),
is_muc ? stanza_utils.getModerationAttributes(stanza, original_stanza, chatbox) : {},
);
const text = stanza_utils.getMessageBody(stanza, is_muc, _converse) || undefined;
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
attrs = Object.assign(
{
'chat_state': stanza_utils.getChatState(stanza),
'is_archived': stanza_utils.isArchived(original_stanza),
'is_delayed': !!delay,
'is_single_emoji': text ? await u.isOnlyEmojis(text) : false,
'message': text,
'msgid': stanza.getAttribute('id') || original_stanza.getAttribute('id'),
'references': stanza_utils.getReferences(stanza),
'subject': propertyOf(stanza.querySelector('subject'))('textContent'),
'thread': propertyOf(stanza.querySelector('thread'))('textContent'),
'time': delay ? dayjs(delay.getAttribute('stamp')).toISOString() : (new Date()).toISOString(),
'type': stanza.getAttribute('type')
},
attrs,
stanza_utils.getSenderAttributes(stanza, chatbox, _converse),
stanza_utils.getOutOfBandAttributes(stanza),
stanza_utils.getSpoilerAttributes(stanza),
stanza_utils.getCorrectionAttributes(stanza, original_stanza)
)
return attrs;
}
}
export default stanza_utils;