moodle-test-userscript/stable.user.js
2021-04-25 11:06:07 +02:00

2630 lines
75 KiB
JavaScript
Executable file

// vim:foldmethod=marker
/* ----------------------------------------------------------------------------
Online Moodle/Elearning/KMOOC test help
GitLab: <https://gitlab.com/MrFry/moodle-test-userscript>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
------------------------------------------------------------------------- */
// : Install info {{{
// ===============================================================================
// ===============================================================================
//
// HA EZT LÁTOD, ÉS TELEPÍTENI AKARTAD A SCRIPTET, AKKOR
// NINCS USERSCRIPT KEZELŐ BŐVÍTMÉNYED.
//
// Telepíts egy userscript kezelőt, például a Tampermonkey-t:
// https://www.tampermonkey.net/
//
// ===============================================================================
//
// IF YOU ARE SEEING THIS MESSAGE, AND YOU WANTED TO
// INSTALL THIS SCRIPT, THEN YOU DON'T HAVE ANY USERSCRIPT
// MANAGER INSTALLED.
//
// Install a userscript manager, for example Tampermonkey:
// https://www.tampermonkey.net/
//
// ===============================================================================
// ===============================================================================
// : }}}
// : Script header {{{
// ==UserScript==
// @name Moodle/Elearning/KMOOC test help
// @version 2.1.1.5
// @description Online Moodle/Elearning/KMOOC test help
// @author MrFry
// @match https://elearning.uni-obuda.hu/*
// @match https://exam.elearning.uni-obuda.hu*
// @match https://mooc.unideb.hu/*
// @match https://itc.semmelweis.hu/moodle/*
// @match https://moodle.gtk.uni-pannon.hu/*
// @match https://oktatas.mai.kvk.uni-obuda.hu/*
// @match https://qmining.frylabs.net/*
// @match http://qmining.frylabs.net/*
// @noframes
// @run-at document-start
// @grant GM_getResourceText
// @grant GM_info
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// @grant GM_xmlhttpRequest
// @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
// ==/UserScript==
// : }}}
;(function() {
// : ESLINT bs {{{
// eslint-disable-line
// GM functions, only to disable ESLINT errors
/* eslint-disable */
const a = Main
const usf = unsafeWindow
function getVal(name) {
return GM_getValue(name)
}
function setVal(name, val) {
return GM_setValue(name, val)
}
function delVal(name) {
return GM_deleteValue(name)
}
function openInTab(address) {
GM_openInTab(address, false)
}
function xmlhttpRequest(opts) {
GM_xmlhttpRequest(opts)
}
function info() {
return GM_info
}
/* eslint-enable */
// : }}}
// : Constants and global variables {{{
// ------------------------------------------------------------------------------
// Devel vars
// ------------------------------------------------------------------------------
// forcing pages for testing. unless you test, do not set these to true!
const isDevel = false
setVal('ISDEVEL', isDevel)
// only one of these should be true for testing
const forceTestPage = isDevel && false
const forceResultPage = isDevel && false
const forceDefaultPage = isDevel && false
// ------------------------------------------------------------------------------
const logElementGetting = false
const logEnabled = true
const showErrors = true
var addEventListener // add event listener function
let serverAdress = 'https://qmining.frylabs.net/'
let apiAdress = 'https://api.frylabs.net/'
// const ircAddress = 'https://kiwiirc.com/nextclient/irc.sub.fm/#qmining'
const motdShowCount = 3 /* Ammount of times to show motd */
const messageOpacityDelta = 0.1
const minMessageOpacity = 0.2
let infoExpireTime = 60 // Every n seconds basic info should be loaded from server
var motd = ''
var userSpecificMotd = undefined
var lastestVersion = ''
var subjInfo
// array, where elems are added to shadow-root, but its position should be at target.
var updatableElements = [] // { elem: ..., target: ... }
var elementUpdaterInterval = -1
const overlayElemUpdateInterval = 2 // seconds
if (getVal('ISDEVEL')) {
log('Moodle script running in developement mode!')
infoExpireTime = 1
serverAdress = 'http://localhost:8080/'
apiAdress = 'http://localhost:8080/'
}
const currUrl = location.href.includes('file:///')
? 'https://elearning.uni-obuda.hu/'
: location.href
const menuButtons = {
website: {
title: 'Weboldal',
onClick: () => {
openInTab(serverAdress + 'menuClick')
},
},
help: {
title: 'Help',
onClick: () => {
ShowHelp()
},
},
donate: {
title: 'Donate',
onClick: () => {
openInTab(serverAdress + 'donate?scriptMenu', {
active: true,
})
},
},
}
// : Localisation {{{
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',
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:
'Nincs találat :( Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez! (új böngésző tab-ban)',
videoHelp: 'Miután elindítottad: Play/pause: space. Seek: Bal/jobb nyíl.',
menuButtonText: 'Kérdések Menu',
couldntLoadDataPopupMenuText:
'A kérdéseket nem lehetett beolvasni, ellenőrizd hogy elérhető-e a szerver',
showGreetingOnEveryPage: 'Üdvözlő üzenet mutatása minden oldalon',
close: 'Bezárás',
help: 'Help',
websiteBugreport: 'Weboldal',
contribute: 'Bug report / Szavazás következő feature-re',
ranklist: 'Ranklista',
donate: 'Donate',
retry: 'Újrapróbálás',
dataEditor: 'Data editor',
dataEditorTitle: 'Adatbázisban lévő kérdések szerkesztése',
invalidPW: 'Hibás jelszó: ',
search: 'Keresés ...',
loading: 'Betöltés ...',
login: 'Belépés',
newPWTitle: 'Új jelszó új felhasználónak',
pwRequest: 'Jelszó új felhasználónak',
noServer: 'Nem elérhető a szerver!',
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`,
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>',
versionUpdated: 'Verzió frissítve ',
newVersionAvaible: 'Új verzió elérhető: ',
userSpecifitMotdAvailable:
'Új üzeneted van, kattints 📬-ra bal alul megtekintéséhez!',
scriptName: 'Moodle/Elearning/KMOOC segéd ',
userMOTD: 'Felhasználó MOTD (ezt csak te látod):\n',
motd: 'MOTD:\n',
}
var texts = huTexts
// : }}}
// : }}}
// : HTML parsers {{{
// : Basic processing helpers {{{
function getTextPromisesFromNode(node) {
return Array.from(node.childNodes).reduce((promises, elem) => {
let img = elem
if (elem.tagName !== 'IMG') {
const t = elem.tagName ? elem.getElementsByTagName('img') : []
if (t.length > 0) {
img = t[0]
}
}
const select = elem.tagName ? elem.getElementsByTagName('select') : []
if (select.length > 0) {
// test: 2c1d92a7-0ea2-4990-9451-7f19299bbbe4
const question = []
Array.from(elem.childNodes).forEach(cn => {
if (cn.nodeValue) {
question.push(cn.nodeValue)
}
})
promises.push({
type: 'txt',
val: question.join('...'),
node: select[0],
})
return promises
}
if (img.tagName === 'IMG') {
promises.push(
new Promise(resolve => {
digestMessage(getBase64Image(img)).then(res => {
resolve({
type: 'img',
val: res,
node: img,
})
})
})
)
} else if (elem.tagName === undefined) {
promises.push({ type: 'txt', val: elem.nodeValue, node: elem })
} else {
promises.push({ type: 'txt', val: elem.innerText, node: elem })
}
return promises
}, [])
}
function makeTextFromElements(acc, item) {
if (emptyOrWhiteSpace(item.val)) {
return acc
}
if (item.type === 'img') {
acc.push('[' + item.val + ']')
} else {
acc.push(item.val)
}
return acc
}
function getImagesFromElements(elements) {
return elements.reduce((acc, element) => {
if (element.type === 'img') {
// FIXME: include check needed?
if (!acc.includes(element.val)) {
acc.push({ val: element.val, node: element.node })
}
}
return acc
}, [])
}
function getLegacyImageID(imgArray) {
try {
return imgArray.map(img => {
if (!img.src.includes('brokenfile')) {
let filePart = img.src.split('/')
filePart = filePart[filePart.length - 1]
// shorten string
let result = ''
let i = 0
while (i < filePart.length && i < 30) {
result += filePart[i]
i++
}
return decodeURI(result)
}
})
} catch (e) {
log("Couldn't get images from result (old)")
}
}
function getCurrentSubjectName() {
if (logElementGetting) {
log('getting current subjects name')
}
return document.getElementById('page-header').innerText.split('\n')[0] || ''
}
// : }}}
// : Test page processing functions {{{
function handleQuiz() {
const { removeMessage: removeLoadingMessage } = ShowMessage(
texts.loadingAnswer
)
getQuizData()
.then(readQuestions => {
if (readQuestions.length === 0) {
warn('readQuestions length is zero, no questions found on page!')
ShowMessage(
texts.unableToParseTestQuestion,
undefined,
() => {
OpenErrorPage({
message: 'No result found',
})
}
)
return
}
const questions = readQuestions.map(question => {
return {
Q: question.question,
possibleAnswers: question.possibleAnswers,
data: question.data,
}
})
const sentData = {
questions: questions,
subj: getCurrentSubjectName(),
testUrl: currUrl,
version: info().script.version,
cid: getCid(),
uid: getUid(),
}
log(sentData)
post('ask', sentData).then(results => {
removeLoadingMessage()
ShowAnswers(
results.map((res, i) => {
return {
answers: res.answers,
question: readQuestions[i],
}
})
)
})
})
.catch(err => {
warn(err)
warn('Error in handleQuiz()')
})
}
const questionNodeVariants = {
formFirst: {
criteria: () => {
const questionNodes = Array.from(
document.getElementsByTagName('form')[0].childNodes[0].childNodes
)
// test: e2c01ff4-d97a-4ab9-8f7f-e28812541097
const notOnlyTextNodes = questionNodes.every(node => {
return node.tagName !== undefined
})
return notOnlyTextNodes && questionNodes.length > 0
},
getter: () => {
return Array.from(
document.getElementsByTagName('form')[0].childNodes[0].childNodes
)
},
},
formSecond: {
criteria: () => {
const questionNodes = Array.from(
document.getElementsByTagName('form')[1].childNodes[0].childNodes
)
return questionNodes.length > 0
},
getter: () => {
return Array.from(
document.getElementsByTagName('form')[1].childNodes[0].childNodes
)
},
},
}
function getQuestionNodes() {
try {
let questionNodes
Object.keys(questionNodeVariants).some(key => {
const variant = questionNodeVariants[key]
if (variant.criteria()) {
questionNodes = variant.getter()
if (questionNodes.length === 0) {
warn(
`question nodes ${key} criteria was true, but result is an empty array!`
)
} else {
return true
}
}
})
return questionNodes
} catch (e) {
warn('Error in getQuestionNodes')
warn(e)
}
}
function getQuizData() {
return new Promise(resolve => {
// TODO: dropdown in question
// TODO: get possible answers too
const promises = []
const questionNodes = getQuestionNodes()
let i = 0
while (
i < questionNodes.length &&
questionNodes[i].tagName === 'DIV' &&
questionNodes[i].className !== 'submitbtns'
) {
promises.push(getQuestionPromiseForSingleQuestion(questionNodes[i]))
i++
}
Promise.all(promises)
.then(result => {
const errorsRemoved = result.reduce((acc, res) => {
if (res.success) {
acc.push(res)
}
return acc
}, [])
resolve(errorsRemoved)
})
.catch(err => {
warn('Error in handleQuiz()')
warn(err)
})
})
}
function getPossibleAnswersFromTest(node) {
try {
const promises = []
let answerRoot = node.getElementsByClassName('answer')[0]
if (!answerRoot) {
answerRoot = node.getElementsByClassName('subquestion')[0]
if (answerRoot) {
// FIXME: is this needed, what is this lol
const options = Array.from(answerRoot.getElementsByTagName('option'))
const possibleAnswers = options.reduce((acc, option) => {
if (!emptyOrWhiteSpace(option.innerText)) {
acc.push([{ type: 'txt', val: option.innerText }])
}
return acc
}, [])
return possibleAnswers
} else {
const select = node.getElementsByTagName('select')[0]
if (select) {
const options = []
Array.from(select).forEach(opt => {
if (!emptyOrWhiteSpace(opt.innerText)) {
options.push([{ type: 'txt', val: opt.innerText }])
}
})
return options
}
}
} else if (answerRoot.tagName === 'DIV') {
const answers = Array.from(answerRoot.childNodes)
answers.forEach(answer => {
if (answer.tagName) {
promises.push(getTextPromisesFromNode(answer))
}
})
return promises
} else if (answerRoot.tagName === 'TABLE') {
const answers = Array.from(answerRoot.childNodes[0].childNodes)
// test: 002203ca-581b-445c-b45d-85374f212e8e NOT WORING
answers.forEach(answer => {
if (answer.tagName) {
// test: 817434df-a103-4edc-870e-c9ac953404dc
promises.push(
getTextPromisesFromNode(answer.getElementsByClassName('text')[0])
)
// here elements with classname 'control' could be added too. Those should be a dropdown,
// containing possible choices
}
})
return promises
}
} catch (e) {
warn('Error in getPossibleAnswersFromTest()!')
warn(e)
}
}
function getImgNodesFromArray(arr) {
return arr.reduce((acc, x) => {
if (Array.isArray(x)) {
x.forEach(y => {
if (y.type === 'img') {
acc.push(y.node)
}
})
} else {
if (x.type === 'img') {
acc.push(x.node)
}
}
return acc
}, [])
}
const rootVariants = {
qtext: {
criteria: node => {
return node.getElementsByClassName('qtext').length > 0
},
getter: node => {
return node.getElementsByClassName('qtext')[0]
},
},
subquestion: {
criteria: node => {
return node.getElementsByClassName('subquestion').length > 0
},
getter: node => {
return node.getElementsByClassName('subquestion')[0].parentNode
},
},
content: {
// test: 002203ca-581b-445c-b45d-85374f212e8e
criteria: node => {
return node.getElementsByClassName('content').length > 0
},
getter: node => {
const content = node.getElementsByClassName('content')[0].childNodes[0]
const pNodes = Array.from(content.childNodes).filter(node => {
return node.tagName === 'P'
})
const parent = document.createElement('div')
pNodes.forEach(node => {
parent.appendChild(node.cloneNode(true))
})
return pNodes[0]
},
},
}
function getQuestionRootNode(node) {
try {
let qtextNode
Object.keys(rootVariants).some(key => {
const variant = rootVariants[key]
if (variant.criteria(node)) {
qtextNode = variant.getter(node)
if (!qtextNode) {
warn(
`question root node ${key} criteria was true, but result is null`
)
} else {
return true
}
}
})
return qtextNode
} catch (e) {
warn('Error in getQuestionRootNode')
warn(e)
}
}
function getQuestionPromiseForSingleQuestion(node) {
return new Promise(resolve => {
try {
const qtextNode = getQuestionRootNode(node)
const questionPromises = getTextPromisesFromNode(qtextNode)
const possibleAnswerPromises = getPossibleAnswersFromTest(node)
const unflattenedPossibleAnswerPromises = possibleAnswerPromises
? possibleAnswerPromises.map(x => {
return Promise.all(x)
})
: []
Promise.all([
Promise.all(questionPromises),
Promise.all(unflattenedPossibleAnswerPromises),
])
.then(([question, possibleAnswerArray]) => {
const questionText = removeUnnecesarySpaces(
question.reduce(makeTextFromElements, []).join(' ')
)
const possibleAnswers = possibleAnswerArray.map(x => {
return removeUnnecesarySpaces(
x.reduce(makeTextFromElements, []).join(' ')
)
})
const images = getImagesFromElements([
...question,
...possibleAnswerArray.reduce((acc, x) => {
return [...acc, ...x]
}, []),
])
const imageNodes = getImgNodesFromArray([
...question,
...possibleAnswerArray,
])
const data = getDataFromTest(
node,
images,
getLegacyImageID(imageNodes)
)
resolve({
question: questionText,
possibleAnswers: possibleAnswers,
images: images,
data: data,
success: true,
})
})
.catch(err => {
warn('Error in getQuestionPromiseForSingleQuestion()')
warn(err)
resolve({ success: false })
})
} catch (err) {
warn('Error in getQuestionPromiseForSingleQuestion()')
warn(err)
resolve({ success: false })
}
})
}
function getDataFromTest(node, hashedImages, legacyImages) {
if (hashedImages.length > 0) {
return {
type: 'image',
hashedImages: hashedImages.map(x => {
return x.val
}),
images: legacyImages,
}
} else {
return {
type: 'simple',
}
}
}
// : }}}
// : Result page processing functions {{{
const resultNodeVariants = questionNodeVariants
function getResultNodes() {
try {
let resultNodes
Object.keys(resultNodeVariants).some(key => {
const variant = resultNodeVariants[key]
if (variant.criteria()) {
resultNodes = variant.getter()
if (resultNodes.length === 0) {
warn(
`result nodes ${key} criteria was true, but result is an empty array!`
)
} else {
return true
}
}
})
return resultNodes
} catch (e) {
warn('Error in getResultNodes')
warn(e)
}
}
function getQuiz() {
return new Promise(resolve => {
const promises = []
const questionNodes = getResultNodes()
// let questionNodes = Array.from(
// document.getElementsByTagName('form')[0].childNodes[0].childNodes
// )
// if (questionNodes.length === 0) {
// questionNodes = Array.from(
// document.getElementsByTagName('form')[1].childNodes[0].childNodes
// )
// }
let i = 0
while (i < questionNodes.length && questionNodes[i].tagName === 'DIV') {
promises.push(getQuizFromNode(questionNodes[i]))
i++
}
Promise.all(promises)
.then(result => {
const errorsRemoved = result.reduce((acc, res) => {
if (res.success) {
acc.push(res)
}
return acc
}, [])
resolve(errorsRemoved)
})
.catch(err => {
warn('Error in getQuiz()')
warn(err)
})
})
}
function getPromisesThatMeetsRequirements(getters, node) {
let res
Object.keys(getters).some(key => {
const getter = getters[key]
if (getter.requirement(node)) {
try {
res = getter.getterFunction(node)
return true
} catch (e) {
log(`${key} failed`)
}
} else {
log(`${key} did not pass`)
}
})
return res
}
function getQuizFromNode(node) {
return new Promise(resolve => {
try {
const questionPromises = getPromisesThatMeetsRequirements(
questionGetters,
node
)
const answerPromises = getPromisesThatMeetsRequirements(
answerGetters,
node
)
const possibleAnswers = getPossibleAnswers(node)
if (!answerPromises || !questionPromises) {
log('Answer or question array is empty, skipping question')
resolve({ success: false })
}
Promise.all([
Promise.all(questionPromises),
Promise.all(answerPromises),
])
.then(([question, answer]) => {
const questionText = question
.reduce(makeTextFromElements, [])
.join(' ')
const answerText = answer.reduce(makeTextFromElements, []).join(' ')
const images = getImagesFromElements([...question, ...answer])
const result = {
Q: removeUnnecesarySpaces(questionText),
A: removeUnnecesarySpaces(answerText),
data: getDataFromResultImages(images),
success: true,
}
result.data.possibleAnswers = possibleAnswers
resolve(result)
})
.catch(err => {
warn('Error in getQuizFromNode()')
warn(err)
resolve({ success: false })
})
} catch (e) {
warn('Error in getQuizFromNode()!')
warn(e)
warn(node)
}
})
}
function getDataFromResultImages(images) {
if (images && images.length > 0) {
return {
type: 'image',
hashedImages: images.map(x => {
return x.val
}),
}
} else {
return {
type: 'simple',
}
}
}
const questionGetters = {
getSimpleQuestion: {
description: 'Basic question getter',
requirement: node => {
return node.getElementsByClassName('qtext').length > 0
},
getterFunction: node => {
const question = node.getElementsByClassName('qtext')[0]
return getTextPromisesFromNode(question)
},
},
}
const answerGetters = {
getSimpleAnswer: {
description: 'Basic answer getter',
requirement: node => {
return node.getElementsByClassName('rightanswer').length > 0
},
getterFunction: node => {
const answer = node.getElementsByClassName('rightanswer')[0]
return getTextPromisesFromNode(answer)
},
},
noCorrect: {
description: 'Gets correct answer, even if the correct is not shown',
requirement: node => {
return (
node.getElementsByClassName('rightanswer').length === 0 &&
node.getElementsByClassName('answer').length > 0
)
},
getterFunction: node => {
const possibleAnswers = getPossibleAnswers(node)
if (getIfSolutionIsCorrect(node)) {
if (possibleAnswers.length === 2) {
return [
{
type: 'txt',
val: possibleAnswers.find(x => {
return x.selectedByUser === false
}).text,
},
]
}
} else {
const state = node.getElementsByClassName('state')[0]
// TODO: what if in english
if (state && state.innerText === 'Hibás') {
return false
}
return [
{
type: 'txt',
val: possibleAnswers.find(x => {
return x.selectedByUser === true
}).text,
},
]
}
},
},
getDropdownAnswer: {
description: 'Dropdown answer getter',
requirement: node => {
return false
},
getterFunction: node => {
// TODO dropdown kérdés.html
return 'asd'
},
},
getTextareaAnswer: {
description: 'Get complex answer',
requirement: node => {
return false
},
getterFunction: node => {
// TODO Ugrás... bug.html
return 'asd'
},
},
getDragBoxAnswer: {
description: 'Get complex answer',
requirement: node => {
return false
},
getterFunction: node => {
// TODO dragboxes
return 'asd'
},
},
}
function getIfSolutionIsCorrect(node) {
const gradeText = node.getElementsByClassName('grade')[0].innerText
const stateText = node.getElementsByClassName('state')[0].innerText
return !(stateText.includes('Helyes') || !gradeText.includes('0,00'))
}
function getPossibleAnswers(node) {
try {
if (node.getElementsByClassName('answer').length > 0) {
const answerNodes = Array.from(
node.getElementsByClassName('answer')[0].childNodes
)
return answerNodes.reduce((acc, answerNode) => {
let selectedByUser
if (answerNode.childNodes.length > 0) {
selectedByUser = answerNode.childNodes[0].checked
}
const text = removeUnnecesarySpaces(answerNode.innerText)
if (text !== '') {
acc.push({
text: text,
selectedByUser: selectedByUser,
})
}
return acc
}, [])
} else {
const select = node.getElementsByTagName('select')[0]
if (select) {
const options = []
Array.from(select.childNodes).forEach(opt => {
if (!emptyOrWhiteSpace(opt.innerText)) {
options.push(opt.innerText)
}
})
return options
}
}
} catch (e) {
warn('Error in getPossibleAnswers()!')
warn(e)
warn(node)
}
}
function digestMessage(message) {
return new Promise(resolve => {
const encoder = new TextEncoder()
const data = encoder.encode(message)
crypto.subtle.digest('SHA-256', data).then(buf => {
let res = String.fromCharCode.apply(null, new Uint8Array(buf))
res = btoa(res)
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_')
resolve(res)
})
})
}
function getBase64Image(img) {
img.crossOrigin = 'Anonymous'
const canvas = document.createElement('canvas')
canvas.width = img.width
canvas.height = img.height
const ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0)
const dataURL = canvas.toDataURL('image/png')
img.crossOrigin = undefined
return dataURL.replace(/^data:image\/(png|jpg);base64,/, '')
}
// : }}}
// : Misc {{{
function getVideo() {
if (logElementGetting) {
log('getting video stuff')
}
return document.getElementsByTagName('video')[0]
}
function getVideoElement() {
if (logElementGetting) {
log('getting video element')
}
return document.getElementById('videoElement').parentNode
}
// : }}}
// : }}}
// : Stealth by An0 with love {{{
function StealthOverlay() {
//call this before the document scripts
const document = window.document
const neverEqualPlaceholder = Symbol(`never equal`) //block probing for undefined values in the hooks
let shadowRootHost = neverEqualPlaceholder
let shadowRootNewHost = neverEqualPlaceholder
const apply = Reflect.apply //save some things in case they get hooked (only for unsafe contexts)
if (usf.Error.hasOwnProperty('stackTraceLimit')) {
Reflect.defineProperty(usf.Error, 'stackTraceLimit', {
value: undefined,
writable: false,
enumerable: false,
configurable: false,
})
}
const shadowGetHandler = {
apply: (target, thisArg, argumentsList) =>
apply(
target,
thisArg === shadowRootHost ? shadowRootNewHost : thisArg,
argumentsList
),
}
const original_attachShadow = usf.Element.prototype.attachShadow
const attachShadowProxy = new Proxy(original_attachShadow, shadowGetHandler)
usf.Element.prototype.attachShadow = attachShadowProxy
const getShadowRootProxy = new Proxy(
Object.getOwnPropertyDescriptor(usf.Element.prototype, 'shadowRoot').get,
shadowGetHandler
)
Object.defineProperty(usf.Element.prototype, 'shadowRoot', {
get: getShadowRootProxy,
})
const getHostHandler = {
apply: function() {
const result = apply(...arguments)
return result === shadowRootNewHost ? shadowRootHost : result
},
}
const getHostProxy = new Proxy(
Object.getOwnPropertyDescriptor(usf.ShadowRoot.prototype, 'host').get,
getHostHandler
)
Object.defineProperty(usf.ShadowRoot.prototype, 'host', {
get: getHostProxy,
})
const shadowRootSetInnerHtml = Object.getOwnPropertyDescriptor(
ShadowRoot.prototype,
'innerHTML'
).set
const documentFragmentGetChildren = Object.getOwnPropertyDescriptor(
DocumentFragment.prototype,
'children'
).get
const documentGetBody = Object.getOwnPropertyDescriptor(
Document.prototype,
'body'
).get
const nodeAppendChild = Node.prototype.appendChild
const overlay = document.createElement('div')
overlay.style.cssText = 'position:absolute;left:0;top:0'
const addOverlay = () => {
shadowRootHost = apply(documentGetBody, document, [])
const shadowRoot = apply(original_attachShadow, shadowRootHost, [
{ mode: 'closed' },
])
apply(shadowRootSetInnerHtml, shadowRoot, [`<div><slot></slot></div>`])
shadowRootNewHost = apply(documentFragmentGetChildren, shadowRoot, [])[0]
apply(nodeAppendChild, shadowRoot, [overlay])
}
if (!document.body) {
document.addEventListener('DOMContentLoaded', addOverlay)
} else {
addOverlay()
}
return overlay
}
const overlay = StealthOverlay()
function createHoverOver(appendTo) {
const overlayElement = document.createElement('div')
overlay.append(overlayElement)
updatableElements.push({ elem: overlayElement, target: appendTo })
if (elementUpdaterInterval === -1) {
elementUpdaterInterval = setInterval(() => {
updatableElements.forEach(({ elem, target }) => {
let { left, top, width, height } = target.getBoundingClientRect()
left += window.scrollX
top += window.scrollY
SetStyle(elem, {
pointerEvents: 'none',
userSelect: 'none',
position: 'absolute',
zIndex: 999999,
top: top + 'px',
left: left + 'px',
width: width + 'px',
height: height - 10 + 'px',
})
})
}, overlayElemUpdateInterval * 1000)
}
return overlayElement
}
function appendBelowElement(el, toAppend) {
const rect = el.getBoundingClientRect()
const correction = 8
const left = rect.left + window.scrollX - correction
const top = rect.top + window.scrollY - correction
SetStyle(toAppend, {
position: 'absolute',
zIndex: 1,
top: top + 'px',
left: left + 'px',
})
overlay.appendChild(toAppend)
}
// : }}}
// : Main logic stuff {{{
// : Main function {{{
// window.addEventListener("load", () => {})
Main()
function Main() {
'use strict'
log('Moodle / E-Learning script')
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', Init)
} else {
Init()
}
}
function AfterLoad() {
const url = currUrl
try {
if (
(url.includes('/quiz/') && url.includes('attempt.php')) ||
forceTestPage
) {
// if the current page is a test
handleQuiz()
} else if (
(url.includes('/quiz/') && url.includes('review.php')) ||
forceResultPage
) {
// if the current window is a test-s result
HandleResults(url)
} else if (
(!url.includes('/quiz/') &&
!url.includes('review.php') &&
!url.includes('.pdf')) ||
forceDefaultPage
) {
// if the current window is any other window than a quiz or pdf.
HandleUI(url)
}
} catch (e) {
ShowMessage(
texts.fatalError,
undefined,
() => {
OpenErrorPage(e)
}
)
Exception(e, 'script error at main:')
}
if (url.includes('eduplayer')) {
AddVideoHotkeys(url)
} // adding video hotkeys
log(texts.consoleErrorInfo)
}
// : }}}
// : Loading {{{
function HandleQminingSite(url) {
try {
const idInput = document.getElementById('cid')
if (idInput) {
idInput.value = getCid()
}
} catch (e) {
warn('Error filling client ID input', e)
}
try {
const sideLinks = document.getElementById('sideBarLinks')
if (!sideLinks) {
return
}
Array.from(sideLinks.childNodes).forEach(link => {
link.addEventListener('mousedown', () => {
FillFeedbackCID(url, link)
})
})
FillFeedbackCID(
url,
document
.getElementById('sideBarLinks')
.getElementsByClassName('active')[0]
)
} catch (e) {
warn('Error filling client ID input', e)
}
}
function FillFeedbackCID(url, link) {
try {
if (link.id === 'feedback') {
const cidSetInterval = setInterval(() => {
const cid = document.getElementById('cid')
if (cid) {
cid.value = getCid() + '|' + info().script.version
window.clearInterval(cidSetInterval)
}
}, 100)
}
} catch (e) {
warn('Error filling client ID input', e)
}
}
function Init() {
const url = currUrl
if (url.includes(serverAdress.split('/')[2])) {
HandleQminingSite(url)
return
}
try {
addEventListener = (function() {
if (document.addEventListener) {
return function(element, event, handler) {
element.addEventListener(event, handler, false)
}
} else {
return function(element, event, handler) {
element.attachEvent('on' + event, handler)
}
}
})()
} catch (e) {
Exception(e, 'script error at addEventListener:')
}
VersionActions()
if (!url.includes('.pdf')) {
ShowMenu()
}
ConnectToServer(AfterLoad)
}
function Auth(pw) {
post('login', { pw: pw, script: true }).then(res => {
if (res.result === 'success') {
ConnectToServer(AfterLoad)
clearAllMessages()
resetMenu()
} else {
SafeGetElementById('infoMainDiv', elem => {
elem.innerText = texts.invalidPW + pw
})
}
})
}
function resetMenu() {
SafeGetElementById('scriptMenuDiv', elem => {
elem.style.backgroundColor = '#262626'
})
SafeGetElementById('retryContainer', elem => {
elem.style.display = 'none'
})
SafeGetElementById('loginDiv', elem => {
elem.style.display = 'none'
})
SafeGetElementById('infoMainDiv', elem => {
elem.innerText = texts.loading
})
}
function ConnectToServer(cwith) {
clearAllMessages()
GetXHRInfos()
.then(inf => {
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)
userSpecificMotd = inf.userSpecificMotd
if (userSpecificMotd) {
overlay.querySelector('#mailButton').style.cursor = 'pointer'
overlay.querySelector('#mailButton').innerText = userSpecificMotd.seen
? '📭'
: '📬'
}
overlay.querySelector('#infoMainDiv').innerText = `${
subjInfo.subjects
} tárgy, ${subjInfo.questions} kérdés. User ID: ${getUid()}`
// FIXME: if cwith() throws an unhandled error it sais server is not avaible
cwith()
})
.catch(() => {
NoServerAction()
})
}
function NoUserAction() {
SafeGetElementById('scriptMenuDiv', elem => {
elem.style.backgroundColor = '#44cc00'
})
SafeGetElementById('infoMainDiv', elem => {
elem.innerText = ''
})
SafeGetElementById('loginDiv', elem => {
elem.style.display = 'flex'
})
}
function NoServerAction() {
SafeGetElementById('scriptMenuDiv', elem => {
elem.style.backgroundColor = 'red'
})
SafeGetElementById('infoMainDiv', elem => {
elem.innerText = texts.noServer
})
SafeGetElementById('retryContainer', elem => {
elem.style.display = 'flex'
})
log(texts.noServerConsoleMessage)
}
function VersionActions() {
FreshStart()
}
// : }}}
// : UI handling {{{
function shouldShowMotd() {
if (!emptyOrWhiteSpace(motd)) {
var prevmotd = getVal('motd')
if (prevmotd !== motd) {
setVal('motdcount', motdShowCount)
setVal('motd', motd)
return true
} else {
var motdcount = getVal('motdcount')
if (motdcount === undefined) {
setVal('motdcount', motdShowCount)
motdcount = motdShowCount
}
motdcount--
if (motdcount > 0) {
setVal('motdcount', motdcount)
return true
}
}
}
}
function HandleUI() {
const newVersion = info().script.version !== getVal('lastVerson')
const showMOTD = shouldShowMotd()
const isNewVersionAvaible =
lastestVersion !== undefined && info().script.version !== lastestVersion
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)
timeout = undefined
}
if (userSpecificMotd && !userSpecificMotd.seen) {
timeout = null
greetMsg.push(texts.userSpecifitMotdAvailable)
timeout = undefined
}
if (newVersion) {
greetMsg.push(texts.versionUpdated + info().script.version)
setVal('lastVerson', info().script.version) // setting lastVersion
}
if (showMOTD) {
greetMsg.push(texts.motd + motd)
timeout = null
}
ShowMessage(
greetMsg.join('\n'),
timeout
)
}
// : }}}
// : Answering stuffs {{{
function PrepareAnswers(result) {
const { answers, question } = result
if (answers.length > 0) {
return answers.map(answer => {
const { Q, A, data } = answer.q
let msg = Q + '\n' + A
// TODO: show 'képek sorrendben' if there are no new kind of image ids
if (data.type === 'image') {
question.images.forEach((img, i) => {
const regex = new RegExp(`\\[${img.val}\\]`, 'g')
msg = msg.replace(regex, '[' + i.toString() + ']')
})
}
return {
m: msg,
p: answer.match,
header:
answer.detailedMatch.matchedSubjName +
' - ' +
answer.detailedMatch.qdb,
}
})
} else {
return [
{
m: 'Erre a kérdésre nincs találat :c',
},
]
}
}
function addImageIdsToImageNodes(imgs) {
imgs.images.forEach((img, i) => {
const text = document.createElement('div')
text.innerText = `[${i}]`
SetStyle(text, {
backgroundColor: '#333',
borderRadius: '5px',
color: 'white',
opacity: 0.7,
fontSize: '13px',
})
appendBelowElement(img.node, text)
})
}
function ShowAnswers(results) {
log(results)
const answers = results.reduce((acc, res) => {
const prepared = PrepareAnswers(res)
addImageIdsToImageNodes(res.question)
if (prepared) {
acc.push(prepared)
}
return acc
}, [])
if (answers.length > 0) {
ShowMessage(answers)
} else {
ShowMessage(
texts.noResult,
undefined,
function() {
OpenErrorPage({
message: 'No result found',
question: Array.isArray(answers[0])
? answers[0][0].replace(/"/g, '').replace(/:/g, '')
: answers[0],
})
}
)
}
}
// : }}}
// : Quiz saving {{{
function HandleResults() {
getQuiz().then(res => {
SaveQuiz(res, ShowSaveQuizDialog) // saves the quiz questions and answers
})
}
function ShowSaveQuizDialog(sendResult, sentData, newQuestions) {
var msg = ''
if (sendResult) {
msg = 'Kérdések elküldve, katt az elküldött adatokért.'
if (newQuestions > 0) {
msg += ' ' + newQuestions + ' új kérdés'
} else {
msg += ' Nincs új kérdés'
}
} else {
msg =
'Szerver nem elérhető, vagy egyéb hiba kérdések elküldésénél! (F12 -> Console)'
}
// showing a message wit the click event, and the generated page
ShowMessage(
msg,
null,
function() {
let towrite = ''
try {
towrite += '</p>Elküldött adatok:</p> ' + JSON.stringify(sentData)
} catch (e) {
towrite += '</p>Elküldött adatok:</p> ' + sentData
}
document.write(towrite)
document.close()
}
)
}
// saves the current quiz. questionData contains the active subjects questions
function SaveQuiz(quiz, next) {
try {
let sentData = {}
if (quiz.length === 0) {
ShowMessage(texts.noParseableQuestionResult)
return
}
try {
sentData = {
version: info().script.version,
id: getCid(),
quiz: quiz,
location: currUrl,
}
try {
sentData.subj = getCurrentSubjectName()
} catch (e) {
sentData.subj = 'NOSUBJ'
log('unable to get subject name :c')
}
log('SENT DATA', sentData)
post('isAdding', sentData).then(res => {
next(res.success, sentData, res.totalNewQuestions)
})
} catch (e) {
Exception(e, 'error at sending data to server.')
}
} catch (e) {
Exception(e, 'script error at saving quiz')
}
}
// : }}}
// : Misc {{{
// : Version action functions {{{
function FreshStart() {
var firstRun = getVal('firstRun') // if the current run is the frst
if (firstRun === undefined || firstRun === true) {
setVal('firstRun', false)
ShowHelp('firstRun') // showing help
registerScript()
document.write(texts.freshStartWarning)
document.close()
throw new Error('something, so this stuff stops')
}
}
function registerScript() {
try {
// uncomment to re-register again every page refresh
// setVal('registeredWithCid', false)
// setVal('registeredWithUid', false)
if (getVal('registeredWithCid')) {
if (getVal('registeredWithUid')) {
return
} else if (!getUid()) {
return
}
}
setVal('registeredWithCid', true)
if (getUid()) {
setVal('registeredWithUid', true)
}
post('registerscript', {
cid: getCid(),
uid: getUid(),
version: info().script.version,
date: new Date(),
installSource: info().script.updateURL,
})
} catch (err) {
warn('Unexpected error while registering script')
log(err)
}
}
// : }}}
// : Video hotkey stuff {{{
// this function adds basic hotkeys for video controll.
function AddVideoHotkeys() {
var seekTime = 20
document.addEventListener('keydown', function(e) {
try {
var video = getVideo()
var keyCode = e.keyCode // getting keycode
if (keyCode === 32) {
// if the keycode is 32 (space)
e.preventDefault() // preventing default action (space scrolles down)
if (video.paused && video.buffered.length > 0) {
video.play()
} else {
video.pause()
}
}
if (keyCode === 39) {
// rigth : 39
video.currentTime += seekTime
}
if (keyCode === 37) {
// left : 37
video.currentTime -= seekTime
}
} catch (err) {
log('Hotkey error.')
log(err.message)
}
})
var toadd = getVideoElement()
var node = CreateNodeWithText(toadd, texts.videoHelp)
node.style.margin = '5px 5px 5px 5px' // fancy margin
}
// : }}}
// : }}}
// : }}}
// : Show message, and script menu stuff {{{
function clearAllMessages() {
overlay.querySelectorAll('#scriptMessage').forEach(x => x.remove())
}
function getConvertedMessageNode(message) {
const messageNode = document.createElement('p')
const resultNode = document.createElement('p')
messageNode.innerHTML = message.replace(/\n/g, '</br>')
Array.from(messageNode.childNodes).forEach(node => {
if (node.tagName === 'A') {
const linkNode = document.createElement('span')
SetStyle(linkNode, {
color: 'lightblue',
textDecoration: 'underline',
cursor: 'pointer',
})
linkNode.innerText = node.innerText
linkNode.addEventListener('mousedown', e => {
e.stopPropagation()
openInTab(node.href, {
active: true,
})
})
resultNode.appendChild(linkNode)
} else {
resultNode.appendChild(node)
}
})
return resultNode
}
function addOpacityChangeEvent(elem) {
if (!elem.id) {
warn('element must have ID to add opacity change event!')
return
}
let currOpacity = getVal(`${elem.id}_opacity`) || 1
elem.addEventListener('wheel', e => {
e.preventDefault()
const isUp = e.deltaY < 0
if (isUp) {
if (currOpacity + messageOpacityDelta <= 1) {
currOpacity = currOpacity + messageOpacityDelta
}
} else {
if (currOpacity - messageOpacityDelta > minMessageOpacity) {
currOpacity = currOpacity - messageOpacityDelta
}
}
elem.style.opacity = currOpacity
setVal(`${elem.id}_opacity`, currOpacity)
})
}
// shows a message with "msg" text, "matchPercent" tip and transp, and "timeout" time
function ShowMessage(msgItem, timeout, funct) {
// msgItem help:
// [ [ {}{}{}{} ] [ {}{}{} ] ]
// msgItem[] <- a questions stuff
// msgItem[][] <- a questions relevant answers array
// msgItem[][].p <- a questions precent
// msgItem[][].m <- a questions message
try {
let defMargin = '0px 5px'
let isSimpleMessage = false
let simpleMessageText = ''
if (typeof msgItem === 'string') {
simpleMessageText = msgItem
if (simpleMessageText === '') {
// if msg is empty
return
}
msgItem = [
[
{
m: simpleMessageText,
},
],
]
isSimpleMessage = true
}
const appedtTo = overlay // will be appended here
const startFromTop = 25 // top distance
let width = window.innerWidth - window.innerWidth / 6 // with of the box
const mainDiv = document.createElement('div') // the main divider, wich items will be attached to
const id = 'scriptMessage'
mainDiv.setAttribute('id', id)
if (funct) {
addEventListener(mainDiv, 'click', funct)
}
// lotsa crap style
SetStyle(mainDiv, {
position: 'fixed',
zIndex: 999999,
textAlign: 'center',
width: width + 'px',
padding: '0px',
background: '#222d32',
color: '#ffffff',
border: '3px solid #99f',
borderRadius: '5px',
top: startFromTop + 'px',
left: (window.innerWidth - width) / 2 + 'px',
opacity: getVal(`${id}_opacity`),
cursor: funct ? 'pointer' : 'move',
})
// ------------------------------------------------------------------
// transparencity
// ------------------------------------------------------------------
addOpacityChangeEvent(mainDiv)
// ------------------------------------------------------------------
// moving msg
// ------------------------------------------------------------------
const movingEnabled = !funct
let isMouseDown = false
let offset = [0, 0]
let mousePosition
if (movingEnabled) {
mainDiv.addEventListener('mousedown', e => {
isMouseDown = true
offset = [
mainDiv.offsetLeft - e.clientX,
mainDiv.offsetTop - e.clientY,
]
})
mainDiv.addEventListener('mouseup', () => {
isMouseDown = false
})
mainDiv.addEventListener('mousemove', e => {
if (isMouseDown) {
mousePosition = {
x: e.clientX,
y: e.clientY,
}
mainDiv.style.left = mousePosition.x + offset[0] + 'px'
mainDiv.style.top = mousePosition.y + offset[1] + 'px'
}
})
}
const xButton = CreateNodeWithText(null, '❌', 'div')
SetStyle(xButton, {
cursor: 'pointer',
position: 'absolute',
right: '0px',
display: 'inline',
margin: '3px',
})
xButton.addEventListener('mousedown', e => {
e.stopPropagation()
mainDiv.parentNode.removeChild(mainDiv)
})
// ------------------------------------------------------------------
var matchPercent = msgItem[0][0].p
if (isSimpleMessage) {
mainDiv.appendChild(xButton)
var simpleMessageParagrapg = document.createElement('p') // new paragraph
simpleMessageParagrapg.style.margin = defMargin // fancy margin
const mesageNode = getConvertedMessageNode(simpleMessageText)
simpleMessageParagrapg.appendChild(mesageNode)
mesageNode.addEventListener('mousedown', e => {
e.stopPropagation()
})
SetStyle(mesageNode, {
margin: '10px 100px',
cursor: funct ? 'pointer' : 'auto',
})
Array.from(mesageNode.getElementsByTagName('a')).forEach(anchorElem => {
anchorElem.style.color = 'lightblue'
})
mainDiv.appendChild(simpleMessageParagrapg) // adding text box to main div
} else {
const headerRow = document.createElement('div')
headerRow.setAttribute('id', 'msgHeader')
SetStyle(headerRow, {
height: '20px',
userSelect: 'none',
})
mainDiv.appendChild(headerRow)
const headerText = CreateNodeWithText(headerRow, '', 'div')
headerText.title = 'Talált kérdés tárgy neve'
SetStyle(headerText, {
padding: '2px 5px',
textAlign: 'center',
display: 'inline',
})
headerRow.appendChild(xButton)
// if its a fucking complicated message
// TABLE SETUP ------------------------------------------------------------------------------------------------------------
var table = document.createElement('table')
table.style.width = '100%'
// ROWS -----------------------------------------------------------------------------------------------------
var rowOne = table.insertRow() // previous suggestion, question text, and prev question
var rowTwo = table.insertRow() // next question button
var rowThree = table.insertRow() // next suggetsion button
// CELLS -----------------------------------------------------------------------------------------------------
// row one
const sideCellWidth = 30
var numberTextCell = rowOne.insertCell()
SetStyle(numberTextCell, {
width: sideCellWidth + 'px',
})
var questionCell = rowOne.insertCell() // QUESTION CELL
questionCell.setAttribute('id', 'questionCell')
questionCell.rowSpan = 3
var prevQuestionCell = rowOne.insertCell()
SetStyle(prevQuestionCell, {
width: sideCellWidth + 'px',
})
// row two
var percentTextCell = rowTwo.insertCell()
SetStyle(percentTextCell, {
width: sideCellWidth + 'px',
})
var nextQuestionCell = rowTwo.insertCell()
SetStyle(nextQuestionCell, {
width: sideCellWidth + 'px',
})
// row three
var prevSuggestionCell = rowThree.insertCell()
var nextSuggestionCell = rowThree.insertCell()
// adding finally
mainDiv.appendChild(table)
// PERCENT TEXT SETUP -----------------------------------------------------------------------------------------------------
var percentTextBox = CreateNodeWithText(percentTextCell, '')
percentTextBox.setAttribute('id', 'percentTextBox')
percentTextBox.title = 'Egyezés %'
if (matchPercent) {
// if match percent param is not null
percentTextBox.innerText = matchPercent + '%'
}
// NUMBER SETUP -----------------------------------------------------------------------------------------------------
var numberTextBox = CreateNodeWithText(numberTextCell, '1.')
numberTextBox.setAttribute('id', 'numberTextBox')
numberTextBox.title = 'Lehetséges válasz index'
// ANSWER NODE SETUP -------------------------------------------------------------------------------------------------------------
var questionTextElement = CreateNodeWithText(
questionCell,
'ur question goes here, mister OwO'
)
questionTextElement.addEventListener('mousedown', e => {
e.stopPropagation()
})
SetStyle(questionTextElement, {
cursor: funct ? 'pointer' : 'auto',
})
questionTextElement.setAttribute('id', 'questionTextElement')
// BUTTON SETUP -----------------------------------------------------------------------------------------------------------
var currItem = 0
var currRelevantQuestion = 0
const GetRelevantQuestion = () => {
// returns the currItemth questions currRelevantQuestionth relevant question
return msgItem[currItem][currRelevantQuestion]
}
const ChangeCurrItemIndex = to => {
currItem += to
if (currItem < 0) {
currItem = 0
}
if (currItem > msgItem.length - 1) {
currItem = msgItem.length - 1
}
currRelevantQuestion = 0
}
const ChangeCurrRelevantQuestionIndex = to => {
currRelevantQuestion += to
if (currRelevantQuestion < 0) {
currRelevantQuestion = 0
}
if (currRelevantQuestion > msgItem[currItem].length - 1) {
currRelevantQuestion = msgItem[currItem].length - 1
}
}
const SetQuestionText = () => {
const relevantQuestion = GetRelevantQuestion()
if (relevantQuestion.header) {
headerText.innerText = relevantQuestion.header
}
questionTextElement.innerText = relevantQuestion.m
if (currItem === 0 && currRelevantQuestion === 0) {
numberTextBox.innerText = currRelevantQuestion + 1 + '.'
} else {
numberTextBox.innerText =
currItem + 1 + './' + (currRelevantQuestion + 1) + '.'
}
if (relevantQuestion.p) {
percentTextBox.innerText = relevantQuestion.p + '%'
}
}
const buttonStyle = {
color: 'white',
backgroundColor: 'transparent',
margin: '2px',
border: 'none',
fontSize: '30px',
cursor: 'pointer',
userSelect: 'none',
}
if (msgItem[currItem].length > 1) {
// PREV SUGG BUTTON ------------------------------------------------------------------------------------------------------------
var prevSuggButton = CreateNodeWithText(
prevSuggestionCell,
'⬅️',
'div'
)
prevSuggButton.title = 'Előző lehetséges válasz'
SetStyle(prevSuggButton, buttonStyle)
prevSuggButton.addEventListener('mousedown', function(e) {
e.stopPropagation()
ChangeCurrRelevantQuestionIndex(-1)
SetQuestionText()
})
// NEXT SUGG BUTTON ------------------------------------------------------------------------------------------------------------
var nextSuggButton = CreateNodeWithText(
nextSuggestionCell,
'➡️',
'div'
)
nextSuggButton.title = 'Következő lehetséges válasz'
SetStyle(nextSuggButton, buttonStyle)
nextSuggButton.addEventListener('mousedown', function(e) {
e.stopPropagation()
ChangeCurrRelevantQuestionIndex(1)
SetQuestionText()
})
}
// deciding if has multiple questions ------------------------------------------------------------------------------------------------
if (msgItem.length === 1) {
SetQuestionText()
} else {
// if there are multiple items to display
// PREV QUESTION BUTTON ------------------------------------------------------------------------------------------------------------
var prevButton = CreateNodeWithText(prevQuestionCell, '⬆️', 'div')
SetStyle(prevButton, buttonStyle)
prevButton.title = 'Előző kérdés'
// event listener
prevButton.addEventListener('click', function() {
ChangeCurrItemIndex(-1)
SetQuestionText()
})
// NEXT QUESTION BUTTON ------------------------------------------------------------------------------------------------------------
var nextButton = CreateNodeWithText(nextQuestionCell, '⬇️', 'div')
SetStyle(nextButton, buttonStyle)
nextButton.title = 'Előző kérdés'
// event listener
nextButton.addEventListener('click', function() {
ChangeCurrItemIndex(1)
SetQuestionText()
})
SetQuestionText()
}
}
appedtTo.appendChild(mainDiv) // THE FINAL APPEND
// setting some events
// addEventListener(window, 'scroll', function () {
// mainDiv.style.top = (pageYOffset + startFromTop) + 'px';
// })
addEventListener(window, 'resize', function() {
mainDiv.style.left = (window.innerWidth - width) / 2 + 'px'
})
var timeOut
if (timeout && timeout > 0) {
// setting timeout if not zero or null
timeOut = setTimeout(function() {
mainDiv.parentNode.removeChild(mainDiv)
}, timeout * 1000)
}
// middle click close event listener
addEventListener(mainDiv, 'mousedown', function(e) {
if (e.which === 2) {
mainDiv.parentNode.removeChild(mainDiv)
if (timeOut) {
clearTimeout(timeOut)
}
}
})
return {
messageElement: mainDiv,
removeMessage: () => {
mainDiv.parentNode.removeChild(mainDiv)
},
}
} catch (e) {
Exception(e, 'script error at showing message:')
}
}
// shows a fancy menu
function ShowMenu() {
try {
// Script menu -----------------------------------------------------------------
const scriptMenuDiv = document.createElement('div')
const id = 'scriptMenuDiv'
scriptMenuDiv.setAttribute('id', id)
SetStyle(scriptMenuDiv, {
width: '300px',
height: '90px',
position: 'fixed',
bottom: '30px',
left: '10px',
zIndex: 999999,
background: '#262626',
border: '3px solid #99f',
borderRadius: '5px',
opacity: getVal(`${id}_opacity`),
fontSize: '14px',
})
addEventListener(scriptMenuDiv, 'mousedown', function(e) {
if (e.which === 2) {
scriptMenuDiv.parentNode.removeChild(scriptMenuDiv)
}
})
addOpacityChangeEvent(scriptMenuDiv)
// X button --------------------------------------------------------------------
const xButton = CreateNodeWithText(scriptMenuDiv, '❌', 'div')
SetStyle(xButton, {
position: 'absolute',
display: 'inline',
right: '0px',
margin: '5px',
cursor: 'pointer',
fontSize: '18px',
})
xButton.addEventListener('mousedown', e => {
e.stopPropagation()
scriptMenuDiv.parentNode.removeChild(scriptMenuDiv)
})
// Mail button -----------------------------------------------------------------
const mailButton = CreateNodeWithText(scriptMenuDiv, '📭', 'div')
mailButton.setAttribute('id', 'mailButton')
SetStyle(mailButton, {
position: 'absolute',
display: 'inline',
left: '0px',
bottom: '0px',
margin: '5px',
fontSize: '30px',
})
mailButton.addEventListener('mousedown', e => {
e.stopPropagation()
if (userSpecificMotd && !userSpecificMotd.seen) {
mailButton.innerText = '📭'
post('infos', {
userSpecificMotdSeen: true,
})
}
if (!userSpecificMotd) {
return
}
clearAllMessages()
ShowMessage(
'Üzenet oldal készítéjétől (csak te látod):\n' + userSpecificMotd.msg
)
})
// -----------------------------------------------------------------------------
// BUTTONS
// -----------------------------------------------------------------------------
const buttonContainer = document.createElement('div')
SetStyle(buttonContainer, {
display: 'flex',
justifyContent: 'center',
})
scriptMenuDiv.appendChild(buttonContainer)
const buttonStyle = {
position: '',
margin: '3px',
padding: '4px 8px',
border: '1px solid #333',
borderRadius: '2px',
color: '#ffffff',
cursor: 'pointer',
}
Object.keys(menuButtons).forEach(key => {
const buttonData = menuButtons[key]
const button = CreateNodeWithText(
buttonContainer,
buttonData.title,
'div'
)
SetStyle(button, buttonStyle)
if (buttonData.onClick) {
button.addEventListener('click', function() {
buttonData.onClick()
})
}
})
// -----------------------------------------------------------------------------
// Info text
// -----------------------------------------------------------------------------
const infoDiv = CreateNodeWithText(scriptMenuDiv, texts.loading, 'div')
infoDiv.setAttribute('id', 'infoMainDiv')
SetStyle(infoDiv, {
color: '#ffffff',
margin: '0px 50px',
textAlign: 'center',
})
// -----------------------------------------------------------------------------
// Login stuff (if user is not logged in)
// -----------------------------------------------------------------------------
const loginContainer = document.createElement('div')
scriptMenuDiv.appendChild(loginContainer)
loginContainer.setAttribute('id', 'loginDiv')
SetStyle(loginContainer, {
display: 'none',
margin: '0px 50px',
})
const loginInput = document.createElement('input')
loginContainer.appendChild(loginInput)
loginInput.type = 'text'
loginInput.placeholder = texts.pwHere
SetStyle(loginInput, {
width: '100%',
textAlign: 'center',
})
const loginButton = document.createElement('div')
loginContainer.appendChild(loginButton)
loginButton.innerText = texts.login
SetStyle(loginButton, buttonStyle)
loginButton.addEventListener('click', function() {
Auth(loginInput.value)
})
// -----------------------------------------------------------------------------
// Retry connection button (if server is not available)
// -----------------------------------------------------------------------------
const retryContainer = CreateNodeWithText(scriptMenuDiv, '', 'div')
SetStyle(retryContainer, {
display: 'hidden',
justifyContent: 'center',
})
retryContainer.style.display = 'none'
retryContainer.setAttribute('id', 'retryContainer')
const retryButton = CreateNodeWithText(retryContainer, texts.retry, 'div')
retryContainer.appendChild(retryButton)
SetStyle(retryButton, {
position: '',
padding: '0px 8px',
border: '1px solid #333',
borderRadius: '2px',
color: '#ffffff',
cursor: 'pointer',
})
retryButton.addEventListener('click', function() {
scriptMenuDiv.style.background = '#262626'
infoDiv.innerText = texts.loading
retryContainer.style.display = 'none'
ConnectToServer(AfterLoad)
})
// APPEND EVERYTHING
overlay.appendChild(scriptMenuDiv)
} catch (e) {
Exception(e, 'script error at showing menu:')
}
}
// : }}}
// : Generic utils {{{
// : String utils 2 {{{
function removeUnnecesarySpaces(toremove) {
if (!toremove) {
return ''
}
toremove = normalizeSpaces(toremove).replace(/\t/g, '')
while (toremove.includes(' ')) {
toremove = toremove.replace(/ {2}/g, ' ')
}
while (toremove.includes('\n\n')) {
toremove = toremove.replace(/\n{2}/g, ' ')
}
return toremove.trim()
}
function normalizeSpaces(input) {
assert(input)
return input.replace(/\s/g, ' ')
}
function emptyOrWhiteSpace(value) {
if (value === undefined) {
return true
}
return (
value
.replace(/\s/g, '')
.replace(/\t/g, '')
.replace(/ /g, '')
.replace(/\n/g, ' ') === ''
)
}
// : }}}
const assert = val => {
if (!val) {
throw new Error('Assertion failed')
}
}
function logHelper(logMethod, ...value) {
if (logEnabled) {
logMethod('[Moodle Script]: ', ...value)
}
}
function warn(value) {
logHelper(console.warn, value)
}
function log() {
logHelper(console.log, ...arguments)
}
function Exception(e, msg) {
log('------------------------------------------')
log(msg)
log(e.message)
log('------------------------------------------')
log(e.stack)
log('------------------------------------------')
}
function getUid() {
return getVal('userId')
}
function getCid() {
let currId = getVal('clientId')
if (currId) {
return currId
} else {
currId = new Date()
currId = currId.getTime() + Math.floor(Math.random() * 1000000000000)
currId = currId.toString().split('')
currId.shift()
currId = '0' + currId.join('')
setVal('clientId', currId)
return currId
}
}
function SafeGetElementById(id, next) {
const element = overlay.querySelector('#' + id)
if (element) {
next(element)
} else {
log(`Unable to safe get element by id: ${id}`)
}
}
function SetStyle(target, style) {
Object.keys(style)
.sort()
.forEach(key => {
target.style[key] = style[key]
})
}
function CreateNodeWithText(to, text, type) {
var paragraphElement = document.createElement(type || 'p') // new paragraph
var textNode = document.createTextNode(text)
paragraphElement.appendChild(textNode)
if (to) {
to.appendChild(paragraphElement)
}
return paragraphElement
}
function GetXHRInfos() {
registerScript()
const now = new Date().getTime()
const lastCheck = getVal('lastInfoCheckTime')
if (!lastCheck) {
setVal('lastInfoCheckTime', now)
}
let lastInfo = { result: 'noLastInfo' }
try {
lastInfo = JSON.parse(getVal('lastInfo'))
} catch (e) {
if (showErrors) {
warn(e)
}
}
if (
lastInfo.result !== 'success' ||
now > lastCheck + infoExpireTime * 1000
) {
return new Promise((resolve, reject) => {
const url =
apiAdress +
'infos?version=true&motd=true&subjinfo=true&cversion=' +
info().script.version +
'&cid=' +
getCid()
xmlhttpRequest({
method: 'GET',
url: url,
crossDomain: true,
xhrFields: { withCredentials: true },
headers: {
'Content-Type': 'application/json',
},
onload: function(response) {
try {
setVal('lastInfoCheckTime', now)
const res = JSON.parse(response.responseText)
setVal('lastInfo', response.responseText)
resolve(res)
} catch (e) {
log('Errro paring JSON in GetXHRInfos')
log(response.responseText)
log(e)
reject(e)
}
},
onerror: e => {
log('Info get Error', e)
reject(e)
},
})
})
} else {
return new Promise((resolve, reject) => {
try {
resolve(lastInfo)
} catch (e) {
log('Errro paring JSON in GetXHRInfos, when using old data!')
log(e)
reject(e)
}
})
}
}
function post(path, message) {
if (typeof message === 'object') {
message = JSON.stringify(message)
}
const url = apiAdress + path
return new Promise((resolve, reject) => {
xmlhttpRequest({
method: 'POST',
url: url,
crossDomain: true,
xhrFields: { withCredentials: true },
data: message,
headers: {
'Content-Type': 'application/json',
},
onerror: function(e) {
log('Data send error', e)
reject(e)
},
onload: resp => {
try {
const res = JSON.parse(resp.responseText)
resolve(res)
} catch (e) {
log('Error paring JSON in post')
log(resp.responseText)
log(e)
reject(e)
}
},
})
})
}
function OpenErrorPage(e) {
const queries = []
try {
Object.keys(e).forEach(key => {
if (e[key]) {
queries.push(`${key}=${encodeURIComponent(e[key])}`)
queries.push('db=all')
}
})
queries.push('version=' + encodeURIComponent(info().script.version))
queries.push('uid=' + encodeURIComponent(getUid()))
queries.push('cid=' + encodeURIComponent(getCid()))
} catch (e) {
Exception(e, 'error at setting error stack/msg link')
}
openInTab(serverAdress + 'lred?' + queries.join('&'), {
active: true,
})
}
// : }}}
// : Help {{{
// shows some neat help
function ShowHelp(query) {
let q = 'scriptMenu'
if (query) {
q = query
}
openInTab(serverAdress + `manual?${q}`, {
active: true,
})
}
// : }}}
// I am not too proud to cry that He and he
// Will never never go out of my mind.
// All his bones crying, and poor in all but pain,
// Being innocent, he dreaded that he died
// Hating his God, but what he was was plain:
// An old kind man brave in his burning pride.
// The sticks of the house were his; his books he owned.
// Even as a baby he had never cried;
// Nor did he now, save to his secret wound.
// Out of his eyes I saw the last light glide.
// Here among the liught of the lording sky
// An old man is with me where I go
// Walking in the meadows of his son's eye
// Too proud to cry, too frail to check the tears,
// And caught between two nights, blindness and death.
// O deepest wound of all that he should die
// On that darkest day.
})() // eslint-disable-line