2021-07-05 18:22:55 +02:00
|
|
|
import URI from 'urijs';
|
2020-12-14 15:24:14 +01:00
|
|
|
import dayjs from 'dayjs';
|
2021-07-05 18:22:55 +02:00
|
|
|
import log from '@converse/headless/log';
|
2020-12-14 15:24:14 +01:00
|
|
|
import sizzle from 'sizzle';
|
|
|
|
import { Strophe } from 'strophe.js/src/strophe';
|
2021-07-07 14:59:29 +02:00
|
|
|
import { URL_PARSE_OPTIONS } from '@converse/headless/shared/constants.js';
|
2020-12-14 15:24:14 +01:00
|
|
|
import { _converse, api } from '@converse/headless/core';
|
2021-07-15 18:45:16 +02:00
|
|
|
import { decodeHTMLEntities } from '@converse/headless/utils/core.js';
|
2020-12-14 15:24:14 +01:00
|
|
|
import { rejectMessage } from '@converse/headless/shared/actions';
|
2021-07-05 18:22:55 +02:00
|
|
|
import {
|
|
|
|
isAudioURL,
|
2021-09-09 16:20:33 +02:00
|
|
|
isEncryptedFileURL,
|
2021-07-05 18:22:55 +02:00
|
|
|
isImageURL,
|
|
|
|
isVideoURL
|
|
|
|
} from '@converse/headless/utils/url.js';
|
2020-12-14 15:24:14 +01:00
|
|
|
|
|
|
|
const { NS } = Strophe;
|
|
|
|
|
|
|
|
export class StanzaParseError extends Error {
|
|
|
|
constructor (message, stanza) {
|
|
|
|
super(message, stanza);
|
|
|
|
this.name = 'StanzaParseError';
|
|
|
|
this.stanza = stanza;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extract the XEP-0359 stanza IDs from the passed in stanza
|
|
|
|
* and return a map containing them.
|
|
|
|
* @private
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stanza - The message stanza
|
2020-12-14 15:24:14 +01:00
|
|
|
* @returns { Object }
|
|
|
|
*/
|
|
|
|
export function getStanzaIDs (stanza, original_stanza) {
|
|
|
|
const attrs = {};
|
|
|
|
// Store generic stanza ids
|
|
|
|
const sids = sizzle(`stanza-id[xmlns="${Strophe.NS.SID}"]`, stanza);
|
|
|
|
const sid_attrs = sids.reduce((acc, s) => {
|
|
|
|
acc[`stanza_id ${s.getAttribute('by')}`] = s.getAttribute('id');
|
|
|
|
return acc;
|
|
|
|
}, {});
|
|
|
|
Object.assign(attrs, sid_attrs);
|
|
|
|
|
|
|
|
// Store the archive id
|
|
|
|
const result = sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
|
|
|
|
if (result) {
|
|
|
|
const by_jid = original_stanza.getAttribute('from') || _converse.bare_jid;
|
|
|
|
attrs[`stanza_id ${by_jid}`] = result.getAttribute('id');
|
|
|
|
}
|
|
|
|
|
|
|
|
// Store the origin id
|
|
|
|
const origin_id = sizzle(`origin-id[xmlns="${Strophe.NS.SID}"]`, stanza).pop();
|
|
|
|
if (origin_id) {
|
|
|
|
attrs['origin_id'] = origin_id.getAttribute('id');
|
|
|
|
}
|
|
|
|
return attrs;
|
|
|
|
}
|
|
|
|
|
2021-11-23 22:56:27 +01:00
|
|
|
export function getEncryptionAttributes (stanza) {
|
2021-07-05 14:56:20 +02:00
|
|
|
const eme_tag = sizzle(`encryption[xmlns="${Strophe.NS.EME}"]`, stanza).pop();
|
|
|
|
const namespace = eme_tag?.getAttribute('namespace');
|
|
|
|
const attrs = {};
|
|
|
|
if (namespace) {
|
|
|
|
attrs.is_encrypted = true;
|
|
|
|
attrs.encryption_namespace = namespace;
|
2021-11-23 22:56:27 +01:00
|
|
|
} else if (sizzle(`encrypted[xmlns="${Strophe.NS.OMEMO}"]`, stanza).pop()) {
|
|
|
|
attrs.is_encrypted = true;
|
|
|
|
attrs.encryption_namespace = Strophe.NS.OMEMO;
|
2020-12-14 15:24:14 +01:00
|
|
|
}
|
|
|
|
return attrs;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @private
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stanza - The message stanza
|
|
|
|
* @param { Element } original_stanza - The original stanza, that contains the
|
2020-12-14 15:24:14 +01:00
|
|
|
* message stanza, if it was contained, otherwise it's the message stanza itself.
|
|
|
|
* @returns { Object }
|
|
|
|
*/
|
|
|
|
export function 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 {
|
|
|
|
'editable': false,
|
|
|
|
'retracted': time,
|
|
|
|
'retracted_id': applies_to_id
|
|
|
|
};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
const tombstone = sizzle(`> retracted[xmlns="${Strophe.NS.RETRACT}"]`, stanza).pop();
|
|
|
|
if (tombstone) {
|
|
|
|
return {
|
|
|
|
'editable': false,
|
|
|
|
'is_tombstone': true,
|
|
|
|
'retracted': tombstone.getAttribute('stamp')
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getCorrectionAttributes (stanza, original_stanza) {
|
|
|
|
const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
|
|
|
|
if (el) {
|
|
|
|
const replace_id = el.getAttribute('id');
|
|
|
|
if (replace_id) {
|
|
|
|
const delay = sizzle(`delay[xmlns="${Strophe.NS.DELAY}"]`, original_stanza).pop();
|
|
|
|
const time = delay ? dayjs(delay.getAttribute('stamp')).toISOString() : new Date().toISOString();
|
|
|
|
return {
|
|
|
|
replace_id,
|
|
|
|
'edited': time
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2021-02-17 15:59:44 +01:00
|
|
|
export function getOpenGraphMetadata (stanza) {
|
|
|
|
const fastening = sizzle(`> apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
|
|
|
|
if (fastening) {
|
|
|
|
const applies_to_id = fastening.getAttribute('id');
|
|
|
|
const meta = sizzle(`> meta[xmlns="${Strophe.NS.XHTML}"]`, fastening);
|
2021-02-22 21:27:30 +01:00
|
|
|
if (meta.length) {
|
2021-03-02 10:41:24 +01:00
|
|
|
const msg_limit = api.settings.get('message_limit');
|
2021-03-01 23:39:14 +01:00
|
|
|
const data = meta.reduce((acc, el) => {
|
2021-02-22 21:27:30 +01:00
|
|
|
const property = el.getAttribute('property');
|
|
|
|
if (property) {
|
2021-03-02 10:41:24 +01:00
|
|
|
let value = decodeHTMLEntities(el.getAttribute('content') || '');
|
|
|
|
if (msg_limit && property === 'og:description' && value.length >= msg_limit) {
|
|
|
|
value = `${value.slice(0, msg_limit)}${decodeHTMLEntities('…')}`;
|
|
|
|
}
|
|
|
|
acc[property] = value;
|
2021-02-22 21:27:30 +01:00
|
|
|
}
|
|
|
|
return acc;
|
|
|
|
}, {
|
|
|
|
'ogp_for_id': applies_to_id,
|
|
|
|
});
|
2021-03-01 23:39:14 +01:00
|
|
|
|
|
|
|
if ("og:description" in data || "og:title" in data || "og:image" in data) {
|
|
|
|
return data;
|
|
|
|
}
|
2021-02-22 21:27:30 +01:00
|
|
|
}
|
2021-02-17 15:59:44 +01:00
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2021-07-05 18:22:55 +02:00
|
|
|
|
2022-02-14 19:08:26 +01:00
|
|
|
export function getMediaURLsMetadata (text, offset=0) {
|
2021-07-05 18:22:55 +02:00
|
|
|
const objs = [];
|
|
|
|
if (!text) {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
try {
|
|
|
|
URI.withinString(
|
|
|
|
text,
|
|
|
|
(url, start, end) => {
|
2021-09-09 16:20:33 +02:00
|
|
|
if (url.startsWith('_')) {
|
|
|
|
url = url.slice(1);
|
|
|
|
start += 1;
|
|
|
|
}
|
|
|
|
if (url.endsWith('_')) {
|
|
|
|
url = url.slice(0, url.length-1);
|
|
|
|
end -= 1;
|
|
|
|
}
|
2022-02-14 19:08:26 +01:00
|
|
|
objs.push({ url, 'start': start+offset, 'end': end+offset });
|
2021-07-05 18:22:55 +02:00
|
|
|
return url;
|
|
|
|
},
|
2021-07-07 14:59:29 +02:00
|
|
|
URL_PARSE_OPTIONS
|
2021-07-05 18:22:55 +02:00
|
|
|
);
|
|
|
|
} catch (error) {
|
|
|
|
log.debug(error);
|
|
|
|
}
|
2021-09-09 16:20:33 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef { Object } MediaURLMetadata
|
|
|
|
* An object representing the metadata of a URL found in a chat message
|
|
|
|
* The actual URL is not saved, it can be extracted via the `start` and `end` indexes.
|
|
|
|
* @property { Boolean } is_audio
|
|
|
|
* @property { Boolean } is_image
|
|
|
|
* @property { Boolean } is_video
|
|
|
|
* @property { String } end
|
|
|
|
* @property { String } start
|
|
|
|
*/
|
|
|
|
const media_urls = objs
|
|
|
|
.map(o => ({
|
|
|
|
'end': o.end,
|
|
|
|
'is_audio': isAudioURL(o.url),
|
|
|
|
'is_image': isImageURL(o.url),
|
|
|
|
'is_video': isVideoURL(o.url),
|
|
|
|
'is_encrypted': isEncryptedFileURL(o.url),
|
|
|
|
'start': o.start
|
|
|
|
|
|
|
|
}));
|
2021-07-05 18:22:55 +02:00
|
|
|
return media_urls.length ? { media_urls } : {};
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2020-12-14 15:24:14 +01:00
|
|
|
export function getSpoilerAttributes (stanza) {
|
|
|
|
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
|
|
|
|
return {
|
|
|
|
'is_spoiler': !!spoiler,
|
|
|
|
'spoiler_hint': spoiler?.textContent
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getOutOfBandAttributes (stanza) {
|
|
|
|
const xform = sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza).pop();
|
|
|
|
if (xform) {
|
|
|
|
return {
|
|
|
|
'oob_url': xform.querySelector('url')?.textContent,
|
|
|
|
'oob_desc': xform.querySelector('desc')?.textContent
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the human readable error message contained in a `groupchat` message stanza of type `error`.
|
|
|
|
* @private
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stanza - The message stanza
|
2020-12-14 15:24:14 +01:00
|
|
|
*/
|
|
|
|
export function getErrorAttributes (stanza) {
|
|
|
|
if (stanza.getAttribute('type') === 'error') {
|
|
|
|
const error = stanza.querySelector('error');
|
|
|
|
const text = sizzle(`text[xmlns="${Strophe.NS.STANZAS}"]`, error).pop();
|
|
|
|
return {
|
|
|
|
'is_error': true,
|
|
|
|
'error_text': text?.textContent,
|
|
|
|
'error_type': error.getAttribute('type'),
|
|
|
|
'error_condition': error.firstElementChild.nodeName
|
|
|
|
};
|
|
|
|
}
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
2022-07-19 11:57:07 +02:00
|
|
|
/**
|
|
|
|
* Given a message stanza, find and return any XEP-0372 references
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stana - The message stanza
|
2022-07-19 11:57:07 +02:00
|
|
|
* @returns { Reference }
|
|
|
|
*/
|
2020-12-14 15:24:14 +01:00
|
|
|
export function getReferences (stanza) {
|
|
|
|
return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
|
2021-08-13 10:18:24 +02:00
|
|
|
const anchor = ref.getAttribute('anchor');
|
|
|
|
const text = stanza.querySelector(anchor ? `#${anchor}` : 'body')?.textContent;
|
|
|
|
if (!text) {
|
|
|
|
log.warn(`Could not find referenced text for ${ref}`);
|
|
|
|
return null;
|
|
|
|
}
|
2020-12-14 15:24:14 +01:00
|
|
|
const begin = ref.getAttribute('begin');
|
|
|
|
const end = ref.getAttribute('end');
|
2022-07-19 11:57:07 +02:00
|
|
|
/**
|
|
|
|
* @typedef { Object } Reference
|
|
|
|
* An object representing XEP-0372 reference data
|
|
|
|
* @property { string } begin
|
|
|
|
* @property { string } end
|
|
|
|
* @property { string } type
|
|
|
|
* @property { String } value
|
|
|
|
* @property { String } uri
|
|
|
|
*/
|
2020-12-14 15:24:14 +01:00
|
|
|
return {
|
2022-07-19 11:57:07 +02:00
|
|
|
begin, end,
|
2020-12-14 15:24:14 +01:00
|
|
|
'type': ref.getAttribute('type'),
|
|
|
|
'value': text.slice(begin, end),
|
|
|
|
'uri': ref.getAttribute('uri')
|
|
|
|
};
|
2021-08-13 10:18:24 +02:00
|
|
|
}).filter(r => r);
|
2020-12-14 15:24:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function getReceiptId (stanza) {
|
|
|
|
const receipt = sizzle(`received[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).pop();
|
|
|
|
return receipt?.getAttribute('id');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether the passed in stanza is a XEP-0280 Carbon
|
|
|
|
* @private
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stanza - The message stanza
|
2020-12-14 15:24:14 +01:00
|
|
|
* @returns { Boolean }
|
|
|
|
*/
|
|
|
|
export function isCarbon (stanza) {
|
|
|
|
const xmlns = Strophe.NS.CARBONS;
|
|
|
|
return (
|
|
|
|
sizzle(`message > received[xmlns="${xmlns}"]`, stanza).length > 0 ||
|
|
|
|
sizzle(`message > sent[xmlns="${xmlns}"]`, stanza).length > 0
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the XEP-0085 chat state contained in a message stanza
|
|
|
|
* @private
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stanza - The message stanza
|
2020-12-14 15:24:14 +01:00
|
|
|
*/
|
|
|
|
export function getChatState (stanza) {
|
|
|
|
return sizzle(
|
|
|
|
`
|
|
|
|
composing[xmlns="${NS.CHATSTATES}"],
|
|
|
|
paused[xmlns="${NS.CHATSTATES}"],
|
|
|
|
inactive[xmlns="${NS.CHATSTATES}"],
|
|
|
|
active[xmlns="${NS.CHATSTATES}"],
|
|
|
|
gone[xmlns="${NS.CHATSTATES}"]`,
|
|
|
|
stanza
|
|
|
|
).pop()?.nodeName;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isValidReceiptRequest (stanza, attrs) {
|
|
|
|
return (
|
|
|
|
attrs.sender !== 'me' &&
|
|
|
|
!attrs.is_carbon &&
|
|
|
|
!attrs.is_archived &&
|
|
|
|
sizzle(`request[xmlns="${Strophe.NS.RECEIPTS}"]`, stanza).length
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-09-09 11:11:43 +02:00
|
|
|
/**
|
|
|
|
* Check whether the passed-in stanza is a forwarded message that is "bare",
|
|
|
|
* i.e. it's not forwarded as part of a larger protocol, like MAM.
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stanza
|
2021-09-09 11:11:43 +02:00
|
|
|
*/
|
|
|
|
export function throwErrorIfInvalidForward (stanza) {
|
2020-12-14 15:24:14 +01:00
|
|
|
const bare_forward = sizzle(`message > forwarded[xmlns="${Strophe.NS.FORWARD}"]`, stanza).length;
|
|
|
|
if (bare_forward) {
|
|
|
|
rejectMessage(stanza, 'Forwarded messages not part of an encapsulating protocol are not supported');
|
|
|
|
const from_jid = stanza.getAttribute('from');
|
2021-09-09 11:11:43 +02:00
|
|
|
throw new StanzaParseError(`Ignoring unencapsulated forwarded message from ${from_jid}`, stanza);
|
2020-12-14 15:24:14 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether the passed in stanza is a XEP-0333 Chat Marker
|
|
|
|
* @private
|
|
|
|
* @method getChatMarker
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stanza - The message stanza
|
2020-12-14 15:24:14 +01:00
|
|
|
* @returns { Boolean }
|
|
|
|
*/
|
|
|
|
export function getChatMarker (stanza) {
|
|
|
|
// If we receive more than one marker (which shouldn't happen), we take
|
|
|
|
// the highest level of acknowledgement.
|
|
|
|
return sizzle(`
|
|
|
|
acknowledged[xmlns="${Strophe.NS.MARKERS}"],
|
|
|
|
displayed[xmlns="${Strophe.NS.MARKERS}"],
|
|
|
|
received[xmlns="${Strophe.NS.MARKERS}"]`,
|
|
|
|
stanza
|
|
|
|
).pop();
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isHeadline (stanza) {
|
|
|
|
return stanza.getAttribute('type') === 'headline';
|
|
|
|
}
|
|
|
|
|
|
|
|
export function isServerMessage (stanza) {
|
2020-12-21 13:45:04 +01:00
|
|
|
if (sizzle(`mentions[xmlns="${Strophe.NS.MENTIONS}"]`, stanza).pop()) {
|
|
|
|
return false;
|
|
|
|
}
|
2020-12-14 15:24:14 +01:00
|
|
|
const from_jid = stanza.getAttribute('from');
|
|
|
|
if (stanza.getAttribute('type') !== 'error' && from_jid && !from_jid.includes('@')) {
|
|
|
|
// Some servers (e.g. Prosody) don't set the stanza
|
|
|
|
// type to "headline" when sending server messages.
|
|
|
|
// For now we check if an @ signal is included, and if not,
|
|
|
|
// we assume it's a headline stanza.
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether the passed in stanza is a XEP-0313 MAM stanza
|
|
|
|
* @private
|
|
|
|
* @method isArchived
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stanza - The message stanza
|
2020-12-14 15:24:14 +01:00
|
|
|
* @returns { Boolean }
|
|
|
|
*/
|
|
|
|
export function isArchived (original_stanza) {
|
|
|
|
return !!sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns an object containing all attribute names and values for a particular element.
|
|
|
|
* @method getAttributes
|
2023-02-23 07:03:50 +01:00
|
|
|
* @param { Element } stanza
|
2020-12-14 15:24:14 +01:00
|
|
|
* @returns { Object }
|
|
|
|
*/
|
|
|
|
export function getAttributes (stanza) {
|
|
|
|
return stanza.getAttributeNames().reduce((acc, name) => {
|
|
|
|
acc[name] = Strophe.xmlunescape(stanza.getAttribute(name));
|
|
|
|
return acc;
|
|
|
|
}, {});
|
|
|
|
}
|