moodle-test-userscript/stable.user.js
2020-10-05 16:38:38 +02:00

2498 lines
89 KiB
JavaScript
Executable file

/* ----------------------------------------------------------------------------
Online Moodle/Elearning/KMOOC test help
Greasyfork: <https://greasyfork.org/en/scripts/38999-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/>.
------------------------------------------------------------------------- */
// ==UserScript==
// @name Moodle/Elearning/KMOOC test help
// @version 2.0.1.13
// @description Online Moodle/Elearning/KMOOC test help
// @author MrFry
// @match https://elearning.uni-obuda.hu/main/*
// @match https://elearning.uni-obuda.hu/kmooc/*
// @match https://mooc.unideb.hu/*
// @match https://itc.semmelweis.hu/moodle/*
// @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==
//
// TODO:
// grabboxes test on quiz page
(function() {
// 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, options) {
GM_openInTab(address, options);
}
function xmlhttpRequest(opts) {
GM_xmlhttpRequest(opts);
}
function info() {
return GM_info;
}
/* eslint-enable */
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";
// forcing pages for testing. unless you test, do not set these to true!
// only one of these should be true for testing
const forceTestPage = false;
const forceResultPage = false;
const forceDefaultPage = false;
const logElementGetting = false;
const log = true;
const motdShowCount = 3; /* Ammount of times to show motd */
let infoExpireTime = 60; // Every n seconds basic info should be loaded from server
var motd = "";
var lastestVersion = "";
var subjInfo;
if (getVal("ISDEVEL")) {
infoExpireTime = 1;
serverAdress = "http://localhost:8080/";
apiAdress = "http://localhost:80/";
}
const huTexts = {
lastChangeLog: "",
fatalError:
"Fatál error. Check console (f12). Kattints az üzenetre az összes kérdés/válaszért manuális kereséshez!",
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!",
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!",
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 / Bug report",
contribute: "Contribute",
donate: "Donate",
retry: "Újrapróbálás",
ircButton: "IRC",
invalidPW: "Hibás jelszó: ",
search: "Keresés ...",
loading: "Betöltés ...",
login: "Belépés",
requestPWInsteadOfLogin: "Jelszó igénylés",
contributeTitle: "Hozzájárulás a script és weboldal fejleszétéshez",
newPWTitle: "Új jelszó új felhasználónak",
pwRequest: "Új jelszó",
noServer: "Nem elérhető a szerver!",
noUser: "Nem vagy bejelentkezve!",
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`,
};
var texts = huTexts;
// : question-classes {{{
const specialChars = ["&", "\\+"];
const assert = val => {
if (!val) {
throw new Error("Assertion failed");
}
};
class StringUtils {
RemoveStuff(value, removableStrings, toReplace) {
removableStrings.forEach(x => {
var regex = new RegExp(x, "g");
value = value.replace(regex, toReplace || "");
});
return value;
}
SimplifyQuery(q) {
assert(q);
var result = q.replace(/\n/g, " ").replace(/\s/g, " ");
return this.RemoveUnnecesarySpaces(result);
}
ShortenString(toShorten, ammount) {
assert(toShorten);
var result = "";
var i = 0;
while (i < toShorten.length && i < ammount) {
result += toShorten[i];
i++;
}
return result;
}
ReplaceCharsWithSpace(val, char) {
assert(val);
assert(char);
var toremove = this.NormalizeSpaces(val);
var regex = new RegExp(char, "g");
toremove = toremove.replace(regex, " ");
return this.RemoveUnnecesarySpaces(toremove);
}
// removes whitespace from begining and and, and replaces multiple spaces with one space
RemoveUnnecesarySpaces(toremove) {
assert(toremove);
toremove = this.NormalizeSpaces(toremove);
while (toremove.includes(" ")) {
toremove = toremove.replace(/ {2}/g, " ");
}
return toremove.trim();
}
RemoveSpecialChars(value) {
assert(value);
return this.RemoveStuff(value, specialChars, " ");
}
// if the value is empty, or whitespace
EmptyOrWhiteSpace(value) {
// replaces /n-s with "". then replaces spaces with "". if it equals "", then its empty, or only consists of white space
if (value === undefined) {
return true;
}
return (
value
.replace(/\n/g, "")
.replace(/ /g, "")
.replace(/\s/g, " ") === ""
);
}
// damn nonbreaking space
NormalizeSpaces(input) {
assert(input);
return input.replace(/\s/g, " ");
}
SimplifyStack(stack) {
return this.SimplifyQuery(stack);
}
}
const SUtils = new StringUtils();
// : }}}
// : DOM getting stuff {{{
// all dom getting stuff are in this sections, so on
// moodle dom change, stuff breaks here
//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() {
let 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(target) {
const overlayElement = document.createElement("div");
overlayElement.style.cssText =
"position:fixed; pointer-events: none; user-select: none; z-index:10000";
overlay.append(overlayElement);
let currX, currY, currWidth, currHeight;
const copyBoundingRect = () => {
let { x, y, width, height } = target.getBoundingClientRect();
if (x !== currX) {
overlayElement.style.left = x + "px";
currX = x;
}
if (y !== currY) {
overlayElement.style.top = y + "px";
currY = y;
}
if (width !== currWidth) {
overlayElement.style.width = width + "px";
currWidth = width;
}
if (height !== currHeight) {
overlayElement.style.height = height + "px";
currHeight = height;
}
};
copyBoundingRect();
const interval = setInterval(copyBoundingRect, 30);
overlayElement.destroy = () => {
clearInterval(interval);
overlayElement.remove();
};
return overlayElement;
}
class QuestionsPageModell {
GetAllQuestionsDropdown() {
if (logElementGetting) {
Log("getting dropdown question");
}
let items = document
.getElementById("responseform")
.getElementsByTagName("p")[0].childNodes;
let r = "";
items.forEach(item => {
if (item.tagName === undefined) {
r += item.nodeValue;
}
});
return r;
}
GetAllQuestionsQtext() {
if (logElementGetting) {
Log("getting all questions qtext");
}
return document
.getElementById("responseform")
.getElementsByClassName("qtext"); // getting questions
}
GetAllQuestionsP() {
if (logElementGetting) {
Log("getting all questions by tag p");
}
return document
.getElementById("responseform")
.getElementsByTagName("p");
}
GetFormulationClearfix() {
if (logElementGetting) {
Log("getting formulation clearfix lol");
}
return document.getElementsByClassName("formulation clearfix");
}
GetAnswerOptions() {
if (logElementGetting) {
Log("getting all answer options");
}
return this.GetFormulationClearfix()[0].childNodes[3].innerText;
}
GetQuestionImages() {
if (logElementGetting) {
Log("getting question images");
}
return this.GetFormulationClearfix()[0].getElementsByTagName("img");
}
// this function should return the question, posible answers, and image names
GetQuestionFromTest() {
var questions; // the important questions
var allQuestions; // all questions
try {
allQuestions = this.GetAllQuestionsQtext(); // getting questions
if (allQuestions.length === 0) {
var ddq = this.GetAllQuestionsDropdown();
if (SUtils.EmptyOrWhiteSpace(ddq)) {
var questionData = "";
for (var j = 0; j < allQuestions.length; j++) {
let subAllQuestions = allQuestions[j].childNodes;
for (let i = 0; i < subAllQuestions.length; i++) {
if (
subAllQuestions[i].data !== undefined &&
!SUtils.EmptyOrWhiteSpace(
subAllQuestions[i].data
)
) {
questionData +=
subAllQuestions[i].data + " "; // adding text to question data
}
}
}
questions = [questionData];
} else {
questions = [ddq];
}
} else {
questions = [];
for (let i = 0; i < allQuestions.length; i++) {
questions.push(allQuestions[i].innerText);
}
}
} catch (e) {
Exception(e, "script error at getting question:");
}
var imgNodes = ""; // the image nodes for questions
try {
imgNodes = this.GetQuestionImages(); // getting question images, if there is any
AddImageNamesToImages(imgNodes); // adding image names to images, so its easier to search for, or even guessing
} catch (e) {
Log(e);
Log("Some error with images");
}
questions = questions.map(item => {
if (item) {
return SUtils.ReplaceCharsWithSpace(item, "\n");
}
});
return {
imgnodes: imgNodes,
allQ: allQuestions,
q: questions,
};
}
}
class ResultsPageModell {
GetFormulationClearfix() {
if (logElementGetting) {
Log("getting formulation clearfix lol");
}
return document.getElementsByClassName("formulation clearfix");
}
GetGrade(i) {
if (logElementGetting) {
Log("getting grade");
}
const fcf = QPM.GetFormulationClearfix()[i];
return fcf.parentNode.parentNode.childNodes[0].childNodes[2]
.innerText;
}
DetermineQuestionType(nodes) {
let qtype = "";
let i = 0;
while (i < nodes.length && qtype === "") {
let inps = nodes[i].getElementsByTagName("input");
if (inps.length > 0) {
qtype = inps[0].type;
}
i++;
}
return qtype;
}
GetSelectAnswer(i) {
if (logElementGetting) {
Log("getting selected answer");
}
var t = document.getElementsByTagName("select");
if (t.length > 0) {
return t[i].options[t[i].selectedIndex].innerText;
}
}
GetCurrQuestion(i) {
if (logElementGetting) {
Log("getting curr questions by index: " + i);
}
return document.getElementsByTagName("form")[0].childNodes[0]
.childNodes[i].childNodes[1].childNodes[0].innerText;
}
GetFormResult() {
if (logElementGetting) {
Log("getting form result");
}
var t = document.getElementsByTagName("form")[0].childNodes[0]
.childNodes;
if (t.length > 0 && t[0].tagName === undefined) {
// debreceni moodle
return document.getElementsByTagName("form")[1].childNodes[0]
.childNodes;
} else {
return t;
}
}
GetAnswerNode(i) {
if (logElementGetting) {
Log("getting answer node");
}
var results = this.GetFormResult(); // getting results element
var r = results[i].getElementsByClassName("answer")[0].childNodes;
var ret = [];
for (var j = 0; j < r.length; j++) {
if (
r[j].tagName !== undefined &&
r[j].tagName.toLowerCase() === "div"
) {
ret.push(r[j]);
}
}
let qtype = this.DetermineQuestionType(ret);
return {
nodes: ret,
type: qtype,
};
}
GetCurrentAnswer(i) {
if (logElementGetting) {
Log("getting curr answer by index: " + i);
}
var results = this.GetFormResult(); // getting results element
var t = results[i]
.getElementsByClassName("formulation clearfix")[0]
.getElementsByTagName("span");
if (t.length > 2) {
return t[1].innerHTML.split("<br>")[1];
}
}
GetQText(i) {
if (logElementGetting) {
Log("getting qtext by index: " + i);
}
var results = this.GetFormResult(); // getting results element
return results[i].getElementsByClassName("qtext");
}
GetDropboxes(i) {
if (logElementGetting) {
Log("getting dropboxes by index: " + i);
}
var results = this.GetFormResult(); // getting results element
return results[i].getElementsByTagName("select");
}
GetAllAnswer(index) {
if (logElementGetting) {
Log("getting all answers, ind: " + index);
}
return document.getElementsByClassName("answer")[index].childNodes;
}
GetPossibleAnswers(i) {
if (logElementGetting) {
Log("getting possible answers");
}
var results = this.GetFormResult(); // getting results element
var items = results[i].getElementsByTagName("label");
var r = [];
for (var j = 0; j < items.length; j++) {
const TryGetCorrect = j => {
var cn = items[j].parentNode.className;
if (cn.includes("correct")) {
return (
cn.includes("correct") && !cn.includes("incorrect")
);
}
};
r.push({
value: items[j].innerText,
iscorrect: TryGetCorrect(j),
});
}
return r;
}
GetAnswersFromGrabBox(i) {
try {
if (logElementGetting) {
Log("testing if question is grab-box");
}
let results = this.GetFormResult(); // getting results element
let t = results[i].getElementsByClassName("dragitems")[0]
.childNodes;
if (t.length !== 1) {
Log("grab box drag items group length is not 1!");
Log(results[i].getElementsByClassName("dragitems")[0]);
}
let placedItems = t[0].getElementsByClassName("placed");
let res = [];
for (let i = 0; i < placedItems.length; i++) {
let item = placedItems[i];
res.push({
text: item.innerText,
left: item.style.left,
top: item.style.top,
});
}
return res;
} catch (e) {
console.info(e);
}
}
GetRightAnswerIfCorrectShown(i) {
if (logElementGetting) {
Log("getting right answer if correct shown");
}
var results = this.GetFormResult(); // getting results element
return results[i].getElementsByClassName("rightanswer");
}
GetWrongAnswerIfCorrectNotShown(i) {
if (logElementGetting) {
Log("getting wrong answer if correct not shown");
}
var results = this.GetFormResult(); // getting results element
var n = results[i].getElementsByTagName("i")[0].parentNode;
if (n.className.includes("incorrect")) {
return results[i].getElementsByTagName("i")[0].parentNode
.innerText;
} else {
return "";
}
}
GetRightAnswerIfCorrectNotShown(i) {
if (logElementGetting) {
Log("Getting right answer if correct not shown");
}
var results = this.GetFormResult(); // getting results element
var n = results[i].getElementsByTagName("i")[0].parentNode;
if (
n.className.includes("correct") &&
!n.className.includes("incorrect")
) {
return results[i].getElementsByTagName("i")[0].parentNode
.innerText;
}
}
GetFormCFOfResult(result) {
if (logElementGetting) {
Log("getting formulation clearfix");
}
return result.getElementsByClassName("formulation clearfix")[0];
}
GetResultText(i) {
if (logElementGetting) {
Log("getting result text");
}
var results = this.GetFormResult(); // getting results element
return this.GetFormCFOfResult(results[i]).getElementsByTagName("p");
}
GetResultImage(i) {
if (logElementGetting) {
Log("getting result image");
}
var results = this.GetFormResult(); // getting results element
return this.GetFormCFOfResult(results[i]).getElementsByTagName(
"img"
);
}
// gets the question from the result page
// i is the index of the question
GetQuestionFromResult(i) {
var temp = this.GetQText(i);
var currQuestion = "";
if (temp.length > 0) {
currQuestion = temp[0].innerText; // adding the question to curr question as .q
} else {
// this is black magic fuckery a bit
if (this.GetDropboxes(i).length > 0) {
var allNodes = this.GetResultText(i);
currQuestion = "";
for (var k = 0; k < allNodes.length; k++) {
var allQuestions = this.GetResultText(i)[k].childNodes;
for (var j = 0; j < allQuestions.length; j++) {
if (
allQuestions[j].data !== undefined &&
!SUtils.EmptyOrWhiteSpace(allQuestions[j].data)
) {
currQuestion += allQuestions[j].data + " ";
}
}
}
} else {
try {
currQuestion = this.GetCurrQuestion(i);
} catch (e) {
currQuestion = "REEEEEEEEEEEEEEEEEEEEE"; // this shouldnt really happen sry guys
Log("Unable to get question in GetQuestionFromResult");
}
}
}
return currQuestion;
}
// tries to get right answer from result page
// i is the index of the question
GetRightAnswerFromResult(i) {
var fun = [];
// "húzza oda ..." skip
fun.push(i => {
let temp = RPM.GetAnswersFromGrabBox(i);
return temp
.map(x => {
return x.text;
})
.join(", ");
});
// the basic type of getting answers
fun.push(i => {
var temp = RPM.GetRightAnswerIfCorrectShown(i); // getting risht answer
if (temp.length > 0) {
return temp[0].innerText;
} // adding the answer to curr question as .a
});
// if there is dropdown list in the current question
fun.push(i => {
if (RPM.GetDropboxes(i).length > 0) {
return RPM.GetCurrentAnswer(i);
}
});
// if the correct answers are not shown, and the selected answer
// is correct
fun.push(i => {
return RPM.GetRightAnswerIfCorrectNotShown(i);
});
// if there is dropbox in the question
fun.push(i => {
return RPM.GetSelectAnswer(i);
});
// if the correct answers are not shown, and the selected answer
// is incorrect, and there are only 2 options
fun.push(i => {
var possibleAnswers = RPM.GetPossibleAnswers(i);
if (possibleAnswers.length === 2) {
for (var k = 0; k < possibleAnswers.length; k++) {
if (possibleAnswers[k].iscorrect === undefined) {
return possibleAnswers[k].value;
}
}
}
});
// if everything fails
fun.push(i => {
return undefined;
});
var j = 0;
var currAnswer;
while (j < fun.length && SUtils.EmptyOrWhiteSpace(currAnswer)) {
try {
currAnswer = fun[j](i);
} catch (e) {
console.info(e);
}
j++;
}
return currAnswer;
}
GuessCorrectIn2LengthAnswersByIncorrect(items) {
const first = items[0];
const second = items[1];
if (first.className.includes("incorrect")) {
return second.innerText;
}
if (second.className.includes("incorrect")) {
return first.innerText;
}
}
GuessCorrectIn2LengthAnswersByPoints(i, items) {
const first = {
elem: items[0],
val: items[0].childNodes[0].checked,
text: items[0].innerText,
};
const second = {
elem: items[1],
val: items[1].childNodes[0].checked,
text: items[1].innerText,
};
const grade = RPM.GetGrade(i); // 1,00 közül 1,00 leosztályozva
const grades = grade.split(" ").reduce((acc, text) => {
if (text.includes(",")) {
// FIXME: fancy regexp
acc.push(parseInt(text));
} else if (text.includes(".")) {
// FIXME: fancy regexp
acc.push(parseInt(text));
}
return acc;
}, []);
if (grades[0] === 1) {
if (first.val) {
return first.text;
} else {
return second.text;
}
} else {
if (!first.val) {
return first.text;
} else {
return second.text;
}
}
}
// version 2 of getting right answer from result page
// i is the index of the question
GetRightAnswerFromResultv2(i) {
try {
var answerNodes = this.GetAnswerNode(i);
let items = answerNodes.nodes;
if (answerNodes.type === "checkbox") {
return RPM.GetRightAnswerFromResult(i);
}
for (let j = 0; j < items.length; j++) {
let cn = items[j].className;
if (cn.includes("correct") && !cn.includes("incorrect")) {
return items[j].getElementsByTagName("label")[0]
.innerText;
}
}
if (items.length === 2) {
const resByIncorrect = this.GuessCorrectIn2LengthAnswersByIncorrect(
items
);
if (!resByIncorrect) {
const resPoints = this.GuessCorrectIn2LengthAnswersByPoints(
i,
items
);
return resPoints;
}
return resByIncorrect;
}
} catch (e) {
Log("error at new nodegetting, trying the oldschool way");
}
}
}
class MiscPageModell {
GetCurrentSubjectName() {
if (logElementGetting) {
Log("getting current subjects name");
}
return (
document
.getElementById("page-header")
.innerText.split("\n")[0] || ""
);
}
GetVideo() {
if (logElementGetting) {
Log("getting video stuff");
}
return document.getElementsByTagName("video")[0];
}
GetVideoElement() {
if (logElementGetting) {
Log("getting video element");
}
return document.getElementById("videoElement").parentNode;
}
GetInputType(answers, i) {
if (logElementGetting) {
Log("getting input type");
}
return answers[i].getElementsByTagName("input")[0].type;
}
}
var QPM = new QuestionsPageModell();
var RPM = new ResultsPageModell();
var MPM = new MiscPageModell();
// : }}}
// : Main function {{{
let timerStarted = false;
Main();
function Main() {
"use strict";
console.log("Moodle / E-Learning script");
console.time("main");
timerStarted = true;
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", Init);
} else {
Init();
}
}
function AfterLoad() {
const url = location.href; // eslint-disable-line
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(
{
m: texts.fatalError,
isSimple: true,
},
undefined,
() => {
OpenErrorPage(e);
}
);
Exception(e, "script error at main:");
}
if (url.includes("eduplayer")) {
AddVideoHotkeys(url);
} // adding video hotkeys
Log(texts.consoleErrorInfo);
if (timerStarted) {
console.log("Moodle Test Script run time:");
console.timeEnd("main");
timerStarted = false;
}
if (forceTestPage || forceResultPage || forceDefaultPage) {
if (overlay.querySelector("#scriptMessage")) {
overlay.querySelector("#scriptMessage").style.background =
"green";
}
}
}
// : }}}
// : Main logic stuff {{{
// : Loading {{{
function HandleQminingSite(url) {
try {
const idInput = document.getElementById("cid");
if (idInput) {
idInput.value = getVal("clientId");
}
} catch (e) {
console.info("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) {
console.info("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 = GetId() + "|" + info().script.version;
window.clearInterval(cidSetInterval);
}
}, 100);
}
} catch (e) {
console.info("Error filling client ID input", e);
}
}
function Init() {
const url = location.href; // eslint-disable-line
if (url.includes(serverAdress.split("/")[2])) {
HandleQminingSite(url);
return;
}
// if (false) {
// // eslint-disable-line
// setVal("version16", undefined);
// setVal("version15", undefined);
// setVal("firstRun", undefined);
// setVal("showQuestions", undefined);
// setVal("showSplash", undefined);
// }
// --------------------------------------------------------------------------------------
// event listener fuckery
// --------------------------------------------------------------------------------------
try {
// adding addeventlistener stuff, for the ability to add more event listeners for the same event
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) {
SendXHRMessage("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("menuButtonDiv", elem => {
elem.style.backgroundColor = "#262626";
});
SafeGetElementById("ircButton", elem => {
elem.style.display = "none";
});
SafeGetElementById("retryButton", 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;
motd = inf.motd;
subjInfo = inf.subjinfo;
overlay.querySelector(
"#infoMainDiv"
).innerText = `${subjInfo.subjects} tárgy, ${subjInfo.questions} kérdés. Felh #${inf.uid}`;
// FIXME: if cwith() throws an unhandled error it sais server is not avaible
cwith();
})
.catch(() => {
NoServerAction();
});
}
function NoUserAction() {
SafeGetElementById("menuButtonDiv", elem => {
elem.style.backgroundColor = "#44cc00";
});
SafeGetElementById("infoMainDiv", elem => {
elem.innerText = texts.noUser;
if (getVal("clientId")) {
elem.innerText += ` (${getVal("clientId")})`;
}
});
SafeGetElementById("loginDiv", elem => {
elem.style.display = "";
});
}
function NoServerAction() {
SafeGetElementById("menuButtonDiv", elem => {
elem.style.backgroundColor = "red";
});
SafeGetElementById("infoMainDiv", elem => {
elem.innerText = texts.noServer;
});
SafeGetElementById("ircButton", elem => {
elem.style.display = "";
});
SafeGetElementById("retryButton", elem => {
elem.style.display = "";
});
Log(texts.noServerConsoleMessage);
}
function VersionActions() {
// FOR TESTING ONLY
// setVal("version15", true);
// setVal("firstRun", true);
// setVal("version16", true);
// throw "asd";
FreshStart();
}
// : 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(); // showing help
document.write(texts.freshStartWarning);
document.close();
throw new Error("something, so this stuff stops");
}
}
// : }}}
// : UI handling {{{
function HandleUI(url) {
// FIXME: normal string building with localisation :/
var newVersion = false; // if the script is newer than last start
try {
newVersion = info().script.version !== getVal("lastVerson");
} catch (e) {
Log("Some weird error trying to set new verison");
}
let showMOTD = false;
if (!SUtils.EmptyOrWhiteSpace(motd)) {
var prevmotd = getVal("motd");
if (prevmotd !== motd) {
showMOTD = true;
setVal("motdcount", motdShowCount);
setVal("motd", motd);
} else {
var motdcount = getVal("motdcount");
if (motdcount === undefined) {
setVal("motdcount", motdShowCount);
motdcount = motdShowCount;
}
motdcount--;
if (motdcount > 0) {
showMOTD = true;
setVal("motdcount", motdcount);
}
}
}
let isNewVersionAvaible =
lastestVersion !== undefined &&
info().script.version !== lastestVersion;
var greetMsg = ""; // message to show at the end
var timeout = null; // the timeout. if null, it wont be hidden
if (isNewVersionAvaible || newVersion || showMOTD) {
greetMsg =
"Moodle/Elearning/KMOOC segéd v. " +
info().script.version +
". ";
}
if (isNewVersionAvaible) {
timeout = 5;
greetMsg += "Új verzió elérhető: " + lastestVersion;
timeout = undefined;
}
if (newVersion) {
// --------------------------------------------------------------------------------------------------------------
greetMsg +=
"Verzió frissítve " +
info().script.version +
"-re. Changelog:\n" +
texts.lastChangeLog;
setVal("lastVerson", info().script.version); // setting lastVersion
}
if (showMOTD) {
greetMsg += "\nMOTD:\n" + motd;
timeout = null;
}
ShowMessage(
{
m: greetMsg,
isSimple: true,
},
timeout
); // showing message. If "m" is empty it wont show it, thats how showSplash works.
}
// : }}}
// : Answering stuffs {{{
function HandleQuiz() {
var q = QPM.GetQuestionFromTest();
var questions = q.q;
var imgNodes = q.imgnodes;
// ------------------------------------------------------------------------------------------------------
let promises = [];
questions.forEach(x => {
let question = SUtils.EmptyOrWhiteSpace(x)
? ""
: SUtils.RemoveUnnecesarySpaces(x); // simplifying question
promises.push(
GetXHRQuestionAnswer({
q: question,
data: GetImageDataFromImgNodes(imgNodes),
subj: MPM.GetCurrentSubjectName(),
})
);
});
// FIXME: promise.all promise resolve order same as original?
Promise.all(promises).then(res => {
let answers = [];
res.forEach((result, j) => {
var r = PrepareAnswers(result, j);
if (r !== undefined) {
answers.push(r);
}
HighLightAnswer(result, j); // highlights the answer for the current result
});
ShowAnswers(answers, q.q);
});
}
function PrepareAnswers(result, j) {
assert(result);
if (result.length > 0) {
var allMessages = []; // preparing all messages
for (var k = 0; k < result.length; k++) {
var msg = ""; // the current message
if (
getVal("showQuestions") === undefined ||
getVal("showQuestions")
) {
msg += result[k].q.Q + "\n"; // adding the question if yes
}
msg += result[k].q.A.replace(/, /g, "\n"); // adding answer
if (result[k].q.data.type === "image") {
msg +=
"\n\nKépek fenti válaszok sorrendjében: " +
result[k].q.data.images.join(", "); // if it has image part, adding that too
}
if (
result[k].detailedMatch &&
result[k].detailedMatch.matchedSubjName
) {
msg +=
"\n(Tárgy: " +
result[k].detailedMatch.matchedSubjName +
")";
}
allMessages.push({
m: msg,
p: result[k].match,
});
}
return allMessages;
}
}
function ShowAnswers(answers, question) {
assert(answers);
if (answers.length > 0) {
// if there are more than 0 answer
ShowMessage(answers);
} else {
ShowMessage(
{
m: texts.noResult,
isSimple: true,
},
undefined,
function() {
OpenErrorPage({
message: "No result found",
stack: JSON.stringify(question),
});
}
);
}
}
// : }}}
// : Quiz saving {{{
function HandleResults(url) {
SaveQuiz(GetQuiz(), ShowSaveQuizDialog); // saves the quiz questions and answers
}
function ShowSaveQuizDialog(sendResult, sentData, newQuestions) {
// FIXME: normal string building with localisation :/
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(
{
m: msg,
isSimple: true,
},
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();
}
);
}
// this should get the image url from a result page
// i is the index of the question
// FIXME: move this to RPM class ??? and refactor this
function GetImageFormResult(i) {
try {
var imgElements = RPM.GetResultImage(i); // trying to get image
var imgURL = []; // image urls
for (var j = 0; j < imgElements.length; j++) {
if (!imgElements[j].src.includes("brokenfile")) {
var filePart = imgElements[j].src.split("/"); // splits the link by "/"
filePart = filePart[filePart.length - 1]; // the last one is the image name
imgURL.push(decodeURI(SUtils.ShortenString(filePart, 30)));
}
}
if (imgURL.length > 0) {
return imgURL;
}
} catch (e) {
Log("Couldn't get images from result");
}
}
function GetDataFormResult(i) {
let data = { type: "simple" };
let img = GetImageFormResult(i);
let grabbox = RPM.GetAnswersFromGrabBox(i);
if (img) {
data = {
type: "image",
images: img,
};
}
if (grabbox) {
data = {
type: "grabbox",
images: img,
grabbox: grabbox,
};
}
return data;
}
// saves the current quiz. questionData contains the active subjects questions
function SaveQuiz(quiz, next) {
try {
let sentData = {};
if (quiz.length === 0) {
throw new Error("quiz length is zero!");
}
try {
try {
sentData.subj = MPM.GetCurrentSubjectName();
} catch (e) {
sentData.subj = "NOSUBJ";
Log("unable to get subject name :c");
}
sentData.version = info().script.version;
sentData.id = GetId();
sentData.quiz = quiz;
console.log("SENT DATA", sentData);
SendXHRMessage("isAdding", sentData).then(res => {
next(res.success, sentData, res.newQuestions);
});
} catch (e) {
Exception(e, "error at sending data to server.");
}
} catch (e) {
Exception(e, "script error at saving quiz");
}
}
// getting quiz from finish page
function GetQuiz() {
try {
var quiz = []; // final quiz stuff
var results = RPM.GetFormResult(); // getting results element
for (var i = 0; i < results.length - 2; i++) {
var question = {}; // the current question
// QUESTION --------------------------------------------------------------------------------------------------------------------
question.Q = RPM.GetQuestionFromResult(i);
// RIGHTANSWER ---------------------------------------------------------------------------------------------------------------------
question.A = RPM.GetRightAnswerFromResultv2(i);
if (question.A === undefined) {
question.A = RPM.GetRightAnswerFromResult(i);
}
// DATA ---------------------------------------------------------------------------------------------------------------------
question.data = GetDataFormResult(i);
if (question.A !== undefined) {
quiz.push(question); // adding current question to quiz
} else {
Log(
"error getting queston, no correct answer given, or its incorrect"
);
Log(question);
}
}
return quiz;
} catch (e) {
Exception(e, "script error at quiz parsing:");
}
}
// : }}}
// : Helpers {{{
function GetImageDataFromImgNodes(imgs) {
var questionImages = []; // the array for the image names in question
for (var i = 0; i < imgs.length; i++) {
if (!imgs[i].src.includes("brokenfile")) {
var filePart = imgs[i].src.split("/"); // splits the link by "/"
filePart = filePart[filePart.length - 1]; // the last one is the image name
questionImages.push(
decodeURI(
SUtils.RemoveUnnecesarySpaces(
SUtils.ShortenString(filePart, 30)
)
)
); // decodes uri codes, and removes exess spaces, and shortening it
}
}
if (questionImages.length > 0) {
return {
type: "image",
images: questionImages,
};
} else {
return {
type: "simple",
};
}
}
// adds image names to image nodes
function AddImageNamesToImages(imgs) {
for (var i = 0; i < imgs.length; i++) {
if (!imgs[i].src.includes("brokenfile")) {
var filePart = imgs[i].src.split("/"); // splits the link by "/"
filePart = filePart[filePart.length - 1]; // the last one is the image name
var appedtTo = imgs[i].parentNode; // it will be appended here
var mainDiv = document.createElement("div");
var fileName = SUtils.ShortenString(decodeURI(filePart), 15); // shortening name, couse it can be long as fuck
var textNode = document.createTextNode("(" + fileName + ")");
mainDiv.appendChild(textNode);
appedtTo.appendChild(mainDiv);
}
}
}
// this function adds basic hotkeys for video controll.
function AddVideoHotkeys(url) {
var seekTime = 20;
document.addEventListener("keydown", function(e) {
try {
var video = MPM.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 = MPM.GetVideoElement();
var node = CreateNodeWithText(toadd, texts.videoHelp);
node.style.margin = "5px 5px 5px 5px"; // fancy margin
}
// removes stuff like " a. q1" -> "q1"
function RemoveLetterMarking(inp) {
let dotIndex = inp.indexOf(".");
let doubledotIndex = inp.indexOf(":");
let maxInd = 4; // inp.length * 0.2;
if (dotIndex < maxInd) {
return SUtils.RemoveUnnecesarySpaces(
inp.substr(inp.indexOf(".") + 1, inp.length)
);
} else if (doubledotIndex < maxInd) {
return SUtils.RemoveUnnecesarySpaces(
inp.substr(inp.indexOf(":") + 1, inp.length)
);
} else {
return inp;
}
}
// highlights the possible solutions to the current question
function HighLightAnswer(results, currQuestionNumber) {
try {
if (results.length > 0) {
var answers = RPM.GetAllAnswer(currQuestionNumber); // getting all answers
var toColor = []; // the numberth in the array will be colored, and .length items will be colored
var type = ""; // type of the question. radio or ticbox or whatitscalled
for (let i = 0; i < answers.length; i++) {
// going thtough answers
if (
answers[i].tagName &&
answers[i].tagName.toLowerCase() === "div"
) {
// if its not null and is "div"
var correct = results[0].q.A.toLowerCase(); // getting current correct answer from data
var answer = answers[i].innerText
.replace(/\n/g, "")
.toLowerCase(); // getting current answer
// removing stuff like "a."
answer = RemoveLetterMarking(answer);
if (
SUtils.EmptyOrWhiteSpace(correct) ||
SUtils.EmptyOrWhiteSpace(answer)
) {
continue;
}
if (
SUtils.NormalizeSpaces(
SUtils.RemoveUnnecesarySpaces(correct)
).includes(answer)
) {
// if the correct answer includes the current answer
toColor.push(i); // adding the index
type = MPM.GetInputType(answers, i); // setting the type
}
}
}
if (results[0].match === 100) {
// if the result is 100% correct
if (type !== "radio" || toColor.length === 1) {
// FIXME why not radio
for (let i = 0; i < toColor.length; i++) {
// going through "toColor"
let highlight = createHoverOver(
answers[toColor[i]]
);
Object.assign(highlight.style, {
border: "7px solid rgba(100, 240, 100, 0.8)",
borderRadius: "10px",
margin: "-13px 0 0 -8px",
});
}
}
} // and coloring the correct index
}
} catch (e) {
// catching errors. Sometimes there are random errors, wich i did not test, but they are rare, and does not break the main script.
Log("script error at highlightin answer: " + e.message);
}
}
// : }}}
function Log(value) {
if (log) {
console.log(value);
}
}
function Exception(e, msg) {
Log("------------------------------------------");
Log(msg);
Log(e.message);
Log("------------------------------------------");
Log(e.stack);
Log("------------------------------------------");
}
// : }}}
// : Minor UI stuff {{{
function ClearAllMessages() {
overlay.querySelectorAll("#scriptMessage").forEach(x => x.remove());
}
// 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 {
var defMargin = "0px 5px 0px 5px";
var isSimpleMessage = false;
var simpleMessageText = "";
if (msgItem.isSimple) {
// parsing msgItem for easier use
simpleMessageText = msgItem.m;
if (simpleMessageText === "") {
// if msg is empty
return;
}
msgItem = [
[
{
m: simpleMessageText,
},
],
];
isSimpleMessage = true;
}
var appedtTo = overlay; // will be appended here
var width = window.innerWidth - window.innerWidth / 6; // with of the box
var startFromTop = 25; // top distance
var mainDiv = document.createElement("div"); // the main divider, wich items will be attached to
mainDiv.setAttribute("id", "messageMainDiv");
if (funct) {
// if there is a function as parameter
addEventListener(mainDiv, "click", funct); // adding it as click
}
// lotsa crap style
SetStyle(mainDiv, {
position: "fixed",
zIndex: 999999,
textAlign: "center",
width: width + "px",
padding: "0px",
background: "#222d32",
color: "#ffffff",
borderColor: "#035a8f",
border: "none",
top: startFromTop + "px",
left: (window.innerWidth - width) / 2 + "px",
opacity: "0.9",
});
mainDiv.setAttribute("id", "scriptMessage");
var matchPercent = msgItem[0][0].p;
if (isSimpleMessage) {
var simpleMessageParagrapg = document.createElement("p"); // new paragraph
simpleMessageParagrapg.style.margin = defMargin; // fancy margin
var mesageNode = document.createElement("p"); // new paragraph
mesageNode.innerHTML = simpleMessageText.replace(
/\n/g,
"</br>"
);
simpleMessageParagrapg.appendChild(mesageNode);
mesageNode.style.margin = defMargin; // fancy margin
mainDiv.appendChild(simpleMessageParagrapg); // adding text box to main div
} else {
// 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
var numberTextCell = rowOne.insertCell();
var questionCell = rowOne.insertCell(); // QUESTION CELL
questionCell.setAttribute("id", "questionCell");
questionCell.rowSpan = 3;
questionCell.style.width = "90%";
var prevQuestionCell = rowOne.insertCell();
// row two
var percentTextCell = rowTwo.insertCell();
var nextQuestionCell = rowTwo.insertCell();
// 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");
if (matchPercent) {
// if match percent param is not null
percentTextBox.innerText = matchPercent + "%";
}
// NUMBER SETUP -----------------------------------------------------------------------------------------------------
var numberTextBox = CreateNodeWithText(numberTextCell, "1.");
numberTextBox.setAttribute("id", "numberTextBox");
// ANSWER NODE SETUP -------------------------------------------------------------------------------------------------------------
var questionTextElement = CreateNodeWithText(
questionCell,
"ur question goes here, mister OwO"
);
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 = () => {
var relevantQuestion = GetRelevantQuestion();
questionTextElement.innerText = relevantQuestion.m;
if (currItem === 0 && currRelevantQuestion === 0) {
numberTextBox.innerText =
currRelevantQuestion + 1 + ".";
} else {
numberTextBox.innerText =
currItem +
1 +
"./" +
(currRelevantQuestion + 1) +
".";
}
percentTextBox.innerText = relevantQuestion.p + "%";
};
var buttonMargin = "2px 2px 2px 2px"; // uniform button margin
if (msgItem[currItem].length > 1) {
// PREV SUGG BUTTON ------------------------------------------------------------------------------------------------------------
var prevSuggButton = CreateNodeWithText(
prevSuggestionCell,
"<",
"button"
);
prevSuggButton.style.margin = buttonMargin; // fancy margin
prevSuggButton.addEventListener("click", function() {
ChangeCurrRelevantQuestionIndex(-1);
SetQuestionText();
});
// NEXT SUGG BUTTON ------------------------------------------------------------------------------------------------------------
var nextSuggButton = CreateNodeWithText(
nextSuggestionCell,
">",
"button"
);
nextSuggButton.style.margin = buttonMargin; // fancy margin
nextSuggButton.addEventListener("click", function() {
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,
"^",
"button"
);
prevButton.style.margin = buttonMargin; // fancy margin
// event listener
prevButton.addEventListener("click", function() {
ChangeCurrItemIndex(-1);
SetQuestionText();
});
// NEXT QUESTION BUTTON ------------------------------------------------------------------------------------------------------------
var nextButton = CreateNodeWithText(
nextQuestionCell,
"ˇ",
"button"
);
nextButton.style.margin = buttonMargin; // fancy margin
// 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);
}
}
});
} catch (e) {
Exception(e, "script error at showing message:");
}
}
// shows a fancy menu
function ShowMenu() {
try {
var appedtTo = overlay; // will be appended here
// mainDiv.style.left = (window.innerWidth - width) / 2 + 'px';
var menuButtonDiv = document.createElement("div");
menuButtonDiv.setAttribute("id", "menuButtonDiv");
SetStyle(menuButtonDiv, {
width: "600px",
// height: buttonHeight + 'px',
top: window.innerHeight - 120 + "px",
left: "10px",
zIndex: 999999,
position: "fixed",
textAlign: "center",
padding: "0px",
margin: "0px",
background: "#262626",
});
var tbl = document.createElement("table");
tbl.style.margin = "5px 5px 5px 5px";
tbl.style.textAlign = "left";
tbl.style.width = "98%";
menuButtonDiv.appendChild(tbl);
var buttonRow = tbl.insertRow();
var buttonCell = buttonRow.insertCell();
buttonCell.style.textAlign = "center";
let buttonStyle = {
position: "",
margin: "5px 5px 5px 5px",
border: "none",
backgroundColor: "#222d32",
color: "#ffffff",
cursor: "pointer",
};
// help button ----------------------------------------------------------------------------------------------------------------
let helpButton = CreateNodeWithText(
buttonCell,
texts.help,
"button"
);
SetStyle(helpButton, buttonStyle);
helpButton.addEventListener("click", function() {
ShowHelp();
}); // adding clicktextNode
// site link ----------------------------------------------------------------------------------------------------------------
let contributeLink = CreateNodeWithText(
buttonCell,
texts.contribute,
"button"
);
contributeLink.title = texts.contributeTitle;
SetStyle(contributeLink, buttonStyle);
contributeLink.addEventListener("click", function() {
openInTab(serverAdress + "contribute?scriptMenu", {
active: true,
});
});
// pw request ----------------------------------------------------------------------------------------------------------------
let pwRequest = CreateNodeWithText(
buttonCell,
texts.pwRequest,
"button"
);
pwRequest.title = texts.newPWTitle;
SetStyle(pwRequest, buttonStyle);
pwRequest.addEventListener("click", function() {
openInTab(serverAdress + "pwRequest", {
active: true,
});
});
// site link ----------------------------------------------------------------------------------------------------------------
let siteLink = CreateNodeWithText(
buttonCell,
texts.websiteBugreport,
"button"
);
SetStyle(siteLink, buttonStyle);
siteLink.addEventListener("click", function() {
openInTab(serverAdress + "menuClick", {
active: true,
});
});
// donate link ----------------------------------------------------------------------------------------------------------------
let donateLink = CreateNodeWithText(
buttonCell,
texts.donate,
"button"
);
SetStyle(donateLink, buttonStyle);
donateLink.addEventListener("click", function() {
openInTab(serverAdress + "donate?scriptMenu", {
active: true,
});
});
addEventListener(window, "resize", function() {
menuButtonDiv.style.top = window.innerHeight - 70 + "px";
});
// INFO TABEL --------------------------------------------------------------------
var itbl = document.createElement("table");
SetStyle(itbl, {
margin: "5px 5px 5px 5px",
textAlign: "left",
width: "98%",
});
menuButtonDiv.appendChild(itbl);
var ibuttonRow = tbl.insertRow();
var ibuttonCell = ibuttonRow.insertCell();
ibuttonCell.style.textAlign = "center";
// INFO DIV ---------------------------------------------------------------------------------
let infoDiv = CreateNodeWithText(
ibuttonCell,
texts.loading,
"span"
);
infoDiv.setAttribute("id", "infoMainDiv");
SetStyle(infoDiv, {
color: "#ffffff",
margin: "5px",
});
// login div ----------------------------------------------------------------------------------------------------------------
const loginDiv = document.createElement("div");
loginDiv.style.display = "none";
loginDiv.setAttribute("id", "loginDiv");
const loginButton = document.createElement("button");
loginButton.innerText = texts.login;
const loginInput = document.createElement("input");
loginInput.type = "text";
loginInput.style.width = "400px";
loginInput.style.textAlign = "center";
const clientId = getVal("clientId");
if (clientId && clientId.toString()[0] !== "0") {
loginInput.value = clientId || "";
loginButton.innerText = texts.requestPWInsteadOfLogin;
}
loginDiv.appendChild(loginInput);
loginDiv.appendChild(loginButton);
SetStyle(loginButton, buttonStyle);
loginInput.addEventListener("keyup", e => {
console.log(e.target.value);
if (e.target.value === clientId) {
loginButton.innerText = texts.requestPWInsteadOfLogin;
} else if (e.target.value !== "") {
loginButton.innerText = texts.login;
}
});
loginButton.addEventListener("click", function() {
if (loginInput.value === clientId.toString()) {
openInTab(serverAdress + "getVeteranPw?cid=" + clientId, {
active: true,
});
} else {
Auth(loginInput.value);
}
});
ibuttonCell.appendChild(loginDiv);
// irc button ----------------------------------------------------------------------------------------------------------------
let ircButton = CreateNodeWithText(
ibuttonCell,
texts.ircButton,
"button"
);
SetStyle(ircButton, buttonStyle);
ircButton.style.display = "none";
ircButton.setAttribute("id", "ircButton");
ircButton.addEventListener("click", function() {
openInTab(ircAddress, {
active: true,
});
});
// retry button ----------------------------------------------------------------------------------------------------------------
let retryButton = CreateNodeWithText(
ibuttonCell,
texts.retry,
"button"
);
SetStyle(retryButton, buttonStyle);
retryButton.style.display = "none";
retryButton.setAttribute("id", "retryButton");
retryButton.addEventListener("click", function() {
menuButtonDiv.style.background = "#262626";
infoDiv.innerText = texts.loading;
retryButton.style.display = "none";
ircButton.style.display = "none";
ConnectToServer(AfterLoad);
});
// window resize event listener ---------------------------------------
addEventListener(window, "resize", function() {
menuButtonDiv.style.top = window.innerHeight - 70 + "px";
});
// APPEND EVERYTHING
appedtTo.appendChild(menuButtonDiv);
} catch (e) {
Exception(e, "script error at showing menu:");
}
}
// : }}}
// : Generic utils {{{
function GetId() {
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) {
let 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);
to.appendChild(paragraphElement);
return paragraphElement;
}
function GetXHRInfos() {
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) {
console.info(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=" +
GetId();
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 GetXHRQuestionAnswer(question) {
return new Promise((resolve, reject) => {
let url = apiAdress + "ask?";
let params = [];
Object.keys(question).forEach(key => {
let val = question[key];
if (typeof val !== "string") {
val = JSON.stringify(val);
}
params.push(key + "=" + encodeURIComponent(val));
});
url +=
params.join("&") +
"&cversion=" +
info().script.version +
"&cid=" +
GetId();
xmlhttpRequest({
method: "GET",
url: url,
onload: function(response) {
try {
let res = JSON.parse(response.responseText);
// FIXME: check if res is a valid answer array
// res.json({
// result: r,
// success: true
// })
// ERROR:
// res.json({
// message: `Invalid question :(`,
// result: [],
// recievedData: JSON.stringify(req.query),
// success: false
// })
resolve(res.result);
} catch (e) {
reject(e);
}
},
onerror: e => {
Log("Errro paring JSON in GetXHRQuestionAnswer");
Log(e);
reject(e);
reject(e);
},
});
});
}
function SendXHRMessage(path, message) {
// message = SUtils.RemoveSpecialChars(message) // TODO: check this
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 SendXHRMessage");
Log(resp.responseText);
Log(e);
reject(e);
}
},
});
});
}
function OpenErrorPage(e) {
let path = "lred";
try {
if (e.message || e.stack) {
path += "?";
}
if (e.message) {
path += "msg:" + SUtils.SimplifyQuery(e.message);
}
if (e.stack) {
path += "___stack:" + SUtils.SimplifyStack(e.stack);
}
path += "___version:" + info().script.version;
path = SUtils.RemoveSpecialChars(path);
} catch (e) {
Exception(e, "error at setting error stack/msg link");
}
path = path.replace(/ /g, "_");
openInTab(serverAdress + path, {
active: true,
});
}
// : }}}
// : Help {{{
// shows some neat help
function ShowHelp() {
openInTab(serverAdress + "manual?scriptMenu", {
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