From b12e7a9a55d7030dcceb14db8144812ed66e25e0 Mon Sep 17 00:00:00 2001 From: mrfry Date: Sun, 2 Apr 2023 09:45:39 +0200 Subject: [PATCH] p2p functionality --- .eslintrc.js | 5 +- stable.user.js | 535 +++++++++++++++++++++++++++++++++++-------------- 2 files changed, 383 insertions(+), 157 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index a19f6e7..7dd0e8d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,8 +2,9 @@ module.exports = { env: { browser: true, es6: true, - node: true, - jest: true, + }, + parserOptions: { + ecmaVersion: 8, }, extends: ['eslint:recommended'], globals: { diff --git a/stable.user.js b/stable.user.js index 0024d1c..6bd7bd1 100755 --- a/stable.user.js +++ b/stable.user.js @@ -46,7 +46,7 @@ // : Script header {{{ // ==UserScript== // @name Moodle/Elearning/KMOOC test help -// @version 2.1.3.15 +// @version 2.1.4.0 // @description Online Moodle/Elearning/KMOOC test help // @author MrFry // @match https://elearning.uni-obuda.hu/* @@ -72,6 +72,7 @@ // @match https://v39.moodle.uniduna.hu/* // @match https://mentok.net/* // @match https://qmining.frylabs.net/* +// @match https://frylabs.net/* // @noframes // @run-at document-start // @grant GM_getResourceText @@ -83,16 +84,25 @@ // @grant GM_openInTab // @grant unsafeWindow // @license GNU General Public License v3.0 or later -// @supportURL qmining.frylabs.net -// @contributionURL qmining.frylabs.net -// @namespace https://qmining.frylabs.net -// @updateURL https://qmining.frylabs.net/moodle-test-userscript/stable.user.js?up +// @supportURL frylabs.net +// @contributionURL frylabs.net +// @namespace https://frylabs.net +// @updateURL https://frylabs.net/moodle-test-userscript/stable.user.js?up // ==/UserScript== // : }}} ;(function () { - // : ESLINT bs {{{ + // CONFIG + let originalServer = { host: 'frylabs.net', port: 443 } + const logElementGetting = false + const logEnabled = true + const motdShowCount = 5 // Ammount of times to show motd + let infoExpireTime = 60 * 5 // Every n seconds basic info should be (re)loaded from server + let p2pInfoExpireTime = 60 * 60 * 24 + const messageOpacityDelta = 0.1 + const minMessageOpacity = 0.2 + // : ESLINT bs {{{ // eslint-disable-line // GM functions, only to disable ESLINT errors /* eslint-disable */ @@ -125,31 +135,21 @@ // Devel vars // ------------------------------------------------------------------------------ // forcing pages for testing. unless you test, do not set these to true! - const isDevel = true + const isDevel = false const forcedMatchString = isDevel ? 'default' : null // only one of these should be true for testing const forceTestPage = isDevel && false - const forceResultPage = isDevel && true + const forceResultPage = isDevel && false const forceDefaultPage = isDevel && false // ------------------------------------------------------------------------------ - const logElementGetting = false - const logEnabled = true - const showErrors = true - + let serverAdress = getPeerUrl(originalServer) + let apiAdress = getPeerUrl(originalServer) + 'api/' var addEventListener // add event listener function - let serverAdress = 'https://qmining.frylabs.net/' - let apiAdress = 'https://api.frylabs.net/' - - const motdShowCount = 5 /* Ammount of times to show motd */ - const messageOpacityDelta = 0.1 - const minMessageOpacity = 0.2 - let infoExpireTime = 60 * 5 // Every n seconds basic info should be loaded from server var motd = '' var lastestVersion = '' var subjInfo - var uiShowing = true - + let uiShowing = getVal('uishowing') // array, where elems are added to shadow-root, but its position should be at target. var updatableElements = [] // { elem: ..., target: ... } var elementUpdaterInterval = -1 @@ -158,8 +158,29 @@ if (isDevel) { warn('Moodle script running in developement mode!') infoExpireTime = 1 - serverAdress = 'http://localhost:8080/' - apiAdress = 'http://localhost:8080/' + p2pInfoExpireTime = 1 + originalServer = { host: 'localhost', port: 8080 } + serverAdress = getPeerUrl(originalServer) + apiAdress = getPeerUrl(originalServer) + 'api/' + setVal('motdcount', 5) + } + + const peerToUseString = getVal('peerToUse') + var usingPeer = false + if (peerToUseString) { + try { + const peerToUse = JSON.parse(peerToUseString) + + if (peerToUse && peerToUse.host && peerToUse.port) { + const url = getPeerUrl(peerToUse) + debugLog('Using saved peer url: ' + url) + serverAdress = url + apiAdress = url + 'api/' + usingPeer = true + } + } catch (e) { + debugLog('peerToUse not JSON') + } } const currUrl = location.href.includes('file:///') @@ -171,8 +192,7 @@ const huTexts = { fatalError: 'Fatál error. Check console (f12). Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez! (új böngésző tab-ban)', - consoleErrorInfo: - 'Itteni hibák 100% a moodle hiba. Kivéve, ha oda van írva hogy script error ;) Ha ilyesmi szerepel itt, akkor olvasd el a segítség szekciót! https://qmining.frylabs.net/manual?scriptcmd', + consoleErrorInfo: `Itteni hibák 100% a moodle hiba. Kivéve, ha oda van írva hogy script error ;) Ha ilyesmi szerepel itt, akkor olvasd el a segítség szekciót! ${serverAdress}`, freshStartWarning: '

Moodle teszt userscript:

1.5.0 verzió: a script mostantól XMLHTTP kéréseket küld szerver fele! Erre a userscript futtató kiegészítőd is figyelmeztetni fog! Ha ez történik, a script rendes működése érdekében engedélyezd (Always allow domain)! Ha nem akarod, hogy ez történjen, akkor ne engedélyezd, vagy a menüben válaszd ki a "helyi fájl használata" opciót!

Elküldött adatok: minden teszt után a kérdés-válasz páros. Fogadott adatok: Az összes eddig ismert kérdés. Érdemes help-et elolvasni!!!

Ez az ablak frissités után eltűnik. Ha nem, akkor a visza gombbal próbálkozz.
', noResult: @@ -193,20 +213,24 @@ dataEditorTitle: 'Adatbázisban lévő kérdések szerkesztése', invalidPW: 'Hibás jelszó: ', search: 'Keresés ...', - loading: 'Betöltés ...', + connecting: 'Csatlakozás: ', login: 'Belépés', newPWTitle: 'Új jelszó új felhasználónak', pwRequest: 'Jelszó új felhasználónak', noServer: 'Nem elérhető a szerver!', + tryingPeer: 'Csatlakozás: ', + noPeersOnline: 'Egy peer sem elérhető!', + peerTryingError: 'Hiba peerek keresése közben!', + usingpeer: `A script eredeti szervere megszűnt, vagy nem elérhető, így egy másik peert használ! Hogy továbbra is kapj frissítéseekt újra kell telepítened a scriptet. További infó: ${serverAdress}faq?tab=newpeer`, pwHere: 'Jelszó ...', - noServerConsoleMessage: `Nem elérhető a szerver, vagy kis eséllyel kezeletlen hiba történt! Ha elérhető a weboldal, akkor ott meg bírod nézni a kérdéseket itt: ${serverAdress}legacy`, + noServerConsoleMessage: `Nem elérhető a szerver! Ha elérhető a weboldal, akkor ott meg bírod nézni a kérdéseket itt: ${serverAdress}allQuestions`, + unhandledErrorConsoleMessage: `Kezeletlen hiba történt! Ha elérhető a weboldal, akkor ott meg bírod nézni a kérdéseket itt: ${serverAdress}allQuestions`, noParseableQuestionResult: 'A tesztben nem találhatók kérdések, amit fel lehet dolgozni, vagy hiba történt feldolgozásuk közben', unableToParseTestQuestion: 'Hiba történt a kérdések beolvasása közben :/ Kattints az üzenetre a manuális kereséshez (új böngésző tab-ban)', loadingAnswer: 'Válaszok betöltése ...', - reinstallFromCorrectSource: - 'Scriptet nem a qmining weboldalról raktad fel. Könnyebb kezelhetőség szempontjából kérlek onnan telepítsd. Részletes leírás', + reinstallFromCorrectSource: `Scriptet nem a qmining weboldalról raktad fel. Könnyebb kezelhetőség szempontjából kérlek onnan telepítsd. Részletes leírás`, versionUpdated: 'Verzió frissítve ', newVersionAvaible: 'Új verzió elérhető: ', userSpecifitMotdAvailable: @@ -1463,10 +1487,6 @@ log('Moodle / E-Learning script') preventWindowClose() - console.log( - '%c Moodle / E-Learning script', - 'font-weight: bold; font-size: 50px;color: yellow; text-shadow: 3px 3px 0 rgb(245,221,8) , 6px 6px 0 rgb(226,91,14) , 9px 9px 0 rgb(217,31,38) , 12px 12px 0 rgb(5,148,68) , 15px 15px 0 rgb(2,135,206) , 18px 18px 0 rgb(4,77,145) , 21px 21px 0 rgb(42,21,113)' - ) if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', Init) @@ -1694,13 +1714,13 @@ SetupMenu() ShowMenu() } - ConnectToServer(AfterLoad) + ConnectToServer() } function Auth(pw) { post('login', { pw: pw, script: true }).then((res) => { if (res.result === 'success') { - ConnectToServer(AfterLoad) + ConnectToServer() clearAllMessages() resetMenu() } else { @@ -1722,44 +1742,137 @@ elem.style.display = 'none' }) SafeGetElementById('infoMainDiv', (elem) => { - elem.innerText = texts.loading + elem.innerText = texts.connecting + getShortServerURL(serverAdress) }) } - function ConnectToServer(cwith) { + function ConnectToServer() { clearAllMessages() GetXHRInfos() .then((inf) => { - if (inf.result === 'nouser') { - NoUserAction() - return - } - lastestVersion = inf.version.replace(/\n/g, '') - motd = inf.motd - if (getUid() !== inf.uid) { + try { + if (inf.result === 'nouser') { + NoUserAction() + return + } + lastestVersion = inf.version.replace(/\n/g, '') + motd = inf.motd + if (getUid() !== inf.uid) { + setVal('userId', inf.uid) + } + subjInfo = inf.subjinfo setVal('userId', inf.uid) + overlay.querySelector( + '#infoMainDiv' + ).innerText = `${subjInfo.subjects.toLocaleString()} tárgy, ${subjInfo.questions.toLocaleString()} kérdés. UID: #${getUid()}\n${getShortServerURL( + serverAdress + )}` + if (inf.unreads.length > 0) { + overlay.querySelector('#mailButton').innerText = '📬' + } + + getPeers().catch(() => warn('unable to get p2p info')) + AfterLoad() + } catch (e) { + warn(texts.unhandledErrorConsoleMessage) + warn(e) } - subjInfo = inf.subjinfo - setVal('userId', inf.uid) - overlay.querySelector( - '#infoMainDiv' - ).innerText = `${subjInfo.subjects.toLocaleString( - 'hu' - )} tárgy, ${subjInfo.questions.toLocaleString( - 'hu' - )} kérdés.\nUser ID: ${getUid()}` - if (inf.unreads.length > 0) { - overlay.querySelector('#mailButton').innerText = '📬' - } - // FIXME: if cwith() throws an unhandled error it sais server is not avaible - cwith() }) - .catch(() => { - NoServerAction() + .catch((e) => { warn(texts.noServerConsoleMessage) + warn(e) + tryAnotherPeer() }) } + async function tryAnotherPeer() { + SafeGetElementById('scriptMenuDiv', (elem) => { + elem.style.backgroundColor = 'red' + }) + + debugLog('Unable to connect to main server, trying peers') + try { + const lastp2pinfo = JSON.parse(getVal('lastp2pinfo')) + if ( + !lastp2pinfo || + (Array.isArray(lastp2pinfo) && lastp2pinfo.length === 0) + ) { + debugLog('No saved p2p info available!') + return + } + debugLog('Saved peers: ', lastp2pinfo) + const shuffledPeers = lastp2pinfo.sort(() => 0.5 - Math.random()) + if (usingPeer) { + debugLog('Added original server') + shuffledPeers.unshift(originalServer) + } + + let suitablePeer = null + let i = 0 + while (suitablePeer === null && i < shuffledPeers.length) { + const peer = shuffledPeers[i] + i++ + + const url = getPeerUrl(peer) + SafeGetElementById('infoMainDiv', (elem) => { + elem.innerText = texts.tryingPeer + getShortServerURL(url) + }) + debugLog('Trying ' + url) + + try { + const res = await head(url) + if (res.status === 401) { + debugLog(url + ' responded with ' + res.status) + } else if (res.status === 200) { + suitablePeer = peer + } + } catch (e) { + debugLog('Unable to connect!') + } + } + + if (suitablePeer) { + const url = getPeerUrl(suitablePeer) + debugLog('Found suitable peer with URL: ' + url + 'index: ' + i) + serverAdress = url + apiAdress = url + 'api/' + if ( + suitablePeer.host === originalServer.host && + suitablePeer.port === originalServer.port + ) { + setVal('peerToUse', undefined) + usingPeer = false + } else { + setVal('peerToUse', JSON.stringify(suitablePeer)) + usingPeer = true + } + + SafeGetElementById('scriptMenuDiv', (elem) => { + elem.style.backgroundColor = '#222426' + }) + + ConnectToServer() + } else { + SafeGetElementById('retryContainer', (elem) => { + elem.style.display = 'flex' + }) + SafeGetElementById('infoMainDiv', (elem) => { + elem.innerText = texts.noPeersOnline + }) + debugLog('None of the peers are online!') + } + } catch (e) { + SafeGetElementById('retryContainer', (elem) => { + elem.style.display = 'flex' + }) + SafeGetElementById('infoMainDiv', (elem) => { + elem.innerText = texts.peerTryingError + }) + warn('Error ocurred during trying to connect to peers!') + warn(e) + } + } + function NoUserAction() { SafeGetElementById('scriptMenuDiv', (elem) => { elem.style.backgroundColor = '#44cc00' @@ -1772,18 +1885,6 @@ }) } - function NoServerAction() { - SafeGetElementById('scriptMenuDiv', (elem) => { - elem.style.backgroundColor = 'red' - }) - SafeGetElementById('infoMainDiv', (elem) => { - elem.innerText = texts.noServer - }) - SafeGetElementById('retryContainer', (elem) => { - elem.style.display = 'flex' - }) - } - function VersionActions() { FreshStart() } @@ -1836,9 +1937,6 @@ let timeout = null const greetMsg = [] - if (isNewVersionAvaible || newVersion || showMOTD) { - greetMsg.push(texts.scriptName + info().script.version) - } if (isNewVersionAvaible) { timeout = 5 greetMsg.push(texts.newVersionAvaible + lastestVersion) @@ -1852,12 +1950,14 @@ greetMsg.push(texts.motd + motd) timeout = null } + if (usingPeer) { + greetMsg.push(texts.usingpeer) + } + if (greetMsg.length > 0) { + greetMsg.unshift(texts.scriptName + info().script.version) + } - ShowMessage( - greetMsg.join('\n'), - - timeout - ) + ShowMessage(greetMsg.join('\n'), timeout) } // : }}} @@ -2167,6 +2267,8 @@ } function getConvertedMessageNode(message) { + if (!message) return '' + const messageNode = document.createElement('p') const resultNode = document.createElement('p') messageNode.innerHTML = message.replace(/\n/g, '
') @@ -2263,6 +2365,7 @@ true ) uiShowing = !uiShowing + setVal('uishowing', uiShowing) } function SetupMenu() { @@ -2315,6 +2418,7 @@ width: width + 'px', opacity: getVal(`${id}_opacity`), cursor: funct ? 'pointer' : 'move', + display: uiShowing ? '' : 'none', }) if (funct) { addEventListener(messageElem, 'click', funct) @@ -2517,7 +2621,14 @@ result.msgContainer.child.leftSideDiv.child.matchPercent.elem.innerText = isNaN(getCurrMsg().p) ? '' : getCurrMsg().p + '%' - result.msgContainer.child.msgDiv.elem.innerText = getCurrMsg().m + if (isSimpleMessage) { + result.msgContainer.child.msgDiv.elem.replaceChildren() + result.msgContainer.child.msgDiv.elem.appendChild( + getConvertedMessageNode(getCurrMsg().m) + ) + } else { + result.msgContainer.child.msgDiv.elem.innerText = getCurrMsg().m + } } catch (e) { warn('Error in message updating') warn(e) @@ -2545,6 +2656,8 @@ const id = 'scriptMenuDiv' scriptMenuDiv.setAttribute('id', id) SetStyle(scriptMenuDiv, { + display: uiShowing ? 'flex' : 'none', + flexDirection: 'column', width: '300px', height: '90px', position: 'fixed', @@ -2599,9 +2712,8 @@ style: { position: 'absolute', display: 'inline', + bottom: '20px', left: '5px', - bottom: '15px', - margin: '5px', fontSize: '30px', cursor: 'pointer', }, @@ -2643,72 +2755,82 @@ }, }, }, - infoContainer: { - id: 'infoMainDiv', - innerText: texts.loading, + statusContainer: { style: { - color: '#ffffff', - margin: '5px 50px', - textAlign: 'center', - }, - }, - loginContainer: { - id: 'loginDiv', - style: { - display: 'none', + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + flex: 1, margin: '0px 50px', }, children: { - loginInput: { - customElem: () => { - const loginInput = document.createElement('input') - loginInput.setAttribute('id', 'pwInput') - loginInput.type = 'text' - loginInput.placeholder = texts.pwHere - SetStyle(loginInput, { - width: '100%', - textAlign: 'center', - }) - return loginInput - }, - }, - loginButton: { - innerText: texts.login, - style: buttonStyle, - onClick: () => { - SafeGetElementById('pwInput', (elem) => { - Auth(elem.value) - }) - }, - }, - }, - }, - retryContainer: { - id: 'retryContainer', - style: { - display: 'none', - justifyContent: 'center', - }, - children: { - retryButton: { - innerText: texts.retry, + infoContainer: { + id: 'infoMainDiv', + innerText: texts.connecting + getShortServerURL(serverAdress), style: { - position: '', - padding: '0px 8px', - border: '1px solid #333', - borderRadius: '2px', color: '#ffffff', - cursor: 'pointer', + textAlign: 'center', }, - onClick: () => { - scriptMenuDiv.style.background = '#262626' - SafeGetElementById('infoMainDiv', (elem) => { - elem.innerText = texts.loading - }) - SafeGetElementById('retryContainer', (elem) => { - elem.style.display = 'none' - }) - ConnectToServer(AfterLoad) + }, + loginContainer: { + id: 'loginDiv', + style: { + display: 'none', + }, + children: { + loginInput: { + customElem: () => { + const loginInput = document.createElement('input') + loginInput.setAttribute('id', 'pwInput') + loginInput.type = 'text' + loginInput.placeholder = texts.pwHere + SetStyle(loginInput, { + width: '100%', + textAlign: 'center', + }) + return loginInput + }, + }, + loginButton: { + innerText: texts.login, + style: buttonStyle, + onClick: () => { + SafeGetElementById('pwInput', (elem) => { + Auth(elem.value) + }) + }, + }, + }, + }, + retryContainer: { + id: 'retryContainer', + style: { + display: 'none', + justifyContent: 'center', + }, + children: { + retryButton: { + innerText: texts.retry, + style: { + position: '', + padding: '0px 8px', + border: '1px solid #333', + borderRadius: '2px', + color: '#ffffff', + cursor: 'pointer', + }, + onClick: () => { + scriptMenuDiv.style.background = '#262626' + SafeGetElementById('infoMainDiv', (elem) => { + elem.innerText = + texts.connecting + getShortServerURL(serverAdress) + }) + SafeGetElementById('retryContainer', (elem) => { + elem.style.display = 'none' + }) + ConnectToServer() + }, + }, }, }, }, @@ -2801,6 +2923,24 @@ log('------------------------------------------') } + function getShortServerURL(url) { + const maxlegnth = 30 + const shortUrl = url.replace('https://', '').replace('http://', '') + if (shortUrl.length <= maxlegnth) { + return shortUrl + } else { + return shortUrl.substring(0, maxlegnth - 3) + '...' + } + } + + function getPeerUrl(peer) { + let protocol = 'https://' + if (isDevel) { + let protocol = 'http://' + } + return protocol + peer.host + ':' + peer.port + '/' + } + function getUid() { return getVal('userId') } @@ -2896,22 +3036,12 @@ const now = new Date().getTime() const lastCheck = getVal('lastInfoCheckTime') if (!lastCheck) { - setVal('lastInfoCheckTime', now) + setVal('lastInfoCheckTime', 0) } let lastInfo = { result: 'noLastInfo' } - try { - lastInfo = JSON.parse(getVal('lastInfo')) - } catch (e) { - if (showErrors) { - warn(e) - } - } - if ( - lastInfo.result !== 'success' || - now > lastCheck + infoExpireTime * 1000 - ) { + if (now > lastCheck + infoExpireTime * 1000) { return new Promise((resolve, reject) => { const url = apiAdress + @@ -2923,11 +3053,11 @@ Promise.all([get(url), get(apiAdress + 'hasNewMsg')]) .then(([{ responseText: infos }, { responseText: hasNewMsg }]) => { try { - setVal('lastInfoCheckTime', now) const infosObj = JSON.parse(infos) const hasNewMsgsObj = JSON.parse(hasNewMsg) const merged = Object.assign({}, infosObj, hasNewMsgsObj) setVal('lastInfo', JSON.stringify(merged)) + setVal('lastInfoCheckTime', now) resolve(merged) } catch (e) { log('Errro paring JSON in GetXHRInfos') @@ -2944,6 +3074,7 @@ } else { return new Promise((resolve, reject) => { try { + lastInfo = JSON.parse(getVal('lastInfo')) resolve(lastInfo) } catch (e) { log('Errro paring JSON in GetXHRInfos, when using old data!') @@ -2954,12 +3085,102 @@ } } + function updateP2pData(newData) { + const lastp2pinfo = getVal('lastp2pinfo') + const oldPeers = lastp2pinfo ? JSON.parse(lastp2pinfo) : [] + const merged = newData.reduce((acc, peer) => { + const peerAlreadyExists = acc.find((existingPeer) => { + const p1 = peer.host + ':' + peer.port + const p2 = existingPeer.host + ':' + existingPeer.port + return p1 === p2 + }) + + if (!peerAlreadyExists) { + peer.added = new Date().getTime() + return [peer, ...acc] + } + + return acc + }, oldPeers) + return merged + } + + function getPeers() { + const now = new Date().getTime() + const lastCheck = getVal('lastp2pchecktime') + if (!lastCheck) { + setVal('lastp2pchecktime', 0) + } + + let lastp2pinfo = {} + + if (now > lastCheck + p2pInfoExpireTime * 1000) { + return new Promise((resolve, reject) => { + const url = apiAdress + 'p2pinfo' + + get(url) + .then(({ responseText: p2pinfo }) => { + try { + const p2pinfoObj = updateP2pData(JSON.parse(p2pinfo).myPeers) + setVal('lastp2pinfo', JSON.stringify(p2pinfoObj)) + setVal('lastp2pchecktime', now) + resolve(p2pinfoObj) + } catch (e) { + log('Errro paring JSON in getPeers') + log(p2pinfo) + log(e) + reject(e) + } + }) + .catch((e) => { + log('Info get Error', e) + reject(e) + }) + }) + } else { + return new Promise((resolve, reject) => { + try { + lastp2pinfo = JSON.parse(getVal('lastp2pinfo')) + resolve(lastp2pinfo) + } catch (e) { + log('Errro paring JSON in GetXHRInfos, when using old data!') + log(e) + reject(e) + } + }) + } + } + + function head(url) { + return new Promise((resolve, reject) => { + xmlhttpRequest({ + method: 'HEAD', + url: url, + crossDomain: true, + timeout: 5 * 1000, + ontimeout: () => { + reject(new Error('HEAD request timed out')) + }, + onload: function (response) { + resolve(response) + }, + onerror: (e) => { + reject(e) + }, + }) + }) + } + function get(url) { return new Promise((resolve, reject) => { xmlhttpRequest({ method: 'GET', url: url, crossDomain: true, + timeout: 15 * 1000, + ontimeout: () => { + reject(new Error('GET request timed out')) + }, xhrFields: { withCredentials: true }, headers: { 'Content-Type': 'application/json', @@ -2984,6 +3205,10 @@ method: 'POST', url: url, crossDomain: true, + timeout: 30 * 1000, + ontimeout: () => { + reject(new Error('POST request timed out')) + }, xhrFields: { withCredentials: true }, data: message, headers: {