|
@@ -2,14 +2,17 @@
|
|
|
* @module:headless-shared-parsers
|
|
|
* @typedef {module:headless-shared-parsers.Reference} Reference
|
|
|
*/
|
|
|
+import sizzle from 'sizzle';
|
|
|
import _converse from './_converse.js';
|
|
|
import api from './api/index.js';
|
|
|
import dayjs from 'dayjs';
|
|
|
import log from '../log.js';
|
|
|
-import sizzle from 'sizzle';
|
|
|
import { Strophe } from 'strophe.js';
|
|
|
import { decodeHTMLEntities } from '../utils/html.js';
|
|
|
-import { rejectMessage } from './actions';
|
|
|
+import { getAttributes } from '../utils/stanza.js';
|
|
|
+import { rejectMessage } from './actions.js';
|
|
|
+import { XFORM_TYPE_MAP, XFORM_VALIDATE_TYPE_MAP } from './constants.js';
|
|
|
+
|
|
|
|
|
|
const { NS } = Strophe;
|
|
|
|
|
@@ -77,11 +80,10 @@ export function getEncryptionAttributes (stanza) {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * @private
|
|
|
- * @param { Element } stanza - The message stanza
|
|
|
- * @param { Element } original_stanza - The original stanza, that contains the
|
|
|
+ * @param {Element} stanza - The message stanza
|
|
|
+ * @param {Element} original_stanza - The original stanza, that contains the
|
|
|
* message stanza, if it was contained, otherwise it's the message stanza itself.
|
|
|
- * @returns { Object }
|
|
|
+ * @returns {Object}
|
|
|
*/
|
|
|
export function getRetractionAttributes (stanza, original_stanza) {
|
|
|
const fastening = sizzle(`> apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
|
|
@@ -110,6 +112,10 @@ export function getRetractionAttributes (stanza, original_stanza) {
|
|
|
return {};
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * @param {Element} stanza
|
|
|
+ * @param {Element} original_stanza
|
|
|
+ */
|
|
|
export function getCorrectionAttributes (stanza, original_stanza) {
|
|
|
const el = sizzle(`replace[xmlns="${Strophe.NS.MESSAGE_CORRECT}"]`, stanza).pop();
|
|
|
if (el) {
|
|
@@ -126,6 +132,9 @@ export function getCorrectionAttributes (stanza, original_stanza) {
|
|
|
return {};
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * @param {Element} stanza
|
|
|
+ */
|
|
|
export function getOpenGraphMetadata (stanza) {
|
|
|
const fastening = sizzle(`> apply-to[xmlns="${Strophe.NS.FASTEN}"]`, stanza).pop();
|
|
|
if (fastening) {
|
|
@@ -156,6 +165,9 @@ export function getOpenGraphMetadata (stanza) {
|
|
|
}
|
|
|
|
|
|
|
|
|
+/**
|
|
|
+ * @param {Element} stanza
|
|
|
+ */
|
|
|
export function getSpoilerAttributes (stanza) {
|
|
|
const spoiler = sizzle(`spoiler[xmlns="${Strophe.NS.SPOILER}"]`, stanza).pop();
|
|
|
return {
|
|
@@ -164,6 +176,9 @@ export function getSpoilerAttributes (stanza) {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * @param {Element} stanza
|
|
|
+ */
|
|
|
export function getOutOfBandAttributes (stanza) {
|
|
|
const xform = sizzle(`x[xmlns="${Strophe.NS.OUTOFBAND}"]`, stanza).pop();
|
|
|
if (xform) {
|
|
@@ -177,8 +192,7 @@ export function getOutOfBandAttributes (stanza) {
|
|
|
|
|
|
/**
|
|
|
* Returns the human readable error message contained in a `groupchat` message stanza of type `error`.
|
|
|
- * @private
|
|
|
- * @param { Element } stanza - The message stanza
|
|
|
+ * @param {Element} stanza - The message stanza
|
|
|
*/
|
|
|
export function getErrorAttributes (stanza) {
|
|
|
if (stanza.getAttribute('type') === 'error') {
|
|
@@ -197,7 +211,7 @@ export function getErrorAttributes (stanza) {
|
|
|
/**
|
|
|
* Given a message stanza, find and return any XEP-0372 references
|
|
|
* @param {Element} stanza - The message stanza
|
|
|
- * @returns { Reference }
|
|
|
+ * @returns {Reference}
|
|
|
*/
|
|
|
export function getReferences (stanza) {
|
|
|
return sizzle(`reference[xmlns="${Strophe.NS.REFERENCE}"]`, stanza).map(ref => {
|
|
@@ -237,9 +251,8 @@ export function getReceiptId (stanza) {
|
|
|
|
|
|
/**
|
|
|
* Determines whether the passed in stanza is a XEP-0280 Carbon
|
|
|
- * @private
|
|
|
- * @param { Element } stanza - The message stanza
|
|
|
- * @returns { Boolean }
|
|
|
+ * @param {Element} stanza - The message stanza
|
|
|
+ * @returns {Boolean}
|
|
|
*/
|
|
|
export function isCarbon (stanza) {
|
|
|
const xmlns = Strophe.NS.CARBONS;
|
|
@@ -251,8 +264,7 @@ export function isCarbon (stanza) {
|
|
|
|
|
|
/**
|
|
|
* Returns the XEP-0085 chat state contained in a message stanza
|
|
|
- * @private
|
|
|
- * @param { Element } stanza - The message stanza
|
|
|
+ * @param {Element} stanza - The message stanza
|
|
|
*/
|
|
|
export function getChatState (stanza) {
|
|
|
return sizzle(
|
|
@@ -266,6 +278,10 @@ export function getChatState (stanza) {
|
|
|
).pop()?.nodeName;
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * @param {Element} stanza
|
|
|
+ * @param {Object} attrs
|
|
|
+ */
|
|
|
export function isValidReceiptRequest (stanza, attrs) {
|
|
|
return (
|
|
|
attrs.sender !== 'me' &&
|
|
@@ -340,3 +356,204 @@ export function isServerMessage (stanza) {
|
|
|
export function isArchived (original_stanza) {
|
|
|
return !!sizzle(`message > result[xmlns="${Strophe.NS.MAM}"]`, original_stanza).pop();
|
|
|
}
|
|
|
+
|
|
|
+/**
|
|
|
+ * @typedef {Object} XFormReportedField
|
|
|
+ * @property {string} var
|
|
|
+ * @property {string} label
|
|
|
+ *
|
|
|
+ * @typedef {Object} XFormResultItemField
|
|
|
+ * @property {string} var
|
|
|
+ * @property {string} value
|
|
|
+ *
|
|
|
+ * @typedef {Object} XFormOption
|
|
|
+ * @property {string} value
|
|
|
+ * @property {string} label
|
|
|
+ * @property {boolean} selected
|
|
|
+ * @property {boolean} required
|
|
|
+ *
|
|
|
+ * @typedef {Object} XFormCaptchaURI
|
|
|
+ * @property {string} type
|
|
|
+ * @property {string} data
|
|
|
+ *
|
|
|
+ * @typedef {'list-single'|'list-multi'} XFormListTypes
|
|
|
+ * @typedef {'jid-single'|'jid-multi'} XFormJIDTypes
|
|
|
+ * @typedef {'text-multi'|'text-private'|'text-single'} XFormTextTypes
|
|
|
+ * @typedef {XFormListTypes|XFormJIDTypes|XFormTextTypes|'fixed'|'boolean'|'url'|'hidden'} XFormFieldTypes
|
|
|
+ *
|
|
|
+ * @typedef {Object} XFormField
|
|
|
+ * @property {string} var
|
|
|
+ * @property {string} label
|
|
|
+ * @property {XFormFieldTypes} [type]
|
|
|
+ * @property {string} [text]
|
|
|
+ * @property {string} [value]
|
|
|
+ * @property {boolean} [required]
|
|
|
+ * @property {boolean} [checked]
|
|
|
+ * @property {XFormOption[]} [options]
|
|
|
+ * @property {XFormCaptchaURI} [uri]
|
|
|
+ *
|
|
|
+ * @typedef {'result'|'form'} XFormResponseType
|
|
|
+ *
|
|
|
+ * @typedef {Object} XForm
|
|
|
+ * @property {XFormResponseType} type
|
|
|
+ * @property {string} [title]
|
|
|
+ * @property {string} [instructions]
|
|
|
+ * @property {XFormReportedField[]} [reported]
|
|
|
+ * @property {XFormResultItemField[][]} [items]
|
|
|
+ * @property {XFormField[]} [fields]
|
|
|
+ */
|
|
|
+
|
|
|
+/**
|
|
|
+ * @param {Element} field
|
|
|
+ * @param {Element} stanza
|
|
|
+ * @return {XFormField}
|
|
|
+ */
|
|
|
+function parseXFormField(field, stanza) {
|
|
|
+ const v = field.getAttribute('var');
|
|
|
+ const label = field.getAttribute('label') || '';
|
|
|
+ const type = field.getAttribute('type');
|
|
|
+
|
|
|
+ if (type === 'list-single' || type === 'list-multi') {
|
|
|
+ const values = Array.from(field.querySelectorAll(':scope > value')).map((el) => el?.textContent);
|
|
|
+ const options = Array.from(field.querySelectorAll(':scope > option')).map(
|
|
|
+ (/** @type {HTMLElement} */ option) => {
|
|
|
+ const value = option.querySelector('value')?.textContent;
|
|
|
+ return {
|
|
|
+ value,
|
|
|
+ label: option.getAttribute('label'),
|
|
|
+ selected: values.includes(value),
|
|
|
+ required: !!field.querySelector('required'),
|
|
|
+ };
|
|
|
+ }
|
|
|
+ );
|
|
|
+ return {
|
|
|
+ type,
|
|
|
+ options,
|
|
|
+ label: field.getAttribute('label'),
|
|
|
+ var: v,
|
|
|
+ required: !!field.querySelector('required'),
|
|
|
+ };
|
|
|
+ } else if (type === 'fixed') {
|
|
|
+ const text = field.querySelector('value')?.textContent;
|
|
|
+ return { text, label, type, var: v };
|
|
|
+ } else if (type === 'jid-multi') {
|
|
|
+ return {
|
|
|
+ type,
|
|
|
+ var: v,
|
|
|
+ label,
|
|
|
+ value: field.querySelector('value')?.textContent,
|
|
|
+ required: !!field.querySelector('required'),
|
|
|
+ };
|
|
|
+ } else if (type === 'boolean') {
|
|
|
+ const value = field.querySelector('value')?.textContent;
|
|
|
+ return {
|
|
|
+ type,
|
|
|
+ var: v,
|
|
|
+ label,
|
|
|
+ checked: ((value === '1' || value === 'true') && true) || false,
|
|
|
+ };
|
|
|
+ } else if (v === 'url') {
|
|
|
+ return {
|
|
|
+ var: v,
|
|
|
+ label,
|
|
|
+ value: field.querySelector('value')?.textContent,
|
|
|
+ };
|
|
|
+ } else if (v === 'username') {
|
|
|
+ return {
|
|
|
+ var: v,
|
|
|
+ label,
|
|
|
+ value: field.querySelector('value')?.textContent,
|
|
|
+ required: !!field.querySelector('required'),
|
|
|
+ type: getInputType(field),
|
|
|
+ };
|
|
|
+ } else if (v === 'password') {
|
|
|
+ return {
|
|
|
+ var: v,
|
|
|
+ label,
|
|
|
+ value: field.querySelector('value')?.textContent,
|
|
|
+ required: !!field.querySelector('required'),
|
|
|
+ };
|
|
|
+ } else if (v === 'ocr') { // Captcha
|
|
|
+ const uri = field.querySelector('uri');
|
|
|
+ const el = sizzle('data[cid="' + uri.textContent.replace(/^cid:/, '') + '"]', stanza)[0];
|
|
|
+ return {
|
|
|
+ label: field.getAttribute('label'),
|
|
|
+ var: v,
|
|
|
+ uri: {
|
|
|
+ type: uri.getAttribute('type'),
|
|
|
+ data: el?.textContent,
|
|
|
+ },
|
|
|
+ required: !!field.querySelector('required'),
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ return {
|
|
|
+ label,
|
|
|
+ var: v,
|
|
|
+ required: !!field.querySelector('required'),
|
|
|
+ value: field.querySelector('value')?.textContent,
|
|
|
+ type: getInputType(field),
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * @param {Element} field
|
|
|
+ */
|
|
|
+export function getInputType(field) {
|
|
|
+ const type = XFORM_TYPE_MAP[field.getAttribute('type')]
|
|
|
+ if (type == 'text') {
|
|
|
+ const datatypes = field.getElementsByTagNameNS("http://jabber.org/protocol/xdata-validate", "validate");
|
|
|
+ if (datatypes.length === 1) {
|
|
|
+ const datatype = datatypes[0].getAttribute("datatype");
|
|
|
+ return XFORM_VALIDATE_TYPE_MAP[datatype] || type;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return type;
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+* @param {Element} stanza
|
|
|
+* @returns {XForm}
|
|
|
+*/
|
|
|
+export function parseXForm(stanza) {
|
|
|
+ const xs = sizzle(`x[xmlns="${Strophe.NS.XFORM}"]`, stanza);
|
|
|
+ if (xs.length > 1) {
|
|
|
+ log.error(stanza);
|
|
|
+ throw new Error('Invalid stanza');
|
|
|
+ } else if (xs.length === 0) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ const x = xs[0];
|
|
|
+ const type = /** @type {XFormResponseType} */ (x.getAttribute('type'));
|
|
|
+ const result = {
|
|
|
+ type,
|
|
|
+ title: x.querySelector('title')?.textContent,
|
|
|
+ };
|
|
|
+
|
|
|
+ if (type === 'result') {
|
|
|
+ const reported = x.querySelector(':scope > reported');
|
|
|
+ const reported_fields = reported?.querySelectorAll(':scope > field');
|
|
|
+ const items = x.querySelectorAll(':scope > item');
|
|
|
+ return /** @type {XForm} */({
|
|
|
+ ...result,
|
|
|
+ reported: /** @type {XFormReportedField[]} */ (Array.from(reported_fields).map(getAttributes)),
|
|
|
+ items: Array.from(items).map((item) => {
|
|
|
+ return Array.from(item.querySelectorAll('field')).map((field) => {
|
|
|
+ return /** @type {XFormResultItemField} */ ({
|
|
|
+ ...getAttributes(field),
|
|
|
+ value: field.querySelector('value')?.textContent ?? '',
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ } else if (type === 'form') {
|
|
|
+ return {
|
|
|
+ ...result,
|
|
|
+ instructions: x.querySelector('instructions')?.textContent,
|
|
|
+ fields: Array.from(x.querySelectorAll('field')).map((field) => parseXFormField(field, stanza)),
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ throw new Error(`Invalid type in XForm response stanza: ${type}`);
|
|
|
+ }
|
|
|
+}
|