p2p functionality

This commit is contained in:
mrfry 2023-04-02 09:45:39 +02:00
parent 3ad9e897f3
commit b12e7a9a55
2 changed files with 383 additions and 157 deletions

View file

@ -2,8 +2,9 @@ module.exports = {
env: {
browser: true,
es6: true,
node: true,
jest: true,
},
parserOptions: {
ecmaVersion: 8,
},
extends: ['eslint:recommended'],
globals: {

View file

@ -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:
'<h1>Moodle teszt userscript:<h1><h3>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!</h3> <h3>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!!!</h3><h5>Ez az ablak frissités után eltűnik. Ha nem, akkor a visza gombbal próbálkozz.</h5>',
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ó: <a href="${serverAdress}faq?tab=newpeer">${serverAdress}faq?tab=newpeer</a>`,
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. <a href="https://qmining.frylabs.net/manual.html#scriptreinstall">Részletes leírás</a>',
reinstallFromCorrectSource: `Scriptet nem a qmining weboldalról raktad fel. Könnyebb kezelhetőség szempontjából kérlek onnan telepítsd. <a href="${serverAdress}">Részletes leírás</a>`,
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, '</br>')
@ -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: {