diff --git a/stable.user.js b/stable.user.js index 4f47dba..dc8ea17 100755 --- a/stable.user.js +++ b/stable.user.js @@ -18,35 +18,11 @@ along with this program. If not, see . ------------------------------------------------------------------------- */ -// : 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.0.3 +// @version 9.9.9.9 // @description Online Moodle/Elearning/KMOOC test help // @author MrFry // @match https://elearning.uni-obuda.hu/* @@ -90,8 +66,8 @@ function delVal(name) { return GM_deleteValue(name) } - function openInTab(address, options) { - GM_openInTab(address, options) + function openInTab(address) { + GM_openInTab(address, false) } function xmlhttpRequest(opts) { GM_xmlhttpRequest(opts) @@ -105,724 +81,11 @@ // : Constants and global variables {{{ - const logElementGetting = false - const logEnabled = true - const showErrors = true - // forcing pages for testing. unless you test, do not set these to true! - setVal('ISDEVEL', false) - // only one of these should be true for testing - const forceTestPage = false - const forceResultPage = false - const forceDefaultPage = false - 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 correctSource = - 'https://qmining.frylabs.net/moodle-test-userscript/stable.user.js?up' - 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 - - // : 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: - '

Moodle teszt userscript:

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

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

Ez az ablak frissités után eltűnik. Ha nem, akkor a visza gombbal próbálkozz.
', - noResult: - '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', - ircButton: 'IRC chat', - ircButtonTitle: 'IRC chat', - 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!', - 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`, - noParseableQuestionResult: - 'A tesztben nem találhatók kérdések, amit fel lehet dolgozni, vagy hiba történt feldolgozásuk közben', - unableToParseTestQuestion: - 'Hiba történt a kérdések beolvasása közben :/ Kattints az üzenetre a manuális kereséshez (új böngésző tab-ban)', - loadingAnswer: 'Válaszok betöltése ...', - reinstallFromCorrectSource: - 'Scriptet nem a qmining weboldalról raktad fel. Könnyebb kezelhetőség szempontjából kérlek onnan telepítsd. Részletes leírás', - 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] - } - } - - let select = elem.tagName ? elem.getElementsByTagName('select') : [] - if (select.length > 0) { - promises.push({ type: 'txt', val: '...', 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] || '' - } - - function uniq(a) { - return [...new Set(a)] - } - - // : }}} - - // : Test page processing functions {{{ - - function handleQuiz() { - const { removeMessage: removeLoadingMessage } = ShowMessage( - texts.loadingAnswer - ) - - getQuizData() - .then(readQuestions => { - if (readQuestions.length === 0) { - 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()') - }) - } - - function getQuizData() { - return new Promise(resolve => { - // TODO: dropdown in question - // TODO: get possible answers too - const promises = [] - const questionNodes = Array.from( - document.getElementsByTagName('form')[0].childNodes[0].childNodes - ) - - 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) { - const promises = [] - let answerRoot = node.getElementsByClassName('answer')[0] - - if (!answerRoot) { - answerRoot = node.getElementsByClassName('subquestion')[0] - const options = Array.from(answerRoot.getElementsByTagName('option')) - const possibleAnswers = options.reduce((acc, option) => { - if (!emptyOrWhiteSpace(option.innerText)) { - acc.push(option.innerText) - } - return acc - }, []) - - return possibleAnswers - } 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) - - answers.forEach(answer => { - if (answer.tagName) { - 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 - } - } - - 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 - }, []) - } - - function getQuestionPromiseForSingleQuestion(node) { - return new Promise(resolve => { - try { - let qtextNode = node.getElementsByClassName('qtext')[0] - if (!qtextNode) { - qtextNode = node.getElementsByClassName('subquestion')[0] - qtextNode = qtextNode.parentNode - } - - 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(' ') - ) - }) - let images = getImagesFromElements([ - ...question, - ...possibleAnswerArray.reduce((acc, x) => { - return [...acc, ...x] - }, []), - ]) - const imageNodes = getImgNodesFromArray([ - ...question, - ...possibleAnswerArray, - ]) - const data = getDataFromTest(images, getLegacyImageID(imageNodes)) - - resolve({ - question: questionText, - possibleAnswers, - images, - 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(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 {{{ - - function getQuiz() { - return new Promise(resolve => { - const promises = [] - const questionNodes = Array.from( - document.getElementsByTagName('form')[0].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 => { - 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(' ') - let 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 }) - }) - }) - } - - 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 => { - let 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 => { - let 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) { - 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 - }, []) - } - - function digestMessage(message) { - return new Promise(resolve => { - const encoder = new TextEncoder() - const data = encoder.encode(message) - const hash = 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' - let canvas = document.createElement('canvas') - canvas.width = img.width - canvas.height = img.height - let ctx = canvas.getContext('2d') - ctx.drawImage(img, 0, 0) - let 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 - } - - // : }}} // : }}} @@ -919,174 +182,13 @@ 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 currX, currY, currWidth, currHeight - 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) { @@ -1100,445 +202,18 @@ } })() } 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('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.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('menuButtonDiv', elem => { - elem.style.backgroundColor = '#44cc00' - }) - SafeGetElementById('infoMainDiv', elem => { - elem.innerText = texts.noUser - if (getCid()) { - elem.innerText += ` (${getCid()})` - } - }) - 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() { - 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(url) { - 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 (!installedFromCorrectSource(correctSource)) { - greetMsg.push(texts.reinstallFromCorrectSource) - log(`update url:${info().script.updateURL}`) - } - if (showMOTD) { - greetMsg.push(texts.motd + motd) - timeout = null + console.log(e) } 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, - } - }) - } - } - - 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(url) { - 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 += '

Elküldött adatok:

' + JSON.stringify(sentData) - } catch (e) { - towrite += '

Elküldött adatok:

' + sentData - } - document.write(towrite) - document.close() + 'Ha ezt az üzeneted látod, akkor a scripted a greasyforkról frissült. Weboldalról is lehet telepíteni a scriptet, és hogy a greasyfork ne legyen egy felesleges kerülő, így innentől ott megszűnt a script. Kattints erre az üzenetre a weboldalról való script telepítés útmutatóhoz.', + undefined, + () => { + openInTab(serverAdress + 'manual.html#scriptreinstall') } ) } - // 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 {{{ - - // : Install source checker {{{ - - function installedFromCorrectSource(source) { - // https://qmining.frylabs.net/moodle-test-userscript/stable.user.js?up - return info().script.updateURL === correctSource - } - - // : }}} - - // : 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(url) { - 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') @@ -1570,7 +245,7 @@ function addOpacityChangeEvent(elem) { if (!elem.id) { - warn('element must have ID to add opacity change event!') + console.warn('element must have ID to add opacity change event!') return } @@ -1945,390 +620,7 @@ }, } } catch (e) { - Exception(e, 'script error at showing message:') - } - } - - // shows a fancy menu - function ShowMenu() { - try { - const appedtTo = overlay // will be appended here - const menuButtonDiv = document.createElement('div') - const id = 'menuButtonDiv' - menuButtonDiv.setAttribute('id', id) - SetStyle(menuButtonDiv, { - width: '600px', - // height: buttonHeight + 'px', - top: window.innerHeight - 160 + 'px', - left: '10px', - zIndex: 999999, - position: 'fixed', - textAlign: 'center', - padding: '0px', - margin: '0px', - background: '#262626', - border: '3px solid #99f', - borderRadius: '5px', - opacity: getVal(`${id}_opacity`), - }) - // ------------------------------------------------------------------ - // transparencity - // ------------------------------------------------------------------ - addOpacityChangeEvent(menuButtonDiv) - - const xButton = CreateNodeWithText(menuButtonDiv, '❌', 'div') - SetStyle(xButton, { - cursor: 'pointer', - position: 'absolute', - right: '0px', - display: 'inline', - margin: '5px', - }) - xButton.addEventListener('mousedown', e => { - e.stopPropagation() - menuButtonDiv.parentNode.removeChild(menuButtonDiv) - }) - - const mailButton = CreateNodeWithText(menuButtonDiv, '📭', 'div') - mailButton.setAttribute('id', 'mailButton') - SetStyle(mailButton, { - fontSize: '30px', - position: 'absolute', - left: '0px', - bottom: '0px', - display: 'inline', - margin: '5px', - }) - 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 - ) - }) - - 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' - - var buttonRow2 = tbl.insertRow() - var buttonCell2 = buttonRow2.insertCell() - buttonCell2.style.textAlign = 'center' - - let buttonStyle = { - position: '', - margin: '5px 5px 5px 5px', - border: 'none', - backgroundColor: '#333333', - padding: '4px', - borderRadius: '2px', - color: '#ffffff', - cursor: 'pointer', - } - // site link ---------------------------------------------------------------------------------------------------------------- - - let siteLink = CreateNodeWithText( - buttonCell, - texts.websiteBugreport, - 'button' - ) - SetStyle(siteLink, buttonStyle) - - siteLink.addEventListener('click', function() { - openInTab(serverAdress + 'menuClick', { - active: true, - }) - }) - - // 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' - ) - SetStyle(contributeLink, buttonStyle) - - contributeLink.addEventListener('click', function() { - openInTab(serverAdress + 'contribute?scriptMenu', { - active: true, - }) - }) - - // pw request ---------------------------------------------------------------------------------------------------------------- - - let pwRequest = CreateNodeWithText(buttonCell2, texts.pwRequest, 'button') - pwRequest.title = texts.newPWTitle - SetStyle(pwRequest, buttonStyle) - - pwRequest.addEventListener('click', function() { - openInTab(serverAdress + 'pwRequest?scriptMenu', { - active: true, - }) - }) - // IRC ---------------------------------------------------------------------------------------------------------------- - - let ircButton2 = CreateNodeWithText( - buttonCell2, - texts.ircButton, - 'button' - ) - ircButton2.title = texts.ircButtonTitle - SetStyle(ircButton2, buttonStyle) - ircButton2.addEventListener('click', function() { - openInTab(serverAdress + 'irc?scriptMenu', { - active: true, - }) - }) - // Dataeditor ---------------------------------------------------------------------------------------------------------------- - - let ranklistButton = CreateNodeWithText( - buttonCell2, - texts.ranklist, - 'button' - ) - SetStyle(ranklistButton, buttonStyle) - ranklistButton.addEventListener('click', function() { - openInTab(serverAdress + 'ranklist?scriptMenu', { - active: true, - }) - }) - // Dataeditor ---------------------------------------------------------------------------------------------------------------- - - let dataEditorButton = CreateNodeWithText( - buttonCell2, - texts.dataEditor, - 'button' - ) - dataEditorButton.title = texts.dataEditorTitle - SetStyle(dataEditorButton, buttonStyle) - dataEditorButton.addEventListener('click', function() { - openInTab(serverAdress + 'dataeditor?scriptMenu', { - 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' - loginDiv.appendChild(loginInput) - loginDiv.appendChild(loginButton) - - SetStyle(loginButton, buttonStyle) - - loginButton.addEventListener('click', function() { - 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) - - addEventListener(menuButtonDiv, 'mousedown', function(e) { - if (e.which === 2) { - menuButtonDiv.parentNode.removeChild(menuButtonDiv) - } - }) - } 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(/\n/g, '') - .replace(/\t/g, '') - .replace(/ /g, '') - .replace(/\s/g, ' ') === '' - ) - } - - // : }}} - - const specialChars = ['&', '\\+'] - - 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) { - let element = overlay.querySelector('#' + id) - if (element) { - next(element) - } else { - log(`Unable to safe get element by id: ${id}`) + console.log(e) } } @@ -2349,206 +641,4 @@ } 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 GetXHRQuestionAnswer(question) { - log('QUESTIONS', 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=' + - getCid() - - xmlhttpRequest({ - method: 'GET', - url: url, - onload: function(response) { - try { - let res = JSON.parse(response.responseText) - resolve(res.result) - } catch (e) { - reject(e) - } - }, - onerror: e => { - log('Errro paring JSON in GetXHRQuestionAnswer') - log(e) - reject(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('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